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 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
344 =head3 effective_itemtype
346 Returns the itemtype for the item based on whether item level itemtypes are set or not.
350 sub effective_itemtype {
353 return $self->_result()->effective_itemtype();
363 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
365 return $self->{_home_branch};
368 =head3 holding_branch
375 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
377 return $self->{_holding_branch};
382 my $biblio = $item->biblio;
384 Return the bibliographic record of this item
390 my $biblio_rs = $self->_result->biblio;
391 return Koha::Biblio->_new_from_dbic( $biblio_rs );
396 my $biblioitem = $item->biblioitem;
398 Return the biblioitem record of this item
404 my $biblioitem_rs = $self->_result->biblioitem;
405 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
410 my $checkout = $item->checkout;
412 Return the checkout for this item
418 my $checkout_rs = $self->_result->issue;
419 return unless $checkout_rs;
420 return Koha::Checkout->_new_from_dbic( $checkout_rs );
425 my $item_group = $item->item_group;
427 Return the item group for this item
434 my $item_group_item = $self->_result->item_group_item;
435 return unless $item_group_item;
437 my $item_group_rs = $item_group_item->item_group;
438 return unless $item_group_rs;
440 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
446 my $holds = $item->holds();
447 my $holds = $item->holds($params);
448 my $holds = $item->holds({ found => 'W'});
450 Return holds attached to an item, optionally accept a hashref of params to pass to search
455 my ( $self,$params ) = @_;
456 my $holds_rs = $self->_result->reserves->search($params);
457 return Koha::Holds->_new_from_dbic( $holds_rs );
460 =head3 request_transfer
462 my $transfer = $item->request_transfer(
466 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
470 Add a transfer request for this item to the given branch for the given reason.
472 An exception will be thrown if the BranchTransferLimits would prevent the requested
473 transfer, unless 'ignore_limits' is passed to override the limits.
475 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
476 The caller should catch such cases and retry the transfer request as appropriate passing
477 an appropriate override.
480 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
481 * replace - Used to replace the existing transfer request with your own.
485 sub request_transfer {
486 my ( $self, $params ) = @_;
488 # check for mandatory params
489 my @mandatory = ( 'to', 'reason' );
490 for my $param (@mandatory) {
491 unless ( defined( $params->{$param} ) ) {
492 Koha::Exceptions::MissingParameter->throw(
493 error => "The $param parameter is mandatory" );
497 Koha::Exceptions::Item::Transfer::Limit->throw()
498 unless ( $params->{ignore_limits}
499 || $self->can_be_transferred( { to => $params->{to} } ) );
501 my $request = $self->get_transfer;
502 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
503 if ( $request && !$params->{enqueue} && !$params->{replace} );
505 $request->cancel( { reason => $params->{reason}, force => 1 } )
506 if ( defined($request) && $params->{replace} );
508 my $transfer = Koha::Item::Transfer->new(
510 itemnumber => $self->itemnumber,
511 daterequested => dt_from_string,
512 frombranch => $self->holdingbranch,
513 tobranch => $params->{to}->branchcode,
514 reason => $params->{reason},
515 comments => $params->{comment}
524 my $transfer = $item->get_transfer;
526 Return the active transfer request or undef
528 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
529 whereby the most recently sent, but not received, transfer will be returned
530 if it exists, otherwise the oldest unsatisfied transfer will be returned.
532 This allows for transfers to queue, which is the case for stock rotation and
533 rotating collections where a manual transfer may need to take precedence but
534 we still expect the item to end up at a final location eventually.
540 my $transfer_rs = $self->_result->branchtransfers->search(
542 datearrived => undef,
543 datecancelled => undef
547 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
551 return unless $transfer_rs;
552 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
557 my $transfer = $item->get_transfers;
559 Return the list of outstanding transfers (i.e requested but not yet cancelled
562 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
563 whereby the most recently sent, but not received, transfer will be returned
564 first if it exists, otherwise requests are in oldest to newest request order.
566 This allows for transfers to queue, which is the case for stock rotation and
567 rotating collections where a manual transfer may need to take precedence but
568 we still expect the item to end up at a final location eventually.
574 my $transfer_rs = $self->_result->branchtransfers->search(
576 datearrived => undef,
577 datecancelled => undef
581 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
584 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
587 =head3 last_returned_by
589 Gets and sets the last borrower to return an item.
591 Accepts and returns Koha::Patron objects
593 $item->last_returned_by( $borrowernumber );
595 $last_returned_by = $item->last_returned_by();
599 sub last_returned_by {
600 my ( $self, $borrower ) = @_;
602 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
605 return $items_last_returned_by_rs->update_or_create(
606 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
609 unless ( $self->{_last_returned_by} ) {
610 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
612 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
616 return $self->{_last_returned_by};
620 =head3 can_article_request
622 my $bool = $item->can_article_request( $borrower )
624 Returns true if item can be specifically requested
626 $borrower must be a Koha::Patron object
630 sub can_article_request {
631 my ( $self, $borrower ) = @_;
633 my $rule = $self->article_request_type($borrower);
635 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
639 =head3 hidden_in_opac
641 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
643 Returns true if item fields match the hidding criteria defined in $rules.
644 Returns false otherwise.
646 Takes HASHref that can have the following parameters:
648 $rules : { <field> => [ value_1, ... ], ... }
650 Note: $rules inherits its structure from the parsed YAML from reading
651 the I<OpacHiddenItems> system preference.
656 my ( $self, $params ) = @_;
658 my $rules = $params->{rules} // {};
661 if C4::Context->preference('hidelostitems') and
664 my $hidden_in_opac = 0;
666 foreach my $field ( keys %{$rules} ) {
668 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
674 return $hidden_in_opac;
677 =head3 can_be_transferred
679 $item->can_be_transferred({ to => $to_library, from => $from_library })
680 Checks if an item can be transferred to given library.
682 This feature is controlled by two system preferences:
683 UseBranchTransferLimits to enable / disable the feature
684 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
685 for setting the limitations
687 Takes HASHref that can have the following parameters:
688 MANDATORY PARAMETERS:
691 $from : Koha::Library # if not given, item holdingbranch
692 # will be used instead
694 Returns 1 if item can be transferred to $to_library, otherwise 0.
696 To find out whether at least one item of a Koha::Biblio can be transferred, please
697 see Koha::Biblio->can_be_transferred() instead of using this method for
698 multiple items of the same biblio.
702 sub can_be_transferred {
703 my ($self, $params) = @_;
705 my $to = $params->{to};
706 my $from = $params->{from};
708 $to = $to->branchcode;
709 $from = defined $from ? $from->branchcode : $self->holdingbranch;
711 return 1 if $from eq $to; # Transfer to current branch is allowed
712 return 1 unless C4::Context->preference('UseBranchTransferLimits');
714 my $limittype = C4::Context->preference('BranchTransferLimitsType');
715 return Koha::Item::Transfer::Limits->search({
718 $limittype => $limittype eq 'itemtype'
719 ? $self->effective_itemtype : $self->ccode
724 =head3 pickup_locations
726 $pickup_locations = $item->pickup_locations( {patron => $patron } )
728 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)
729 and if item can be transferred to each pickup location.
733 sub pickup_locations {
734 my ($self, $params) = @_;
736 my $patron = $params->{patron};
738 my $circ_control_branch =
739 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
741 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
743 if(defined $patron) {
744 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
745 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
748 my $pickup_libraries = Koha::Libraries->search();
749 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
750 $pickup_libraries = $self->home_branch->get_hold_libraries;
751 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
752 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
753 $pickup_libraries = $plib->get_hold_libraries;
754 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
755 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
756 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
757 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
760 return $pickup_libraries->search(
765 order_by => ['branchname']
767 ) unless C4::Context->preference('UseBranchTransferLimits');
769 my $limittype = C4::Context->preference('BranchTransferLimitsType');
770 my ($ccode, $itype) = (undef, undef);
771 if( $limittype eq 'ccode' ){
772 $ccode = $self->ccode;
774 $itype = $self->itype;
776 my $limits = Koha::Item::Transfer::Limits->search(
778 fromBranch => $self->holdingbranch,
782 { columns => ['toBranch'] }
785 return $pickup_libraries->search(
787 pickup_location => 1,
789 '-not_in' => $limits->_resultset->as_query
793 order_by => ['branchname']
798 =head3 article_request_type
800 my $type = $item->article_request_type( $borrower )
802 returns 'yes', 'no', 'bib_only', or 'item_only'
804 $borrower must be a Koha::Patron object
808 sub article_request_type {
809 my ( $self, $borrower ) = @_;
811 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
813 $branch_control eq 'homebranch' ? $self->homebranch
814 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
816 my $borrowertype = $borrower->categorycode;
817 my $itemtype = $self->effective_itemtype();
818 my $rule = Koha::CirculationRules->get_effective_rule(
820 rule_name => 'article_requests',
821 categorycode => $borrowertype,
822 itemtype => $itemtype,
823 branchcode => $branchcode
827 return q{} unless $rule;
828 return $rule->rule_value || q{}
837 my $attributes = { order_by => 'priority' };
838 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
840 itemnumber => $self->itemnumber,
843 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
844 waitingdate => { '!=' => undef },
847 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
848 return Koha::Holds->_new_from_dbic($hold_rs);
851 =head3 stockrotationitem
853 my $sritem = Koha::Item->stockrotationitem;
855 Returns the stock rotation item associated with the current item.
859 sub stockrotationitem {
861 my $rs = $self->_result->stockrotationitem;
863 return Koha::StockRotationItem->_new_from_dbic( $rs );
868 my $item = $item->add_to_rota($rota_id);
870 Add this item to the rota identified by $ROTA_ID, which means associating it
871 with the first stage of that rota. Should this item already be associated
872 with a rota, then we will move it to the new rota.
877 my ( $self, $rota_id ) = @_;
878 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
882 =head3 has_pending_hold
884 my $is_pending_hold = $item->has_pending_hold();
886 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
890 sub has_pending_hold {
892 my $pending_hold = $self->_result->tmp_holdsqueues;
893 return $pending_hold->count ? 1: 0;
898 my $field = $item->as_marc_field;
900 This method returns a MARC::Field object representing the Koha::Item object
901 with the current mappings configuration.
908 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
910 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
914 my $item_field = $tagslib->{$itemtag};
916 my $more_subfields = $self->additional_attributes->to_hashref;
917 foreach my $subfield (
919 $a->{display_order} <=> $b->{display_order}
920 || $a->{subfield} cmp $b->{subfield}
921 } grep { ref($_) && %$_ } values %$item_field
924 my $kohafield = $subfield->{kohafield};
925 my $tagsubfield = $subfield->{tagsubfield};
927 if ( defined $kohafield ) {
928 next if $kohafield !~ m{^items\.}; # That would be weird!
929 ( my $attribute = $kohafield ) =~ s|^items\.||;
930 $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
931 if defined $self->$attribute and $self->$attribute ne '';
933 $value = $more_subfields->{$tagsubfield}
936 next unless defined $value
939 if ( $subfield->{repeatable} ) {
940 my @values = split '\|', $value;
941 push @subfields, ( $tagsubfield => $_ ) for @values;
944 push @subfields, ( $tagsubfield => $value );
949 return unless @subfields;
951 return MARC::Field->new(
952 "$itemtag", ' ', ' ', @subfields
956 =head3 renewal_branchcode
958 Returns the branchcode to be recorded in statistics renewal of the item
962 sub renewal_branchcode {
964 my ($self, $params ) = @_;
966 my $interface = C4::Context->interface;
968 if ( $interface eq 'opac' ){
969 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
970 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
971 $branchcode = 'OPACRenew';
973 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
974 $branchcode = $self->homebranch;
976 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
977 $branchcode = $self->checkout->patron->branchcode;
979 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
980 $branchcode = $self->checkout->branchcode;
986 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
987 ? C4::Context->userenv->{branch} : $params->{branch};
994 Return the cover images associated with this item.
1001 my $cover_image_rs = $self->_result->cover_images;
1002 return unless $cover_image_rs;
1003 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1006 =head3 columns_to_str
1008 my $values = $items->columns_to_str;
1010 Return a hashref with the string representation of the different attribute of the item.
1012 This is meant to be used for display purpose only.
1016 sub columns_to_str {
1019 my $frameworkcode = $self->biblio->frameworkcode;
1020 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1021 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1023 my $columns_info = $self->_result->result_source->columns_info;
1025 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1027 for my $column ( keys %$columns_info ) {
1029 next if $column eq 'more_subfields_xml';
1031 my $value = $self->$column;
1032 # 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
1034 if ( not defined $value or $value eq "" ) {
1035 $values->{$column} = $value;
1040 exists $mss->{"items.$column"}
1041 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1044 $values->{$column} =
1046 ? $subfield->{authorised_value}
1047 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1048 $subfield->{tagsubfield}, $value, '', $tagslib )
1054 $self->more_subfields_xml
1055 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1060 my ( $field ) = $marc_more->fields;
1061 for my $sf ( $field->subfields ) {
1062 my $subfield_code = $sf->[0];
1063 my $value = $sf->[1];
1064 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1065 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1067 $subfield->{authorised_value}
1068 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1069 $subfield->{tagsubfield}, $value, '', $tagslib )
1072 push @{$more_values->{$subfield_code}}, $value;
1075 while ( my ( $k, $v ) = each %$more_values ) {
1076 $values->{$k} = join ' | ', @$v;
1083 =head3 additional_attributes
1085 my $attributes = $item->additional_attributes;
1086 $attributes->{k} = 'new k';
1087 $item->update({ more_subfields => $attributes->to_marcxml });
1089 Returns a Koha::Item::Attributes object that represents the non-mapped
1090 attributes for this item.
1094 sub additional_attributes {
1097 return Koha::Item::Attributes->new_from_marcxml(
1098 $self->more_subfields_xml,
1102 =head3 _set_found_trigger
1104 $self->_set_found_trigger
1106 Finds the most recent lost item charge for this item and refunds the patron
1107 appropriately, taking into account any payments or writeoffs already applied
1110 Internal function, not exported, called only by Koha::Item->store.
1114 sub _set_found_trigger {
1115 my ( $self, $pre_mod_item ) = @_;
1117 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1118 my $no_refund_after_days =
1119 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1120 if ($no_refund_after_days) {
1121 my $today = dt_from_string();
1122 my $lost_age_in_days =
1123 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1126 return $self unless $lost_age_in_days < $no_refund_after_days;
1129 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1132 return_branch => C4::Context->userenv
1133 ? C4::Context->userenv->{'branch'}
1138 if ( $lostreturn_policy ) {
1140 # refund charge made for lost book
1141 my $lost_charge = Koha::Account::Lines->search(
1143 itemnumber => $self->itemnumber,
1144 debit_type_code => 'LOST',
1145 status => [ undef, { '<>' => 'FOUND' } ]
1148 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1153 if ( $lost_charge ) {
1155 my $patron = $lost_charge->patron;
1158 my $account = $patron->account;
1159 my $total_to_refund = 0;
1162 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1164 # some amount has been cancelled. collect the offsets that are not writeoffs
1165 # this works because the only way to subtract from this kind of a debt is
1166 # using the UI buttons 'Pay' and 'Write off'
1167 my $credit_offsets = $lost_charge->debit_offsets(
1169 'credit_id' => { '!=' => undef },
1170 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1172 { join => 'credit' }
1175 $total_to_refund = ( $credit_offsets->count > 0 )
1176 ? $credit_offsets->total * -1 # credits are negative on the DB
1180 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1183 if ( $credit_total > 0 ) {
1185 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1186 $credit = $account->add_credit(
1188 amount => $credit_total,
1189 description => 'Item found ' . $self->itemnumber,
1190 type => 'LOST_FOUND',
1191 interface => C4::Context->interface,
1192 library_id => $branchcode,
1193 item_id => $self->itemnumber,
1194 issue_id => $lost_charge->issue_id
1198 $credit->apply( { debits => [$lost_charge] } );
1202 message => 'lost_refunded',
1203 payload => { credit_id => $credit->id }
1208 # Update the account status
1209 $lost_charge->status('FOUND');
1210 $lost_charge->store();
1212 # Reconcile balances if required
1213 if ( C4::Context->preference('AccountAutoReconcile') ) {
1214 $account->reconcile_balance;
1219 # restore fine for lost book
1220 if ( $lostreturn_policy eq 'restore' ) {
1221 my $lost_overdue = Koha::Account::Lines->search(
1223 itemnumber => $self->itemnumber,
1224 debit_type_code => 'OVERDUE',
1228 order_by => { '-desc' => 'date' },
1233 if ( $lost_overdue ) {
1235 my $patron = $lost_overdue->patron;
1237 my $account = $patron->account;
1239 # Update status of fine
1240 $lost_overdue->status('FOUND')->store();
1242 # Find related forgive credit
1243 my $refund = $lost_overdue->credits(
1245 credit_type_code => 'FORGIVEN',
1246 itemnumber => $self->itemnumber,
1247 status => [ { '!=' => 'VOID' }, undef ]
1249 { order_by => { '-desc' => 'date' }, rows => 1 }
1253 # Revert the forgive credit
1254 $refund->void({ interface => 'trigger' });
1258 message => 'lost_restored',
1259 payload => { refund_id => $refund->id }
1264 # Reconcile balances if required
1265 if ( C4::Context->preference('AccountAutoReconcile') ) {
1266 $account->reconcile_balance;
1270 } elsif ( $lostreturn_policy eq 'charge' ) {
1274 message => 'lost_charge',
1283 =head3 public_read_list
1285 This method returns the list of publicly readable database fields for both API and UI output purposes
1289 sub public_read_list {
1291 'itemnumber', 'biblionumber', 'homebranch',
1292 'holdingbranch', 'location', 'collectioncode',
1293 'itemcallnumber', 'copynumber', 'enumchron',
1294 'barcode', 'dateaccessioned', 'itemnotes',
1295 'onloan', 'uri', 'itype',
1296 'notforloan', 'damaged', 'itemlost',
1297 'withdrawn', 'restricted'
1301 =head3 to_api_mapping
1303 This method returns the mapping for representing a Koha::Item object
1308 sub to_api_mapping {
1310 itemnumber => 'item_id',
1311 biblionumber => 'biblio_id',
1312 biblioitemnumber => undef,
1313 barcode => 'external_id',
1314 dateaccessioned => 'acquisition_date',
1315 booksellerid => 'acquisition_source',
1316 homebranch => 'home_library_id',
1317 price => 'purchase_price',
1318 replacementprice => 'replacement_price',
1319 replacementpricedate => 'replacement_price_date',
1320 datelastborrowed => 'last_checkout_date',
1321 datelastseen => 'last_seen_date',
1323 notforloan => 'not_for_loan_status',
1324 damaged => 'damaged_status',
1325 damaged_on => 'damaged_date',
1326 itemlost => 'lost_status',
1327 itemlost_on => 'lost_date',
1328 withdrawn => 'withdrawn',
1329 withdrawn_on => 'withdrawn_date',
1330 itemcallnumber => 'callnumber',
1331 coded_location_qualifier => 'coded_location_qualifier',
1332 issues => 'checkouts_count',
1333 renewals => 'renewals_count',
1334 reserves => 'holds_count',
1335 restricted => 'restricted_status',
1336 itemnotes => 'public_notes',
1337 itemnotes_nonpublic => 'internal_notes',
1338 holdingbranch => 'holding_library_id',
1339 timestamp => 'timestamp',
1340 location => 'location',
1341 permanent_location => 'permanent_location',
1342 onloan => 'checked_out_date',
1343 cn_source => 'call_number_source',
1344 cn_sort => 'call_number_sort',
1345 ccode => 'collection_code',
1346 materials => 'materials_notes',
1348 itype => 'item_type_id',
1349 more_subfields_xml => 'extended_subfields',
1350 enumchron => 'serial_issue_number',
1351 copynumber => 'copy_number',
1352 stocknumber => 'inventory_number',
1353 new_status => 'new_status'
1359 my $itemtype = $item->itemtype;
1361 Returns Koha object for effective itemtype
1367 return Koha::ItemTypes->find( $self->effective_itemtype );
1372 my $orders = $item->orders();
1374 Returns a Koha::Acquisition::Orders object
1381 my $orders = $self->_result->item_orders;
1382 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1385 =head3 tracked_links
1387 my $tracked_links = $item->tracked_links();
1389 Returns a Koha::TrackedLinks object
1396 my $tracked_links = $self->_result->linktrackers;
1397 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1400 =head3 move_to_biblio
1402 $item->move_to_biblio($to_biblio[, $params]);
1404 Move the item to another biblio and update any references in other tables.
1406 The final optional parameter, C<$params>, is expected to contain the
1407 'skip_record_index' key, which is relayed down to Koha::Item->store.
1408 There it prevents calling index_records, which takes most of the
1409 time in batch adds/deletes. The caller must take care of calling
1410 index_records separately.
1413 skip_record_index => 1|0
1415 Returns undef if the move failed or the biblionumber of the destination record otherwise
1419 sub move_to_biblio {
1420 my ( $self, $to_biblio, $params ) = @_;
1424 return if $self->biblionumber == $to_biblio->biblionumber;
1426 my $from_biblionumber = $self->biblionumber;
1427 my $to_biblionumber = $to_biblio->biblionumber;
1429 # Own biblionumber and biblioitemnumber
1431 biblionumber => $to_biblionumber,
1432 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1433 })->store({ skip_record_index => $params->{skip_record_index} });
1435 unless ($params->{skip_record_index}) {
1436 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1437 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1440 # Acquisition orders
1441 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1444 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1446 # hold_fill_target (there's no Koha object available yet)
1447 my $hold_fill_target = $self->_result->hold_fill_target;
1448 if ($hold_fill_target) {
1449 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1452 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1453 # and can't even fake one since the significant columns are nullable.
1454 my $storage = $self->_result->result_source->storage;
1457 my ($storage, $dbh, @cols) = @_;
1459 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1464 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1466 return $to_biblionumber;
1471 my $bundle_items = $item->bundle_items;
1473 Returns the items associated with this bundle
1480 if ( !$self->{_bundle_items_cached} ) {
1481 my $bundle_items = Koha::Items->search(
1482 { 'item_bundles_item.host' => $self->itemnumber },
1483 { join => 'item_bundles_item' } );
1484 $self->{_bundle_items} = $bundle_items;
1485 $self->{_bundle_items_cached} = 1;
1488 return $self->{_bundle_items};
1493 my $is_bundle = $item->is_bundle;
1495 Returns whether the item is a bundle or not
1501 return $self->bundle_items->count ? 1 : 0;
1506 my $bundle = $item->bundle_host;
1508 Returns the bundle item this item is attached to
1515 my $bundle_items_rs = $self->_result->item_bundles_item;
1516 return unless $bundle_items_rs;
1517 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1522 my $in_bundle = $item->in_bundle;
1524 Returns whether this item is currently in a bundle
1530 return $self->bundle_host ? 1 : 0;
1533 =head3 add_to_bundle
1535 my $link = $item->add_to_bundle($bundle_item);
1537 Adds the bundle_item passed to this item
1542 my ( $self, $bundle_item ) = @_;
1544 my $schema = Koha::Database->new->schema;
1546 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1551 $self->_result->add_to_item_bundles_hosts(
1552 { item => $bundle_item->itemnumber } );
1554 $bundle_item->notforloan($BundleNotLoanValue)->store();
1560 # 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 fro DBIx::Error
1561 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1563 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1565 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1566 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1567 Koha::Exceptions::Object::FKConstraint->throw(
1568 error => 'Broken FK constraint',
1569 broken_fk => $+{column}
1574 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1576 Koha::Exceptions::Object::DuplicateID->throw(
1577 error => 'Duplicate ID',
1578 duplicate_id => $+{key}
1581 elsif ( $_->{msg} =~
1582 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1584 { # The optional \W in the regex might be a quote or backtick
1585 my $type = $+{type};
1586 my $value = $+{value};
1587 my $property = $+{property};
1588 $property =~ s/['`]//g;
1589 Koha::Exceptions::Object::BadValue->throw(
1592 property => $property =~ /(\w+\.\w+)$/
1595 , # results in table.column without quotes or backtics
1599 # Catch-all for foreign key breakages. It will help find other use cases
1608 =head3 remove_from_bundle
1610 Remove this item from any bundle it may have been attached to.
1614 sub remove_from_bundle {
1617 my $bundle_item_rs = $self->_result->item_bundles_item;
1618 if ( $bundle_item_rs ) {
1619 $bundle_item_rs->delete;
1620 $self->notforloan(0)->store();
1626 =head2 Internal methods
1628 =head3 _after_item_action_hooks
1630 Helper method that takes care of calling all plugin hooks
1634 sub _after_item_action_hooks {
1635 my ( $self, $params ) = @_;
1637 my $action = $params->{action};
1639 Koha::Plugins->call(
1640 'after_item_action',
1644 item_id => $self->itemnumber,
1651 my $recall = $item->recall;
1653 Return the relevant recall for this item
1659 my @recalls = Koha::Recalls->search(
1661 biblio_id => $self->biblionumber,
1664 { order_by => { -asc => 'created_date' } }
1666 foreach my $recall (@recalls) {
1667 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1671 # no item-level recall to return, so return earliest biblio-level
1672 # FIXME: eventually this will be based on priority
1676 =head3 can_be_recalled
1678 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1680 Does item-level checks and returns if items can be recalled by this borrower
1684 sub can_be_recalled {
1685 my ( $self, $params ) = @_;
1687 return 0 if !( C4::Context->preference('UseRecalls') );
1689 # check if this item is not for loan, withdrawn or lost
1690 return 0 if ( $self->notforloan != 0 );
1691 return 0 if ( $self->itemlost != 0 );
1692 return 0 if ( $self->withdrawn != 0 );
1694 # check if this item is not checked out - if not checked out, can't be recalled
1695 return 0 if ( !defined( $self->checkout ) );
1697 my $patron = $params->{patron};
1699 my $branchcode = C4::Context->userenv->{'branch'};
1701 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1704 # Check the circulation rule for each relevant itemtype for this item
1705 my $rule = Koha::CirculationRules->get_effective_rules({
1706 branchcode => $branchcode,
1707 categorycode => $patron ? $patron->categorycode : undef,
1708 itemtype => $self->effective_itemtype,
1711 'recalls_per_record',
1716 # check recalls allowed has been set and is not zero
1717 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1720 # check borrower has not reached open recalls allowed limit
1721 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1723 # check borrower has not reach open recalls allowed per record limit
1724 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1726 # check if this patron has already recalled this item
1727 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1729 # check if this patron has already checked out this item
1730 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1732 # check if this patron has already reserved this item
1733 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1736 # check item availability
1737 # items are unavailable for recall if they are lost, withdrawn or notforloan
1738 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1740 # if there are no available items at all, no recall can be placed
1741 return 0 if ( scalar @items == 0 );
1743 my $checked_out_count = 0;
1745 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1748 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1749 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1751 # can't recall if no items have been checked out
1752 return 0 if ( $checked_out_count == 0 );
1758 =head3 can_be_waiting_recall
1760 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1762 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1763 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1767 sub can_be_waiting_recall {
1770 return 0 if !( C4::Context->preference('UseRecalls') );
1772 # check if this item is not for loan, withdrawn or lost
1773 return 0 if ( $self->notforloan != 0 );
1774 return 0 if ( $self->itemlost != 0 );
1775 return 0 if ( $self->withdrawn != 0 );
1777 my $branchcode = $self->holdingbranch;
1778 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1779 $branchcode = C4::Context->userenv->{'branch'};
1781 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1784 # Check the circulation rule for each relevant itemtype for this item
1785 my $rule = Koha::CirculationRules->get_effective_rules({
1786 branchcode => $branchcode,
1787 categorycode => undef,
1788 itemtype => $self->effective_itemtype,
1794 # check recalls allowed has been set and is not zero
1795 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1801 =head3 check_recalls
1803 my $recall = $item->check_recalls;
1805 Get the most relevant recall for this item.
1812 my @recalls = Koha::Recalls->search(
1813 { biblio_id => $self->biblionumber,
1814 item_id => [ $self->itemnumber, undef ]
1816 { order_by => { -asc => 'created_date' } }
1817 )->filter_by_current->as_list;
1820 # iterate through relevant recalls to find the best one.
1821 # if we come across a waiting recall, use this one.
1822 # 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.
1823 foreach my $r ( @recalls ) {
1824 if ( $r->waiting ) {
1829 unless ( defined $recall ) {
1830 $recall = $recalls[0];
1836 =head3 is_notforloan
1838 my $is_notforloan = $item->is_notforloan;
1840 Determine whether or not this item is "notforloan" based on
1841 the item's notforloan status or its item type
1847 my $is_notforloan = 0;
1849 if ( $self->notforloan ){
1853 my $itemtype = $self->itemtype;
1855 if ( $itemtype->notforloan ){
1861 return $is_notforloan;
1874 Kyle M Hall <kyle@bywatersolutions.com>