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>.
22 use List::MoreUtils qw( any );
23 use Data::Dumper qw( Dumper );
26 use Koha::DateUtils qw( dt_from_string );
29 use C4::Circulation qw( GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
35 use Koha::CirculationRules;
36 use Koha::CoverImages;
37 use Koha::SearchEngine::Indexer;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
47 use Koha::TrackedLinks;
49 use base qw(Koha::Object);
53 Koha::Item - Koha Item object class
65 $params can take an optional 'skip_record_index' parameter.
66 If set, the reindexation process will not happen (index_records not called)
68 NOTE: This is a temporary fix to answer a performance issue when lot of items
69 are added (or modified) at the same time.
70 The correct way to fix this is to make the ES reindexation process async.
71 You should not turn it on if you do not understand what it is doing exactly.
77 my $params = @_ ? shift : {};
79 my $log_action = $params->{log_action} // 1;
81 # We do not want to oblige callers to pass this value
82 # Dev conveniences vs performance?
83 unless ( $self->biblioitemnumber ) {
84 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
87 # See related changes from C4::Items::AddItem
88 unless ( $self->itype ) {
89 $self->itype($self->biblio->biblioitem->itemtype);
92 my $today = dt_from_string;
93 my $action = 'create';
95 unless ( $self->in_storage ) { #AddItem
97 unless ( $self->permanent_location ) {
98 $self->permanent_location($self->location);
101 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
102 unless ( $self->location || !$default_location ) {
103 $self->permanent_location( $self->location || $default_location )
104 unless $self->permanent_location;
105 $self->location($default_location);
108 unless ( $self->replacementpricedate ) {
109 $self->replacementpricedate($today);
111 unless ( $self->datelastseen ) {
112 $self->datelastseen($today);
115 unless ( $self->dateaccessioned ) {
116 $self->dateaccessioned($today);
119 if ( $self->itemcallnumber
120 or $self->cn_source )
122 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
123 $self->cn_sort($cn_sort);
130 my %updated_columns = $self->_result->get_dirty_columns;
131 return $self->SUPER::store unless %updated_columns;
133 # Retrieve the item for comparison if we need to
135 exists $updated_columns{itemlost}
136 or exists $updated_columns{withdrawn}
137 or exists $updated_columns{damaged}
138 ) ? $self->get_from_storage : undef;
140 # Update *_on fields if needed
141 # FIXME: Why not for AddItem as well?
142 my @fields = qw( itemlost withdrawn damaged );
143 for my $field (@fields) {
145 # If the field is defined but empty or 0, we are
146 # removing/unsetting and thus need to clear out
148 if ( exists $updated_columns{$field}
149 && defined( $self->$field )
152 my $field_on = "${field}_on";
153 $self->$field_on(undef);
155 # If the field has changed otherwise, we much update
157 elsif (exists $updated_columns{$field}
158 && $updated_columns{$field}
159 && !$pre_mod_item->$field )
161 my $field_on = "${field}_on";
163 DateTime::Format::MySQL->format_datetime(
170 if ( exists $updated_columns{itemcallnumber}
171 or exists $updated_columns{cn_source} )
173 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
174 $self->cn_sort($cn_sort);
178 if ( exists $updated_columns{location}
179 and $self->location ne 'CART'
180 and $self->location ne 'PROC'
181 and not exists $updated_columns{permanent_location} )
183 $self->permanent_location( $self->location );
186 # If item was lost and has now been found,
187 # reverse any list item charges if necessary.
188 if ( exists $updated_columns{itemlost}
189 and $updated_columns{itemlost} <= 0
190 and $pre_mod_item->itemlost > 0 )
192 $self->_set_found_trigger($pre_mod_item);
197 unless ( $self->dateaccessioned ) {
198 $self->dateaccessioned($today);
201 my $result = $self->SUPER::store;
202 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
204 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
205 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
207 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
208 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
209 unless $params->{skip_record_index};
210 $self->get_from_storage->_after_item_action_hooks({ action => $action });
221 my $params = @_ ? shift : {};
223 # FIXME check the item has no current issues
224 # i.e. raise the appropriate exception
226 my $result = $self->SUPER::delete;
228 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
229 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
230 unless $params->{skip_record_index};
232 $self->_after_item_action_hooks({ action => 'delete' });
234 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
235 if C4::Context->preference("CataloguingLog");
246 my $params = @_ ? shift : {};
248 my $safe_to_delete = $self->safe_to_delete;
249 return $safe_to_delete unless $safe_to_delete eq '1';
251 $self->move_to_deleted;
253 return $self->delete($params);
256 =head3 safe_to_delete
258 returns 1 if the item is safe to delete,
260 "book_on_loan" if the item is checked out,
262 "not_same_branch" if the item is blocked by independent branches,
264 "book_reserved" if the there are holds aganst the item, or
266 "linked_analytics" if the item has linked analytic records.
268 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
275 return "book_on_loan" if $self->checkout;
277 return "not_same_branch"
278 if defined C4::Context->userenv
279 and !C4::Context->IsSuperLibrarian()
280 and C4::Context->preference("IndependentBranches")
281 and ( C4::Context->userenv->{branch} ne $self->homebranch );
283 # check it doesn't have a waiting reserve
284 return "book_reserved"
285 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
287 return "linked_analytics"
288 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
290 return "last_item_for_hold"
291 if $self->biblio->items->count == 1
292 && $self->biblio->holds->search(
301 =head3 move_to_deleted
303 my $is_moved = $item->move_to_deleted;
305 Move an item to the deleteditems table.
306 This can be done before deleting an item, to make sure the data are not completely deleted.
310 sub move_to_deleted {
312 my $item_infos = $self->unblessed;
313 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
314 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
318 =head3 effective_itemtype
320 Returns the itemtype for the item based on whether item level itemtypes are set or not.
324 sub effective_itemtype {
327 return $self->_result()->effective_itemtype();
337 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
339 return $self->{_home_branch};
342 =head3 holding_branch
349 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
351 return $self->{_holding_branch};
356 my $biblio = $item->biblio;
358 Return the bibliographic record of this item
364 my $biblio_rs = $self->_result->biblio;
365 return Koha::Biblio->_new_from_dbic( $biblio_rs );
370 my $biblioitem = $item->biblioitem;
372 Return the biblioitem record of this item
378 my $biblioitem_rs = $self->_result->biblioitem;
379 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
384 my $checkout = $item->checkout;
386 Return the checkout for this item
392 my $checkout_rs = $self->_result->issue;
393 return unless $checkout_rs;
394 return Koha::Checkout->_new_from_dbic( $checkout_rs );
399 my $holds = $item->holds();
400 my $holds = $item->holds($params);
401 my $holds = $item->holds({ found => 'W'});
403 Return holds attached to an item, optionally accept a hashref of params to pass to search
408 my ( $self,$params ) = @_;
409 my $holds_rs = $self->_result->reserves->search($params);
410 return Koha::Holds->_new_from_dbic( $holds_rs );
413 =head3 request_transfer
415 my $transfer = $item->request_transfer(
419 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
423 Add a transfer request for this item to the given branch for the given reason.
425 An exception will be thrown if the BranchTransferLimits would prevent the requested
426 transfer, unless 'ignore_limits' is passed to override the limits.
428 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
429 The caller should catch such cases and retry the transfer request as appropriate passing
430 an appropriate override.
433 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
434 * replace - Used to replace the existing transfer request with your own.
438 sub request_transfer {
439 my ( $self, $params ) = @_;
441 # check for mandatory params
442 my @mandatory = ( 'to', 'reason' );
443 for my $param (@mandatory) {
444 unless ( defined( $params->{$param} ) ) {
445 Koha::Exceptions::MissingParameter->throw(
446 error => "The $param parameter is mandatory" );
450 Koha::Exceptions::Item::Transfer::Limit->throw()
451 unless ( $params->{ignore_limits}
452 || $self->can_be_transferred( { to => $params->{to} } ) );
454 my $request = $self->get_transfer;
455 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
456 if ( $request && !$params->{enqueue} && !$params->{replace} );
458 $request->cancel( { reason => $params->{reason}, force => 1 } )
459 if ( defined($request) && $params->{replace} );
461 my $transfer = Koha::Item::Transfer->new(
463 itemnumber => $self->itemnumber,
464 daterequested => dt_from_string,
465 frombranch => $self->holdingbranch,
466 tobranch => $params->{to}->branchcode,
467 reason => $params->{reason},
468 comments => $params->{comment}
477 my $transfer = $item->get_transfer;
479 Return the active transfer request or undef
481 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
482 whereby the most recently sent, but not received, transfer will be returned
483 if it exists, otherwise the oldest unsatisfied transfer will be returned.
485 This allows for transfers to queue, which is the case for stock rotation and
486 rotating collections where a manual transfer may need to take precedence but
487 we still expect the item to end up at a final location eventually.
493 my $transfer_rs = $self->_result->branchtransfers->search(
495 datearrived => undef,
496 datecancelled => undef
500 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
504 return unless $transfer_rs;
505 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
510 my $transfer = $item->get_transfers;
512 Return the list of outstanding transfers (i.e requested but not yet cancelled
515 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
516 whereby the most recently sent, but not received, transfer will be returned
517 first if it exists, otherwise requests are in oldest to newest request order.
519 This allows for transfers to queue, which is the case for stock rotation and
520 rotating collections where a manual transfer may need to take precedence but
521 we still expect the item to end up at a final location eventually.
527 my $transfer_rs = $self->_result->branchtransfers->search(
529 datearrived => undef,
530 datecancelled => undef
534 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
537 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
540 =head3 last_returned_by
542 Gets and sets the last borrower to return an item.
544 Accepts and returns Koha::Patron objects
546 $item->last_returned_by( $borrowernumber );
548 $last_returned_by = $item->last_returned_by();
552 sub last_returned_by {
553 my ( $self, $borrower ) = @_;
555 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
558 return $items_last_returned_by_rs->update_or_create(
559 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
562 unless ( $self->{_last_returned_by} ) {
563 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
565 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
569 return $self->{_last_returned_by};
573 =head3 can_article_request
575 my $bool = $item->can_article_request( $borrower )
577 Returns true if item can be specifically requested
579 $borrower must be a Koha::Patron object
583 sub can_article_request {
584 my ( $self, $borrower ) = @_;
586 my $rule = $self->article_request_type($borrower);
588 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
592 =head3 hidden_in_opac
594 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
596 Returns true if item fields match the hidding criteria defined in $rules.
597 Returns false otherwise.
599 Takes HASHref that can have the following parameters:
601 $rules : { <field> => [ value_1, ... ], ... }
603 Note: $rules inherits its structure from the parsed YAML from reading
604 the I<OpacHiddenItems> system preference.
609 my ( $self, $params ) = @_;
611 my $rules = $params->{rules} // {};
614 if C4::Context->preference('hidelostitems') and
617 my $hidden_in_opac = 0;
619 foreach my $field ( keys %{$rules} ) {
621 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
627 return $hidden_in_opac;
630 =head3 can_be_transferred
632 $item->can_be_transferred({ to => $to_library, from => $from_library })
633 Checks if an item can be transferred to given library.
635 This feature is controlled by two system preferences:
636 UseBranchTransferLimits to enable / disable the feature
637 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
638 for setting the limitations
640 Takes HASHref that can have the following parameters:
641 MANDATORY PARAMETERS:
644 $from : Koha::Library # if not given, item holdingbranch
645 # will be used instead
647 Returns 1 if item can be transferred to $to_library, otherwise 0.
649 To find out whether at least one item of a Koha::Biblio can be transferred, please
650 see Koha::Biblio->can_be_transferred() instead of using this method for
651 multiple items of the same biblio.
655 sub can_be_transferred {
656 my ($self, $params) = @_;
658 my $to = $params->{to};
659 my $from = $params->{from};
661 $to = $to->branchcode;
662 $from = defined $from ? $from->branchcode : $self->holdingbranch;
664 return 1 if $from eq $to; # Transfer to current branch is allowed
665 return 1 unless C4::Context->preference('UseBranchTransferLimits');
667 my $limittype = C4::Context->preference('BranchTransferLimitsType');
668 return Koha::Item::Transfer::Limits->search({
671 $limittype => $limittype eq 'itemtype'
672 ? $self->effective_itemtype : $self->ccode
677 =head3 pickup_locations
679 $pickup_locations = $item->pickup_locations( {patron => $patron } )
681 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)
682 and if item can be transferred to each pickup location.
686 sub pickup_locations {
687 my ($self, $params) = @_;
689 my $patron = $params->{patron};
691 my $circ_control_branch =
692 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
694 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
696 if(defined $patron) {
697 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
698 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
701 my $pickup_libraries = Koha::Libraries->search();
702 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
703 $pickup_libraries = $self->home_branch->get_hold_libraries;
704 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
705 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
706 $pickup_libraries = $plib->get_hold_libraries;
707 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
708 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
709 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
710 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
713 return $pickup_libraries->search(
718 order_by => ['branchname']
720 ) unless C4::Context->preference('UseBranchTransferLimits');
722 my $limittype = C4::Context->preference('BranchTransferLimitsType');
723 my ($ccode, $itype) = (undef, undef);
724 if( $limittype eq 'ccode' ){
725 $ccode = $self->ccode;
727 $itype = $self->itype;
729 my $limits = Koha::Item::Transfer::Limits->search(
731 fromBranch => $self->holdingbranch,
735 { columns => ['toBranch'] }
738 return $pickup_libraries->search(
740 pickup_location => 1,
742 '-not_in' => $limits->_resultset->as_query
746 order_by => ['branchname']
751 =head3 article_request_type
753 my $type = $item->article_request_type( $borrower )
755 returns 'yes', 'no', 'bib_only', or 'item_only'
757 $borrower must be a Koha::Patron object
761 sub article_request_type {
762 my ( $self, $borrower ) = @_;
764 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
766 $branch_control eq 'homebranch' ? $self->homebranch
767 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
769 my $borrowertype = $borrower->categorycode;
770 my $itemtype = $self->effective_itemtype();
771 my $rule = Koha::CirculationRules->get_effective_rule(
773 rule_name => 'article_requests',
774 categorycode => $borrowertype,
775 itemtype => $itemtype,
776 branchcode => $branchcode
780 return q{} unless $rule;
781 return $rule->rule_value || q{}
790 my $attributes = { order_by => 'priority' };
791 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
793 itemnumber => $self->itemnumber,
796 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
797 waitingdate => { '!=' => undef },
800 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
801 return Koha::Holds->_new_from_dbic($hold_rs);
804 =head3 stockrotationitem
806 my $sritem = Koha::Item->stockrotationitem;
808 Returns the stock rotation item associated with the current item.
812 sub stockrotationitem {
814 my $rs = $self->_result->stockrotationitem;
816 return Koha::StockRotationItem->_new_from_dbic( $rs );
821 my $item = $item->add_to_rota($rota_id);
823 Add this item to the rota identified by $ROTA_ID, which means associating it
824 with the first stage of that rota. Should this item already be associated
825 with a rota, then we will move it to the new rota.
830 my ( $self, $rota_id ) = @_;
831 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
835 =head3 has_pending_hold
837 my $is_pending_hold = $item->has_pending_hold();
839 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
843 sub has_pending_hold {
845 my $pending_hold = $self->_result->tmp_holdsqueues;
846 return $pending_hold->count ? 1: 0;
851 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
852 my $field = $item->as_marc_field({ [ mss => $mss ] });
854 This method returns a MARC::Field object representing the Koha::Item object
855 with the current mappings configuration.
860 my ( $self, $params ) = @_;
862 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
863 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
867 my @columns = $self->_result->result_source->columns;
869 foreach my $item_field ( @columns ) {
870 my $mapping = $mss->{ "items.$item_field"}[0];
871 my $tagfield = $mapping->{tagfield};
872 my $tagsubfield = $mapping->{tagsubfield};
873 next if !$tagfield; # TODO: Should we raise an exception instead?
874 # Feels like safe fallback is better
876 push @subfields, $tagsubfield => $self->$item_field
877 if defined $self->$item_field and $item_field ne '';
880 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
881 push( @subfields, @{$unlinked_item_subfields} )
882 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
886 $field = MARC::Field->new(
887 "$item_tag", ' ', ' ', @subfields
893 =head3 renewal_branchcode
895 Returns the branchcode to be recorded in statistics renewal of the item
899 sub renewal_branchcode {
901 my ($self, $params ) = @_;
903 my $interface = C4::Context->interface;
905 if ( $interface eq 'opac' ){
906 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
907 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
908 $branchcode = 'OPACRenew';
910 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
911 $branchcode = $self->homebranch;
913 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
914 $branchcode = $self->checkout->patron->branchcode;
916 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
917 $branchcode = $self->checkout->branchcode;
923 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
924 ? C4::Context->userenv->{branch} : $params->{branch};
931 Return the cover images associated with this item.
938 my $cover_image_rs = $self->_result->cover_images;
939 return unless $cover_image_rs;
940 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
943 =head3 _set_found_trigger
945 $self->_set_found_trigger
947 Finds the most recent lost item charge for this item and refunds the patron
948 appropriately, taking into account any payments or writeoffs already applied
951 Internal function, not exported, called only by Koha::Item->store.
955 sub _set_found_trigger {
956 my ( $self, $pre_mod_item ) = @_;
958 ## If item was lost, it has now been found, reverse any list item charges if necessary.
959 my $no_refund_after_days =
960 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
961 if ($no_refund_after_days) {
962 my $today = dt_from_string();
963 my $lost_age_in_days =
964 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
967 return $self unless $lost_age_in_days < $no_refund_after_days;
970 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
973 return_branch => C4::Context->userenv
974 ? C4::Context->userenv->{'branch'}
979 if ( $lostreturn_policy ) {
981 # refund charge made for lost book
982 my $lost_charge = Koha::Account::Lines->search(
984 itemnumber => $self->itemnumber,
985 debit_type_code => 'LOST',
986 status => [ undef, { '<>' => 'FOUND' } ]
989 order_by => { -desc => [ 'date', 'accountlines_id' ] },
994 if ( $lost_charge ) {
996 my $patron = $lost_charge->patron;
999 my $account = $patron->account;
1000 my $total_to_refund = 0;
1003 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1005 # some amount has been cancelled. collect the offsets that are not writeoffs
1006 # this works because the only way to subtract from this kind of a debt is
1007 # using the UI buttons 'Pay' and 'Write off'
1008 my $credit_offsets = $lost_charge->debit_offsets(
1010 'credit_id' => { '!=' => undef },
1011 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1013 { join => 'credit' }
1016 $total_to_refund = ( $credit_offsets->count > 0 )
1017 ? $credit_offsets->total * -1 # credits are negative on the DB
1021 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1024 if ( $credit_total > 0 ) {
1026 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1027 $credit = $account->add_credit(
1029 amount => $credit_total,
1030 description => 'Item found ' . $self->itemnumber,
1031 type => 'LOST_FOUND',
1032 interface => C4::Context->interface,
1033 library_id => $branchcode,
1034 item_id => $self->itemnumber,
1035 issue_id => $lost_charge->issue_id
1039 $credit->apply( { debits => [$lost_charge] } );
1040 $self->{_refunded} = 1;
1043 # Update the account status
1044 $lost_charge->status('FOUND');
1045 $lost_charge->store();
1047 # Reconcile balances if required
1048 if ( C4::Context->preference('AccountAutoReconcile') ) {
1049 $account->reconcile_balance;
1054 # restore fine for lost book
1055 if ( $lostreturn_policy eq 'restore' ) {
1056 my $lost_overdue = Koha::Account::Lines->search(
1058 itemnumber => $self->itemnumber,
1059 debit_type_code => 'OVERDUE',
1063 order_by => { '-desc' => 'date' },
1068 if ( $lost_overdue ) {
1070 my $patron = $lost_overdue->patron;
1072 my $account = $patron->account;
1074 # Update status of fine
1075 $lost_overdue->status('FOUND')->store();
1077 # Find related forgive credit
1078 my $refund = $lost_overdue->credits(
1080 credit_type_code => 'FORGIVEN',
1081 itemnumber => $self->itemnumber,
1082 status => [ { '!=' => 'VOID' }, undef ]
1084 { order_by => { '-desc' => 'date' }, rows => 1 }
1088 # Revert the forgive credit
1089 $refund->void({ interface => 'trigger' });
1090 $self->{_restored} = 1;
1093 # Reconcile balances if required
1094 if ( C4::Context->preference('AccountAutoReconcile') ) {
1095 $account->reconcile_balance;
1099 } elsif ( $lostreturn_policy eq 'charge' ) {
1100 $self->{_charge} = 1;
1107 =head3 to_api_mapping
1109 This method returns the mapping for representing a Koha::Item object
1114 sub to_api_mapping {
1116 itemnumber => 'item_id',
1117 biblionumber => 'biblio_id',
1118 biblioitemnumber => undef,
1119 barcode => 'external_id',
1120 dateaccessioned => 'acquisition_date',
1121 booksellerid => 'acquisition_source',
1122 homebranch => 'home_library_id',
1123 price => 'purchase_price',
1124 replacementprice => 'replacement_price',
1125 replacementpricedate => 'replacement_price_date',
1126 datelastborrowed => 'last_checkout_date',
1127 datelastseen => 'last_seen_date',
1129 notforloan => 'not_for_loan_status',
1130 damaged => 'damaged_status',
1131 damaged_on => 'damaged_date',
1132 itemlost => 'lost_status',
1133 itemlost_on => 'lost_date',
1134 withdrawn => 'withdrawn',
1135 withdrawn_on => 'withdrawn_date',
1136 itemcallnumber => 'callnumber',
1137 coded_location_qualifier => 'coded_location_qualifier',
1138 issues => 'checkouts_count',
1139 renewals => 'renewals_count',
1140 reserves => 'holds_count',
1141 restricted => 'restricted_status',
1142 itemnotes => 'public_notes',
1143 itemnotes_nonpublic => 'internal_notes',
1144 holdingbranch => 'holding_library_id',
1145 timestamp => 'timestamp',
1146 location => 'location',
1147 permanent_location => 'permanent_location',
1148 onloan => 'checked_out_date',
1149 cn_source => 'call_number_source',
1150 cn_sort => 'call_number_sort',
1151 ccode => 'collection_code',
1152 materials => 'materials_notes',
1154 itype => 'item_type',
1155 more_subfields_xml => 'extended_subfields',
1156 enumchron => 'serial_issue_number',
1157 copynumber => 'copy_number',
1158 stocknumber => 'inventory_number',
1159 new_status => 'new_status'
1165 my $itemtype = $item->itemtype;
1167 Returns Koha object for effective itemtype
1173 return Koha::ItemTypes->find( $self->effective_itemtype );
1178 my $orders = $item->orders();
1180 Returns a Koha::Acquisition::Orders object
1187 my $orders = $self->_result->item_orders;
1188 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1191 =head3 tracked_links
1193 my $tracked_links = $item->tracked_links();
1195 Returns a Koha::TrackedLinks object
1202 my $tracked_links = $self->_result->linktrackers;
1203 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1206 =head3 move_to_biblio
1208 $item->move_to_biblio($to_biblio[, $params]);
1210 Move the item to another biblio and update any references in other tables.
1212 The final optional parameter, C<$params>, is expected to contain the
1213 'skip_record_index' key, which is relayed down to Koha::Item->store.
1214 There it prevents calling index_records, which takes most of the
1215 time in batch adds/deletes. The caller must take care of calling
1216 index_records separately.
1219 skip_record_index => 1|0
1221 Returns undef if the move failed or the biblionumber of the destination record otherwise
1225 sub move_to_biblio {
1226 my ( $self, $to_biblio, $params ) = @_;
1230 return if $self->biblionumber == $to_biblio->biblionumber;
1232 my $from_biblionumber = $self->biblionumber;
1233 my $to_biblionumber = $to_biblio->biblionumber;
1235 # Own biblionumber and biblioitemnumber
1237 biblionumber => $to_biblionumber,
1238 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1239 })->store({ skip_record_index => $params->{skip_record_index} });
1241 unless ($params->{skip_record_index}) {
1242 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1243 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1246 # Acquisition orders
1247 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1250 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1252 # hold_fill_target (there's no Koha object available yet)
1253 my $hold_fill_target = $self->_result->hold_fill_target;
1254 if ($hold_fill_target) {
1255 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1258 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1259 # and can't even fake one since the significant columns are nullable.
1260 my $storage = $self->_result->result_source->storage;
1263 my ($storage, $dbh, @cols) = @_;
1265 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1270 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1272 return $to_biblionumber;
1275 =head2 Internal methods
1277 =head3 _after_item_action_hooks
1279 Helper method that takes care of calling all plugin hooks
1283 sub _after_item_action_hooks {
1284 my ( $self, $params ) = @_;
1286 my $action = $params->{action};
1288 Koha::Plugins->call(
1289 'after_item_action',
1293 item_id => $self->itemnumber,
1308 Kyle M Hall <kyle@bywatersolutions.com>