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 output_pref );
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);
946 my $frameworkcode = $self->biblio->frameworkcode;
947 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
948 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
950 my $columns_info = $self->_result->result_source->columns_info;
952 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
954 for my $column ( keys %$columns_info ) {
956 next if $column eq 'more_subfields_xml';
959 if ( Koha::Object::_datetime_column_type( $columns_info->{$column}->{data_type} ) ) {
960 $value = output_pref({ dateformat => 'rfc3339', dt => dt_from_string($value, 'sql')});
962 $value = $self->$column;
965 if ( not defined $value or $value eq "" ) {
966 $values->{$column} = $value;
971 exists $mss->{"items.$column"}
972 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
977 ? $subfield->{authorised_value}
978 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
979 $subfield->{tagsubfield}, $value, '', $tagslib )
985 $self->more_subfields_xml
986 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
991 my ( $field ) = $marc_more->fields;
992 for my $sf ( $field->subfields ) {
993 my $subfield_code = $sf->[0];
994 my $value = $sf->[1];
995 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
996 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
998 $subfield->{authorised_value}
999 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1000 $subfield->{tagsubfield}, $value, '', $tagslib )
1003 push @{$more_values->{$subfield_code}}, $value;
1006 while ( my ( $k, $v ) = each %$more_values ) {
1007 $values->{$k} = join ' | ', @$v;
1014 =head3 _set_found_trigger
1016 $self->_set_found_trigger
1018 Finds the most recent lost item charge for this item and refunds the patron
1019 appropriately, taking into account any payments or writeoffs already applied
1022 Internal function, not exported, called only by Koha::Item->store.
1026 sub _set_found_trigger {
1027 my ( $self, $pre_mod_item ) = @_;
1029 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1030 my $no_refund_after_days =
1031 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1032 if ($no_refund_after_days) {
1033 my $today = dt_from_string();
1034 my $lost_age_in_days =
1035 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1038 return $self unless $lost_age_in_days < $no_refund_after_days;
1041 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1044 return_branch => C4::Context->userenv
1045 ? C4::Context->userenv->{'branch'}
1050 if ( $lostreturn_policy ) {
1052 # refund charge made for lost book
1053 my $lost_charge = Koha::Account::Lines->search(
1055 itemnumber => $self->itemnumber,
1056 debit_type_code => 'LOST',
1057 status => [ undef, { '<>' => 'FOUND' } ]
1060 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1065 if ( $lost_charge ) {
1067 my $patron = $lost_charge->patron;
1070 my $account = $patron->account;
1071 my $total_to_refund = 0;
1074 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1076 # some amount has been cancelled. collect the offsets that are not writeoffs
1077 # this works because the only way to subtract from this kind of a debt is
1078 # using the UI buttons 'Pay' and 'Write off'
1079 my $credit_offsets = $lost_charge->debit_offsets(
1081 'credit_id' => { '!=' => undef },
1082 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1084 { join => 'credit' }
1087 $total_to_refund = ( $credit_offsets->count > 0 )
1088 ? $credit_offsets->total * -1 # credits are negative on the DB
1092 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1095 if ( $credit_total > 0 ) {
1097 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1098 $credit = $account->add_credit(
1100 amount => $credit_total,
1101 description => 'Item found ' . $self->itemnumber,
1102 type => 'LOST_FOUND',
1103 interface => C4::Context->interface,
1104 library_id => $branchcode,
1105 item_id => $self->itemnumber,
1106 issue_id => $lost_charge->issue_id
1110 $credit->apply( { debits => [$lost_charge] } );
1111 $self->{_refunded} = 1;
1114 # Update the account status
1115 $lost_charge->status('FOUND');
1116 $lost_charge->store();
1118 # Reconcile balances if required
1119 if ( C4::Context->preference('AccountAutoReconcile') ) {
1120 $account->reconcile_balance;
1125 # restore fine for lost book
1126 if ( $lostreturn_policy eq 'restore' ) {
1127 my $lost_overdue = Koha::Account::Lines->search(
1129 itemnumber => $self->itemnumber,
1130 debit_type_code => 'OVERDUE',
1134 order_by => { '-desc' => 'date' },
1139 if ( $lost_overdue ) {
1141 my $patron = $lost_overdue->patron;
1143 my $account = $patron->account;
1145 # Update status of fine
1146 $lost_overdue->status('FOUND')->store();
1148 # Find related forgive credit
1149 my $refund = $lost_overdue->credits(
1151 credit_type_code => 'FORGIVEN',
1152 itemnumber => $self->itemnumber,
1153 status => [ { '!=' => 'VOID' }, undef ]
1155 { order_by => { '-desc' => 'date' }, rows => 1 }
1159 # Revert the forgive credit
1160 $refund->void({ interface => 'trigger' });
1161 $self->{_restored} = 1;
1164 # Reconcile balances if required
1165 if ( C4::Context->preference('AccountAutoReconcile') ) {
1166 $account->reconcile_balance;
1170 } elsif ( $lostreturn_policy eq 'charge' ) {
1171 $self->{_charge} = 1;
1178 =head3 to_api_mapping
1180 This method returns the mapping for representing a Koha::Item object
1185 sub to_api_mapping {
1187 itemnumber => 'item_id',
1188 biblionumber => 'biblio_id',
1189 biblioitemnumber => undef,
1190 barcode => 'external_id',
1191 dateaccessioned => 'acquisition_date',
1192 booksellerid => 'acquisition_source',
1193 homebranch => 'home_library_id',
1194 price => 'purchase_price',
1195 replacementprice => 'replacement_price',
1196 replacementpricedate => 'replacement_price_date',
1197 datelastborrowed => 'last_checkout_date',
1198 datelastseen => 'last_seen_date',
1200 notforloan => 'not_for_loan_status',
1201 damaged => 'damaged_status',
1202 damaged_on => 'damaged_date',
1203 itemlost => 'lost_status',
1204 itemlost_on => 'lost_date',
1205 withdrawn => 'withdrawn',
1206 withdrawn_on => 'withdrawn_date',
1207 itemcallnumber => 'callnumber',
1208 coded_location_qualifier => 'coded_location_qualifier',
1209 issues => 'checkouts_count',
1210 renewals => 'renewals_count',
1211 reserves => 'holds_count',
1212 restricted => 'restricted_status',
1213 itemnotes => 'public_notes',
1214 itemnotes_nonpublic => 'internal_notes',
1215 holdingbranch => 'holding_library_id',
1216 timestamp => 'timestamp',
1217 location => 'location',
1218 permanent_location => 'permanent_location',
1219 onloan => 'checked_out_date',
1220 cn_source => 'call_number_source',
1221 cn_sort => 'call_number_sort',
1222 ccode => 'collection_code',
1223 materials => 'materials_notes',
1225 itype => 'item_type',
1226 more_subfields_xml => 'extended_subfields',
1227 enumchron => 'serial_issue_number',
1228 copynumber => 'copy_number',
1229 stocknumber => 'inventory_number',
1230 new_status => 'new_status'
1236 my $itemtype = $item->itemtype;
1238 Returns Koha object for effective itemtype
1244 return Koha::ItemTypes->find( $self->effective_itemtype );
1249 my $orders = $item->orders();
1251 Returns a Koha::Acquisition::Orders object
1258 my $orders = $self->_result->item_orders;
1259 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1262 =head3 tracked_links
1264 my $tracked_links = $item->tracked_links();
1266 Returns a Koha::TrackedLinks object
1273 my $tracked_links = $self->_result->linktrackers;
1274 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1277 =head3 move_to_biblio
1279 $item->move_to_biblio($to_biblio[, $params]);
1281 Move the item to another biblio and update any references in other tables.
1283 The final optional parameter, C<$params>, is expected to contain the
1284 'skip_record_index' key, which is relayed down to Koha::Item->store.
1285 There it prevents calling index_records, which takes most of the
1286 time in batch adds/deletes. The caller must take care of calling
1287 index_records separately.
1290 skip_record_index => 1|0
1292 Returns undef if the move failed or the biblionumber of the destination record otherwise
1296 sub move_to_biblio {
1297 my ( $self, $to_biblio, $params ) = @_;
1301 return if $self->biblionumber == $to_biblio->biblionumber;
1303 my $from_biblionumber = $self->biblionumber;
1304 my $to_biblionumber = $to_biblio->biblionumber;
1306 # Own biblionumber and biblioitemnumber
1308 biblionumber => $to_biblionumber,
1309 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1310 })->store({ skip_record_index => $params->{skip_record_index} });
1312 unless ($params->{skip_record_index}) {
1313 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1314 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1317 # Acquisition orders
1318 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1321 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1323 # hold_fill_target (there's no Koha object available yet)
1324 my $hold_fill_target = $self->_result->hold_fill_target;
1325 if ($hold_fill_target) {
1326 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1329 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1330 # and can't even fake one since the significant columns are nullable.
1331 my $storage = $self->_result->result_source->storage;
1334 my ($storage, $dbh, @cols) = @_;
1336 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1341 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1343 return $to_biblionumber;
1346 =head2 Internal methods
1348 =head3 _after_item_action_hooks
1350 Helper method that takes care of calling all plugin hooks
1354 sub _after_item_action_hooks {
1355 my ( $self, $params ) = @_;
1357 my $action = $params->{action};
1359 Koha::Plugins->call(
1360 'after_item_action',
1364 item_id => $self->itemnumber,
1379 Kyle M Hall <kyle@bywatersolutions.com>