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;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
57 use Koha::Policy::Holds;
59 use base qw(Koha::Object);
63 Koha::Item - Koha Item object class
75 $params can take an optional 'skip_record_index' parameter.
76 If set, the reindexation process will not happen (index_records not called)
77 You should not turn it on if you do not understand what it is doing exactly.
83 my $params = @_ ? shift : {};
85 my $log_action = $params->{log_action} // 1;
87 # We do not want to oblige callers to pass this value
88 # Dev conveniences vs performance?
89 unless ( $self->biblioitemnumber ) {
90 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
93 # See related changes from C4::Items::AddItem
94 unless ( $self->itype ) {
95 $self->itype($self->biblio->biblioitem->itemtype);
98 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
100 my $today = dt_from_string;
101 my $action = 'create';
103 unless ( $self->in_storage ) { #AddItem
105 unless ( $self->permanent_location ) {
106 $self->permanent_location($self->location);
109 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
110 unless ( $self->location || !$default_location ) {
111 $self->permanent_location( $self->location || $default_location )
112 unless $self->permanent_location;
113 $self->location($default_location);
116 unless ( $self->replacementpricedate ) {
117 $self->replacementpricedate($today);
119 unless ( $self->datelastseen ) {
120 $self->datelastseen($today);
123 unless ( $self->dateaccessioned ) {
124 $self->dateaccessioned($today);
127 if ( $self->itemcallnumber
128 or $self->cn_source )
130 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
131 $self->cn_sort($cn_sort);
138 my %updated_columns = $self->_result->get_dirty_columns;
139 return $self->SUPER::store unless %updated_columns;
141 # Retrieve the item for comparison if we need to
143 exists $updated_columns{itemlost}
144 or exists $updated_columns{withdrawn}
145 or exists $updated_columns{damaged}
146 ) ? $self->get_from_storage : undef;
148 # Update *_on fields if needed
149 # FIXME: Why not for AddItem as well?
150 my @fields = qw( itemlost withdrawn damaged );
151 for my $field (@fields) {
153 # If the field is defined but empty or 0, we are
154 # removing/unsetting and thus need to clear out
156 if ( exists $updated_columns{$field}
157 && defined( $self->$field )
160 my $field_on = "${field}_on";
161 $self->$field_on(undef);
163 # If the field has changed otherwise, we much update
165 elsif (exists $updated_columns{$field}
166 && $updated_columns{$field}
167 && !$pre_mod_item->$field )
169 my $field_on = "${field}_on";
170 $self->$field_on(dt_from_string);
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 ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
184 and not exists $updated_columns{permanent_location} )
186 $self->permanent_location( $self->location );
189 # If item was lost and has now been found,
190 # reverse any list item charges if necessary.
191 if ( exists $updated_columns{itemlost}
192 and $updated_columns{itemlost} <= 0
193 and $pre_mod_item->itemlost > 0 )
195 $self->_set_found_trigger($pre_mod_item);
200 my $result = $self->SUPER::store;
201 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
203 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
204 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
206 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
207 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
208 unless $params->{skip_record_index};
209 $self->get_from_storage->_after_item_action_hooks({ action => $action });
211 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
213 biblio_ids => [ $self->biblionumber ]
215 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
226 my $params = @_ ? shift : {};
228 # FIXME check the item has no current issues
229 # i.e. raise the appropriate exception
231 # Get the item group so we can delete it later if it has no items left
232 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
234 my $result = $self->SUPER::delete;
236 # Delete the item group if it has no items left
237 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
239 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
240 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
241 unless $params->{skip_record_index};
243 $self->_after_item_action_hooks({ action => 'delete' });
245 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
246 if C4::Context->preference("CataloguingLog");
248 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
250 biblio_ids => [ $self->biblionumber ]
252 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
263 my $params = @_ ? shift : {};
265 my $safe_to_delete = $self->safe_to_delete;
266 return $safe_to_delete unless $safe_to_delete;
268 $self->move_to_deleted;
270 return $self->delete($params);
273 =head3 safe_to_delete
275 returns 1 if the item is safe to delete,
277 "book_on_loan" if the item is checked out,
279 "not_same_branch" if the item is blocked by independent branches,
281 "book_reserved" if the there are holds aganst the item, or
283 "linked_analytics" if the item has linked analytic records.
285 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
294 $error = "book_on_loan" if $self->checkout;
296 $error //= "not_same_branch"
297 if defined C4::Context->userenv
298 and defined C4::Context->userenv->{number}
299 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
301 # check it doesn't have a waiting reserve
302 $error //= "book_reserved"
303 if $self->holds->filter_by_found->count;
305 $error //= "linked_analytics"
306 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
308 $error //= "last_item_for_hold"
309 if $self->biblio->items->count == 1
310 && $self->biblio->holds->search(
317 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
320 return Koha::Result::Boolean->new(1);
323 =head3 move_to_deleted
325 my $is_moved = $item->move_to_deleted;
327 Move an item to the deleteditems table.
328 This can be done before deleting an item, to make sure the data are not completely deleted.
332 sub move_to_deleted {
334 my $item_infos = $self->unblessed;
335 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
336 $item_infos->{deleted_on} = dt_from_string;
337 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
340 =head3 effective_itemtype
342 Returns the itemtype for the item based on whether item level itemtypes are set or not.
346 sub effective_itemtype {
349 return $self->_result()->effective_itemtype();
359 my $hb_rs = $self->_result->homebranch;
361 return Koha::Library->_new_from_dbic( $hb_rs );
364 =head3 holding_branch
371 my $hb_rs = $self->_result->holdingbranch;
373 return Koha::Library->_new_from_dbic( $hb_rs );
378 my $biblio = $item->biblio;
380 Return the bibliographic record of this item
386 my $biblio_rs = $self->_result->biblio;
387 return Koha::Biblio->_new_from_dbic( $biblio_rs );
392 my $biblioitem = $item->biblioitem;
394 Return the biblioitem record of this item
400 my $biblioitem_rs = $self->_result->biblioitem;
401 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
406 my $checkout = $item->checkout;
408 Return the checkout for this item
414 my $checkout_rs = $self->_result->issue;
415 return unless $checkout_rs;
416 return Koha::Checkout->_new_from_dbic( $checkout_rs );
421 my $item_group = $item->item_group;
423 Return the item group for this item
430 my $item_group_item = $self->_result->item_group_item;
431 return unless $item_group_item;
433 my $item_group_rs = $item_group_item->item_group;
434 return unless $item_group_rs;
436 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
442 my $return_claims = $item->return_claims;
444 Return any return_claims associated with this item
449 my ( $self, $params, $attrs ) = @_;
450 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
451 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
456 my $return_claim = $item->return_claim;
458 Returns the most recent unresolved return_claims associated with this item
465 $self->_result->return_claims->search( { resolution => undef },
466 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
467 return unless $claims_rs;
468 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
473 my $holds = $item->holds();
474 my $holds = $item->holds($params);
475 my $holds = $item->holds({ found => 'W'});
477 Return holds attached to an item, optionally accept a hashref of params to pass to search
482 my ( $self,$params ) = @_;
483 my $holds_rs = $self->_result->reserves->search($params);
484 return Koha::Holds->_new_from_dbic( $holds_rs );
487 =head3 request_transfer
489 my $transfer = $item->request_transfer(
493 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
497 Add a transfer request for this item to the given branch for the given reason.
499 An exception will be thrown if the BranchTransferLimits would prevent the requested
500 transfer, unless 'ignore_limits' is passed to override the limits.
502 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
503 The caller should catch such cases and retry the transfer request as appropriate passing
504 an appropriate override.
507 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
508 * replace - Used to replace the existing transfer request with your own.
512 sub request_transfer {
513 my ( $self, $params ) = @_;
515 # check for mandatory params
516 my @mandatory = ( 'to', 'reason' );
517 for my $param (@mandatory) {
518 unless ( defined( $params->{$param} ) ) {
519 Koha::Exceptions::MissingParameter->throw(
520 error => "The $param parameter is mandatory" );
524 Koha::Exceptions::Item::Transfer::Limit->throw()
525 unless ( $params->{ignore_limits}
526 || $self->can_be_transferred( { to => $params->{to} } ) );
528 my $request = $self->get_transfer;
529 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
530 if ( $request && !$params->{enqueue} && !$params->{replace} );
532 $request->cancel( { reason => $params->{reason}, force => 1 } )
533 if ( defined($request) && $params->{replace} );
535 my $transfer = Koha::Item::Transfer->new(
537 itemnumber => $self->itemnumber,
538 daterequested => dt_from_string,
539 frombranch => $self->holdingbranch,
540 tobranch => $params->{to}->branchcode,
541 reason => $params->{reason},
542 comments => $params->{comment}
551 my $transfer = $item->get_transfer;
553 Return the active transfer request or undef
555 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
556 whereby the most recently sent, but not received, transfer will be returned
557 if it exists, otherwise the oldest unsatisfied transfer will be returned.
559 This allows for transfers to queue, which is the case for stock rotation and
560 rotating collections where a manual transfer may need to take precedence but
561 we still expect the item to end up at a final location eventually.
568 my $transfer = $self->_result->current_branchtransfers->next;
569 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
574 my $transfer = $item->get_transfers;
576 Return the list of outstanding transfers (i.e requested but not yet cancelled
579 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
580 whereby the most recently sent, but not received, transfer will be returned
581 first if it exists, otherwise requests are in oldest to newest request order.
583 This allows for transfers to queue, which is the case for stock rotation and
584 rotating collections where a manual transfer may need to take precedence but
585 we still expect the item to end up at a final location eventually.
592 my $transfer_rs = $self->_result->current_branchtransfers;
594 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
597 =head3 last_returned_by
599 Gets and sets the last patron to return an item.
601 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
603 $item->last_returned_by( $borrowernumber );
605 my $patron = $item->last_returned_by();
609 sub last_returned_by {
610 my ( $self, $borrowernumber ) = @_;
611 if ( $borrowernumber ) {
612 $self->_result->update_or_create_related('last_returned_by',
613 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
615 my $rs = $self->_result->last_returned_by;
617 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
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 my $pickup_locations = $item->pickup_locations({ patron => $patron })
728 Returns possible pickup locations for this item, according to patron's home library
729 and if item can be transferred to each pickup location.
731 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
736 sub pickup_locations {
737 my ($self, $params) = @_;
739 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
740 unless exists $params->{patron};
742 my $patron = $params->{patron};
744 my $circ_control_branch = Koha::Policy::Holds->holds_control_library( $self, $patron );
746 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
748 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
749 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
751 my $pickup_libraries = Koha::Libraries->search();
752 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
753 $pickup_libraries = $self->home_branch->get_hold_libraries;
754 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
755 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
756 $pickup_libraries = $plib->get_hold_libraries;
757 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
758 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
759 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
760 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
763 return $pickup_libraries->search(
768 order_by => ['branchname']
770 ) unless C4::Context->preference('UseBranchTransferLimits');
772 my $limittype = C4::Context->preference('BranchTransferLimitsType');
773 my ($ccode, $itype) = (undef, undef);
774 if( $limittype eq 'ccode' ){
775 $ccode = $self->ccode;
777 $itype = $self->itype;
779 my $limits = Koha::Item::Transfer::Limits->search(
781 fromBranch => $self->holdingbranch,
785 { columns => ['toBranch'] }
788 return $pickup_libraries->search(
790 pickup_location => 1,
792 '-not_in' => $limits->_resultset->as_query
796 order_by => ['branchname']
801 =head3 article_request_type
803 my $type = $item->article_request_type( $borrower )
805 returns 'yes', 'no', 'bib_only', or 'item_only'
807 $borrower must be a Koha::Patron object
811 sub article_request_type {
812 my ( $self, $borrower ) = @_;
814 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
816 $branch_control eq 'homebranch' ? $self->homebranch
817 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
819 my $borrowertype = $borrower->categorycode;
820 my $itemtype = $self->effective_itemtype();
821 my $rule = Koha::CirculationRules->get_effective_rule(
823 rule_name => 'article_requests',
824 categorycode => $borrowertype,
825 itemtype => $itemtype,
826 branchcode => $branchcode
830 return q{} unless $rule;
831 return $rule->rule_value || q{}
840 my $attributes = { order_by => 'priority' };
841 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
843 itemnumber => $self->itemnumber,
846 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
847 waitingdate => { '!=' => undef },
850 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
851 return Koha::Holds->_new_from_dbic($hold_rs);
854 =head3 stockrotationitem
856 my $sritem = Koha::Item->stockrotationitem;
858 Returns the stock rotation item associated with the current item.
862 sub stockrotationitem {
864 my $rs = $self->_result->stockrotationitem;
866 return Koha::StockRotationItem->_new_from_dbic( $rs );
871 my $item = $item->add_to_rota($rota_id);
873 Add this item to the rota identified by $ROTA_ID, which means associating it
874 with the first stage of that rota. Should this item already be associated
875 with a rota, then we will move it to the new rota.
880 my ( $self, $rota_id ) = @_;
881 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
885 =head3 has_pending_hold
887 my $is_pending_hold = $item->has_pending_hold();
889 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
893 sub has_pending_hold {
895 return $self->_result->tmp_holdsqueue ? 1 : 0;
898 =head3 has_pending_recall {
900 my $has_pending_recall
902 Return if whether has pending recall of not.
906 sub has_pending_recall {
909 # FIXME Must be moved to $self->recalls
910 return Koha::Recalls->search(
912 item_id => $self->itemnumber,
920 my $field = $item->as_marc_field;
922 This method returns a MARC::Field object representing the Koha::Item object
923 with the current mappings configuration.
930 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
932 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
936 my $item_field = $tagslib->{$itemtag};
938 my $more_subfields = $self->additional_attributes->to_hashref;
939 foreach my $subfield (
941 $a->{display_order} <=> $b->{display_order}
942 || $a->{subfield} cmp $b->{subfield}
943 } grep { ref($_) && %$_ } values %$item_field
946 my $kohafield = $subfield->{kohafield};
947 my $tagsubfield = $subfield->{tagsubfield};
949 if ( defined $kohafield && $kohafield ne '' ) {
950 next if $kohafield !~ m{^items\.}; # That would be weird!
951 ( my $attribute = $kohafield ) =~ s|^items\.||;
952 $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
953 if defined $self->$attribute and $self->$attribute ne '';
955 $value = $more_subfields->{$tagsubfield}
958 next unless defined $value
961 if ( $subfield->{repeatable} ) {
962 my @values = split '\|', $value;
963 push @subfields, ( $tagsubfield => $_ ) for @values;
966 push @subfields, ( $tagsubfield => $value );
971 return unless @subfields;
973 return MARC::Field->new(
974 "$itemtag", ' ', ' ', @subfields
978 =head3 renewal_branchcode
980 Returns the branchcode to be recorded in statistics renewal of the item
984 sub renewal_branchcode {
986 my ($self, $params ) = @_;
988 my $interface = C4::Context->interface;
990 if ( $interface eq 'opac' ){
991 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
992 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
993 $branchcode = 'OPACRenew';
995 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
996 $branchcode = $self->homebranch;
998 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
999 $branchcode = $self->checkout->patron->branchcode;
1001 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1002 $branchcode = $self->checkout->branchcode;
1008 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1009 ? C4::Context->userenv->{branch} : $params->{branch};
1016 Return the cover images associated with this item.
1023 my $cover_image_rs = $self->_result->cover_images;
1024 return unless $cover_image_rs;
1025 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1028 =head3 columns_to_str
1030 my $values = $items->columns_to_str;
1032 Return a hashref with the string representation of the different attribute of the item.
1034 This is meant to be used for display purpose only.
1038 sub columns_to_str {
1040 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1041 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1042 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1044 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1047 for my $column ( @{$self->_columns}) {
1049 next if $column eq 'more_subfields_xml';
1051 my $value = $self->$column;
1052 # 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
1054 if ( not defined $value or $value eq "" ) {
1055 $values->{$column} = $value;
1060 exists $mss->{"items.$column"}
1061 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1064 $values->{$column} =
1066 ? $subfield->{authorised_value}
1067 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1068 $subfield->{tagsubfield}, $value, '', $tagslib )
1074 $self->more_subfields_xml
1075 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1080 my ( $field ) = $marc_more->fields;
1081 for my $sf ( $field->subfields ) {
1082 my $subfield_code = $sf->[0];
1083 my $value = $sf->[1];
1084 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1085 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1087 $subfield->{authorised_value}
1088 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1089 $subfield->{tagsubfield}, $value, '', $tagslib )
1092 push @{$more_values->{$subfield_code}}, $value;
1095 while ( my ( $k, $v ) = each %$more_values ) {
1096 $values->{$k} = join ' | ', @$v;
1103 =head3 additional_attributes
1105 my $attributes = $item->additional_attributes;
1106 $attributes->{k} = 'new k';
1107 $item->update({ more_subfields => $attributes->to_marcxml });
1109 Returns a Koha::Item::Attributes object that represents the non-mapped
1110 attributes for this item.
1114 sub additional_attributes {
1117 return Koha::Item::Attributes->new_from_marcxml(
1118 $self->more_subfields_xml,
1122 =head3 _set_found_trigger
1124 $self->_set_found_trigger
1126 Finds the most recent lost item charge for this item and refunds the patron
1127 appropriately, taking into account any payments or writeoffs already applied
1130 Internal function, not exported, called only by Koha::Item->store.
1134 sub _set_found_trigger {
1135 my ( $self, $pre_mod_item ) = @_;
1137 # Reverse any lost item charges if necessary.
1138 my $no_refund_after_days =
1139 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1140 if ($no_refund_after_days) {
1141 my $today = dt_from_string();
1142 my $lost_age_in_days =
1143 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1146 return $self unless $lost_age_in_days < $no_refund_after_days;
1149 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1152 return_branch => C4::Context->userenv
1153 ? C4::Context->userenv->{'branch'}
1157 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1159 if ( $lostreturn_policy ) {
1161 # refund charge made for lost book
1162 my $lost_charge = Koha::Account::Lines->search(
1164 itemnumber => $self->itemnumber,
1165 debit_type_code => 'LOST',
1166 status => [ undef, { '<>' => 'FOUND' } ]
1169 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1174 if ( $lost_charge ) {
1176 my $patron = $lost_charge->patron;
1179 my $account = $patron->account;
1181 # Credit outstanding amount
1182 my $credit_total = $lost_charge->amountoutstanding;
1186 $lost_charge->amount > $lost_charge->amountoutstanding &&
1187 $lostreturn_policy ne "refund_unpaid"
1189 # some amount has been cancelled. collect the offsets that are not writeoffs
1190 # this works because the only way to subtract from this kind of a debt is
1191 # using the UI buttons 'Pay' and 'Write off'
1193 # We don't credit any payments if return policy is
1196 # In that case only unpaid/outstanding amount
1197 # will be credited which settles the debt without
1198 # creating extra credits
1200 my $credit_offsets = $lost_charge->debit_offsets(
1202 'credit_id' => { '!=' => undef },
1203 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1205 { join => 'credit' }
1208 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1209 # credits are negative on the DB
1210 $credit_offsets->total * -1 :
1212 # Credit the outstanding amount, then add what has been
1213 # paid to create a net credit for this amount
1214 $credit_total += $total_to_refund;
1218 if ( $credit_total > 0 ) {
1220 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1221 $credit = $account->add_credit(
1223 amount => $credit_total,
1224 description => 'Item found ' . $self->itemnumber,
1225 type => 'LOST_FOUND',
1226 interface => C4::Context->interface,
1227 library_id => $branchcode,
1228 item_id => $self->itemnumber,
1229 issue_id => $lost_charge->issue_id
1233 $credit->apply( { debits => [$lost_charge] } );
1237 message => 'lost_refunded',
1238 payload => { credit_id => $credit->id }
1243 # Update the account status
1244 $lost_charge->status('FOUND');
1245 $lost_charge->store();
1247 # Reconcile balances if required
1248 if ( C4::Context->preference('AccountAutoReconcile') ) {
1249 $account->reconcile_balance;
1254 # possibly restore fine for lost book
1255 my $lost_overdue = Koha::Account::Lines->search(
1257 itemnumber => $self->itemnumber,
1258 debit_type_code => 'OVERDUE',
1262 order_by => { '-desc' => 'date' },
1266 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1268 my $patron = $lost_overdue->patron;
1270 my $account = $patron->account;
1272 # Update status of fine
1273 $lost_overdue->status('FOUND')->store();
1275 # Find related forgive credit
1276 my $refund = $lost_overdue->credits(
1278 credit_type_code => 'FORGIVEN',
1279 itemnumber => $self->itemnumber,
1280 status => [ { '!=' => 'VOID' }, undef ]
1282 { order_by => { '-desc' => 'date' }, rows => 1 }
1286 # Revert the forgive credit
1287 $refund->void({ interface => 'trigger' });
1291 message => 'lost_restored',
1292 payload => { refund_id => $refund->id }
1297 # Reconcile balances if required
1298 if ( C4::Context->preference('AccountAutoReconcile') ) {
1299 $account->reconcile_balance;
1303 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1307 message => 'lost_charge',
1313 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1315 if ( $processingreturn_policy ) {
1317 # refund processing charge made for lost book
1318 my $processing_charge = Koha::Account::Lines->search(
1320 itemnumber => $self->itemnumber,
1321 debit_type_code => 'PROCESSING',
1322 status => [ undef, { '<>' => 'FOUND' } ]
1325 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1330 if ( $processing_charge ) {
1332 my $patron = $processing_charge->patron;
1335 my $account = $patron->account;
1337 # Credit outstanding amount
1338 my $credit_total = $processing_charge->amountoutstanding;
1342 $processing_charge->amount > $processing_charge->amountoutstanding &&
1343 $processingreturn_policy ne "refund_unpaid"
1345 # some amount has been cancelled. collect the offsets that are not writeoffs
1346 # this works because the only way to subtract from this kind of a debt is
1347 # using the UI buttons 'Pay' and 'Write off'
1349 # We don't credit any payments if return policy is
1352 # In that case only unpaid/outstanding amount
1353 # will be credited which settles the debt without
1354 # creating extra credits
1356 my $credit_offsets = $processing_charge->debit_offsets(
1358 'credit_id' => { '!=' => undef },
1359 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1361 { join => 'credit' }
1364 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1365 # credits are negative on the DB
1366 $credit_offsets->total * -1 :
1368 # Credit the outstanding amount, then add what has been
1369 # paid to create a net credit for this amount
1370 $credit_total += $total_to_refund;
1374 if ( $credit_total > 0 ) {
1376 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1377 $credit = $account->add_credit(
1379 amount => $credit_total,
1380 description => 'Item found ' . $self->itemnumber,
1381 type => 'PROCESSING_FOUND',
1382 interface => C4::Context->interface,
1383 library_id => $branchcode,
1384 item_id => $self->itemnumber,
1385 issue_id => $processing_charge->issue_id
1389 $credit->apply( { debits => [$processing_charge] } );
1393 message => 'processing_refunded',
1394 payload => { credit_id => $credit->id }
1399 # Update the account status
1400 $processing_charge->status('FOUND');
1401 $processing_charge->store();
1403 # Reconcile balances if required
1404 if ( C4::Context->preference('AccountAutoReconcile') ) {
1405 $account->reconcile_balance;
1414 =head3 public_read_list
1416 This method returns the list of publicly readable database fields for both API and UI output purposes
1420 sub public_read_list {
1422 'itemnumber', 'biblionumber', 'homebranch',
1423 'holdingbranch', 'location', 'collectioncode',
1424 'itemcallnumber', 'copynumber', 'enumchron',
1425 'barcode', 'dateaccessioned', 'itemnotes',
1426 'onloan', 'uri', 'itype',
1427 'notforloan', 'damaged', 'itemlost',
1428 'withdrawn', 'restricted'
1434 Overloaded to_api method to ensure item-level itypes is adhered to.
1439 my ($self, $params) = @_;
1441 my $response = $self->SUPER::to_api($params);
1444 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1445 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1447 return { %$response, %$overrides };
1450 =head3 to_api_mapping
1452 This method returns the mapping for representing a Koha::Item object
1457 sub to_api_mapping {
1459 itemnumber => 'item_id',
1460 biblionumber => 'biblio_id',
1461 biblioitemnumber => undef,
1462 barcode => 'external_id',
1463 dateaccessioned => 'acquisition_date',
1464 booksellerid => 'acquisition_source',
1465 homebranch => 'home_library_id',
1466 price => 'purchase_price',
1467 replacementprice => 'replacement_price',
1468 replacementpricedate => 'replacement_price_date',
1469 datelastborrowed => 'last_checkout_date',
1470 datelastseen => 'last_seen_date',
1472 notforloan => 'not_for_loan_status',
1473 damaged => 'damaged_status',
1474 damaged_on => 'damaged_date',
1475 itemlost => 'lost_status',
1476 itemlost_on => 'lost_date',
1477 withdrawn => 'withdrawn',
1478 withdrawn_on => 'withdrawn_date',
1479 itemcallnumber => 'callnumber',
1480 coded_location_qualifier => 'coded_location_qualifier',
1481 issues => 'checkouts_count',
1482 renewals => 'renewals_count',
1483 reserves => 'holds_count',
1484 restricted => 'restricted_status',
1485 itemnotes => 'public_notes',
1486 itemnotes_nonpublic => 'internal_notes',
1487 holdingbranch => 'holding_library_id',
1488 timestamp => 'timestamp',
1489 location => 'location',
1490 permanent_location => 'permanent_location',
1491 onloan => 'checked_out_date',
1492 cn_source => 'call_number_source',
1493 cn_sort => 'call_number_sort',
1494 ccode => 'collection_code',
1495 materials => 'materials_notes',
1497 itype => 'item_type_id',
1498 more_subfields_xml => 'extended_subfields',
1499 enumchron => 'serial_issue_number',
1500 copynumber => 'copy_number',
1501 stocknumber => 'inventory_number',
1502 new_status => 'new_status',
1503 deleted_on => undef,
1509 my $itemtype = $item->itemtype;
1511 Returns Koha object for effective itemtype
1518 return Koha::ItemTypes->find( $self->effective_itemtype );
1523 my $orders = $item->orders();
1525 Returns a Koha::Acquisition::Orders object
1532 my $orders = $self->_result->item_orders;
1533 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1536 =head3 tracked_links
1538 my $tracked_links = $item->tracked_links();
1540 Returns a Koha::TrackedLinks object
1547 my $tracked_links = $self->_result->linktrackers;
1548 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1551 =head3 move_to_biblio
1553 $item->move_to_biblio($to_biblio[, $params]);
1555 Move the item to another biblio and update any references in other tables.
1557 The final optional parameter, C<$params>, is expected to contain the
1558 'skip_record_index' key, which is relayed down to Koha::Item->store.
1559 There it prevents calling index_records, which takes most of the
1560 time in batch adds/deletes. The caller must take care of calling
1561 index_records separately.
1564 skip_record_index => 1|0
1566 Returns undef if the move failed or the biblionumber of the destination record otherwise
1570 sub move_to_biblio {
1571 my ( $self, $to_biblio, $params ) = @_;
1575 return if $self->biblionumber == $to_biblio->biblionumber;
1577 my $from_biblionumber = $self->biblionumber;
1578 my $to_biblionumber = $to_biblio->biblionumber;
1580 # Own biblionumber and biblioitemnumber
1582 biblionumber => $to_biblionumber,
1583 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1584 })->store({ skip_record_index => $params->{skip_record_index} });
1586 unless ($params->{skip_record_index}) {
1587 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1588 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1591 # Acquisition orders
1592 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1595 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1597 # hold_fill_target (there's no Koha object available yet)
1598 my $hold_fill_target = $self->_result->hold_fill_target;
1599 if ($hold_fill_target) {
1600 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1603 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1604 # and can't even fake one since the significant columns are nullable.
1605 my $storage = $self->_result->result_source->storage;
1608 my ($storage, $dbh, @cols) = @_;
1610 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1615 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1617 return $to_biblionumber;
1622 my $bundle_items = $item->bundle_items;
1624 Returns the items associated with this bundle
1631 my $rs = $self->_result->bundle_items;
1632 return Koha::Items->_new_from_dbic($rs);
1637 my $is_bundle = $item->is_bundle;
1639 Returns whether the item is a bundle or not
1645 return $self->bundle_items->count ? 1 : 0;
1650 my $bundle = $item->bundle_host;
1652 Returns the bundle item this item is attached to
1659 my $bundle_items_rs = $self->_result->item_bundles_item;
1660 return unless $bundle_items_rs;
1661 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1666 my $in_bundle = $item->in_bundle;
1668 Returns whether this item is currently in a bundle
1674 return $self->bundle_host ? 1 : 0;
1677 =head3 add_to_bundle
1679 my $link = $item->add_to_bundle($bundle_item);
1681 Adds the bundle_item passed to this item
1686 my ( $self, $bundle_item, $options ) = @_;
1690 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1691 if ( $self->itemnumber eq $bundle_item->itemnumber
1692 || $bundle_item->is_bundle
1693 || $self->in_bundle );
1695 my $schema = Koha::Database->new->schema;
1697 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1703 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
1705 my $checkout = $bundle_item->checkout;
1707 unless ($options->{force_checkin}) {
1708 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1711 my $branchcode = C4::Context->userenv->{'branch'};
1712 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1714 Koha::Exceptions::Checkin::FailedCheckin->throw();
1718 my $holds = $bundle_item->current_holds;
1719 if ($holds->count) {
1720 unless ($options->{ignore_holds}) {
1721 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1725 $self->_result->add_to_item_bundles_hosts(
1726 { item => $bundle_item->itemnumber } );
1728 $bundle_item->notforloan($BundleNotLoanValue)->store();
1734 # 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
1735 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1736 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1738 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1739 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1740 Koha::Exceptions::Object::FKConstraint->throw(
1741 error => 'Broken FK constraint',
1742 broken_fk => $+{column}
1747 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1749 Koha::Exceptions::Object::DuplicateID->throw(
1750 error => 'Duplicate ID',
1751 duplicate_id => $+{key}
1754 elsif ( $_->{msg} =~
1755 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1757 { # The optional \W in the regex might be a quote or backtick
1758 my $type = $+{type};
1759 my $value = $+{value};
1760 my $property = $+{property};
1761 $property =~ s/['`]//g;
1762 Koha::Exceptions::Object::BadValue->throw(
1765 property => $property =~ /(\w+\.\w+)$/
1768 , # results in table.column without quotes or backtics
1772 # Catch-all for foreign key breakages. It will help find other use cases
1781 =head3 remove_from_bundle
1783 Remove this item from any bundle it may have been attached to.
1787 sub remove_from_bundle {
1790 my $bundle_host = $self->bundle_host;
1792 return 0 unless $bundle_host; # Should not we raise an exception here?
1794 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $bundle_host->checkout;
1796 my $bundle_item_rs = $self->_result->item_bundles_item;
1797 if ( $bundle_item_rs ) {
1798 $bundle_item_rs->delete;
1799 $self->notforloan(0)->store();
1805 =head2 Internal methods
1807 =head3 _after_item_action_hooks
1809 Helper method that takes care of calling all plugin hooks
1813 sub _after_item_action_hooks {
1814 my ( $self, $params ) = @_;
1816 my $action = $params->{action};
1818 Koha::Plugins->call(
1819 'after_item_action',
1823 item_id => $self->itemnumber,
1830 my $recall = $item->recall;
1832 Return the relevant recall for this item
1838 my @recalls = Koha::Recalls->search(
1840 biblio_id => $self->biblionumber,
1843 { order_by => { -asc => 'created_date' } }
1846 my $item_level_recall;
1847 foreach my $recall (@recalls) {
1848 if ( $recall->item_level ) {
1849 $item_level_recall = 1;
1850 if ( $recall->item_id == $self->itemnumber ) {
1855 if ($item_level_recall) {
1857 # recall needs to be filled be a specific item only
1858 # no other item is relevant to return
1862 # no item-level recall to return, so return earliest biblio-level
1863 # FIXME: eventually this will be based on priority
1867 =head3 can_be_recalled
1869 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1871 Does item-level checks and returns if items can be recalled by this borrower
1875 sub can_be_recalled {
1876 my ( $self, $params ) = @_;
1878 return 0 if !( C4::Context->preference('UseRecalls') );
1880 # check if this item is not for loan, withdrawn or lost
1881 return 0 if ( $self->notforloan != 0 );
1882 return 0 if ( $self->itemlost != 0 );
1883 return 0 if ( $self->withdrawn != 0 );
1885 # check if this item is not checked out - if not checked out, can't be recalled
1886 return 0 if ( !defined( $self->checkout ) );
1888 my $patron = $params->{patron};
1890 my $branchcode = C4::Context->userenv->{'branch'};
1892 $branchcode = C4::Circulation::_GetCircControlBranch( $self, $patron );
1895 # Check the circulation rule for each relevant itemtype for this item
1896 my $rule = Koha::CirculationRules->get_effective_rules({
1897 branchcode => $branchcode,
1898 categorycode => $patron ? $patron->categorycode : undef,
1899 itemtype => $self->effective_itemtype,
1902 'recalls_per_record',
1907 # check recalls allowed has been set and is not zero
1908 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1911 # check borrower has not reached open recalls allowed limit
1912 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1914 # check borrower has not reach open recalls allowed per record limit
1915 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1917 # check if this patron has already recalled this item
1918 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1920 # check if this patron has already checked out this item
1921 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1923 # check if this patron has already reserved this item
1924 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1927 # check item availability
1928 # items are unavailable for recall if they are lost, withdrawn or notforloan
1929 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1931 # if there are no available items at all, no recall can be placed
1932 return 0 if ( scalar @items == 0 );
1934 my $checked_out_count = 0;
1936 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1939 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1940 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1942 # can't recall if no items have been checked out
1943 return 0 if ( $checked_out_count == 0 );
1949 =head3 can_be_waiting_recall
1951 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1953 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1954 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1958 sub can_be_waiting_recall {
1961 return 0 if !( C4::Context->preference('UseRecalls') );
1963 # check if this item is not for loan, withdrawn or lost
1964 return 0 if ( $self->notforloan != 0 );
1965 return 0 if ( $self->itemlost != 0 );
1966 return 0 if ( $self->withdrawn != 0 );
1968 my $branchcode = $self->holdingbranch;
1969 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1970 $branchcode = C4::Context->userenv->{'branch'};
1972 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1975 # Check the circulation rule for each relevant itemtype for this item
1976 my $most_relevant_recall = $self->check_recalls;
1977 my $rule = Koha::CirculationRules->get_effective_rules(
1979 branchcode => $branchcode,
1980 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1981 itemtype => $self->effective_itemtype,
1982 rules => [ 'recalls_allowed', ],
1986 # check recalls allowed has been set and is not zero
1987 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1993 =head3 check_recalls
1995 my $recall = $item->check_recalls;
1997 Get the most relevant recall for this item.
2004 my @recalls = Koha::Recalls->search(
2005 { biblio_id => $self->biblionumber,
2006 item_id => [ $self->itemnumber, undef ]
2008 { order_by => { -asc => 'created_date' } }
2009 )->filter_by_current->as_list;
2012 # iterate through relevant recalls to find the best one.
2013 # if we come across a waiting recall, use this one.
2014 # 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.
2015 foreach my $r ( @recalls ) {
2016 if ( $r->waiting ) {
2021 unless ( defined $recall ) {
2022 $recall = $recalls[0];
2028 =head3 is_notforloan
2030 my $is_notforloan = $item->is_notforloan;
2032 Determine whether or not this item is "notforloan" based on
2033 the item's notforloan status or its item type
2039 my $is_notforloan = 0;
2041 if ( $self->notforloan ){
2045 my $itemtype = $self->itemtype;
2047 if ( $itemtype->notforloan ){
2053 return $is_notforloan;
2056 =head3 is_denied_renewal
2058 my $is_denied_renewal = $item->is_denied_renewal;
2060 Determine whether or not this item can be renewed based on the
2061 rules set in the ItemsDeniedRenewal system preference.
2065 sub is_denied_renewal {
2067 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2068 return 0 unless $denyingrules;
2069 foreach my $field (keys %$denyingrules) {
2070 # Silently ignore bad column names; TODO we should validate elsewhere
2071 next if !$self->_result->result_source->has_column($field);
2072 my $val = $self->$field;
2073 if( !defined $val) {
2074 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2077 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2078 # If the results matches the values in the syspref
2079 # We return true if match found
2088 Returns a map of column name to string representations including the string,
2089 the mapping type and the mapping category where appropriate.
2091 Currently handles authorised value mappings, library, callnumber and itemtype
2094 Accepts a param hashref where the 'public' key denotes whether we want the public
2095 or staff client strings.
2100 my ( $self, $params ) = @_;
2101 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2102 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2103 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2105 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2107 # Hardcoded known 'authorised_value' values mapped to API codes
2108 my $code_to_type = {
2109 branches => 'library',
2110 cn_source => 'call_number_source',
2111 itemtypes => 'item_type',
2114 # Handle not null and default values for integers and dates
2117 foreach my $col ( @{$self->_columns} ) {
2119 # By now, we are done with known columns, now check the framework for mappings
2120 my $field = $self->_result->result_source->name . '.' . $col;
2122 # Check there's an entry in the MARC subfield structure for the field
2123 if ( exists $mss->{$field}
2124 && scalar @{ $mss->{$field} } > 0
2125 && $mss->{$field}[0]->{authorised_value} )
2127 my $subfield = $mss->{$field}[0];
2128 my $code = $subfield->{authorised_value};
2130 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2131 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2132 $strings->{$col} = {
2135 ( $type eq 'av' ? ( category => $code ) : () ),
2153 Kyle M Hall <kyle@bywatersolutions.com>