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 );
25 use Koha::DateUtils qw( dt_from_string output_pref );
28 use C4::Circulation qw( barcodedecode GetBranchItemRule );
30 use C4::ClassSource qw( GetClassSort );
31 use C4::Log qw( logaction );
33 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
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;
41 use Koha::Item::Attributes;
46 use Koha::StockRotationItem;
47 use Koha::StockRotationRotas;
48 use Koha::TrackedLinks;
49 use Koha::Result::Boolean;
51 use base qw(Koha::Object);
55 Koha::Item - Koha Item object class
67 $params can take an optional 'skip_record_index' parameter.
68 If set, the reindexation process will not happen (index_records not called)
70 NOTE: This is a temporary fix to answer a performance issue when lot of items
71 are added (or modified) at the same time.
72 The correct way to fix this is to make the ES reindexation process async.
73 You should not turn it on if you do not understand what it is doing exactly.
79 my $params = @_ ? shift : {};
81 my $log_action = $params->{log_action} // 1;
83 # We do not want to oblige callers to pass this value
84 # Dev conveniences vs performance?
85 unless ( $self->biblioitemnumber ) {
86 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
89 # See related changes from C4::Items::AddItem
90 unless ( $self->itype ) {
91 $self->itype($self->biblio->biblioitem->itemtype);
94 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
96 my $today = dt_from_string;
97 my $action = 'create';
99 unless ( $self->in_storage ) { #AddItem
101 unless ( $self->permanent_location ) {
102 $self->permanent_location($self->location);
105 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
106 unless ( $self->location || !$default_location ) {
107 $self->permanent_location( $self->location || $default_location )
108 unless $self->permanent_location;
109 $self->location($default_location);
112 unless ( $self->replacementpricedate ) {
113 $self->replacementpricedate($today);
115 unless ( $self->datelastseen ) {
116 $self->datelastseen($today);
119 unless ( $self->dateaccessioned ) {
120 $self->dateaccessioned($today);
123 if ( $self->itemcallnumber
124 or $self->cn_source )
126 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
127 $self->cn_sort($cn_sort);
134 my %updated_columns = $self->_result->get_dirty_columns;
135 return $self->SUPER::store unless %updated_columns;
137 # Retrieve the item for comparison if we need to
139 exists $updated_columns{itemlost}
140 or exists $updated_columns{withdrawn}
141 or exists $updated_columns{damaged}
142 ) ? $self->get_from_storage : undef;
144 # Update *_on fields if needed
145 # FIXME: Why not for AddItem as well?
146 my @fields = qw( itemlost withdrawn damaged );
147 for my $field (@fields) {
149 # If the field is defined but empty or 0, we are
150 # removing/unsetting and thus need to clear out
152 if ( exists $updated_columns{$field}
153 && defined( $self->$field )
156 my $field_on = "${field}_on";
157 $self->$field_on(undef);
159 # If the field has changed otherwise, we much update
161 elsif (exists $updated_columns{$field}
162 && $updated_columns{$field}
163 && !$pre_mod_item->$field )
165 my $field_on = "${field}_on";
167 DateTime::Format::MySQL->format_datetime(
174 if ( exists $updated_columns{itemcallnumber}
175 or exists $updated_columns{cn_source} )
177 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
178 $self->cn_sort($cn_sort);
182 if ( exists $updated_columns{location}
183 and $self->location ne 'CART'
184 and $self->location ne 'PROC'
185 and not exists $updated_columns{permanent_location} )
187 $self->permanent_location( $self->location );
190 # If item was lost and has now been found,
191 # reverse any list item charges if necessary.
192 if ( exists $updated_columns{itemlost}
193 and $updated_columns{itemlost} <= 0
194 and $pre_mod_item->itemlost > 0 )
196 $self->_set_found_trigger($pre_mod_item);
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, $self );
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 });
212 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
214 biblio_ids => [ $self->biblionumber ]
216 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
227 my $params = @_ ? shift : {};
229 # FIXME check the item has no current issues
230 # i.e. raise the appropriate exception
232 my $result = $self->SUPER::delete;
234 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
235 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
236 unless $params->{skip_record_index};
238 $self->_after_item_action_hooks({ action => 'delete' });
240 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
241 if C4::Context->preference("CataloguingLog");
243 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
245 biblio_ids => [ $self->biblionumber ]
247 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
258 my $params = @_ ? shift : {};
260 my $safe_to_delete = $self->safe_to_delete;
261 return $safe_to_delete unless $safe_to_delete;
263 $self->move_to_deleted;
265 return $self->delete($params);
268 =head3 safe_to_delete
270 returns 1 if the item is safe to delete,
272 "book_on_loan" if the item is checked out,
274 "not_same_branch" if the item is blocked by independent branches,
276 "book_reserved" if the there are holds aganst the item, or
278 "linked_analytics" if the item has linked analytic records.
280 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
289 $error = "book_on_loan" if $self->checkout;
291 $error = "not_same_branch"
292 if defined C4::Context->userenv
293 and !C4::Context->IsSuperLibrarian()
294 and C4::Context->preference("IndependentBranches")
295 and ( C4::Context->userenv->{branch} ne $self->homebranch );
297 # check it doesn't have a waiting reserve
298 $error = "book_reserved"
299 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
301 $error = "linked_analytics"
302 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
304 $error = "last_item_for_hold"
305 if $self->biblio->items->count == 1
306 && $self->biblio->holds->search(
313 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
316 return Koha::Result::Boolean->new(1);
319 =head3 move_to_deleted
321 my $is_moved = $item->move_to_deleted;
323 Move an item to the deleteditems table.
324 This can be done before deleting an item, to make sure the data are not completely deleted.
328 sub move_to_deleted {
330 my $item_infos = $self->unblessed;
331 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
332 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
336 =head3 effective_itemtype
338 Returns the itemtype for the item based on whether item level itemtypes are set or not.
342 sub effective_itemtype {
345 return $self->_result()->effective_itemtype();
355 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
357 return $self->{_home_branch};
360 =head3 holding_branch
367 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
369 return $self->{_holding_branch};
374 my $biblio = $item->biblio;
376 Return the bibliographic record of this item
382 my $biblio_rs = $self->_result->biblio;
383 return Koha::Biblio->_new_from_dbic( $biblio_rs );
388 my $biblioitem = $item->biblioitem;
390 Return the biblioitem record of this item
396 my $biblioitem_rs = $self->_result->biblioitem;
397 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
402 my $checkout = $item->checkout;
404 Return the checkout for this item
410 my $checkout_rs = $self->_result->issue;
411 return unless $checkout_rs;
412 return Koha::Checkout->_new_from_dbic( $checkout_rs );
417 my $holds = $item->holds();
418 my $holds = $item->holds($params);
419 my $holds = $item->holds({ found => 'W'});
421 Return holds attached to an item, optionally accept a hashref of params to pass to search
426 my ( $self,$params ) = @_;
427 my $holds_rs = $self->_result->reserves->search($params);
428 return Koha::Holds->_new_from_dbic( $holds_rs );
431 =head3 request_transfer
433 my $transfer = $item->request_transfer(
437 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
441 Add a transfer request for this item to the given branch for the given reason.
443 An exception will be thrown if the BranchTransferLimits would prevent the requested
444 transfer, unless 'ignore_limits' is passed to override the limits.
446 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
447 The caller should catch such cases and retry the transfer request as appropriate passing
448 an appropriate override.
451 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
452 * replace - Used to replace the existing transfer request with your own.
456 sub request_transfer {
457 my ( $self, $params ) = @_;
459 # check for mandatory params
460 my @mandatory = ( 'to', 'reason' );
461 for my $param (@mandatory) {
462 unless ( defined( $params->{$param} ) ) {
463 Koha::Exceptions::MissingParameter->throw(
464 error => "The $param parameter is mandatory" );
468 Koha::Exceptions::Item::Transfer::Limit->throw()
469 unless ( $params->{ignore_limits}
470 || $self->can_be_transferred( { to => $params->{to} } ) );
472 my $request = $self->get_transfer;
473 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
474 if ( $request && !$params->{enqueue} && !$params->{replace} );
476 $request->cancel( { reason => $params->{reason}, force => 1 } )
477 if ( defined($request) && $params->{replace} );
479 my $transfer = Koha::Item::Transfer->new(
481 itemnumber => $self->itemnumber,
482 daterequested => dt_from_string,
483 frombranch => $self->holdingbranch,
484 tobranch => $params->{to}->branchcode,
485 reason => $params->{reason},
486 comments => $params->{comment}
495 my $transfer = $item->get_transfer;
497 Return the active transfer request or undef
499 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
500 whereby the most recently sent, but not received, transfer will be returned
501 if it exists, otherwise the oldest unsatisfied transfer will be returned.
503 This allows for transfers to queue, which is the case for stock rotation and
504 rotating collections where a manual transfer may need to take precedence but
505 we still expect the item to end up at a final location eventually.
511 my $transfer_rs = $self->_result->branchtransfers->search(
513 datearrived => undef,
514 datecancelled => undef
518 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
522 return unless $transfer_rs;
523 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
528 my $transfer = $item->get_transfers;
530 Return the list of outstanding transfers (i.e requested but not yet cancelled
533 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
534 whereby the most recently sent, but not received, transfer will be returned
535 first if it exists, otherwise requests are in oldest to newest request order.
537 This allows for transfers to queue, which is the case for stock rotation and
538 rotating collections where a manual transfer may need to take precedence but
539 we still expect the item to end up at a final location eventually.
545 my $transfer_rs = $self->_result->branchtransfers->search(
547 datearrived => undef,
548 datecancelled => undef
552 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
555 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
558 =head3 last_returned_by
560 Gets and sets the last borrower to return an item.
562 Accepts and returns Koha::Patron objects
564 $item->last_returned_by( $borrowernumber );
566 $last_returned_by = $item->last_returned_by();
570 sub last_returned_by {
571 my ( $self, $borrower ) = @_;
573 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
576 return $items_last_returned_by_rs->update_or_create(
577 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
580 unless ( $self->{_last_returned_by} ) {
581 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
583 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
587 return $self->{_last_returned_by};
591 =head3 can_article_request
593 my $bool = $item->can_article_request( $borrower )
595 Returns true if item can be specifically requested
597 $borrower must be a Koha::Patron object
601 sub can_article_request {
602 my ( $self, $borrower ) = @_;
604 my $rule = $self->article_request_type($borrower);
606 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
610 =head3 hidden_in_opac
612 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
614 Returns true if item fields match the hidding criteria defined in $rules.
615 Returns false otherwise.
617 Takes HASHref that can have the following parameters:
619 $rules : { <field> => [ value_1, ... ], ... }
621 Note: $rules inherits its structure from the parsed YAML from reading
622 the I<OpacHiddenItems> system preference.
627 my ( $self, $params ) = @_;
629 my $rules = $params->{rules} // {};
632 if C4::Context->preference('hidelostitems') and
635 my $hidden_in_opac = 0;
637 foreach my $field ( keys %{$rules} ) {
639 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
645 return $hidden_in_opac;
648 =head3 can_be_transferred
650 $item->can_be_transferred({ to => $to_library, from => $from_library })
651 Checks if an item can be transferred to given library.
653 This feature is controlled by two system preferences:
654 UseBranchTransferLimits to enable / disable the feature
655 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
656 for setting the limitations
658 Takes HASHref that can have the following parameters:
659 MANDATORY PARAMETERS:
662 $from : Koha::Library # if not given, item holdingbranch
663 # will be used instead
665 Returns 1 if item can be transferred to $to_library, otherwise 0.
667 To find out whether at least one item of a Koha::Biblio can be transferred, please
668 see Koha::Biblio->can_be_transferred() instead of using this method for
669 multiple items of the same biblio.
673 sub can_be_transferred {
674 my ($self, $params) = @_;
676 my $to = $params->{to};
677 my $from = $params->{from};
679 $to = $to->branchcode;
680 $from = defined $from ? $from->branchcode : $self->holdingbranch;
682 return 1 if $from eq $to; # Transfer to current branch is allowed
683 return 1 unless C4::Context->preference('UseBranchTransferLimits');
685 my $limittype = C4::Context->preference('BranchTransferLimitsType');
686 return Koha::Item::Transfer::Limits->search({
689 $limittype => $limittype eq 'itemtype'
690 ? $self->effective_itemtype : $self->ccode
695 =head3 pickup_locations
697 $pickup_locations = $item->pickup_locations( {patron => $patron } )
699 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)
700 and if item can be transferred to each pickup location.
704 sub pickup_locations {
705 my ($self, $params) = @_;
707 my $patron = $params->{patron};
709 my $circ_control_branch =
710 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
712 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
714 if(defined $patron) {
715 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
716 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
719 my $pickup_libraries = Koha::Libraries->search();
720 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
721 $pickup_libraries = $self->home_branch->get_hold_libraries;
722 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
723 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
724 $pickup_libraries = $plib->get_hold_libraries;
725 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
726 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
727 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
728 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
731 return $pickup_libraries->search(
736 order_by => ['branchname']
738 ) unless C4::Context->preference('UseBranchTransferLimits');
740 my $limittype = C4::Context->preference('BranchTransferLimitsType');
741 my ($ccode, $itype) = (undef, undef);
742 if( $limittype eq 'ccode' ){
743 $ccode = $self->ccode;
745 $itype = $self->itype;
747 my $limits = Koha::Item::Transfer::Limits->search(
749 fromBranch => $self->holdingbranch,
753 { columns => ['toBranch'] }
756 return $pickup_libraries->search(
758 pickup_location => 1,
760 '-not_in' => $limits->_resultset->as_query
764 order_by => ['branchname']
769 =head3 article_request_type
771 my $type = $item->article_request_type( $borrower )
773 returns 'yes', 'no', 'bib_only', or 'item_only'
775 $borrower must be a Koha::Patron object
779 sub article_request_type {
780 my ( $self, $borrower ) = @_;
782 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
784 $branch_control eq 'homebranch' ? $self->homebranch
785 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
787 my $borrowertype = $borrower->categorycode;
788 my $itemtype = $self->effective_itemtype();
789 my $rule = Koha::CirculationRules->get_effective_rule(
791 rule_name => 'article_requests',
792 categorycode => $borrowertype,
793 itemtype => $itemtype,
794 branchcode => $branchcode
798 return q{} unless $rule;
799 return $rule->rule_value || q{}
808 my $attributes = { order_by => 'priority' };
809 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
811 itemnumber => $self->itemnumber,
814 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
815 waitingdate => { '!=' => undef },
818 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
819 return Koha::Holds->_new_from_dbic($hold_rs);
822 =head3 stockrotationitem
824 my $sritem = Koha::Item->stockrotationitem;
826 Returns the stock rotation item associated with the current item.
830 sub stockrotationitem {
832 my $rs = $self->_result->stockrotationitem;
834 return Koha::StockRotationItem->_new_from_dbic( $rs );
839 my $item = $item->add_to_rota($rota_id);
841 Add this item to the rota identified by $ROTA_ID, which means associating it
842 with the first stage of that rota. Should this item already be associated
843 with a rota, then we will move it to the new rota.
848 my ( $self, $rota_id ) = @_;
849 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
853 =head3 has_pending_hold
855 my $is_pending_hold = $item->has_pending_hold();
857 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
861 sub has_pending_hold {
863 my $pending_hold = $self->_result->tmp_holdsqueues;
864 return $pending_hold->count ? 1: 0;
869 my $field = $item->as_marc_field;
871 This method returns a MARC::Field object representing the Koha::Item object
872 with the current mappings configuration.
879 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
881 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
885 my $item_field = $tagslib->{$itemtag};
887 my $more_subfields = $self->additional_attributes->to_hashref;
888 foreach my $subfield (
890 $a->{display_order} <=> $b->{display_order}
891 || $a->{subfield} cmp $b->{subfield}
892 } grep { ref($_) && %$_ } values %$item_field
895 my $kohafield = $subfield->{kohafield};
896 my $tagsubfield = $subfield->{tagsubfield};
898 if ( defined $kohafield && $kohafield ne '' ) {
899 next if $kohafield !~ m{^items\.}; # That would be weird!
900 ( my $attribute = $kohafield ) =~ s|^items\.||;
901 $value = $self->$attribute # This call may fail if a kohafield is not a DB column but we don't want to add extra work for that there
902 if defined $self->$attribute and $self->$attribute ne '';
904 $value = $more_subfields->{$tagsubfield}
907 next unless defined $value
910 if ( $subfield->{repeatable} ) {
911 my @values = split '\|', $value;
912 push @subfields, ( $tagsubfield => $_ ) for @values;
915 push @subfields, ( $tagsubfield => $value );
920 return unless @subfields;
922 return MARC::Field->new(
923 "$itemtag", ' ', ' ', @subfields
927 =head3 renewal_branchcode
929 Returns the branchcode to be recorded in statistics renewal of the item
933 sub renewal_branchcode {
935 my ($self, $params ) = @_;
937 my $interface = C4::Context->interface;
939 if ( $interface eq 'opac' ){
940 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
941 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
942 $branchcode = 'OPACRenew';
944 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
945 $branchcode = $self->homebranch;
947 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
948 $branchcode = $self->checkout->patron->branchcode;
950 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
951 $branchcode = $self->checkout->branchcode;
957 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
958 ? C4::Context->userenv->{branch} : $params->{branch};
965 Return the cover images associated with this item.
972 my $cover_image_rs = $self->_result->cover_images;
973 return unless $cover_image_rs;
974 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
977 =head3 columns_to_str
979 my $values = $items->columns_to_str;
981 Return a hashref with the string representation of the different attribute of the item.
983 This is meant to be used for display purpose only.
990 my $frameworkcode = $self->biblio->frameworkcode;
991 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
992 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
994 my $columns_info = $self->_result->result_source->columns_info;
996 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
998 for my $column ( keys %$columns_info ) {
1000 next if $column eq 'more_subfields_xml';
1002 my $value = $self->$column;
1003 # 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
1005 if ( not defined $value or $value eq "" ) {
1006 $values->{$column} = $value;
1011 exists $mss->{"items.$column"}
1012 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1015 $values->{$column} =
1017 ? $subfield->{authorised_value}
1018 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1019 $subfield->{tagsubfield}, $value, '', $tagslib )
1025 $self->more_subfields_xml
1026 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1031 my ( $field ) = $marc_more->fields;
1032 for my $sf ( $field->subfields ) {
1033 my $subfield_code = $sf->[0];
1034 my $value = $sf->[1];
1035 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1036 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1038 $subfield->{authorised_value}
1039 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1040 $subfield->{tagsubfield}, $value, '', $tagslib )
1043 push @{$more_values->{$subfield_code}}, $value;
1046 while ( my ( $k, $v ) = each %$more_values ) {
1047 $values->{$k} = join ' | ', @$v;
1054 =head3 additional_attributes
1056 my $attributes = $item->additional_attributes;
1057 $attributes->{k} = 'new k';
1058 $item->update({ more_subfields => $attributes->to_marcxml });
1060 Returns a Koha::Item::Attributes object that represents the non-mapped
1061 attributes for this item.
1065 sub additional_attributes {
1068 return Koha::Item::Attributes->new_from_marcxml(
1069 $self->more_subfields_xml,
1073 =head3 _set_found_trigger
1075 $self->_set_found_trigger
1077 Finds the most recent lost item charge for this item and refunds the patron
1078 appropriately, taking into account any payments or writeoffs already applied
1081 Internal function, not exported, called only by Koha::Item->store.
1085 sub _set_found_trigger {
1086 my ( $self, $pre_mod_item ) = @_;
1088 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1089 my $no_refund_after_days =
1090 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1091 if ($no_refund_after_days) {
1092 my $today = dt_from_string();
1093 my $lost_age_in_days =
1094 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1097 return $self unless $lost_age_in_days < $no_refund_after_days;
1100 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1103 return_branch => C4::Context->userenv
1104 ? C4::Context->userenv->{'branch'}
1109 if ( $lostreturn_policy ) {
1111 # refund charge made for lost book
1112 my $lost_charge = Koha::Account::Lines->search(
1114 itemnumber => $self->itemnumber,
1115 debit_type_code => 'LOST',
1116 status => [ undef, { '<>' => 'FOUND' } ]
1119 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1124 if ( $lost_charge ) {
1126 my $patron = $lost_charge->patron;
1129 my $account = $patron->account;
1130 my $total_to_refund = 0;
1133 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1135 # some amount has been cancelled. collect the offsets that are not writeoffs
1136 # this works because the only way to subtract from this kind of a debt is
1137 # using the UI buttons 'Pay' and 'Write off'
1138 my $credit_offsets = $lost_charge->debit_offsets(
1140 'credit_id' => { '!=' => undef },
1141 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1143 { join => 'credit' }
1146 $total_to_refund = ( $credit_offsets->count > 0 )
1147 ? $credit_offsets->total * -1 # credits are negative on the DB
1151 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1154 if ( $credit_total > 0 ) {
1156 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1157 $credit = $account->add_credit(
1159 amount => $credit_total,
1160 description => 'Item found ' . $self->itemnumber,
1161 type => 'LOST_FOUND',
1162 interface => C4::Context->interface,
1163 library_id => $branchcode,
1164 item_id => $self->itemnumber,
1165 issue_id => $lost_charge->issue_id
1169 $credit->apply( { debits => [$lost_charge] } );
1173 message => 'lost_refunded',
1174 payload => { credit_id => $credit->id }
1179 # Update the account status
1180 $lost_charge->status('FOUND');
1181 $lost_charge->store();
1183 # Reconcile balances if required
1184 if ( C4::Context->preference('AccountAutoReconcile') ) {
1185 $account->reconcile_balance;
1190 # restore fine for lost book
1191 if ( $lostreturn_policy eq 'restore' ) {
1192 my $lost_overdue = Koha::Account::Lines->search(
1194 itemnumber => $self->itemnumber,
1195 debit_type_code => 'OVERDUE',
1199 order_by => { '-desc' => 'date' },
1204 if ( $lost_overdue ) {
1206 my $patron = $lost_overdue->patron;
1208 my $account = $patron->account;
1210 # Update status of fine
1211 $lost_overdue->status('FOUND')->store();
1213 # Find related forgive credit
1214 my $refund = $lost_overdue->credits(
1216 credit_type_code => 'FORGIVEN',
1217 itemnumber => $self->itemnumber,
1218 status => [ { '!=' => 'VOID' }, undef ]
1220 { order_by => { '-desc' => 'date' }, rows => 1 }
1224 # Revert the forgive credit
1225 $refund->void({ interface => 'trigger' });
1229 message => 'lost_restored',
1230 payload => { refund_id => $refund->id }
1235 # Reconcile balances if required
1236 if ( C4::Context->preference('AccountAutoReconcile') ) {
1237 $account->reconcile_balance;
1241 } elsif ( $lostreturn_policy eq 'charge' ) {
1245 message => 'lost_charge',
1254 =head3 public_read_list
1256 This method returns the list of publicly readable database fields for both API and UI output purposes
1260 sub public_read_list {
1262 'itemnumber', 'biblionumber', 'homebranch',
1263 'holdingbranch', 'location', 'collectioncode',
1264 'itemcallnumber', 'copynumber', 'enumchron',
1265 'barcode', 'dateaccessioned', 'itemnotes',
1266 'onloan', 'uri', 'itype',
1267 'notforloan', 'damaged', 'itemlost',
1268 'withdrawn', 'restricted'
1274 Overloaded to_api method to ensure item-level itypes is adhered to.
1279 my ($self, $params) = @_;
1281 my $response = $self->SUPER::to_api($params);
1284 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1286 return { %$response, %$overrides };
1289 =head3 to_api_mapping
1291 This method returns the mapping for representing a Koha::Item object
1296 sub to_api_mapping {
1298 itemnumber => 'item_id',
1299 biblionumber => 'biblio_id',
1300 biblioitemnumber => undef,
1301 barcode => 'external_id',
1302 dateaccessioned => 'acquisition_date',
1303 booksellerid => 'acquisition_source',
1304 homebranch => 'home_library_id',
1305 price => 'purchase_price',
1306 replacementprice => 'replacement_price',
1307 replacementpricedate => 'replacement_price_date',
1308 datelastborrowed => 'last_checkout_date',
1309 datelastseen => 'last_seen_date',
1311 notforloan => 'not_for_loan_status',
1312 damaged => 'damaged_status',
1313 damaged_on => 'damaged_date',
1314 itemlost => 'lost_status',
1315 itemlost_on => 'lost_date',
1316 withdrawn => 'withdrawn',
1317 withdrawn_on => 'withdrawn_date',
1318 itemcallnumber => 'callnumber',
1319 coded_location_qualifier => 'coded_location_qualifier',
1320 issues => 'checkouts_count',
1321 renewals => 'renewals_count',
1322 reserves => 'holds_count',
1323 restricted => 'restricted_status',
1324 itemnotes => 'public_notes',
1325 itemnotes_nonpublic => 'internal_notes',
1326 holdingbranch => 'holding_library_id',
1327 timestamp => 'timestamp',
1328 location => 'location',
1329 permanent_location => 'permanent_location',
1330 onloan => 'checked_out_date',
1331 cn_source => 'call_number_source',
1332 cn_sort => 'call_number_sort',
1333 ccode => 'collection_code',
1334 materials => 'materials_notes',
1336 itype => 'item_type_id',
1337 more_subfields_xml => 'extended_subfields',
1338 enumchron => 'serial_issue_number',
1339 copynumber => 'copy_number',
1340 stocknumber => 'inventory_number',
1341 new_status => 'new_status'
1347 my $itemtype = $item->itemtype;
1349 Returns Koha object for effective itemtype
1355 return Koha::ItemTypes->find( $self->effective_itemtype );
1360 my $orders = $item->orders();
1362 Returns a Koha::Acquisition::Orders object
1369 my $orders = $self->_result->item_orders;
1370 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1373 =head3 tracked_links
1375 my $tracked_links = $item->tracked_links();
1377 Returns a Koha::TrackedLinks object
1384 my $tracked_links = $self->_result->linktrackers;
1385 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1388 =head3 move_to_biblio
1390 $item->move_to_biblio($to_biblio[, $params]);
1392 Move the item to another biblio and update any references in other tables.
1394 The final optional parameter, C<$params>, is expected to contain the
1395 'skip_record_index' key, which is relayed down to Koha::Item->store.
1396 There it prevents calling index_records, which takes most of the
1397 time in batch adds/deletes. The caller must take care of calling
1398 index_records separately.
1401 skip_record_index => 1|0
1403 Returns undef if the move failed or the biblionumber of the destination record otherwise
1407 sub move_to_biblio {
1408 my ( $self, $to_biblio, $params ) = @_;
1412 return if $self->biblionumber == $to_biblio->biblionumber;
1414 my $from_biblionumber = $self->biblionumber;
1415 my $to_biblionumber = $to_biblio->biblionumber;
1417 # Own biblionumber and biblioitemnumber
1419 biblionumber => $to_biblionumber,
1420 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1421 })->store({ skip_record_index => $params->{skip_record_index} });
1423 unless ($params->{skip_record_index}) {
1424 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1425 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1428 # Acquisition orders
1429 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1432 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1434 # hold_fill_target (there's no Koha object available yet)
1435 my $hold_fill_target = $self->_result->hold_fill_target;
1436 if ($hold_fill_target) {
1437 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1440 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1441 # and can't even fake one since the significant columns are nullable.
1442 my $storage = $self->_result->result_source->storage;
1445 my ($storage, $dbh, @cols) = @_;
1447 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1452 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1454 return $to_biblionumber;
1457 =head2 Internal methods
1459 =head3 _after_item_action_hooks
1461 Helper method that takes care of calling all plugin hooks
1465 sub _after_item_action_hooks {
1466 my ( $self, $params ) = @_;
1468 my $action = $params->{action};
1470 Koha::Plugins->call(
1471 'after_item_action',
1475 item_id => $self->itemnumber,
1482 my $recall = $item->recall;
1484 Return the relevant recall for this item
1490 my @recalls = Koha::Recalls->search(
1492 biblio_id => $self->biblionumber,
1495 { order_by => { -asc => 'created_date' } }
1497 foreach my $recall (@recalls) {
1498 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1502 # no item-level recall to return, so return earliest biblio-level
1503 # FIXME: eventually this will be based on priority
1507 =head3 can_be_recalled
1509 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1511 Does item-level checks and returns if items can be recalled by this borrower
1515 sub can_be_recalled {
1516 my ( $self, $params ) = @_;
1518 return 0 if !( C4::Context->preference('UseRecalls') );
1520 # check if this item is not for loan, withdrawn or lost
1521 return 0 if ( $self->notforloan != 0 );
1522 return 0 if ( $self->itemlost != 0 );
1523 return 0 if ( $self->withdrawn != 0 );
1525 # check if this item is not checked out - if not checked out, can't be recalled
1526 return 0 if ( !defined( $self->checkout ) );
1528 my $patron = $params->{patron};
1530 my $branchcode = C4::Context->userenv->{'branch'};
1532 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1535 # Check the circulation rule for each relevant itemtype for this item
1536 my $rule = Koha::CirculationRules->get_effective_rules({
1537 branchcode => $branchcode,
1538 categorycode => $patron ? $patron->categorycode : undef,
1539 itemtype => $self->effective_itemtype,
1542 'recalls_per_record',
1547 # check recalls allowed has been set and is not zero
1548 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1551 # check borrower has not reached open recalls allowed limit
1552 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1554 # check borrower has not reach open recalls allowed per record limit
1555 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1557 # check if this patron has already recalled this item
1558 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1560 # check if this patron has already checked out this item
1561 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1563 # check if this patron has already reserved this item
1564 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1567 # check item availability
1568 # items are unavailable for recall if they are lost, withdrawn or notforloan
1569 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1571 # if there are no available items at all, no recall can be placed
1572 return 0 if ( scalar @items == 0 );
1574 my $checked_out_count = 0;
1576 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1579 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1580 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1582 # can't recall if no items have been checked out
1583 return 0 if ( $checked_out_count == 0 );
1589 =head3 can_be_waiting_recall
1591 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1593 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1594 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1598 sub can_be_waiting_recall {
1601 return 0 if !( C4::Context->preference('UseRecalls') );
1603 # check if this item is not for loan, withdrawn or lost
1604 return 0 if ( $self->notforloan != 0 );
1605 return 0 if ( $self->itemlost != 0 );
1606 return 0 if ( $self->withdrawn != 0 );
1608 my $branchcode = $self->holdingbranch;
1609 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1610 $branchcode = C4::Context->userenv->{'branch'};
1612 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1615 # Check the circulation rule for each relevant itemtype for this item
1616 my $rule = Koha::CirculationRules->get_effective_rules({
1617 branchcode => $branchcode,
1618 categorycode => undef,
1619 itemtype => $self->effective_itemtype,
1625 # check recalls allowed has been set and is not zero
1626 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1632 =head3 check_recalls
1634 my $recall = $item->check_recalls;
1636 Get the most relevant recall for this item.
1643 my @recalls = Koha::Recalls->search(
1644 { biblio_id => $self->biblionumber,
1645 item_id => [ $self->itemnumber, undef ]
1647 { order_by => { -asc => 'created_date' } }
1648 )->filter_by_current->as_list;
1651 # iterate through relevant recalls to find the best one.
1652 # if we come across a waiting recall, use this one.
1653 # if we have iterated through all recalls and not found a waiting recall, use the first recall in the array, which should be the oldest recall.
1654 foreach my $r ( @recalls ) {
1655 if ( $r->waiting ) {
1660 unless ( defined $recall ) {
1661 $recall = $recalls[0];
1677 Kyle M Hall <kyle@bywatersolutions.com>