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;
48 use base qw(Koha::Object);
52 Koha::Item - Koha Item object class
64 $params can take an optional 'skip_record_index' parameter.
65 If set, the reindexation process will not happen (index_records 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.
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 $action = 'create';
94 unless ( $self->in_storage ) { #AddItem
96 unless ( $self->permanent_location ) {
97 $self->permanent_location($self->location);
100 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
101 unless ( $self->location || !$default_location ) {
102 $self->permanent_location( $self->location || $default_location )
103 unless $self->permanent_location;
104 $self->location($default_location);
107 unless ( $self->replacementpricedate ) {
108 $self->replacementpricedate($today);
110 unless ( $self->datelastseen ) {
111 $self->datelastseen($today);
114 unless ( $self->dateaccessioned ) {
115 $self->dateaccessioned($today);
118 if ( $self->itemcallnumber
119 or $self->cn_source )
121 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
122 $self->cn_sort($cn_sort);
129 my %updated_columns = $self->_result->get_dirty_columns;
130 return $self->SUPER::store unless %updated_columns;
132 # Retrieve the item for comparison if we need to
134 exists $updated_columns{itemlost}
135 or exists $updated_columns{withdrawn}
136 or exists $updated_columns{damaged}
137 ) ? $self->get_from_storage : undef;
139 # Update *_on fields if needed
140 # FIXME: Why not for AddItem as well?
141 my @fields = qw( itemlost withdrawn damaged );
142 for my $field (@fields) {
144 # If the field is defined but empty or 0, we are
145 # removing/unsetting and thus need to clear out
147 if ( exists $updated_columns{$field}
148 && defined( $self->$field )
151 my $field_on = "${field}_on";
152 $self->$field_on(undef);
154 # If the field has changed otherwise, we much update
156 elsif (exists $updated_columns{$field}
157 && $updated_columns{$field}
158 && !$pre_mod_item->$field )
160 my $field_on = "${field}_on";
162 DateTime::Format::MySQL->format_datetime(
169 if ( exists $updated_columns{itemcallnumber}
170 or exists $updated_columns{cn_source} )
172 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
173 $self->cn_sort($cn_sort);
177 if ( exists $updated_columns{location}
178 and $self->location ne 'CART'
179 and $self->location ne 'PROC'
180 and not exists $updated_columns{permanent_location} )
182 $self->permanent_location( $self->location );
185 # If item was lost and has now been found,
186 # reverse any list item charges if necessary.
187 if ( exists $updated_columns{itemlost}
188 and $updated_columns{itemlost} <= 0
189 and $pre_mod_item->itemlost > 0 )
191 $self->_set_found_trigger($pre_mod_item);
196 unless ( $self->dateaccessioned ) {
197 $self->dateaccessioned($today);
200 my $result = $self->SUPER::store;
201 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
203 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
204 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
206 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
207 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
208 unless $params->{skip_record_index};
209 $self->get_from_storage->_after_item_action_hooks({ action => $action });
220 my $params = @_ ? shift : {};
222 # FIXME check the item has no current issues
223 # i.e. raise the appropriate exception
225 my $result = $self->SUPER::delete;
227 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
228 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
229 unless $params->{skip_record_index};
231 $self->_after_item_action_hooks({ action => 'delete' });
233 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
234 if C4::Context->preference("CataloguingLog");
245 my $params = @_ ? shift : {};
247 my $safe_to_delete = $self->safe_to_delete;
248 return $safe_to_delete unless $safe_to_delete eq '1';
250 $self->move_to_deleted;
252 return $self->delete($params);
255 =head3 safe_to_delete
257 returns 1 if the item is safe to delete,
259 "book_on_loan" if the item is checked out,
261 "not_same_branch" if the item is blocked by independent branches,
263 "book_reserved" if the there are holds aganst the item, or
265 "linked_analytics" if the item has linked analytic records.
267 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
274 return "book_on_loan" if $self->checkout;
276 return "not_same_branch"
277 if defined C4::Context->userenv
278 and !C4::Context->IsSuperLibrarian()
279 and C4::Context->preference("IndependentBranches")
280 and ( C4::Context->userenv->{branch} ne $self->homebranch );
282 # check it doesn't have a waiting reserve
283 return "book_reserved"
284 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
286 return "linked_analytics"
287 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
289 return "last_item_for_hold"
290 if $self->biblio->items->count == 1
291 && $self->biblio->holds->search(
300 =head3 move_to_deleted
302 my $is_moved = $item->move_to_deleted;
304 Move an item to the deleteditems table.
305 This can be done before deleting an item, to make sure the data are not completely deleted.
309 sub move_to_deleted {
311 my $item_infos = $self->unblessed;
312 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
313 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
317 =head3 effective_itemtype
319 Returns the itemtype for the item based on whether item level itemtypes are set or not.
323 sub effective_itemtype {
326 return $self->_result()->effective_itemtype();
336 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
338 return $self->{_home_branch};
341 =head3 holding_branch
348 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
350 return $self->{_holding_branch};
355 my $biblio = $item->biblio;
357 Return the bibliographic record of this item
363 my $biblio_rs = $self->_result->biblio;
364 return Koha::Biblio->_new_from_dbic( $biblio_rs );
369 my $biblioitem = $item->biblioitem;
371 Return the biblioitem record of this item
377 my $biblioitem_rs = $self->_result->biblioitem;
378 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
383 my $checkout = $item->checkout;
385 Return the checkout for this item
391 my $checkout_rs = $self->_result->issue;
392 return unless $checkout_rs;
393 return Koha::Checkout->_new_from_dbic( $checkout_rs );
398 my $holds = $item->holds();
399 my $holds = $item->holds($params);
400 my $holds = $item->holds({ found => 'W'});
402 Return holds attached to an item, optionally accept a hashref of params to pass to search
407 my ( $self,$params ) = @_;
408 my $holds_rs = $self->_result->reserves->search($params);
409 return Koha::Holds->_new_from_dbic( $holds_rs );
412 =head3 request_transfer
414 my $transfer = $item->request_transfer(
418 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
422 Add a transfer request for this item to the given branch for the given reason.
424 An exception will be thrown if the BranchTransferLimits would prevent the requested
425 transfer, unless 'ignore_limits' is passed to override the limits.
427 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
428 The caller should catch such cases and retry the transfer request as appropriate passing
429 an appropriate override.
432 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
433 * replace - Used to replace the existing transfer request with your own.
437 sub request_transfer {
438 my ( $self, $params ) = @_;
440 # check for mandatory params
441 my @mandatory = ( 'to', 'reason' );
442 for my $param (@mandatory) {
443 unless ( defined( $params->{$param} ) ) {
444 Koha::Exceptions::MissingParameter->throw(
445 error => "The $param parameter is mandatory" );
449 Koha::Exceptions::Item::Transfer::Limit->throw()
450 unless ( $params->{ignore_limits}
451 || $self->can_be_transferred( { to => $params->{to} } ) );
453 my $request = $self->get_transfer;
454 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
455 if ( $request && !$params->{enqueue} && !$params->{replace} );
457 $request->cancel( { reason => $params->{reason}, force => 1 } )
458 if ( defined($request) && $params->{replace} );
460 my $transfer = Koha::Item::Transfer->new(
462 itemnumber => $self->itemnumber,
463 daterequested => dt_from_string,
464 frombranch => $self->holdingbranch,
465 tobranch => $params->{to}->branchcode,
466 reason => $params->{reason},
467 comments => $params->{comment}
476 my $transfer = $item->get_transfer;
478 Return the active transfer request or undef
480 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
481 whereby the most recently sent, but not received, transfer will be returned
482 if it exists, otherwise the oldest unsatisfied transfer will be returned.
484 This allows for transfers to queue, which is the case for stock rotation and
485 rotating collections where a manual transfer may need to take precedence but
486 we still expect the item to end up at a final location eventually.
492 my $transfer_rs = $self->_result->branchtransfers->search(
494 datearrived => undef,
495 datecancelled => undef
499 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
503 return unless $transfer_rs;
504 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
509 my $transfer = $item->get_transfers;
511 Return the list of outstanding transfers (i.e requested but not yet cancelled
514 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
515 whereby the most recently sent, but not received, transfer will be returned
516 first if it exists, otherwise requests are in oldest to newest request order.
518 This allows for transfers to queue, which is the case for stock rotation and
519 rotating collections where a manual transfer may need to take precedence but
520 we still expect the item to end up at a final location eventually.
526 my $transfer_rs = $self->_result->branchtransfers->search(
528 datearrived => undef,
529 datecancelled => undef
533 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
536 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
539 =head3 last_returned_by
541 Gets and sets the last borrower to return an item.
543 Accepts and returns Koha::Patron objects
545 $item->last_returned_by( $borrowernumber );
547 $last_returned_by = $item->last_returned_by();
551 sub last_returned_by {
552 my ( $self, $borrower ) = @_;
554 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
557 return $items_last_returned_by_rs->update_or_create(
558 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
561 unless ( $self->{_last_returned_by} ) {
562 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
564 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
568 return $self->{_last_returned_by};
572 =head3 can_article_request
574 my $bool = $item->can_article_request( $borrower )
576 Returns true if item can be specifically requested
578 $borrower must be a Koha::Patron object
582 sub can_article_request {
583 my ( $self, $borrower ) = @_;
585 my $rule = $self->article_request_type($borrower);
587 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
591 =head3 hidden_in_opac
593 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
595 Returns true if item fields match the hidding criteria defined in $rules.
596 Returns false otherwise.
598 Takes HASHref that can have the following parameters:
600 $rules : { <field> => [ value_1, ... ], ... }
602 Note: $rules inherits its structure from the parsed YAML from reading
603 the I<OpacHiddenItems> system preference.
608 my ( $self, $params ) = @_;
610 my $rules = $params->{rules} // {};
613 if C4::Context->preference('hidelostitems') and
616 my $hidden_in_opac = 0;
618 foreach my $field ( keys %{$rules} ) {
620 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
626 return $hidden_in_opac;
629 =head3 can_be_transferred
631 $item->can_be_transferred({ to => $to_library, from => $from_library })
632 Checks if an item can be transferred to given library.
634 This feature is controlled by two system preferences:
635 UseBranchTransferLimits to enable / disable the feature
636 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
637 for setting the limitations
639 Takes HASHref that can have the following parameters:
640 MANDATORY PARAMETERS:
643 $from : Koha::Library # if not given, item holdingbranch
644 # will be used instead
646 Returns 1 if item can be transferred to $to_library, otherwise 0.
648 To find out whether at least one item of a Koha::Biblio can be transferred, please
649 see Koha::Biblio->can_be_transferred() instead of using this method for
650 multiple items of the same biblio.
654 sub can_be_transferred {
655 my ($self, $params) = @_;
657 my $to = $params->{to};
658 my $from = $params->{from};
660 $to = $to->branchcode;
661 $from = defined $from ? $from->branchcode : $self->holdingbranch;
663 return 1 if $from eq $to; # Transfer to current branch is allowed
664 return 1 unless C4::Context->preference('UseBranchTransferLimits');
666 my $limittype = C4::Context->preference('BranchTransferLimitsType');
667 return Koha::Item::Transfer::Limits->search({
670 $limittype => $limittype eq 'itemtype'
671 ? $self->effective_itemtype : $self->ccode
676 =head3 pickup_locations
678 $pickup_locations = $item->pickup_locations( {patron => $patron } )
680 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)
681 and if item can be transferred to each pickup location.
685 sub pickup_locations {
686 my ($self, $params) = @_;
688 my $patron = $params->{patron};
690 my $circ_control_branch =
691 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
693 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
695 if(defined $patron) {
696 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
697 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
700 my $pickup_libraries = Koha::Libraries->search();
701 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
702 $pickup_libraries = $self->home_branch->get_hold_libraries;
703 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
704 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
705 $pickup_libraries = $plib->get_hold_libraries;
706 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
707 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
708 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
709 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
712 return $pickup_libraries->search(
717 order_by => ['branchname']
719 ) unless C4::Context->preference('UseBranchTransferLimits');
721 my $limittype = C4::Context->preference('BranchTransferLimitsType');
722 my ($ccode, $itype) = (undef, undef);
723 if( $limittype eq 'ccode' ){
724 $ccode = $self->ccode;
726 $itype = $self->itype;
728 my $limits = Koha::Item::Transfer::Limits->search(
730 fromBranch => $self->holdingbranch,
734 { columns => ['toBranch'] }
737 return $pickup_libraries->search(
739 pickup_location => 1,
741 '-not_in' => $limits->_resultset->as_query
745 order_by => ['branchname']
750 =head3 article_request_type
752 my $type = $item->article_request_type( $borrower )
754 returns 'yes', 'no', 'bib_only', or 'item_only'
756 $borrower must be a Koha::Patron object
760 sub article_request_type {
761 my ( $self, $borrower ) = @_;
763 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
765 $branch_control eq 'homebranch' ? $self->homebranch
766 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
768 my $borrowertype = $borrower->categorycode;
769 my $itemtype = $self->effective_itemtype();
770 my $rule = Koha::CirculationRules->get_effective_rule(
772 rule_name => 'article_requests',
773 categorycode => $borrowertype,
774 itemtype => $itemtype,
775 branchcode => $branchcode
779 return q{} unless $rule;
780 return $rule->rule_value || q{}
789 my $attributes = { order_by => 'priority' };
790 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
792 itemnumber => $self->itemnumber,
795 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
796 waitingdate => { '!=' => undef },
799 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
800 return Koha::Holds->_new_from_dbic($hold_rs);
803 =head3 stockrotationitem
805 my $sritem = Koha::Item->stockrotationitem;
807 Returns the stock rotation item associated with the current item.
811 sub stockrotationitem {
813 my $rs = $self->_result->stockrotationitem;
815 return Koha::StockRotationItem->_new_from_dbic( $rs );
820 my $item = $item->add_to_rota($rota_id);
822 Add this item to the rota identified by $ROTA_ID, which means associating it
823 with the first stage of that rota. Should this item already be associated
824 with a rota, then we will move it to the new rota.
829 my ( $self, $rota_id ) = @_;
830 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
834 =head3 has_pending_hold
836 my $is_pending_hold = $item->has_pending_hold();
838 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
842 sub has_pending_hold {
844 my $pending_hold = $self->_result->tmp_holdsqueues;
845 return $pending_hold->count ? 1: 0;
850 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
851 my $field = $item->as_marc_field({ [ mss => $mss ] });
853 This method returns a MARC::Field object representing the Koha::Item object
854 with the current mappings configuration.
859 my ( $self, $params ) = @_;
861 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
862 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
866 my @columns = $self->_result->result_source->columns;
868 foreach my $item_field ( @columns ) {
869 my $mapping = $mss->{ "items.$item_field"}[0];
870 my $tagfield = $mapping->{tagfield};
871 my $tagsubfield = $mapping->{tagsubfield};
872 next if !$tagfield; # TODO: Should we raise an exception instead?
873 # Feels like safe fallback is better
875 push @subfields, $tagsubfield => $self->$item_field
876 if defined $self->$item_field and $item_field ne '';
879 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
880 push( @subfields, @{$unlinked_item_subfields} )
881 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
885 $field = MARC::Field->new(
886 "$item_tag", ' ', ' ', @subfields
892 =head3 renewal_branchcode
894 Returns the branchcode to be recorded in statistics renewal of the item
898 sub renewal_branchcode {
900 my ($self, $params ) = @_;
902 my $interface = C4::Context->interface;
904 if ( $interface eq 'opac' ){
905 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
906 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
907 $branchcode = 'OPACRenew';
909 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
910 $branchcode = $self->homebranch;
912 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
913 $branchcode = $self->checkout->patron->branchcode;
915 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
916 $branchcode = $self->checkout->branchcode;
922 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
923 ? C4::Context->userenv->{branch} : $params->{branch};
930 Return the cover images associated with this item.
937 my $cover_image_rs = $self->_result->cover_images;
938 return unless $cover_image_rs;
939 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
942 =head3 _set_found_trigger
944 $self->_set_found_trigger
946 Finds the most recent lost item charge for this item and refunds the patron
947 appropriately, taking into account any payments or writeoffs already applied
950 Internal function, not exported, called only by Koha::Item->store.
954 sub _set_found_trigger {
955 my ( $self, $pre_mod_item ) = @_;
957 ## If item was lost, it has now been found, reverse any list item charges if necessary.
958 my $no_refund_after_days =
959 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
960 if ($no_refund_after_days) {
961 my $today = dt_from_string();
962 my $lost_age_in_days =
963 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
966 return $self unless $lost_age_in_days < $no_refund_after_days;
969 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
972 return_branch => C4::Context->userenv
973 ? C4::Context->userenv->{'branch'}
978 if ( $lostreturn_policy ) {
980 # refund charge made for lost book
981 my $lost_charge = Koha::Account::Lines->search(
983 itemnumber => $self->itemnumber,
984 debit_type_code => 'LOST',
985 status => [ undef, { '<>' => 'FOUND' } ]
988 order_by => { -desc => [ 'date', 'accountlines_id' ] },
993 if ( $lost_charge ) {
995 my $patron = $lost_charge->patron;
998 my $account = $patron->account;
999 my $total_to_refund = 0;
1002 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1004 # some amount has been cancelled. collect the offsets that are not writeoffs
1005 # this works because the only way to subtract from this kind of a debt is
1006 # using the UI buttons 'Pay' and 'Write off'
1007 my $credit_offsets = $lost_charge->debit_offsets(
1009 'credit_id' => { '!=' => undef },
1010 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1012 { join => 'credit' }
1015 $total_to_refund = ( $credit_offsets->count > 0 )
1016 ? $credit_offsets->total * -1 # credits are negative on the DB
1020 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1023 if ( $credit_total > 0 ) {
1025 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1026 $credit = $account->add_credit(
1028 amount => $credit_total,
1029 description => 'Item found ' . $self->itemnumber,
1030 type => 'LOST_FOUND',
1031 interface => C4::Context->interface,
1032 library_id => $branchcode,
1033 item_id => $self->itemnumber,
1034 issue_id => $lost_charge->issue_id
1038 $credit->apply( { debits => [$lost_charge] } );
1039 $self->{_refunded} = 1;
1042 # Update the account status
1043 $lost_charge->status('FOUND');
1044 $lost_charge->store();
1046 # Reconcile balances if required
1047 if ( C4::Context->preference('AccountAutoReconcile') ) {
1048 $account->reconcile_balance;
1053 # restore fine for lost book
1054 if ( $lostreturn_policy eq 'restore' ) {
1055 my $lost_overdue = Koha::Account::Lines->search(
1057 itemnumber => $self->itemnumber,
1058 debit_type_code => 'OVERDUE',
1062 order_by => { '-desc' => 'date' },
1067 if ( $lost_overdue ) {
1069 my $patron = $lost_overdue->patron;
1071 my $account = $patron->account;
1073 # Update status of fine
1074 $lost_overdue->status('FOUND')->store();
1076 # Find related forgive credit
1077 my $refund = $lost_overdue->credits(
1079 credit_type_code => 'FORGIVEN',
1080 itemnumber => $self->itemnumber,
1081 status => [ { '!=' => 'VOID' }, undef ]
1083 { order_by => { '-desc' => 'date' }, rows => 1 }
1087 # Revert the forgive credit
1088 $refund->void({ interface => 'trigger' });
1089 $self->{_restored} = 1;
1092 # Reconcile balances if required
1093 if ( C4::Context->preference('AccountAutoReconcile') ) {
1094 $account->reconcile_balance;
1098 } elsif ( $lostreturn_policy eq 'charge' ) {
1099 $self->{_charge} = 1;
1106 =head3 to_api_mapping
1108 This method returns the mapping for representing a Koha::Item object
1113 sub to_api_mapping {
1115 itemnumber => 'item_id',
1116 biblionumber => 'biblio_id',
1117 biblioitemnumber => undef,
1118 barcode => 'external_id',
1119 dateaccessioned => 'acquisition_date',
1120 booksellerid => 'acquisition_source',
1121 homebranch => 'home_library_id',
1122 price => 'purchase_price',
1123 replacementprice => 'replacement_price',
1124 replacementpricedate => 'replacement_price_date',
1125 datelastborrowed => 'last_checkout_date',
1126 datelastseen => 'last_seen_date',
1128 notforloan => 'not_for_loan_status',
1129 damaged => 'damaged_status',
1130 damaged_on => 'damaged_date',
1131 itemlost => 'lost_status',
1132 itemlost_on => 'lost_date',
1133 withdrawn => 'withdrawn',
1134 withdrawn_on => 'withdrawn_date',
1135 itemcallnumber => 'callnumber',
1136 coded_location_qualifier => 'coded_location_qualifier',
1137 issues => 'checkouts_count',
1138 renewals => 'renewals_count',
1139 reserves => 'holds_count',
1140 restricted => 'restricted_status',
1141 itemnotes => 'public_notes',
1142 itemnotes_nonpublic => 'internal_notes',
1143 holdingbranch => 'holding_library_id',
1144 timestamp => 'timestamp',
1145 location => 'location',
1146 permanent_location => 'permanent_location',
1147 onloan => 'checked_out_date',
1148 cn_source => 'call_number_source',
1149 cn_sort => 'call_number_sort',
1150 ccode => 'collection_code',
1151 materials => 'materials_notes',
1153 itype => 'item_type',
1154 more_subfields_xml => 'extended_subfields',
1155 enumchron => 'serial_issue_number',
1156 copynumber => 'copy_number',
1157 stocknumber => 'inventory_number',
1158 new_status => 'new_status'
1164 my $itemtype = $item->itemtype;
1166 Returns Koha object for effective itemtype
1172 return Koha::ItemTypes->find( $self->effective_itemtype );
1177 my $orders = $item->item_orders();
1179 Returns a Koha::Acquisition::Orders object
1186 my $orders = $self->_result->item_orders;
1187 return unless $orders;
1188 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1191 =head3 move_to_biblio
1193 $item->move_to_biblio($to_biblio[, $params]);
1195 Move the item to another biblio and update any references also in other tables.
1197 The final optional parameter, C<$params>, is expected to contain the
1198 'skip_record_index' key, which is relayed down to Koha::Item->store.
1199 There it prevents calling index_records, which takes most of the
1200 time in batch adds/deletes. The caller must take care of calling
1201 index_records separately.
1204 skip_record_index => 1|0
1206 Returns undef if the move failed or the biblionumber of the destination record otherwise
1210 sub move_to_biblio {
1211 my ( $self, $to_biblio, $params ) = @_;
1215 return unless $self->biblionumber != $to_biblio->biblionumber;
1217 my $biblionumber = $to_biblio->biblionumber;
1219 # Own biblionumber and biblioitemnumber
1221 biblionumber => $biblionumber,
1222 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1223 })->store({ skip_record_index => $params->{skip_record_index} });
1225 # Acquisition orders
1226 my $orders = $self->item_orders;
1228 $orders->update({ biblionumber => $biblionumber }, { no_triggers => 1 });
1232 $self->holds->update({ biblionumber => $biblionumber });
1234 # hold_fill_target (there's no Koha object available)
1235 my $hold_fill_target = $self->_result->hold_fill_target;
1236 if ($hold_fill_target) {
1237 $hold_fill_target->update({ biblionumber => $biblionumber });
1240 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1241 # and can't even fake one since the significant columns are nullable.
1242 my $storage = $self->_result->result_source->storage;
1245 my ($storage, $dbh, @cols) = @_;
1247 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $biblionumber, $self->itemnumber);
1252 my $schema = Koha::Database->new()->schema();
1253 my $linktrackers = $schema->resultset('Linktracker')->search({ itemnumber => $self->itemnumber });
1254 $linktrackers->update_all({ biblionumber => $biblionumber });
1256 return $biblionumber;
1259 =head2 Internal methods
1261 =head3 _after_item_action_hooks
1263 Helper method that takes care of calling all plugin hooks
1267 sub _after_item_action_hooks {
1268 my ( $self, $params ) = @_;
1270 my $action = $params->{action};
1272 Koha::Plugins->call(
1273 'after_item_action',
1277 item_id => $self->itemnumber,
1292 Kyle M Hall <kyle@bywatersolutions.com>