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>.
23 use List::MoreUtils qw(any);
28 use Koha::DateUtils qw( dt_from_string );
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::SearchEngine::Indexer;
40 use Koha::Exceptions::Item::Transfer;
41 use Koha::Item::Transfer::Limits;
42 use Koha::Item::Transfers;
47 use Koha::StockRotationItem;
48 use Koha::StockRotationRotas;
50 use base qw(Koha::Object);
54 Koha::Item - Koha Item object class
66 $params can take an optional 'skip_record_index' parameter.
67 If set, the reindexation process will not happen (index_records not called)
69 NOTE: This is a temporary fix to answer a performance issue when lot of items
70 are added (or modified) at the same time.
71 The correct way to fix this is to make the ES reindexation process async.
72 You should not turn it on if you do not understand what it is doing exactly.
78 my $params = @_ ? shift : {};
80 my $log_action = $params->{log_action} // 1;
82 # We do not want to oblige callers to pass this value
83 # Dev conveniences vs performance?
84 unless ( $self->biblioitemnumber ) {
85 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
88 # See related changes from C4::Items::AddItem
89 unless ( $self->itype ) {
90 $self->itype($self->biblio->biblioitem->itemtype);
93 my $today = dt_from_string;
94 my $action = 'create';
96 unless ( $self->in_storage ) { #AddItem
98 unless ( $self->permanent_location ) {
99 $self->permanent_location($self->location);
102 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
103 unless ( $self->location || !$default_location ) {
104 $self->permanent_location( $self->location || $default_location )
105 unless $self->permanent_location;
106 $self->location($default_location);
109 unless ( $self->replacementpricedate ) {
110 $self->replacementpricedate($today);
112 unless ( $self->datelastseen ) {
113 $self->datelastseen($today);
116 unless ( $self->dateaccessioned ) {
117 $self->dateaccessioned($today);
120 if ( $self->itemcallnumber
121 or $self->cn_source )
123 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
124 $self->cn_sort($cn_sort);
131 my %updated_columns = $self->_result->get_dirty_columns;
132 return $self->SUPER::store unless %updated_columns;
134 # Retrieve the item for comparison if we need to
136 exists $updated_columns{itemlost}
137 or exists $updated_columns{withdrawn}
138 or exists $updated_columns{damaged}
139 ) ? $self->get_from_storage : undef;
141 # Update *_on fields if needed
142 # FIXME: Why not for AddItem as well?
143 my @fields = qw( itemlost withdrawn damaged );
144 for my $field (@fields) {
146 # If the field is defined but empty or 0, we are
147 # removing/unsetting and thus need to clear out
149 if ( exists $updated_columns{$field}
150 && defined( $self->$field )
153 my $field_on = "${field}_on";
154 $self->$field_on(undef);
156 # If the field has changed otherwise, we much update
158 elsif (exists $updated_columns{$field}
159 && $updated_columns{$field}
160 && !$pre_mod_item->$field )
162 my $field_on = "${field}_on";
164 DateTime::Format::MySQL->format_datetime(
171 if ( exists $updated_columns{itemcallnumber}
172 or exists $updated_columns{cn_source} )
174 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
175 $self->cn_sort($cn_sort);
179 if ( exists $updated_columns{location}
180 and $self->location ne 'CART'
181 and $self->location ne 'PROC'
182 and not exists $updated_columns{permanent_location} )
184 $self->permanent_location( $self->location );
187 # If item was lost and has now been found,
188 # reverse any list item charges if necessary.
189 if ( exists $updated_columns{itemlost}
190 and $updated_columns{itemlost} <= 0
191 and $pre_mod_item->itemlost > 0 )
193 $self->_set_found_trigger($pre_mod_item);
198 unless ( $self->dateaccessioned ) {
199 $self->dateaccessioned($today);
202 my $result = $self->SUPER::store;
203 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
205 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
206 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
208 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
209 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
210 unless $params->{skip_record_index};
211 $self->get_from_storage->_after_item_action_hooks({ action => $action });
222 my $params = @_ ? shift : {};
224 # FIXME check the item has no current issues
225 # i.e. raise the appropriate exception
227 my $result = $self->SUPER::delete;
229 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
230 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
231 unless $params->{skip_record_index};
233 $self->_after_item_action_hooks({ action => 'delete' });
235 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
236 if C4::Context->preference("CataloguingLog");
247 my $params = @_ ? shift : {};
249 my $safe_to_delete = $self->safe_to_delete;
250 return $safe_to_delete unless $safe_to_delete eq '1';
252 $self->move_to_deleted;
254 return $self->delete($params);
257 =head3 safe_to_delete
259 returns 1 if the item is safe to delete,
261 "book_on_loan" if the item is checked out,
263 "not_same_branch" if the item is blocked by independent branches,
265 "book_reserved" if the there are holds aganst the item, or
267 "linked_analytics" if the item has linked analytic records.
269 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
276 return "book_on_loan" if $self->checkout;
278 return "not_same_branch"
279 if defined C4::Context->userenv
280 and !C4::Context->IsSuperLibrarian()
281 and C4::Context->preference("IndependentBranches")
282 and ( C4::Context->userenv->{branch} ne $self->homebranch );
284 # check it doesn't have a waiting reserve
285 return "book_reserved"
286 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
288 return "linked_analytics"
289 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
291 return "last_item_for_hold"
292 if $self->biblio->items->count == 1
293 && $self->biblio->holds->search(
302 =head3 move_to_deleted
304 my $is_moved = $item->move_to_deleted;
306 Move an item to the deleteditems table.
307 This can be done before deleting an item, to make sure the data are not completely deleted.
311 sub move_to_deleted {
313 my $item_infos = $self->unblessed;
314 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
315 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
319 =head3 effective_itemtype
321 Returns the itemtype for the item based on whether item level itemtypes are set or not.
325 sub effective_itemtype {
328 return $self->_result()->effective_itemtype();
338 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
340 return $self->{_home_branch};
343 =head3 holding_branch
350 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
352 return $self->{_holding_branch};
357 my $biblio = $item->biblio;
359 Return the bibliographic record of this item
365 my $biblio_rs = $self->_result->biblio;
366 return Koha::Biblio->_new_from_dbic( $biblio_rs );
371 my $biblioitem = $item->biblioitem;
373 Return the biblioitem record of this item
379 my $biblioitem_rs = $self->_result->biblioitem;
380 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
385 my $checkout = $item->checkout;
387 Return the checkout for this item
393 my $checkout_rs = $self->_result->issue;
394 return unless $checkout_rs;
395 return Koha::Checkout->_new_from_dbic( $checkout_rs );
400 my $holds = $item->holds();
401 my $holds = $item->holds($params);
402 my $holds = $item->holds({ found => 'W'});
404 Return holds attached to an item, optionally accept a hashref of params to pass to search
409 my ( $self,$params ) = @_;
410 my $holds_rs = $self->_result->reserves->search($params);
411 return Koha::Holds->_new_from_dbic( $holds_rs );
414 =head3 request_transfer
416 my $transfer = $item->request_transfer(
420 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
424 Add a transfer request for this item to the given branch for the given reason.
426 An exception will be thrown if the BranchTransferLimits would prevent the requested
427 transfer, unless 'ignore_limits' is passed to override the limits.
429 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
430 The caller should catch such cases and retry the transfer request as appropriate passing
431 an appropriate override.
434 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
435 * replace - Used to replace the existing transfer request with your own.
439 sub request_transfer {
440 my ( $self, $params ) = @_;
442 # check for mandatory params
443 my @mandatory = ( 'to', 'reason' );
444 for my $param (@mandatory) {
445 unless ( defined( $params->{$param} ) ) {
446 Koha::Exceptions::MissingParameter->throw(
447 error => "The $param parameter is mandatory" );
451 Koha::Exceptions::Item::Transfer::Limit->throw()
452 unless ( $params->{ignore_limits}
453 || $self->can_be_transferred( { to => $params->{to} } ) );
455 my $request = $self->get_transfer;
456 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
457 if ( $request && !$params->{enqueue} && !$params->{replace} );
459 my $transfer = Koha::Item::Transfer->new(
461 itemnumber => $self->itemnumber,
462 daterequested => dt_from_string,
463 frombranch => $self->holdingbranch,
464 tobranch => $params->{to}->branchcode,
465 reason => $params->{reason},
466 comments => $params->{comment}
470 $request->cancel( { reason => $params->{reason}, force => 1, replace => 1 } )
471 if ( defined($request) && $params->{replace} );
478 my $transfer = $item->get_transfer;
480 Return the active transfer request or undef
482 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
483 whereby the most recently sent, but not received, transfer will be returned
484 if it exists, otherwise the oldest unsatisfied transfer will be returned.
486 This allows for transfers to queue, which is the case for stock rotation and
487 rotating collections where a manual transfer may need to take precedence but
488 we still expect the item to end up at a final location eventually.
494 my $transfer_rs = $self->_result->branchtransfers->search(
496 datearrived => undef,
497 datecancelled => undef
501 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
505 return unless $transfer_rs;
506 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
511 my $transfer = $item->get_transfers;
513 Return the list of outstanding transfers (i.e requested but not yet cancelled
516 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
517 whereby the most recently sent, but not received, transfer will be returned
518 first if it exists, otherwise requests are in oldest to newest request order.
520 This allows for transfers to queue, which is the case for stock rotation and
521 rotating collections where a manual transfer may need to take precedence but
522 we still expect the item to end up at a final location eventually.
528 my $transfer_rs = $self->_result->branchtransfers->search(
530 datearrived => undef,
531 datecancelled => undef
535 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
538 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
541 =head3 last_returned_by
543 Gets and sets the last borrower to return an item.
545 Accepts and returns Koha::Patron objects
547 $item->last_returned_by( $borrowernumber );
549 $last_returned_by = $item->last_returned_by();
553 sub last_returned_by {
554 my ( $self, $borrower ) = @_;
556 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
559 return $items_last_returned_by_rs->update_or_create(
560 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
563 unless ( $self->{_last_returned_by} ) {
564 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
566 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
570 return $self->{_last_returned_by};
574 =head3 can_article_request
576 my $bool = $item->can_article_request( $borrower )
578 Returns true if item can be specifically requested
580 $borrower must be a Koha::Patron object
584 sub can_article_request {
585 my ( $self, $borrower ) = @_;
587 my $rule = $self->article_request_type($borrower);
589 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
593 =head3 hidden_in_opac
595 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
597 Returns true if item fields match the hidding criteria defined in $rules.
598 Returns false otherwise.
600 Takes HASHref that can have the following parameters:
602 $rules : { <field> => [ value_1, ... ], ... }
604 Note: $rules inherits its structure from the parsed YAML from reading
605 the I<OpacHiddenItems> system preference.
610 my ( $self, $params ) = @_;
612 my $rules = $params->{rules} // {};
615 if C4::Context->preference('hidelostitems') and
618 my $hidden_in_opac = 0;
620 foreach my $field ( keys %{$rules} ) {
622 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
628 return $hidden_in_opac;
631 =head3 can_be_transferred
633 $item->can_be_transferred({ to => $to_library, from => $from_library })
634 Checks if an item can be transferred to given library.
636 This feature is controlled by two system preferences:
637 UseBranchTransferLimits to enable / disable the feature
638 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
639 for setting the limitations
641 Takes HASHref that can have the following parameters:
642 MANDATORY PARAMETERS:
645 $from : Koha::Library # if not given, item holdingbranch
646 # will be used instead
648 Returns 1 if item can be transferred to $to_library, otherwise 0.
650 To find out whether at least one item of a Koha::Biblio can be transferred, please
651 see Koha::Biblio->can_be_transferred() instead of using this method for
652 multiple items of the same biblio.
656 sub can_be_transferred {
657 my ($self, $params) = @_;
659 my $to = $params->{to};
660 my $from = $params->{from};
662 $to = $to->branchcode;
663 $from = defined $from ? $from->branchcode : $self->holdingbranch;
665 return 1 if $from eq $to; # Transfer to current branch is allowed
666 return 1 unless C4::Context->preference('UseBranchTransferLimits');
668 my $limittype = C4::Context->preference('BranchTransferLimitsType');
669 return Koha::Item::Transfer::Limits->search({
672 $limittype => $limittype eq 'itemtype'
673 ? $self->effective_itemtype : $self->ccode
678 =head3 pickup_locations
680 $pickup_locations = $item->pickup_locations( {patron => $patron } )
682 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)
683 and if item can be transferred to each pickup location.
687 sub pickup_locations {
688 my ($self, $params) = @_;
690 my $patron = $params->{patron};
692 my $circ_control_branch =
693 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
695 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
697 if(defined $patron) {
698 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
699 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
702 my $pickup_libraries = Koha::Libraries->search();
703 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
704 $pickup_libraries = $self->home_branch->get_hold_libraries;
705 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
706 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
707 $pickup_libraries = $plib->get_hold_libraries;
708 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
709 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
710 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
711 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
714 return $pickup_libraries->search(
719 order_by => ['branchname']
721 ) unless C4::Context->preference('UseBranchTransferLimits');
723 my $limittype = C4::Context->preference('BranchTransferLimitsType');
724 my ($ccode, $itype) = (undef, undef);
725 if( $limittype eq 'ccode' ){
726 $ccode = $self->ccode;
728 $itype = $self->itype;
730 my $limits = Koha::Item::Transfer::Limits->search(
732 fromBranch => $self->holdingbranch,
736 { columns => ['toBranch'] }
739 return $pickup_libraries->search(
741 pickup_location => 1,
743 '-not_in' => $limits->_resultset->as_query
747 order_by => ['branchname']
752 =head3 article_request_type
754 my $type = $item->article_request_type( $borrower )
756 returns 'yes', 'no', 'bib_only', or 'item_only'
758 $borrower must be a Koha::Patron object
762 sub article_request_type {
763 my ( $self, $borrower ) = @_;
765 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
767 $branch_control eq 'homebranch' ? $self->homebranch
768 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
770 my $borrowertype = $borrower->categorycode;
771 my $itemtype = $self->effective_itemtype();
772 my $rule = Koha::CirculationRules->get_effective_rule(
774 rule_name => 'article_requests',
775 categorycode => $borrowertype,
776 itemtype => $itemtype,
777 branchcode => $branchcode
781 return q{} unless $rule;
782 return $rule->rule_value || q{}
791 my $attributes = { order_by => 'priority' };
792 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
794 itemnumber => $self->itemnumber,
797 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
798 waitingdate => { '!=' => undef },
801 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
802 return Koha::Holds->_new_from_dbic($hold_rs);
805 =head3 stockrotationitem
807 my $sritem = Koha::Item->stockrotationitem;
809 Returns the stock rotation item associated with the current item.
813 sub stockrotationitem {
815 my $rs = $self->_result->stockrotationitem;
817 return Koha::StockRotationItem->_new_from_dbic( $rs );
822 my $item = $item->add_to_rota($rota_id);
824 Add this item to the rota identified by $ROTA_ID, which means associating it
825 with the first stage of that rota. Should this item already be associated
826 with a rota, then we will move it to the new rota.
831 my ( $self, $rota_id ) = @_;
832 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
836 =head3 has_pending_hold
838 my $is_pending_hold = $item->has_pending_hold();
840 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
844 sub has_pending_hold {
846 my $pending_hold = $self->_result->tmp_holdsqueues;
847 return $pending_hold->count ? 1: 0;
852 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
853 my $field = $item->as_marc_field({ [ mss => $mss ] });
855 This method returns a MARC::Field object representing the Koha::Item object
856 with the current mappings configuration.
861 my ( $self, $params ) = @_;
863 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
864 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
868 my @columns = $self->_result->result_source->columns;
870 foreach my $item_field ( @columns ) {
871 my $mapping = $mss->{ "items.$item_field"}[0];
872 my $tagfield = $mapping->{tagfield};
873 my $tagsubfield = $mapping->{tagsubfield};
874 next if !$tagfield; # TODO: Should we raise an exception instead?
875 # Feels like safe fallback is better
877 push @subfields, $tagsubfield => $self->$item_field
878 if defined $self->$item_field and $item_field ne '';
881 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
882 push( @subfields, @{$unlinked_item_subfields} )
883 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
887 $field = MARC::Field->new(
888 "$item_tag", ' ', ' ', @subfields
894 =head3 renewal_branchcode
896 Returns the branchcode to be recorded in statistics renewal of the item
900 sub renewal_branchcode {
902 my ($self, $params ) = @_;
904 my $interface = C4::Context->interface;
906 if ( $interface eq 'opac' ){
907 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
908 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
909 $branchcode = 'OPACRenew';
911 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
912 $branchcode = $self->homebranch;
914 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
915 $branchcode = $self->checkout->patron->branchcode;
917 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
918 $branchcode = $self->checkout->branchcode;
924 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
925 ? C4::Context->userenv->{branch} : $params->{branch};
932 Return the cover images associated with this item.
939 my $cover_image_rs = $self->_result->cover_images;
940 return unless $cover_image_rs;
941 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
944 =head3 _set_found_trigger
946 $self->_set_found_trigger
948 Finds the most recent lost item charge for this item and refunds the patron
949 appropriately, taking into account any payments or writeoffs already applied
952 Internal function, not exported, called only by Koha::Item->store.
956 sub _set_found_trigger {
957 my ( $self, $pre_mod_item ) = @_;
959 ## If item was lost, it has now been found, reverse any list item charges if necessary.
960 my $no_refund_after_days =
961 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
962 if ($no_refund_after_days) {
963 my $today = dt_from_string();
964 my $lost_age_in_days =
965 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
968 return $self unless $lost_age_in_days < $no_refund_after_days;
971 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
974 return_branch => C4::Context->userenv
975 ? C4::Context->userenv->{'branch'}
980 if ( $lostreturn_policy ) {
982 # refund charge made for lost book
983 my $lost_charge = Koha::Account::Lines->search(
985 itemnumber => $self->itemnumber,
986 debit_type_code => 'LOST',
987 status => [ undef, { '<>' => 'FOUND' } ]
990 order_by => { -desc => [ 'date', 'accountlines_id' ] },
995 if ( $lost_charge ) {
997 my $patron = $lost_charge->patron;
1000 my $account = $patron->account;
1001 my $total_to_refund = 0;
1004 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1006 # some amount has been cancelled. collect the offsets that are not writeoffs
1007 # this works because the only way to subtract from this kind of a debt is
1008 # using the UI buttons 'Pay' and 'Write off'
1009 my $credits_offsets = Koha::Account::Offsets->search(
1011 debit_id => $lost_charge->id,
1012 credit_id => { '!=' => undef }, # it is not the debit itself
1013 type => { '!=' => 'Writeoff' },
1014 amount => { '<' => 0 } # credits are negative on the DB
1018 $total_to_refund = ( $credits_offsets->count > 0 )
1019 ? $credits_offsets->total * -1 # credits are negative on the DB
1023 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1026 if ( $credit_total > 0 ) {
1028 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1029 $credit = $account->add_credit(
1031 amount => $credit_total,
1032 description => 'Item found ' . $self->itemnumber,
1033 type => 'LOST_FOUND',
1034 interface => C4::Context->interface,
1035 library_id => $branchcode,
1036 item_id => $self->itemnumber,
1037 issue_id => $lost_charge->issue_id
1041 $credit->apply( { debits => [$lost_charge] } );
1042 $self->{_refunded} = 1;
1045 # Update the account status
1046 $lost_charge->status('FOUND');
1047 $lost_charge->store();
1049 # Reconcile balances if required
1050 if ( C4::Context->preference('AccountAutoReconcile') ) {
1051 $account->reconcile_balance;
1056 # restore fine for lost book
1057 if ( $lostreturn_policy eq 'restore' ) {
1058 my $lost_overdue = Koha::Account::Lines->search(
1060 itemnumber => $self->itemnumber,
1061 debit_type_code => 'OVERDUE',
1065 order_by => { '-desc' => 'date' },
1070 if ( $lost_overdue ) {
1072 my $patron = $lost_overdue->patron;
1074 my $account = $patron->account;
1076 # Update status of fine
1077 $lost_overdue->status('FOUND')->store();
1079 # Find related forgive credit
1080 my $refund = $lost_overdue->credits(
1082 credit_type_code => 'FORGIVEN',
1083 itemnumber => $self->itemnumber,
1084 status => [ { '!=' => 'VOID' }, undef ]
1086 { order_by => { '-desc' => 'date' }, rows => 1 }
1090 # Revert the forgive credit
1091 $refund->void({ interface => 'trigger' });
1092 $self->{_restored} = 1;
1095 # Reconcile balances if required
1096 if ( C4::Context->preference('AccountAutoReconcile') ) {
1097 $account->reconcile_balance;
1101 } elsif ( $lostreturn_policy eq 'charge' ) {
1102 $self->{_charge} = 1;
1109 =head3 to_api_mapping
1111 This method returns the mapping for representing a Koha::Item object
1116 sub to_api_mapping {
1118 itemnumber => 'item_id',
1119 biblionumber => 'biblio_id',
1120 biblioitemnumber => undef,
1121 barcode => 'external_id',
1122 dateaccessioned => 'acquisition_date',
1123 booksellerid => 'acquisition_source',
1124 homebranch => 'home_library_id',
1125 price => 'purchase_price',
1126 replacementprice => 'replacement_price',
1127 replacementpricedate => 'replacement_price_date',
1128 datelastborrowed => 'last_checkout_date',
1129 datelastseen => 'last_seen_date',
1131 notforloan => 'not_for_loan_status',
1132 damaged => 'damaged_status',
1133 damaged_on => 'damaged_date',
1134 itemlost => 'lost_status',
1135 itemlost_on => 'lost_date',
1136 withdrawn => 'withdrawn',
1137 withdrawn_on => 'withdrawn_date',
1138 itemcallnumber => 'callnumber',
1139 coded_location_qualifier => 'coded_location_qualifier',
1140 issues => 'checkouts_count',
1141 renewals => 'renewals_count',
1142 reserves => 'holds_count',
1143 restricted => 'restricted_status',
1144 itemnotes => 'public_notes',
1145 itemnotes_nonpublic => 'internal_notes',
1146 holdingbranch => 'holding_library_id',
1147 timestamp => 'timestamp',
1148 location => 'location',
1149 permanent_location => 'permanent_location',
1150 onloan => 'checked_out_date',
1151 cn_source => 'call_number_source',
1152 cn_sort => 'call_number_sort',
1153 ccode => 'collection_code',
1154 materials => 'materials_notes',
1156 itype => 'item_type',
1157 more_subfields_xml => 'extended_subfields',
1158 enumchron => 'serial_issue_number',
1159 copynumber => 'copy_number',
1160 stocknumber => 'inventory_number',
1161 new_status => 'new_status'
1167 my $itemtype = $item->itemtype;
1169 Returns Koha object for effective itemtype
1175 return Koha::ItemTypes->find( $self->effective_itemtype );
1178 =head2 Internal methods
1180 =head3 _after_item_action_hooks
1182 Helper method that takes care of calling all plugin hooks
1186 sub _after_item_action_hooks {
1187 my ( $self, $params ) = @_;
1189 my $action = $params->{action};
1191 Koha::Plugins->call(
1192 'after_item_action',
1196 item_id => $self->itemnumber,
1211 Kyle M Hall <kyle@bywatersolutions.com>