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);
943 =head3 columns_to_str
945 my $values = $items->columns_to_str;
947 Return a hashref with the string representation of the different attribute of the item.
949 This is meant to be used for display purpose only.
956 my $frameworkcode = $self->biblio->frameworkcode;
957 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
958 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
960 my $columns_info = $self->_result->result_source->columns_info;
962 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
964 for my $column ( keys %$columns_info ) {
966 next if $column eq 'more_subfields_xml';
968 my $value = $self->$column;
969 # Maybe we need to deal with datetime columns here, but so far we have damaged_on, itemlost_on and withdrawn_on, and they are not linked with kohafield
971 if ( not defined $value or $value eq "" ) {
972 $values->{$column} = $value;
977 exists $mss->{"items.$column"}
978 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
983 ? $subfield->{authorised_value}
984 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
985 $subfield->{tagsubfield}, $value, '', $tagslib )
991 $self->more_subfields_xml
992 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
997 my ( $field ) = $marc_more->fields;
998 for my $sf ( $field->subfields ) {
999 my $subfield_code = $sf->[0];
1000 my $value = $sf->[1];
1001 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1002 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1004 $subfield->{authorised_value}
1005 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1006 $subfield->{tagsubfield}, $value, '', $tagslib )
1009 push @{$more_values->{$subfield_code}}, $value;
1012 while ( my ( $k, $v ) = each %$more_values ) {
1013 $values->{$k} = join ' | ', @$v;
1020 =head3 _set_found_trigger
1022 $self->_set_found_trigger
1024 Finds the most recent lost item charge for this item and refunds the patron
1025 appropriately, taking into account any payments or writeoffs already applied
1028 Internal function, not exported, called only by Koha::Item->store.
1032 sub _set_found_trigger {
1033 my ( $self, $pre_mod_item ) = @_;
1035 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1036 my $no_refund_after_days =
1037 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1038 if ($no_refund_after_days) {
1039 my $today = dt_from_string();
1040 my $lost_age_in_days =
1041 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1044 return $self unless $lost_age_in_days < $no_refund_after_days;
1047 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1050 return_branch => C4::Context->userenv
1051 ? C4::Context->userenv->{'branch'}
1056 if ( $lostreturn_policy ) {
1058 # refund charge made for lost book
1059 my $lost_charge = Koha::Account::Lines->search(
1061 itemnumber => $self->itemnumber,
1062 debit_type_code => 'LOST',
1063 status => [ undef, { '<>' => 'FOUND' } ]
1066 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1071 if ( $lost_charge ) {
1073 my $patron = $lost_charge->patron;
1076 my $account = $patron->account;
1077 my $total_to_refund = 0;
1080 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1082 # some amount has been cancelled. collect the offsets that are not writeoffs
1083 # this works because the only way to subtract from this kind of a debt is
1084 # using the UI buttons 'Pay' and 'Write off'
1085 my $credit_offsets = $lost_charge->debit_offsets(
1087 'credit_id' => { '!=' => undef },
1088 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1090 { join => 'credit' }
1093 $total_to_refund = ( $credit_offsets->count > 0 )
1094 ? $credit_offsets->total * -1 # credits are negative on the DB
1098 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1101 if ( $credit_total > 0 ) {
1103 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1104 $credit = $account->add_credit(
1106 amount => $credit_total,
1107 description => 'Item found ' . $self->itemnumber,
1108 type => 'LOST_FOUND',
1109 interface => C4::Context->interface,
1110 library_id => $branchcode,
1111 item_id => $self->itemnumber,
1112 issue_id => $lost_charge->issue_id
1116 $credit->apply( { debits => [$lost_charge] } );
1117 $self->{_refunded} = 1;
1120 # Update the account status
1121 $lost_charge->status('FOUND');
1122 $lost_charge->store();
1124 # Reconcile balances if required
1125 if ( C4::Context->preference('AccountAutoReconcile') ) {
1126 $account->reconcile_balance;
1131 # restore fine for lost book
1132 if ( $lostreturn_policy eq 'restore' ) {
1133 my $lost_overdue = Koha::Account::Lines->search(
1135 itemnumber => $self->itemnumber,
1136 debit_type_code => 'OVERDUE',
1140 order_by => { '-desc' => 'date' },
1145 if ( $lost_overdue ) {
1147 my $patron = $lost_overdue->patron;
1149 my $account = $patron->account;
1151 # Update status of fine
1152 $lost_overdue->status('FOUND')->store();
1154 # Find related forgive credit
1155 my $refund = $lost_overdue->credits(
1157 credit_type_code => 'FORGIVEN',
1158 itemnumber => $self->itemnumber,
1159 status => [ { '!=' => 'VOID' }, undef ]
1161 { order_by => { '-desc' => 'date' }, rows => 1 }
1165 # Revert the forgive credit
1166 $refund->void({ interface => 'trigger' });
1167 $self->{_restored} = 1;
1170 # Reconcile balances if required
1171 if ( C4::Context->preference('AccountAutoReconcile') ) {
1172 $account->reconcile_balance;
1176 } elsif ( $lostreturn_policy eq 'charge' ) {
1177 $self->{_charge} = 1;
1184 =head3 to_api_mapping
1186 This method returns the mapping for representing a Koha::Item object
1191 sub to_api_mapping {
1193 itemnumber => 'item_id',
1194 biblionumber => 'biblio_id',
1195 biblioitemnumber => undef,
1196 barcode => 'external_id',
1197 dateaccessioned => 'acquisition_date',
1198 booksellerid => 'acquisition_source',
1199 homebranch => 'home_library_id',
1200 price => 'purchase_price',
1201 replacementprice => 'replacement_price',
1202 replacementpricedate => 'replacement_price_date',
1203 datelastborrowed => 'last_checkout_date',
1204 datelastseen => 'last_seen_date',
1206 notforloan => 'not_for_loan_status',
1207 damaged => 'damaged_status',
1208 damaged_on => 'damaged_date',
1209 itemlost => 'lost_status',
1210 itemlost_on => 'lost_date',
1211 withdrawn => 'withdrawn',
1212 withdrawn_on => 'withdrawn_date',
1213 itemcallnumber => 'callnumber',
1214 coded_location_qualifier => 'coded_location_qualifier',
1215 issues => 'checkouts_count',
1216 renewals => 'renewals_count',
1217 reserves => 'holds_count',
1218 restricted => 'restricted_status',
1219 itemnotes => 'public_notes',
1220 itemnotes_nonpublic => 'internal_notes',
1221 holdingbranch => 'holding_library_id',
1222 timestamp => 'timestamp',
1223 location => 'location',
1224 permanent_location => 'permanent_location',
1225 onloan => 'checked_out_date',
1226 cn_source => 'call_number_source',
1227 cn_sort => 'call_number_sort',
1228 ccode => 'collection_code',
1229 materials => 'materials_notes',
1231 itype => 'item_type',
1232 more_subfields_xml => 'extended_subfields',
1233 enumchron => 'serial_issue_number',
1234 copynumber => 'copy_number',
1235 stocknumber => 'inventory_number',
1236 new_status => 'new_status'
1242 my $itemtype = $item->itemtype;
1244 Returns Koha object for effective itemtype
1250 return Koha::ItemTypes->find( $self->effective_itemtype );
1255 my $orders = $item->orders();
1257 Returns a Koha::Acquisition::Orders object
1264 my $orders = $self->_result->item_orders;
1265 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1268 =head3 tracked_links
1270 my $tracked_links = $item->tracked_links();
1272 Returns a Koha::TrackedLinks object
1279 my $tracked_links = $self->_result->linktrackers;
1280 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1283 =head3 move_to_biblio
1285 $item->move_to_biblio($to_biblio[, $params]);
1287 Move the item to another biblio and update any references in other tables.
1289 The final optional parameter, C<$params>, is expected to contain the
1290 'skip_record_index' key, which is relayed down to Koha::Item->store.
1291 There it prevents calling index_records, which takes most of the
1292 time in batch adds/deletes. The caller must take care of calling
1293 index_records separately.
1296 skip_record_index => 1|0
1298 Returns undef if the move failed or the biblionumber of the destination record otherwise
1302 sub move_to_biblio {
1303 my ( $self, $to_biblio, $params ) = @_;
1307 return if $self->biblionumber == $to_biblio->biblionumber;
1309 my $from_biblionumber = $self->biblionumber;
1310 my $to_biblionumber = $to_biblio->biblionumber;
1312 # Own biblionumber and biblioitemnumber
1314 biblionumber => $to_biblionumber,
1315 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1316 })->store({ skip_record_index => $params->{skip_record_index} });
1318 unless ($params->{skip_record_index}) {
1319 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1320 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1323 # Acquisition orders
1324 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1327 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1329 # hold_fill_target (there's no Koha object available yet)
1330 my $hold_fill_target = $self->_result->hold_fill_target;
1331 if ($hold_fill_target) {
1332 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1335 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1336 # and can't even fake one since the significant columns are nullable.
1337 my $storage = $self->_result->result_source->storage;
1340 my ($storage, $dbh, @cols) = @_;
1342 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1347 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1349 return $to_biblionumber;
1352 =head2 Internal methods
1354 =head3 _after_item_action_hooks
1356 Helper method that takes care of calling all plugin hooks
1360 sub _after_item_action_hooks {
1361 my ( $self, $params ) = @_;
1363 my $action = $params->{action};
1365 Koha::Plugins->call(
1366 'after_item_action',
1370 item_id => $self->itemnumber,
1385 Kyle M Hall <kyle@bywatersolutions.com>