Bug 18958: Make hold_fill_targets specific to reserves
[koha.git] / Koha / Item.pm
blob8b7a12653f0c4a0aa277f7fcab35d767f9fdb81d
1 package Koha::Item;
3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 use Modern::Perl;
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
34 use C4::ClassSource; # FIXME We would like to avoid that
35 use C4::Log qw( logaction );
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::ItemTypes;
42 use Koha::Patrons;
43 use Koha::Plugins;
44 use Koha::Libraries;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
48 use base qw(Koha::Object);
50 =head1 NAME
52 Koha::Item - Koha Item object class
54 =head1 API
56 =head2 Class methods
58 =cut
60 =head3 store
62 $item->store;
64 $params can take an optional 'skip_modzebra_update' parameter.
65 If set, the reindexation process will not happen (ModZebra not called)
67 NOTE: This is a temporary fix to answer a performance issue when lot of items
68 are added (or modified) at the same time.
69 The correct way to fix this is to make the ES reindexation process async.
70 You should not turn it on if you do not understand what it is doing exactly.
72 =cut
74 sub store {
75 my $self = shift;
76 my $params = @_ ? shift : {};
78 my $log_action = $params->{log_action} // 1;
80 # We do not want to oblige callers to pass this value
81 # Dev conveniences vs performance?
82 unless ( $self->biblioitemnumber ) {
83 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
86 # See related changes from C4::Items::AddItem
87 unless ( $self->itype ) {
88 $self->itype($self->biblio->biblioitem->itemtype);
91 my $today = dt_from_string;
92 my $plugin_action = 'create';
94 unless ( $self->in_storage ) { #AddItem
95 unless ( $self->permanent_location ) {
96 $self->permanent_location($self->location);
98 unless ( $self->replacementpricedate ) {
99 $self->replacementpricedate($today);
101 unless ( $self->datelastseen ) {
102 $self->datelastseen($today);
105 unless ( $self->dateaccessioned ) {
106 $self->dateaccessioned($today);
109 if ( $self->itemcallnumber
110 or $self->cn_source )
112 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
113 $self->cn_sort($cn_sort);
116 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
117 unless $params->{skip_modzebra_update};
119 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
120 if $log_action && C4::Context->preference("CataloguingLog");
122 } else { # ModItem
124 $plugin_action = 'modify';
126 my %updated_columns = $self->_result->get_dirty_columns;
127 return $self->SUPER::store unless %updated_columns;
129 # Retrieve the item for comparison if we need to
130 my $pre_mod_item = (
131 exists $updated_columns{itemlost}
132 or exists $updated_columns{withdrawn}
133 or exists $updated_columns{damaged}
134 ) ? $self->get_from_storage : undef;
136 # Update *_on fields if needed
137 # FIXME: Why not for AddItem as well?
138 my @fields = qw( itemlost withdrawn damaged );
139 for my $field (@fields) {
141 # If the field is defined but empty or 0, we are
142 # removing/unsetting and thus need to clear out
143 # the 'on' field
144 if ( exists $updated_columns{$field}
145 && defined( $self->$field )
146 && !$self->$field )
148 my $field_on = "${field}_on";
149 $self->$field_on(undef);
151 # If the field has changed otherwise, we much update
152 # the 'on' field
153 elsif (exists $updated_columns{$field}
154 && $updated_columns{$field}
155 && !$pre_mod_item->$field )
157 my $field_on = "${field}_on";
158 $self->$field_on(
159 DateTime::Format::MySQL->format_datetime(
160 dt_from_string()
166 if ( exists $updated_columns{itemcallnumber}
167 or exists $updated_columns{cn_source} )
169 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
170 $self->cn_sort($cn_sort);
174 if ( exists $updated_columns{location}
175 and $self->location ne 'CART'
176 and $self->location ne 'PROC'
177 and not exists $updated_columns{permanent_location} )
179 $self->permanent_location( $self->location );
182 # If item was lost and has now been found,
183 # reverse any list item charges if necessary.
184 if ( exists $updated_columns{itemlost}
185 and $updated_columns{itemlost} <= 0
186 and $pre_mod_item->itemlost > 0 )
188 $self->_set_found_trigger($pre_mod_item);
189 $self->paidfor('');
192 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
193 unless $params->{skip_modzebra_update};
195 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
196 if $log_action && C4::Context->preference("CataloguingLog");
199 unless ( $self->dateaccessioned ) {
200 $self->dateaccessioned($today);
203 my $result = $self->SUPER::store;
204 $self->get_from_storage->_after_item_action_hooks({ action => $plugin_action });
206 return $result;
209 =head3 delete
211 =cut
213 sub delete {
214 my $self = shift;
215 my $params = @_ ? shift : {};
217 # FIXME check the item has no current issues
218 # i.e. raise the appropriate exception
220 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
221 unless $params->{skip_modzebra_update};
223 $self->_after_item_action_hooks({ action => 'delete' });
225 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
226 if C4::Context->preference("CataloguingLog");
228 return $self->SUPER::delete;
231 =head3 safe_delete
233 =cut
235 sub safe_delete {
236 my $self = shift;
237 my $params = @_ ? shift : {};
239 my $safe_to_delete = $self->safe_to_delete;
240 return $safe_to_delete unless $safe_to_delete eq '1';
242 $self->move_to_deleted;
244 return $self->delete($params);
247 =head3 safe_to_delete
249 returns 1 if the item is safe to delete,
251 "book_on_loan" if the item is checked out,
253 "not_same_branch" if the item is blocked by independent branches,
255 "book_reserved" if the there are holds aganst the item, or
257 "linked_analytics" if the item has linked analytic records.
259 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
261 =cut
263 sub safe_to_delete {
264 my ($self) = @_;
266 return "book_on_loan" if $self->checkout;
268 return "not_same_branch"
269 if defined C4::Context->userenv
270 and !C4::Context->IsSuperLibrarian()
271 and C4::Context->preference("IndependentBranches")
272 and ( C4::Context->userenv->{branch} ne $self->homebranch );
274 # check it doesn't have a waiting reserve
275 return "book_reserved"
276 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
278 return "linked_analytics"
279 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
281 return "last_item_for_hold"
282 if $self->biblio->items->count == 1
283 && $self->biblio->holds->search(
285 itemnumber => undef,
287 )->count;
289 return 1;
292 =head3 move_to_deleted
294 my $is_moved = $item->move_to_deleted;
296 Move an item to the deleteditems table.
297 This can be done before deleting an item, to make sure the data are not completely deleted.
299 =cut
301 sub move_to_deleted {
302 my ($self) = @_;
303 my $item_infos = $self->unblessed;
304 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
305 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
309 =head3 effective_itemtype
311 Returns the itemtype for the item based on whether item level itemtypes are set or not.
313 =cut
315 sub effective_itemtype {
316 my ( $self ) = @_;
318 return $self->_result()->effective_itemtype();
321 =head3 home_branch
323 =cut
325 sub home_branch {
326 my ($self) = @_;
328 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
330 return $self->{_home_branch};
333 =head3 holding_branch
335 =cut
337 sub holding_branch {
338 my ($self) = @_;
340 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
342 return $self->{_holding_branch};
345 =head3 biblio
347 my $biblio = $item->biblio;
349 Return the bibliographic record of this item
351 =cut
353 sub biblio {
354 my ( $self ) = @_;
355 my $biblio_rs = $self->_result->biblio;
356 return Koha::Biblio->_new_from_dbic( $biblio_rs );
359 =head3 biblioitem
361 my $biblioitem = $item->biblioitem;
363 Return the biblioitem record of this item
365 =cut
367 sub biblioitem {
368 my ( $self ) = @_;
369 my $biblioitem_rs = $self->_result->biblioitem;
370 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
373 =head3 checkout
375 my $checkout = $item->checkout;
377 Return the checkout for this item
379 =cut
381 sub checkout {
382 my ( $self ) = @_;
383 my $checkout_rs = $self->_result->issue;
384 return unless $checkout_rs;
385 return Koha::Checkout->_new_from_dbic( $checkout_rs );
388 =head3 holds
390 my $holds = $item->holds();
391 my $holds = $item->holds($params);
392 my $holds = $item->holds({ found => 'W'});
394 Return holds attached to an item, optionally accept a hashref of params to pass to search
396 =cut
398 sub holds {
399 my ( $self,$params ) = @_;
400 my $holds_rs = $self->_result->reserves->search($params);
401 return Koha::Holds->_new_from_dbic( $holds_rs );
404 =head3 get_transfer
406 my $transfer = $item->get_transfer;
408 Return the transfer if the item is in transit or undef
410 =cut
412 sub get_transfer {
413 my ( $self ) = @_;
414 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
415 return unless $transfer_rs;
416 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
419 =head3 last_returned_by
421 Gets and sets the last borrower to return an item.
423 Accepts and returns Koha::Patron objects
425 $item->last_returned_by( $borrowernumber );
427 $last_returned_by = $item->last_returned_by();
429 =cut
431 sub last_returned_by {
432 my ( $self, $borrower ) = @_;
434 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
436 if ($borrower) {
437 return $items_last_returned_by_rs->update_or_create(
438 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
440 else {
441 unless ( $self->{_last_returned_by} ) {
442 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
443 if ($result) {
444 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
448 return $self->{_last_returned_by};
452 =head3 can_article_request
454 my $bool = $item->can_article_request( $borrower )
456 Returns true if item can be specifically requested
458 $borrower must be a Koha::Patron object
460 =cut
462 sub can_article_request {
463 my ( $self, $borrower ) = @_;
465 my $rule = $self->article_request_type($borrower);
467 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
468 return q{};
471 =head3 hidden_in_opac
473 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
475 Returns true if item fields match the hidding criteria defined in $rules.
476 Returns false otherwise.
478 Takes HASHref that can have the following parameters:
479 OPTIONAL PARAMETERS:
480 $rules : { <field> => [ value_1, ... ], ... }
482 Note: $rules inherits its structure from the parsed YAML from reading
483 the I<OpacHiddenItems> system preference.
485 =cut
487 sub hidden_in_opac {
488 my ( $self, $params ) = @_;
490 my $rules = $params->{rules} // {};
492 return 1
493 if C4::Context->preference('hidelostitems') and
494 $self->itemlost > 0;
496 my $hidden_in_opac = 0;
498 foreach my $field ( keys %{$rules} ) {
500 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
501 $hidden_in_opac = 1;
502 last;
506 return $hidden_in_opac;
509 =head3 can_be_transferred
511 $item->can_be_transferred({ to => $to_library, from => $from_library })
512 Checks if an item can be transferred to given library.
514 This feature is controlled by two system preferences:
515 UseBranchTransferLimits to enable / disable the feature
516 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
517 for setting the limitations
519 Takes HASHref that can have the following parameters:
520 MANDATORY PARAMETERS:
521 $to : Koha::Library
522 OPTIONAL PARAMETERS:
523 $from : Koha::Library # if not given, item holdingbranch
524 # will be used instead
526 Returns 1 if item can be transferred to $to_library, otherwise 0.
528 To find out whether at least one item of a Koha::Biblio can be transferred, please
529 see Koha::Biblio->can_be_transferred() instead of using this method for
530 multiple items of the same biblio.
532 =cut
534 sub can_be_transferred {
535 my ($self, $params) = @_;
537 my $to = $params->{to};
538 my $from = $params->{from};
540 $to = $to->branchcode;
541 $from = defined $from ? $from->branchcode : $self->holdingbranch;
543 return 1 if $from eq $to; # Transfer to current branch is allowed
544 return 1 unless C4::Context->preference('UseBranchTransferLimits');
546 my $limittype = C4::Context->preference('BranchTransferLimitsType');
547 return Koha::Item::Transfer::Limits->search({
548 toBranch => $to,
549 fromBranch => $from,
550 $limittype => $limittype eq 'itemtype'
551 ? $self->effective_itemtype : $self->ccode
552 })->count ? 0 : 1;
555 =head3 pickup_locations
557 $pickup_locations = $item->pickup_locations( {patron => $patron } )
559 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
560 and if item can be transferred to each pickup location.
562 =cut
564 sub pickup_locations {
565 my ($self, $params) = @_;
567 my $patron = $params->{patron};
569 my $circ_control_branch =
570 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
571 my $branchitemrule =
572 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
574 my @libs;
575 if(defined $patron) {
576 return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
577 return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
580 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
581 @libs = $self->home_branch->get_hold_libraries;
582 push @libs, $self->home_branch unless scalar(@libs) > 0;
583 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
584 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
585 @libs = $plib->get_hold_libraries;
586 push @libs, $self->home_branch unless scalar(@libs) > 0;
587 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
588 push @libs, $self->home_branch;
589 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
590 push @libs, $self->holding_branch;
591 } else {
592 @libs = Koha::Libraries->search({
593 pickup_location => 1
594 }, {
595 order_by => ['branchname']
596 })->as_list;
599 my @pickup_locations;
600 foreach my $library (@libs) {
601 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
602 push @pickup_locations, $library;
606 return \@pickup_locations;
609 =head3 article_request_type
611 my $type = $item->article_request_type( $borrower )
613 returns 'yes', 'no', 'bib_only', or 'item_only'
615 $borrower must be a Koha::Patron object
617 =cut
619 sub article_request_type {
620 my ( $self, $borrower ) = @_;
622 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
623 my $branchcode =
624 $branch_control eq 'homebranch' ? $self->homebranch
625 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
626 : undef;
627 my $borrowertype = $borrower->categorycode;
628 my $itemtype = $self->effective_itemtype();
629 my $rule = Koha::CirculationRules->get_effective_rule(
631 rule_name => 'article_requests',
632 categorycode => $borrowertype,
633 itemtype => $itemtype,
634 branchcode => $branchcode
638 return q{} unless $rule;
639 return $rule->rule_value || q{}
642 =head3 current_holds
644 =cut
646 sub current_holds {
647 my ( $self ) = @_;
648 my $attributes = { order_by => 'priority' };
649 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
650 my $params = {
651 itemnumber => $self->itemnumber,
652 suspend => 0,
653 -or => [
654 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
655 waitingdate => { '!=' => undef },
658 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
659 return Koha::Holds->_new_from_dbic($hold_rs);
662 =head3 stockrotationitem
664 my $sritem = Koha::Item->stockrotationitem;
666 Returns the stock rotation item associated with the current item.
668 =cut
670 sub stockrotationitem {
671 my ( $self ) = @_;
672 my $rs = $self->_result->stockrotationitem;
673 return 0 if !$rs;
674 return Koha::StockRotationItem->_new_from_dbic( $rs );
677 =head3 add_to_rota
679 my $item = $item->add_to_rota($rota_id);
681 Add this item to the rota identified by $ROTA_ID, which means associating it
682 with the first stage of that rota. Should this item already be associated
683 with a rota, then we will move it to the new rota.
685 =cut
687 sub add_to_rota {
688 my ( $self, $rota_id ) = @_;
689 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
690 return $self;
693 =head3 has_pending_hold
695 my $is_pending_hold = $item->has_pending_hold();
697 This method checks the tmp_holdsqueue to see if this item has been selected for a hold, but not filled yet and returns true or false
699 =cut
701 sub has_pending_hold {
702 my ( $self ) = @_;
703 my $pending_hold = $self->_result->tmp_holdsqueues;
704 return $pending_hold->count ? 1: 0;
707 =head3 as_marc_field
709 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
710 my $field = $item->as_marc_field({ [ mss => $mss ] });
712 This method returns a MARC::Field object representing the Koha::Item object
713 with the current mappings configuration.
715 =cut
717 sub as_marc_field {
718 my ( $self, $params ) = @_;
720 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
721 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
723 my @subfields;
725 my @columns = $self->_result->result_source->columns;
727 foreach my $item_field ( @columns ) {
728 my $mapping = $mss->{ "items.$item_field"}[0];
729 my $tagfield = $mapping->{tagfield};
730 my $tagsubfield = $mapping->{tagsubfield};
731 next if !$tagfield; # TODO: Should we raise an exception instead?
732 # Feels like safe fallback is better
734 push @subfields, $tagsubfield => $self->$item_field
735 if defined $self->$item_field and $item_field ne '';
738 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
739 push( @subfields, @{$unlinked_item_subfields} )
740 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
742 my $field;
744 $field = MARC::Field->new(
745 "$item_tag", ' ', ' ', @subfields
746 ) if @subfields;
748 return $field;
751 =head3 renewal_branchcode
753 Returns the branchcode to be recorded in statistics renewal of the item
755 =cut
757 sub renewal_branchcode {
759 my ($self, $params ) = @_;
761 my $interface = C4::Context->interface;
762 my $branchcode;
763 if ( $interface eq 'opac' ){
764 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
765 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
766 $branchcode = 'OPACRenew';
768 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
769 $branchcode = $self->homebranch;
771 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
772 $branchcode = $self->checkout->patron->branchcode;
774 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
775 $branchcode = $self->checkout->branchcode;
777 else {
778 $branchcode = "";
780 } else {
781 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
782 ? C4::Context->userenv->{branch} : $params->{branch};
784 return $branchcode;
787 =head3 _set_found_trigger
789 $self->_set_found_trigger
791 Finds the most recent lost item charge for this item and refunds the patron
792 appropriately, taking into account any payments or writeoffs already applied
793 against the charge.
795 Internal function, not exported, called only by Koha::Item->store.
797 =cut
799 sub _set_found_trigger {
800 my ( $self, $pre_mod_item ) = @_;
802 ## If item was lost, it has now been found, reverse any list item charges if necessary.
803 my $no_refund_after_days =
804 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
805 if ($no_refund_after_days) {
806 my $today = dt_from_string();
807 my $lost_age_in_days =
808 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
809 ->in_units('days');
811 return $self unless $lost_age_in_days < $no_refund_after_days;
814 return $self
815 unless Koha::CirculationRules->get_lostreturn_policy(
817 item => $self,
818 return_branch => C4::Context->userenv
819 ? C4::Context->userenv->{'branch'}
820 : undef,
824 # check for charge made for lost book
825 my $accountlines = Koha::Account::Lines->search(
827 itemnumber => $self->itemnumber,
828 debit_type_code => 'LOST',
829 status => [ undef, { '<>' => 'FOUND' } ]
832 order_by => { -desc => [ 'date', 'accountlines_id' ] }
836 return $self unless $accountlines->count > 0;
838 my $accountline = $accountlines->next;
839 my $total_to_refund = 0;
841 return $self unless $accountline->borrowernumber;
843 my $patron = Koha::Patrons->find( $accountline->borrowernumber );
844 return $self
845 unless $patron; # Patron has been deleted, nobody to credit the return to
846 # FIXME Should not we notify this somewhere
848 my $account = $patron->account;
850 # Use cases
851 if ( $accountline->amount > $accountline->amountoutstanding ) {
853 # some amount has been cancelled. collect the offsets that are not writeoffs
854 # this works because the only way to subtract from this kind of a debt is
855 # using the UI buttons 'Pay' and 'Write off'
856 my $credits_offsets = Koha::Account::Offsets->search(
858 debit_id => $accountline->id,
859 credit_id => { '!=' => undef }, # it is not the debit itself
860 type => { '!=' => 'Writeoff' },
861 amount => { '<' => 0 } # credits are negative on the DB
865 $total_to_refund = ( $credits_offsets->count > 0 )
866 ? $credits_offsets->total * -1 # credits are negative on the DB
867 : 0;
870 my $credit_total = $accountline->amountoutstanding + $total_to_refund;
872 my $credit;
873 if ( $credit_total > 0 ) {
874 my $branchcode =
875 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
876 $credit = $account->add_credit(
878 amount => $credit_total,
879 description => 'Item found ' . $self->itemnumber,
880 type => 'LOST_FOUND',
881 interface => C4::Context->interface,
882 library_id => $branchcode,
883 item_id => $self->itemnumber,
884 issue_id => $accountline->issue_id
888 $credit->apply( { debits => [$accountline] } );
889 $self->{_refunded} = 1;
892 # Update the account status
893 $accountline->status('FOUND');
894 $accountline->store();
896 if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
897 $account->reconcile_balance;
900 return $self;
903 =head3 to_api_mapping
905 This method returns the mapping for representing a Koha::Item object
906 on the API.
908 =cut
910 sub to_api_mapping {
911 return {
912 itemnumber => 'item_id',
913 biblionumber => 'biblio_id',
914 biblioitemnumber => undef,
915 barcode => 'external_id',
916 dateaccessioned => 'acquisition_date',
917 booksellerid => 'acquisition_source',
918 homebranch => 'home_library_id',
919 price => 'purchase_price',
920 replacementprice => 'replacement_price',
921 replacementpricedate => 'replacement_price_date',
922 datelastborrowed => 'last_checkout_date',
923 datelastseen => 'last_seen_date',
924 stack => undef,
925 notforloan => 'not_for_loan_status',
926 damaged => 'damaged_status',
927 damaged_on => 'damaged_date',
928 itemlost => 'lost_status',
929 itemlost_on => 'lost_date',
930 withdrawn => 'withdrawn',
931 withdrawn_on => 'withdrawn_date',
932 itemcallnumber => 'callnumber',
933 coded_location_qualifier => 'coded_location_qualifier',
934 issues => 'checkouts_count',
935 renewals => 'renewals_count',
936 reserves => 'holds_count',
937 restricted => 'restricted_status',
938 itemnotes => 'public_notes',
939 itemnotes_nonpublic => 'internal_notes',
940 holdingbranch => 'holding_library_id',
941 paidfor => undef,
942 timestamp => 'timestamp',
943 location => 'location',
944 permanent_location => 'permanent_location',
945 onloan => 'checked_out_date',
946 cn_source => 'call_number_source',
947 cn_sort => 'call_number_sort',
948 ccode => 'collection_code',
949 materials => 'materials_notes',
950 uri => 'uri',
951 itype => 'item_type',
952 more_subfields_xml => 'extended_subfields',
953 enumchron => 'serial_issue_number',
954 copynumber => 'copy_number',
955 stocknumber => 'inventory_number',
956 new_status => 'new_status'
960 =head3 itemtype
962 my $itemtype = $item->itemtype;
964 Returns Koha object for effective itemtype
966 =cut
968 sub itemtype {
969 my ( $self ) = @_;
970 return Koha::ItemTypes->find( $self->effective_itemtype );
973 =head2 Internal methods
975 =head3 _after_item_action_hooks
977 Helper method that takes care of calling all plugin hooks
979 =cut
981 sub _after_item_action_hooks {
982 my ( $self, $params ) = @_;
984 my $action = $params->{action};
986 Koha::Plugins->call(
987 'after_item_action',
989 action => $action,
990 item => $self,
991 item_id => $self->itemnumber,
996 =head3 _type
998 =cut
1000 sub _type {
1001 return 'Item';
1004 =head1 AUTHOR
1006 Kyle M Hall <kyle@bywatersolutions.com>
1008 =cut