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 Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::Exceptions::Item::Transfer;
40 use Koha::Item::Attributes;
41 use Koha::Item::Transfer::Limits;
42 use Koha::Item::Transfers;
47 use Koha::Result::Boolean;
48 use Koha::SearchEngine::Indexer;
49 use Koha::StockRotationItem;
50 use Koha::StockRotationRotas;
51 use Koha::TrackedLinks;
53 use base qw(Koha::Object);
57 Koha::Item - Koha Item object class
69 $params can take an optional 'skip_record_index' parameter.
70 If set, the reindexation process will not happen (index_records not called)
72 NOTE: This is a temporary fix to answer a performance issue when lot of items
73 are added (or modified) at the same time.
74 The correct way to fix this is to make the ES reindexation process async.
75 You should not turn it on if you do not understand what it is doing exactly.
81 my $params = @_ ? shift : {};
83 my $log_action = $params->{log_action} // 1;
85 # We do not want to oblige callers to pass this value
86 # Dev conveniences vs performance?
87 unless ( $self->biblioitemnumber ) {
88 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
91 # See related changes from C4::Items::AddItem
92 unless ( $self->itype ) {
93 $self->itype($self->biblio->biblioitem->itemtype);
96 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
98 my $today = dt_from_string;
99 my $action = 'create';
101 unless ( $self->in_storage ) { #AddItem
103 unless ( $self->permanent_location ) {
104 $self->permanent_location($self->location);
107 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
108 unless ( $self->location || !$default_location ) {
109 $self->permanent_location( $self->location || $default_location )
110 unless $self->permanent_location;
111 $self->location($default_location);
114 unless ( $self->replacementpricedate ) {
115 $self->replacementpricedate($today);
117 unless ( $self->datelastseen ) {
118 $self->datelastseen($today);
121 unless ( $self->dateaccessioned ) {
122 $self->dateaccessioned($today);
125 if ( $self->itemcallnumber
126 or $self->cn_source )
128 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
129 $self->cn_sort($cn_sort);
136 my %updated_columns = $self->_result->get_dirty_columns;
137 return $self->SUPER::store unless %updated_columns;
139 # Retrieve the item for comparison if we need to
141 exists $updated_columns{itemlost}
142 or exists $updated_columns{withdrawn}
143 or exists $updated_columns{damaged}
144 ) ? $self->get_from_storage : undef;
146 # Update *_on fields if needed
147 # FIXME: Why not for AddItem as well?
148 my @fields = qw( itemlost withdrawn damaged );
149 for my $field (@fields) {
151 # If the field is defined but empty or 0, we are
152 # removing/unsetting and thus need to clear out
154 if ( exists $updated_columns{$field}
155 && defined( $self->$field )
158 my $field_on = "${field}_on";
159 $self->$field_on(undef);
161 # If the field has changed otherwise, we much update
163 elsif (exists $updated_columns{$field}
164 && $updated_columns{$field}
165 && !$pre_mod_item->$field )
167 my $field_on = "${field}_on";
169 DateTime::Format::MySQL->format_datetime(
176 if ( exists $updated_columns{itemcallnumber}
177 or exists $updated_columns{cn_source} )
179 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
180 $self->cn_sort($cn_sort);
184 if ( exists $updated_columns{location}
185 and $self->location ne 'CART'
186 and $self->location ne 'PROC'
187 and not exists $updated_columns{permanent_location} )
189 $self->permanent_location( $self->location );
192 # If item was lost and has now been found,
193 # reverse any list item charges if necessary.
194 if ( exists $updated_columns{itemlost}
195 and $updated_columns{itemlost} <= 0
196 and $pre_mod_item->itemlost > 0 )
198 $self->_set_found_trigger($pre_mod_item);
203 my $result = $self->SUPER::store;
204 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
206 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
207 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
209 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
210 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
211 unless $params->{skip_record_index};
212 $self->get_from_storage->_after_item_action_hooks({ action => $action });
214 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
216 biblio_ids => [ $self->biblionumber ]
218 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
229 my $params = @_ ? shift : {};
231 # FIXME check the item has no current issues
232 # i.e. raise the appropriate exception
234 # Get the item group so we can delete it later if it has no items left
235 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
237 my $result = $self->SUPER::delete;
239 # Delete the item gorup if it has no items left
240 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
242 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
243 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
244 unless $params->{skip_record_index};
246 $self->_after_item_action_hooks({ action => 'delete' });
248 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
249 if C4::Context->preference("CataloguingLog");
251 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
253 biblio_ids => [ $self->biblionumber ]
255 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
266 my $params = @_ ? shift : {};
268 my $safe_to_delete = $self->safe_to_delete;
269 return $safe_to_delete unless $safe_to_delete;
271 $self->move_to_deleted;
273 return $self->delete($params);
276 =head3 safe_to_delete
278 returns 1 if the item is safe to delete,
280 "book_on_loan" if the item is checked out,
282 "not_same_branch" if the item is blocked by independent branches,
284 "book_reserved" if the there are holds aganst the item, or
286 "linked_analytics" if the item has linked analytic records.
288 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
297 $error = "book_on_loan" if $self->checkout;
299 $error = "not_same_branch"
300 if defined C4::Context->userenv
301 and !C4::Context->IsSuperLibrarian()
302 and C4::Context->preference("IndependentBranches")
303 and ( C4::Context->userenv->{branch} ne $self->homebranch );
305 # check it doesn't have a waiting reserve
306 $error = "book_reserved"
307 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
309 $error = "linked_analytics"
310 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
312 $error = "last_item_for_hold"
313 if $self->biblio->items->count == 1
314 && $self->biblio->holds->search(
321 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
324 return Koha::Result::Boolean->new(1);
327 =head3 move_to_deleted
329 my $is_moved = $item->move_to_deleted;
331 Move an item to the deleteditems table.
332 This can be done before deleting an item, to make sure the data are not completely deleted.
336 sub move_to_deleted {
338 my $item_infos = $self->unblessed;
339 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
340 $item_infos->{deleted_on} = dt_from_string;
341 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
345 =head3 effective_itemtype
347 Returns the itemtype for the item based on whether item level itemtypes are set or not.
351 sub effective_itemtype {
354 return $self->_result()->effective_itemtype();
364 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
366 return $self->{_home_branch};
369 =head3 holding_branch
376 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
378 return $self->{_holding_branch};
383 my $biblio = $item->biblio;
385 Return the bibliographic record of this item
391 my $biblio_rs = $self->_result->biblio;
392 return Koha::Biblio->_new_from_dbic( $biblio_rs );
397 my $biblioitem = $item->biblioitem;
399 Return the biblioitem record of this item
405 my $biblioitem_rs = $self->_result->biblioitem;
406 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
411 my $checkout = $item->checkout;
413 Return the checkout for this item
419 my $checkout_rs = $self->_result->issue;
420 return unless $checkout_rs;
421 return Koha::Checkout->_new_from_dbic( $checkout_rs );
426 my $item_group = $item->item_group;
428 Return the item group for this item
435 my $item_group_item = $self->_result->item_group_item;
436 return unless $item_group_item;
438 my $item_group_rs = $item_group_item->item_group;
439 return unless $item_group_rs;
441 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
447 my $return_claims = $item->return_claims;
449 Return any return_claims associated with this item
454 my ( $self, $params, $attrs ) = @_;
455 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
456 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
461 my $return_claim = $item->return_claim;
463 Returns the most recent unresolved return_claims associated with this item
470 $self->_result->return_claims->search( { resolution => undef },
471 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
472 return unless $claims_rs;
473 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
478 my $holds = $item->holds();
479 my $holds = $item->holds($params);
480 my $holds = $item->holds({ found => 'W'});
482 Return holds attached to an item, optionally accept a hashref of params to pass to search
487 my ( $self,$params ) = @_;
488 my $holds_rs = $self->_result->reserves->search($params);
489 return Koha::Holds->_new_from_dbic( $holds_rs );
492 =head3 request_transfer
494 my $transfer = $item->request_transfer(
498 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
502 Add a transfer request for this item to the given branch for the given reason.
504 An exception will be thrown if the BranchTransferLimits would prevent the requested
505 transfer, unless 'ignore_limits' is passed to override the limits.
507 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
508 The caller should catch such cases and retry the transfer request as appropriate passing
509 an appropriate override.
512 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
513 * replace - Used to replace the existing transfer request with your own.
517 sub request_transfer {
518 my ( $self, $params ) = @_;
520 # check for mandatory params
521 my @mandatory = ( 'to', 'reason' );
522 for my $param (@mandatory) {
523 unless ( defined( $params->{$param} ) ) {
524 Koha::Exceptions::MissingParameter->throw(
525 error => "The $param parameter is mandatory" );
529 Koha::Exceptions::Item::Transfer::Limit->throw()
530 unless ( $params->{ignore_limits}
531 || $self->can_be_transferred( { to => $params->{to} } ) );
533 my $request = $self->get_transfer;
534 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
535 if ( $request && !$params->{enqueue} && !$params->{replace} );
537 $request->cancel( { reason => $params->{reason}, force => 1 } )
538 if ( defined($request) && $params->{replace} );
540 my $transfer = Koha::Item::Transfer->new(
542 itemnumber => $self->itemnumber,
543 daterequested => dt_from_string,
544 frombranch => $self->holdingbranch,
545 tobranch => $params->{to}->branchcode,
546 reason => $params->{reason},
547 comments => $params->{comment}
556 my $transfer = $item->get_transfer;
558 Return the active transfer request or undef
560 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
561 whereby the most recently sent, but not received, transfer will be returned
562 if it exists, otherwise the oldest unsatisfied transfer will be returned.
564 This allows for transfers to queue, which is the case for stock rotation and
565 rotating collections where a manual transfer may need to take precedence but
566 we still expect the item to end up at a final location eventually.
573 return $self->get_transfers->search( {}, { rows => 1 } )->next;
578 my $transfer = $item->get_transfers;
580 Return the list of outstanding transfers (i.e requested but not yet cancelled
583 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
584 whereby the most recently sent, but not received, transfer will be returned
585 first if it exists, otherwise requests are in oldest to newest request order.
587 This allows for transfers to queue, which is the case for stock rotation and
588 rotating collections where a manual transfer may need to take precedence but
589 we still expect the item to end up at a final location eventually.
596 my $transfer_rs = $self->_result->branchtransfers;
598 return Koha::Item::Transfers
599 ->_new_from_dbic($transfer_rs)
601 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
604 =head3 last_returned_by
606 Gets and sets the last borrower to return an item.
608 Accepts and returns Koha::Patron objects
610 $item->last_returned_by( $borrowernumber );
612 $last_returned_by = $item->last_returned_by();
616 sub last_returned_by {
617 my ( $self, $borrower ) = @_;
619 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
622 return $items_last_returned_by_rs->update_or_create(
623 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
626 unless ( $self->{_last_returned_by} ) {
627 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
629 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
633 return $self->{_last_returned_by};
637 =head3 can_article_request
639 my $bool = $item->can_article_request( $borrower )
641 Returns true if item can be specifically requested
643 $borrower must be a Koha::Patron object
647 sub can_article_request {
648 my ( $self, $borrower ) = @_;
650 my $rule = $self->article_request_type($borrower);
652 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
656 =head3 hidden_in_opac
658 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
660 Returns true if item fields match the hidding criteria defined in $rules.
661 Returns false otherwise.
663 Takes HASHref that can have the following parameters:
665 $rules : { <field> => [ value_1, ... ], ... }
667 Note: $rules inherits its structure from the parsed YAML from reading
668 the I<OpacHiddenItems> system preference.
673 my ( $self, $params ) = @_;
675 my $rules = $params->{rules} // {};
678 if C4::Context->preference('hidelostitems') and
681 my $hidden_in_opac = 0;
683 foreach my $field ( keys %{$rules} ) {
685 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
691 return $hidden_in_opac;
694 =head3 can_be_transferred
696 $item->can_be_transferred({ to => $to_library, from => $from_library })
697 Checks if an item can be transferred to given library.
699 This feature is controlled by two system preferences:
700 UseBranchTransferLimits to enable / disable the feature
701 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
702 for setting the limitations
704 Takes HASHref that can have the following parameters:
705 MANDATORY PARAMETERS:
708 $from : Koha::Library # if not given, item holdingbranch
709 # will be used instead
711 Returns 1 if item can be transferred to $to_library, otherwise 0.
713 To find out whether at least one item of a Koha::Biblio can be transferred, please
714 see Koha::Biblio->can_be_transferred() instead of using this method for
715 multiple items of the same biblio.
719 sub can_be_transferred {
720 my ($self, $params) = @_;
722 my $to = $params->{to};
723 my $from = $params->{from};
725 $to = $to->branchcode;
726 $from = defined $from ? $from->branchcode : $self->holdingbranch;
728 return 1 if $from eq $to; # Transfer to current branch is allowed
729 return 1 unless C4::Context->preference('UseBranchTransferLimits');
731 my $limittype = C4::Context->preference('BranchTransferLimitsType');
732 return Koha::Item::Transfer::Limits->search({
735 $limittype => $limittype eq 'itemtype'
736 ? $self->effective_itemtype : $self->ccode
741 =head3 pickup_locations
743 $pickup_locations = $item->pickup_locations( {patron => $patron } )
745 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)
746 and if item can be transferred to each pickup location.
750 sub pickup_locations {
751 my ($self, $params) = @_;
753 my $patron = $params->{patron};
755 my $circ_control_branch =
756 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
758 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
760 if(defined $patron) {
761 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
762 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
765 my $pickup_libraries = Koha::Libraries->search();
766 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
767 $pickup_libraries = $self->home_branch->get_hold_libraries;
768 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
769 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
770 $pickup_libraries = $plib->get_hold_libraries;
771 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
772 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
773 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
774 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
777 return $pickup_libraries->search(
782 order_by => ['branchname']
784 ) unless C4::Context->preference('UseBranchTransferLimits');
786 my $limittype = C4::Context->preference('BranchTransferLimitsType');
787 my ($ccode, $itype) = (undef, undef);
788 if( $limittype eq 'ccode' ){
789 $ccode = $self->ccode;
791 $itype = $self->itype;
793 my $limits = Koha::Item::Transfer::Limits->search(
795 fromBranch => $self->holdingbranch,
799 { columns => ['toBranch'] }
802 return $pickup_libraries->search(
804 pickup_location => 1,
806 '-not_in' => $limits->_resultset->as_query
810 order_by => ['branchname']
815 =head3 article_request_type
817 my $type = $item->article_request_type( $borrower )
819 returns 'yes', 'no', 'bib_only', or 'item_only'
821 $borrower must be a Koha::Patron object
825 sub article_request_type {
826 my ( $self, $borrower ) = @_;
828 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
830 $branch_control eq 'homebranch' ? $self->homebranch
831 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
833 my $borrowertype = $borrower->categorycode;
834 my $itemtype = $self->effective_itemtype();
835 my $rule = Koha::CirculationRules->get_effective_rule(
837 rule_name => 'article_requests',
838 categorycode => $borrowertype,
839 itemtype => $itemtype,
840 branchcode => $branchcode
844 return q{} unless $rule;
845 return $rule->rule_value || q{}
854 my $attributes = { order_by => 'priority' };
855 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
857 itemnumber => $self->itemnumber,
860 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
861 waitingdate => { '!=' => undef },
864 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
865 return Koha::Holds->_new_from_dbic($hold_rs);
868 =head3 stockrotationitem
870 my $sritem = Koha::Item->stockrotationitem;
872 Returns the stock rotation item associated with the current item.
876 sub stockrotationitem {
878 my $rs = $self->_result->stockrotationitem;
880 return Koha::StockRotationItem->_new_from_dbic( $rs );
885 my $item = $item->add_to_rota($rota_id);
887 Add this item to the rota identified by $ROTA_ID, which means associating it
888 with the first stage of that rota. Should this item already be associated
889 with a rota, then we will move it to the new rota.
894 my ( $self, $rota_id ) = @_;
895 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
899 =head3 has_pending_hold
901 my $is_pending_hold = $item->has_pending_hold();
903 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
907 sub has_pending_hold {
909 my $pending_hold = $self->_result->tmp_holdsqueues;
910 return $pending_hold->count ? 1: 0;
915 my $field = $item->as_marc_field;
917 This method returns a MARC::Field object representing the Koha::Item object
918 with the current mappings configuration.
925 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
927 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
931 my $item_field = $tagslib->{$itemtag};
933 my $more_subfields = $self->additional_attributes->to_hashref;
934 foreach my $subfield (
936 $a->{display_order} <=> $b->{display_order}
937 || $a->{subfield} cmp $b->{subfield}
938 } grep { ref($_) && %$_ } values %$item_field
941 my $kohafield = $subfield->{kohafield};
942 my $tagsubfield = $subfield->{tagsubfield};
944 if ( defined $kohafield ) {
945 next if $kohafield !~ m{^items\.}; # That would be weird!
946 ( my $attribute = $kohafield ) =~ s|^items\.||;
947 $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
948 if defined $self->$attribute and $self->$attribute ne '';
950 $value = $more_subfields->{$tagsubfield}
953 next unless defined $value
956 if ( $subfield->{repeatable} ) {
957 my @values = split '\|', $value;
958 push @subfields, ( $tagsubfield => $_ ) for @values;
961 push @subfields, ( $tagsubfield => $value );
966 return unless @subfields;
968 return MARC::Field->new(
969 "$itemtag", ' ', ' ', @subfields
973 =head3 renewal_branchcode
975 Returns the branchcode to be recorded in statistics renewal of the item
979 sub renewal_branchcode {
981 my ($self, $params ) = @_;
983 my $interface = C4::Context->interface;
985 if ( $interface eq 'opac' ){
986 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
987 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
988 $branchcode = 'OPACRenew';
990 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
991 $branchcode = $self->homebranch;
993 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
994 $branchcode = $self->checkout->patron->branchcode;
996 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
997 $branchcode = $self->checkout->branchcode;
1003 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1004 ? C4::Context->userenv->{branch} : $params->{branch};
1011 Return the cover images associated with this item.
1018 my $cover_image_rs = $self->_result->cover_images;
1019 return unless $cover_image_rs;
1020 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1023 =head3 columns_to_str
1025 my $values = $items->columns_to_str;
1027 Return a hashref with the string representation of the different attribute of the item.
1029 This is meant to be used for display purpose only.
1033 sub columns_to_str {
1036 my $frameworkcode = $self->biblio->frameworkcode;
1037 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1038 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1040 my $columns_info = $self->_result->result_source->columns_info;
1042 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1044 for my $column ( keys %$columns_info ) {
1046 next if $column eq 'more_subfields_xml';
1048 my $value = $self->$column;
1049 # 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
1051 if ( not defined $value or $value eq "" ) {
1052 $values->{$column} = $value;
1057 exists $mss->{"items.$column"}
1058 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1061 $values->{$column} =
1063 ? $subfield->{authorised_value}
1064 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1065 $subfield->{tagsubfield}, $value, '', $tagslib )
1071 $self->more_subfields_xml
1072 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1077 my ( $field ) = $marc_more->fields;
1078 for my $sf ( $field->subfields ) {
1079 my $subfield_code = $sf->[0];
1080 my $value = $sf->[1];
1081 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1082 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1084 $subfield->{authorised_value}
1085 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1086 $subfield->{tagsubfield}, $value, '', $tagslib )
1089 push @{$more_values->{$subfield_code}}, $value;
1092 while ( my ( $k, $v ) = each %$more_values ) {
1093 $values->{$k} = join ' | ', @$v;
1100 =head3 additional_attributes
1102 my $attributes = $item->additional_attributes;
1103 $attributes->{k} = 'new k';
1104 $item->update({ more_subfields => $attributes->to_marcxml });
1106 Returns a Koha::Item::Attributes object that represents the non-mapped
1107 attributes for this item.
1111 sub additional_attributes {
1114 return Koha::Item::Attributes->new_from_marcxml(
1115 $self->more_subfields_xml,
1119 =head3 _set_found_trigger
1121 $self->_set_found_trigger
1123 Finds the most recent lost item charge for this item and refunds the patron
1124 appropriately, taking into account any payments or writeoffs already applied
1127 Internal function, not exported, called only by Koha::Item->store.
1131 sub _set_found_trigger {
1132 my ( $self, $pre_mod_item ) = @_;
1134 # Reverse any lost item charges if necessary.
1135 my $no_refund_after_days =
1136 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1137 if ($no_refund_after_days) {
1138 my $today = dt_from_string();
1139 my $lost_age_in_days =
1140 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1143 return $self unless $lost_age_in_days < $no_refund_after_days;
1146 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1149 return_branch => C4::Context->userenv
1150 ? C4::Context->userenv->{'branch'}
1155 if ( $lostreturn_policy ) {
1157 # refund charge made for lost book
1158 my $lost_charge = Koha::Account::Lines->search(
1160 itemnumber => $self->itemnumber,
1161 debit_type_code => 'LOST',
1162 status => [ undef, { '<>' => 'FOUND' } ]
1165 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1170 if ( $lost_charge ) {
1172 my $patron = $lost_charge->patron;
1175 my $account = $patron->account;
1176 my $total_to_refund = 0;
1179 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1181 # some amount has been cancelled. collect the offsets that are not writeoffs
1182 # this works because the only way to subtract from this kind of a debt is
1183 # using the UI buttons 'Pay' and 'Write off'
1184 my $credit_offsets = $lost_charge->debit_offsets(
1186 'credit_id' => { '!=' => undef },
1187 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1189 { join => 'credit' }
1192 $total_to_refund = ( $credit_offsets->count > 0 )
1193 ? $credit_offsets->total * -1 # credits are negative on the DB
1197 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1200 if ( $credit_total > 0 ) {
1202 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1203 $credit = $account->add_credit(
1205 amount => $credit_total,
1206 description => 'Item found ' . $self->itemnumber,
1207 type => 'LOST_FOUND',
1208 interface => C4::Context->interface,
1209 library_id => $branchcode,
1210 item_id => $self->itemnumber,
1211 issue_id => $lost_charge->issue_id
1215 $credit->apply( { debits => [$lost_charge] } );
1219 message => 'lost_refunded',
1220 payload => { credit_id => $credit->id }
1225 # Update the account status
1226 $lost_charge->status('FOUND');
1227 $lost_charge->store();
1229 # Reconcile balances if required
1230 if ( C4::Context->preference('AccountAutoReconcile') ) {
1231 $account->reconcile_balance;
1236 # restore fine for lost book
1237 if ( $lostreturn_policy eq 'restore' ) {
1238 my $lost_overdue = Koha::Account::Lines->search(
1240 itemnumber => $self->itemnumber,
1241 debit_type_code => 'OVERDUE',
1245 order_by => { '-desc' => 'date' },
1250 if ( $lost_overdue ) {
1252 my $patron = $lost_overdue->patron;
1254 my $account = $patron->account;
1256 # Update status of fine
1257 $lost_overdue->status('FOUND')->store();
1259 # Find related forgive credit
1260 my $refund = $lost_overdue->credits(
1262 credit_type_code => 'FORGIVEN',
1263 itemnumber => $self->itemnumber,
1264 status => [ { '!=' => 'VOID' }, undef ]
1266 { order_by => { '-desc' => 'date' }, rows => 1 }
1270 # Revert the forgive credit
1271 $refund->void({ interface => 'trigger' });
1275 message => 'lost_restored',
1276 payload => { refund_id => $refund->id }
1281 # Reconcile balances if required
1282 if ( C4::Context->preference('AccountAutoReconcile') ) {
1283 $account->reconcile_balance;
1287 } elsif ( $lostreturn_policy eq 'charge' ) {
1291 message => 'lost_charge',
1300 =head3 public_read_list
1302 This method returns the list of publicly readable database fields for both API and UI output purposes
1306 sub public_read_list {
1308 'itemnumber', 'biblionumber', 'homebranch',
1309 'holdingbranch', 'location', 'collectioncode',
1310 'itemcallnumber', 'copynumber', 'enumchron',
1311 'barcode', 'dateaccessioned', 'itemnotes',
1312 'onloan', 'uri', 'itype',
1313 'notforloan', 'damaged', 'itemlost',
1314 'withdrawn', 'restricted'
1320 Overloaded to_api method to ensure item-level itypes is adhered to.
1325 my ($self, $params) = @_;
1327 my $response = $self->SUPER::to_api($params);
1330 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1331 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1333 return { %$response, %$overrides };
1336 =head3 to_api_mapping
1338 This method returns the mapping for representing a Koha::Item object
1343 sub to_api_mapping {
1345 itemnumber => 'item_id',
1346 biblionumber => 'biblio_id',
1347 biblioitemnumber => undef,
1348 barcode => 'external_id',
1349 dateaccessioned => 'acquisition_date',
1350 booksellerid => 'acquisition_source',
1351 homebranch => 'home_library_id',
1352 price => 'purchase_price',
1353 replacementprice => 'replacement_price',
1354 replacementpricedate => 'replacement_price_date',
1355 datelastborrowed => 'last_checkout_date',
1356 datelastseen => 'last_seen_date',
1358 notforloan => 'not_for_loan_status',
1359 damaged => 'damaged_status',
1360 damaged_on => 'damaged_date',
1361 itemlost => 'lost_status',
1362 itemlost_on => 'lost_date',
1363 withdrawn => 'withdrawn',
1364 withdrawn_on => 'withdrawn_date',
1365 itemcallnumber => 'callnumber',
1366 coded_location_qualifier => 'coded_location_qualifier',
1367 issues => 'checkouts_count',
1368 renewals => 'renewals_count',
1369 reserves => 'holds_count',
1370 restricted => 'restricted_status',
1371 itemnotes => 'public_notes',
1372 itemnotes_nonpublic => 'internal_notes',
1373 holdingbranch => 'holding_library_id',
1374 timestamp => 'timestamp',
1375 location => 'location',
1376 permanent_location => 'permanent_location',
1377 onloan => 'checked_out_date',
1378 cn_source => 'call_number_source',
1379 cn_sort => 'call_number_sort',
1380 ccode => 'collection_code',
1381 materials => 'materials_notes',
1383 itype => 'item_type_id',
1384 more_subfields_xml => 'extended_subfields',
1385 enumchron => 'serial_issue_number',
1386 copynumber => 'copy_number',
1387 stocknumber => 'inventory_number',
1388 new_status => 'new_status',
1389 deleted_on => undef,
1395 my $itemtype = $item->itemtype;
1397 Returns Koha object for effective itemtype
1403 return Koha::ItemTypes->find( $self->effective_itemtype );
1408 my $orders = $item->orders();
1410 Returns a Koha::Acquisition::Orders object
1417 my $orders = $self->_result->item_orders;
1418 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1421 =head3 tracked_links
1423 my $tracked_links = $item->tracked_links();
1425 Returns a Koha::TrackedLinks object
1432 my $tracked_links = $self->_result->linktrackers;
1433 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1436 =head3 move_to_biblio
1438 $item->move_to_biblio($to_biblio[, $params]);
1440 Move the item to another biblio and update any references in other tables.
1442 The final optional parameter, C<$params>, is expected to contain the
1443 'skip_record_index' key, which is relayed down to Koha::Item->store.
1444 There it prevents calling index_records, which takes most of the
1445 time in batch adds/deletes. The caller must take care of calling
1446 index_records separately.
1449 skip_record_index => 1|0
1451 Returns undef if the move failed or the biblionumber of the destination record otherwise
1455 sub move_to_biblio {
1456 my ( $self, $to_biblio, $params ) = @_;
1460 return if $self->biblionumber == $to_biblio->biblionumber;
1462 my $from_biblionumber = $self->biblionumber;
1463 my $to_biblionumber = $to_biblio->biblionumber;
1465 # Own biblionumber and biblioitemnumber
1467 biblionumber => $to_biblionumber,
1468 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1469 })->store({ skip_record_index => $params->{skip_record_index} });
1471 unless ($params->{skip_record_index}) {
1472 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1473 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1476 # Acquisition orders
1477 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1480 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1482 # hold_fill_target (there's no Koha object available yet)
1483 my $hold_fill_target = $self->_result->hold_fill_target;
1484 if ($hold_fill_target) {
1485 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1488 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1489 # and can't even fake one since the significant columns are nullable.
1490 my $storage = $self->_result->result_source->storage;
1493 my ($storage, $dbh, @cols) = @_;
1495 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1500 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1502 return $to_biblionumber;
1507 my $bundle_items = $item->bundle_items;
1509 Returns the items associated with this bundle
1516 if ( !$self->{_bundle_items_cached} ) {
1517 my $bundle_items = Koha::Items->search(
1518 { 'item_bundles_item.host' => $self->itemnumber },
1519 { join => 'item_bundles_item' } );
1520 $self->{_bundle_items} = $bundle_items;
1521 $self->{_bundle_items_cached} = 1;
1524 return $self->{_bundle_items};
1529 my $is_bundle = $item->is_bundle;
1531 Returns whether the item is a bundle or not
1537 return $self->bundle_items->count ? 1 : 0;
1542 my $bundle = $item->bundle_host;
1544 Returns the bundle item this item is attached to
1551 my $bundle_items_rs = $self->_result->item_bundles_item;
1552 return unless $bundle_items_rs;
1553 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1558 my $in_bundle = $item->in_bundle;
1560 Returns whether this item is currently in a bundle
1566 return $self->bundle_host ? 1 : 0;
1569 =head3 add_to_bundle
1571 my $link = $item->add_to_bundle($bundle_item);
1573 Adds the bundle_item passed to this item
1578 my ( $self, $bundle_item ) = @_;
1580 my $schema = Koha::Database->new->schema;
1582 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1587 $self->_result->add_to_item_bundles_hosts(
1588 { item => $bundle_item->itemnumber } );
1590 $bundle_item->notforloan($BundleNotLoanValue)->store();
1596 # FIXME: See if we can move the below copy/paste from Koha::Object::store into it's own class and catch at a lower level in the Schema instantiation, take inspiration from DBIx::Error
1597 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1599 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1601 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1602 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1603 Koha::Exceptions::Object::FKConstraint->throw(
1604 error => 'Broken FK constraint',
1605 broken_fk => $+{column}
1610 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1612 Koha::Exceptions::Object::DuplicateID->throw(
1613 error => 'Duplicate ID',
1614 duplicate_id => $+{key}
1617 elsif ( $_->{msg} =~
1618 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1620 { # The optional \W in the regex might be a quote or backtick
1621 my $type = $+{type};
1622 my $value = $+{value};
1623 my $property = $+{property};
1624 $property =~ s/['`]//g;
1625 Koha::Exceptions::Object::BadValue->throw(
1628 property => $property =~ /(\w+\.\w+)$/
1631 , # results in table.column without quotes or backtics
1635 # Catch-all for foreign key breakages. It will help find other use cases
1644 =head3 remove_from_bundle
1646 Remove this item from any bundle it may have been attached to.
1650 sub remove_from_bundle {
1653 my $bundle_item_rs = $self->_result->item_bundles_item;
1654 if ( $bundle_item_rs ) {
1655 $bundle_item_rs->delete;
1656 $self->notforloan(0)->store();
1662 =head2 Internal methods
1664 =head3 _after_item_action_hooks
1666 Helper method that takes care of calling all plugin hooks
1670 sub _after_item_action_hooks {
1671 my ( $self, $params ) = @_;
1673 my $action = $params->{action};
1675 Koha::Plugins->call(
1676 'after_item_action',
1680 item_id => $self->itemnumber,
1687 my $recall = $item->recall;
1689 Return the relevant recall for this item
1695 my @recalls = Koha::Recalls->search(
1697 biblio_id => $self->biblionumber,
1700 { order_by => { -asc => 'created_date' } }
1702 foreach my $recall (@recalls) {
1703 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1707 # no item-level recall to return, so return earliest biblio-level
1708 # FIXME: eventually this will be based on priority
1712 =head3 can_be_recalled
1714 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1716 Does item-level checks and returns if items can be recalled by this borrower
1720 sub can_be_recalled {
1721 my ( $self, $params ) = @_;
1723 return 0 if !( C4::Context->preference('UseRecalls') );
1725 # check if this item is not for loan, withdrawn or lost
1726 return 0 if ( $self->notforloan != 0 );
1727 return 0 if ( $self->itemlost != 0 );
1728 return 0 if ( $self->withdrawn != 0 );
1730 # check if this item is not checked out - if not checked out, can't be recalled
1731 return 0 if ( !defined( $self->checkout ) );
1733 my $patron = $params->{patron};
1735 my $branchcode = C4::Context->userenv->{'branch'};
1737 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1740 # Check the circulation rule for each relevant itemtype for this item
1741 my $rule = Koha::CirculationRules->get_effective_rules({
1742 branchcode => $branchcode,
1743 categorycode => $patron ? $patron->categorycode : undef,
1744 itemtype => $self->effective_itemtype,
1747 'recalls_per_record',
1752 # check recalls allowed has been set and is not zero
1753 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1756 # check borrower has not reached open recalls allowed limit
1757 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1759 # check borrower has not reach open recalls allowed per record limit
1760 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1762 # check if this patron has already recalled this item
1763 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1765 # check if this patron has already checked out this item
1766 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1768 # check if this patron has already reserved this item
1769 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1772 # check item availability
1773 # items are unavailable for recall if they are lost, withdrawn or notforloan
1774 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1776 # if there are no available items at all, no recall can be placed
1777 return 0 if ( scalar @items == 0 );
1779 my $checked_out_count = 0;
1781 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1784 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1785 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1787 # can't recall if no items have been checked out
1788 return 0 if ( $checked_out_count == 0 );
1794 =head3 can_be_waiting_recall
1796 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1798 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1799 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1803 sub can_be_waiting_recall {
1806 return 0 if !( C4::Context->preference('UseRecalls') );
1808 # check if this item is not for loan, withdrawn or lost
1809 return 0 if ( $self->notforloan != 0 );
1810 return 0 if ( $self->itemlost != 0 );
1811 return 0 if ( $self->withdrawn != 0 );
1813 my $branchcode = $self->holdingbranch;
1814 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1815 $branchcode = C4::Context->userenv->{'branch'};
1817 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1820 # Check the circulation rule for each relevant itemtype for this item
1821 my $rule = Koha::CirculationRules->get_effective_rules({
1822 branchcode => $branchcode,
1823 categorycode => undef,
1824 itemtype => $self->effective_itemtype,
1830 # check recalls allowed has been set and is not zero
1831 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1837 =head3 check_recalls
1839 my $recall = $item->check_recalls;
1841 Get the most relevant recall for this item.
1848 my @recalls = Koha::Recalls->search(
1849 { biblio_id => $self->biblionumber,
1850 item_id => [ $self->itemnumber, undef ]
1852 { order_by => { -asc => 'created_date' } }
1853 )->filter_by_current->as_list;
1856 # iterate through relevant recalls to find the best one.
1857 # if we come across a waiting recall, use this one.
1858 # 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.
1859 foreach my $r ( @recalls ) {
1860 if ( $r->waiting ) {
1865 unless ( defined $recall ) {
1866 $recall = $recalls[0];
1872 =head3 is_notforloan
1874 my $is_notforloan = $item->is_notforloan;
1876 Determine whether or not this item is "notforloan" based on
1877 the item's notforloan status or its item type
1883 my $is_notforloan = 0;
1885 if ( $self->notforloan ){
1889 my $itemtype = $self->itemtype;
1891 if ( $itemtype->notforloan ){
1897 return $is_notforloan;
1910 Kyle M Hall <kyle@bywatersolutions.com>