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;
58 use base qw(Koha::Object);
62 Koha::Item - Koha Item object class
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
82 my $params = @_ ? shift : {};
84 my $log_action = $params->{log_action} // 1;
86 # We do not want to oblige callers to pass this value
87 # Dev conveniences vs performance?
88 unless ( $self->biblioitemnumber ) {
89 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
92 # See related changes from C4::Items::AddItem
93 unless ( $self->itype ) {
94 $self->itype($self->biblio->biblioitem->itemtype);
97 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
99 my $today = dt_from_string;
100 my $action = 'create';
102 unless ( $self->in_storage ) { #AddItem
104 unless ( $self->permanent_location ) {
105 $self->permanent_location($self->location);
108 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109 unless ( $self->location || !$default_location ) {
110 $self->permanent_location( $self->location || $default_location )
111 unless $self->permanent_location;
112 $self->location($default_location);
115 unless ( $self->replacementpricedate ) {
116 $self->replacementpricedate($today);
118 unless ( $self->datelastseen ) {
119 $self->datelastseen($today);
122 unless ( $self->dateaccessioned ) {
123 $self->dateaccessioned($today);
126 if ( $self->itemcallnumber
127 or $self->cn_source )
129 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130 $self->cn_sort($cn_sort);
137 my %updated_columns = $self->_result->get_dirty_columns;
138 return $self->SUPER::store unless %updated_columns;
140 # Retrieve the item for comparison if we need to
142 exists $updated_columns{itemlost}
143 or exists $updated_columns{withdrawn}
144 or exists $updated_columns{damaged}
145 ) ? $self->get_from_storage : undef;
147 # Update *_on fields if needed
148 # FIXME: Why not for AddItem as well?
149 my @fields = qw( itemlost withdrawn damaged );
150 for my $field (@fields) {
152 # If the field is defined but empty or 0, we are
153 # removing/unsetting and thus need to clear out
155 if ( exists $updated_columns{$field}
156 && defined( $self->$field )
159 my $field_on = "${field}_on";
160 $self->$field_on(undef);
162 # If the field has changed otherwise, we much update
164 elsif (exists $updated_columns{$field}
165 && $updated_columns{$field}
166 && !$pre_mod_item->$field )
168 my $field_on = "${field}_on";
169 $self->$field_on(dt_from_string);
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
210 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
212 biblio_ids => [ $self->biblionumber ]
214 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
225 my $params = @_ ? shift : {};
227 # FIXME check the item has no current issues
228 # i.e. raise the appropriate exception
230 # Get the item group so we can delete it later if it has no items left
231 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
233 my $result = $self->SUPER::delete;
235 # Delete the item gorup if it has no items left
236 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
251 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error //= "not_same_branch"
296 if defined C4::Context->userenv
297 and defined C4::Context->userenv->{number}
298 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
300 # check it doesn't have a waiting reserve
301 $error //= "book_reserved"
302 if $self->holds->filter_by_found->count;
304 $error //= "linked_analytics"
305 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
307 $error //= "last_item_for_hold"
308 if $self->biblio->items->count == 1
309 && $self->biblio->holds->search(
316 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
319 return Koha::Result::Boolean->new(1);
322 =head3 move_to_deleted
324 my $is_moved = $item->move_to_deleted;
326 Move an item to the deleteditems table.
327 This can be done before deleting an item, to make sure the data are not completely deleted.
331 sub move_to_deleted {
333 my $item_infos = $self->unblessed;
334 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
335 $item_infos->{deleted_on} = dt_from_string;
336 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;
570 return Koha::Item::Transfers->_new_from_dbic($transfer)->next;
575 my $transfer = $item->get_transfers;
577 Return the list of outstanding transfers (i.e requested but not yet cancelled
580 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
581 whereby the most recently sent, but not received, transfer will be returned
582 first if it exists, otherwise requests are in oldest to newest request order.
584 This allows for transfers to queue, which is the case for stock rotation and
585 rotating collections where a manual transfer may need to take precedence but
586 we still expect the item to end up at a final location eventually.
593 my $transfer_rs = $self->_result->current_branchtransfers;
595 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
598 =head3 last_returned_by
600 Gets and sets the last borrower to return an item.
602 Accepts and returns Koha::Patron objects
604 $item->last_returned_by( $borrowernumber );
606 $last_returned_by = $item->last_returned_by();
610 sub last_returned_by {
611 my ( $self, $borrower ) = @_;
613 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
616 return $items_last_returned_by_rs->update_or_create(
617 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
620 unless ( $self->{_last_returned_by} ) {
621 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
623 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
627 return $self->{_last_returned_by};
631 =head3 can_article_request
633 my $bool = $item->can_article_request( $borrower )
635 Returns true if item can be specifically requested
637 $borrower must be a Koha::Patron object
641 sub can_article_request {
642 my ( $self, $borrower ) = @_;
644 my $rule = $self->article_request_type($borrower);
646 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
650 =head3 hidden_in_opac
652 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
654 Returns true if item fields match the hidding criteria defined in $rules.
655 Returns false otherwise.
657 Takes HASHref that can have the following parameters:
659 $rules : { <field> => [ value_1, ... ], ... }
661 Note: $rules inherits its structure from the parsed YAML from reading
662 the I<OpacHiddenItems> system preference.
667 my ( $self, $params ) = @_;
669 my $rules = $params->{rules} // {};
672 if C4::Context->preference('hidelostitems') and
675 my $hidden_in_opac = 0;
677 foreach my $field ( keys %{$rules} ) {
679 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
685 return $hidden_in_opac;
688 =head3 can_be_transferred
690 $item->can_be_transferred({ to => $to_library, from => $from_library })
691 Checks if an item can be transferred to given library.
693 This feature is controlled by two system preferences:
694 UseBranchTransferLimits to enable / disable the feature
695 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
696 for setting the limitations
698 Takes HASHref that can have the following parameters:
699 MANDATORY PARAMETERS:
702 $from : Koha::Library # if not given, item holdingbranch
703 # will be used instead
705 Returns 1 if item can be transferred to $to_library, otherwise 0.
707 To find out whether at least one item of a Koha::Biblio can be transferred, please
708 see Koha::Biblio->can_be_transferred() instead of using this method for
709 multiple items of the same biblio.
713 sub can_be_transferred {
714 my ($self, $params) = @_;
716 my $to = $params->{to};
717 my $from = $params->{from};
719 $to = $to->branchcode;
720 $from = defined $from ? $from->branchcode : $self->holdingbranch;
722 return 1 if $from eq $to; # Transfer to current branch is allowed
723 return 1 unless C4::Context->preference('UseBranchTransferLimits');
725 my $limittype = C4::Context->preference('BranchTransferLimitsType');
726 return Koha::Item::Transfer::Limits->search({
729 $limittype => $limittype eq 'itemtype'
730 ? $self->effective_itemtype : $self->ccode
735 =head3 pickup_locations
737 my $pickup_locations = $item->pickup_locations({ patron => $patron })
739 Returns possible pickup locations for this item, according to patron's home library
740 and if item can be transferred to each pickup location.
742 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
747 sub pickup_locations {
748 my ($self, $params) = @_;
750 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
751 unless exists $params->{patron};
753 my $patron = $params->{patron};
755 my $circ_control_branch =
756 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
758 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
760 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
761 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
763 my $pickup_libraries = Koha::Libraries->search();
764 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
765 $pickup_libraries = $self->home_branch->get_hold_libraries;
766 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
767 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
768 $pickup_libraries = $plib->get_hold_libraries;
769 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
770 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
771 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
772 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
775 return $pickup_libraries->search(
780 order_by => ['branchname']
782 ) unless C4::Context->preference('UseBranchTransferLimits');
784 my $limittype = C4::Context->preference('BranchTransferLimitsType');
785 my ($ccode, $itype) = (undef, undef);
786 if( $limittype eq 'ccode' ){
787 $ccode = $self->ccode;
789 $itype = $self->itype;
791 my $limits = Koha::Item::Transfer::Limits->search(
793 fromBranch => $self->holdingbranch,
797 { columns => ['toBranch'] }
800 return $pickup_libraries->search(
802 pickup_location => 1,
804 '-not_in' => $limits->_resultset->as_query
808 order_by => ['branchname']
813 =head3 article_request_type
815 my $type = $item->article_request_type( $borrower )
817 returns 'yes', 'no', 'bib_only', or 'item_only'
819 $borrower must be a Koha::Patron object
823 sub article_request_type {
824 my ( $self, $borrower ) = @_;
826 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
828 $branch_control eq 'homebranch' ? $self->homebranch
829 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
831 my $borrowertype = $borrower->categorycode;
832 my $itemtype = $self->effective_itemtype();
833 my $rule = Koha::CirculationRules->get_effective_rule(
835 rule_name => 'article_requests',
836 categorycode => $borrowertype,
837 itemtype => $itemtype,
838 branchcode => $branchcode
842 return q{} unless $rule;
843 return $rule->rule_value || q{}
852 my $attributes = { order_by => 'priority' };
853 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
855 itemnumber => $self->itemnumber,
858 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
859 waitingdate => { '!=' => undef },
862 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
863 return Koha::Holds->_new_from_dbic($hold_rs);
866 =head3 stockrotationitem
868 my $sritem = Koha::Item->stockrotationitem;
870 Returns the stock rotation item associated with the current item.
874 sub stockrotationitem {
876 my $rs = $self->_result->stockrotationitem;
878 return Koha::StockRotationItem->_new_from_dbic( $rs );
883 my $item = $item->add_to_rota($rota_id);
885 Add this item to the rota identified by $ROTA_ID, which means associating it
886 with the first stage of that rota. Should this item already be associated
887 with a rota, then we will move it to the new rota.
892 my ( $self, $rota_id ) = @_;
893 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
897 =head3 has_pending_hold
899 my $is_pending_hold = $item->has_pending_hold();
901 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
905 sub has_pending_hold {
907 my $pending_hold = $self->_result->tmp_holdsqueues;
908 return $pending_hold->count ? 1: 0;
911 =head3 has_pending_recall {
913 my $has_pending_recall
915 Return if whether has pending recall of not.
919 sub has_pending_recall {
922 # FIXME Must be moved to $self->recalls
923 return Koha::Recalls->search(
925 item_id => $self->itemnumber,
933 my $field = $item->as_marc_field;
935 This method returns a MARC::Field object representing the Koha::Item object
936 with the current mappings configuration.
943 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
945 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
949 my $item_field = $tagslib->{$itemtag};
951 my $more_subfields = $self->additional_attributes->to_hashref;
952 foreach my $subfield (
954 $a->{display_order} <=> $b->{display_order}
955 || $a->{subfield} cmp $b->{subfield}
956 } grep { ref($_) && %$_ } values %$item_field
959 my $kohafield = $subfield->{kohafield};
960 my $tagsubfield = $subfield->{tagsubfield};
962 if ( defined $kohafield && $kohafield ne '' ) {
963 next if $kohafield !~ m{^items\.}; # That would be weird!
964 ( my $attribute = $kohafield ) =~ s|^items\.||;
965 $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
966 if defined $self->$attribute and $self->$attribute ne '';
968 $value = $more_subfields->{$tagsubfield}
971 next unless defined $value
974 if ( $subfield->{repeatable} ) {
975 my @values = split '\|', $value;
976 push @subfields, ( $tagsubfield => $_ ) for @values;
979 push @subfields, ( $tagsubfield => $value );
984 return unless @subfields;
986 return MARC::Field->new(
987 "$itemtag", ' ', ' ', @subfields
991 =head3 renewal_branchcode
993 Returns the branchcode to be recorded in statistics renewal of the item
997 sub renewal_branchcode {
999 my ($self, $params ) = @_;
1001 my $interface = C4::Context->interface;
1003 if ( $interface eq 'opac' ){
1004 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1005 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1006 $branchcode = 'OPACRenew';
1008 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1009 $branchcode = $self->homebranch;
1011 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1012 $branchcode = $self->checkout->patron->branchcode;
1014 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1015 $branchcode = $self->checkout->branchcode;
1021 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1022 ? C4::Context->userenv->{branch} : $params->{branch};
1029 Return the cover images associated with this item.
1036 my $cover_image_rs = $self->_result->cover_images;
1037 return unless $cover_image_rs;
1038 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1041 =head3 columns_to_str
1043 my $values = $items->columns_to_str;
1045 Return a hashref with the string representation of the different attribute of the item.
1047 This is meant to be used for display purpose only.
1051 sub columns_to_str {
1054 my $frameworkcode = $self->biblio->frameworkcode;
1055 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1056 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1058 my $columns_info = $self->_result->result_source->columns_info;
1060 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1062 for my $column ( keys %$columns_info ) {
1064 next if $column eq 'more_subfields_xml';
1066 my $value = $self->$column;
1067 # 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
1069 if ( not defined $value or $value eq "" ) {
1070 $values->{$column} = $value;
1075 exists $mss->{"items.$column"}
1076 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1079 $values->{$column} =
1081 ? $subfield->{authorised_value}
1082 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1083 $subfield->{tagsubfield}, $value, '', $tagslib )
1089 $self->more_subfields_xml
1090 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1095 my ( $field ) = $marc_more->fields;
1096 for my $sf ( $field->subfields ) {
1097 my $subfield_code = $sf->[0];
1098 my $value = $sf->[1];
1099 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1100 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1102 $subfield->{authorised_value}
1103 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1104 $subfield->{tagsubfield}, $value, '', $tagslib )
1107 push @{$more_values->{$subfield_code}}, $value;
1110 while ( my ( $k, $v ) = each %$more_values ) {
1111 $values->{$k} = join ' | ', @$v;
1118 =head3 additional_attributes
1120 my $attributes = $item->additional_attributes;
1121 $attributes->{k} = 'new k';
1122 $item->update({ more_subfields => $attributes->to_marcxml });
1124 Returns a Koha::Item::Attributes object that represents the non-mapped
1125 attributes for this item.
1129 sub additional_attributes {
1132 return Koha::Item::Attributes->new_from_marcxml(
1133 $self->more_subfields_xml,
1137 =head3 _set_found_trigger
1139 $self->_set_found_trigger
1141 Finds the most recent lost item charge for this item and refunds the patron
1142 appropriately, taking into account any payments or writeoffs already applied
1145 Internal function, not exported, called only by Koha::Item->store.
1149 sub _set_found_trigger {
1150 my ( $self, $pre_mod_item ) = @_;
1152 # Reverse any lost item charges if necessary.
1153 my $no_refund_after_days =
1154 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1155 if ($no_refund_after_days) {
1156 my $today = dt_from_string();
1157 my $lost_age_in_days =
1158 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1161 return $self unless $lost_age_in_days < $no_refund_after_days;
1164 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1167 return_branch => C4::Context->userenv
1168 ? C4::Context->userenv->{'branch'}
1172 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1174 if ( $lostreturn_policy ) {
1176 # refund charge made for lost book
1177 my $lost_charge = Koha::Account::Lines->search(
1179 itemnumber => $self->itemnumber,
1180 debit_type_code => 'LOST',
1181 status => [ undef, { '<>' => 'FOUND' } ]
1184 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1189 if ( $lost_charge ) {
1191 my $patron = $lost_charge->patron;
1194 my $account = $patron->account;
1196 # Credit outstanding amount
1197 my $credit_total = $lost_charge->amountoutstanding;
1201 $lost_charge->amount > $lost_charge->amountoutstanding &&
1202 $lostreturn_policy ne "refund_unpaid"
1204 # some amount has been cancelled. collect the offsets that are not writeoffs
1205 # this works because the only way to subtract from this kind of a debt is
1206 # using the UI buttons 'Pay' and 'Write off'
1208 # We don't credit any payments if return policy is
1211 # In that case only unpaid/outstanding amount
1212 # will be credited which settles the debt without
1213 # creating extra credits
1215 my $credit_offsets = $lost_charge->debit_offsets(
1217 'credit_id' => { '!=' => undef },
1218 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1220 { join => 'credit' }
1223 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1224 # credits are negative on the DB
1225 $credit_offsets->total * -1 :
1227 # Credit the outstanding amount, then add what has been
1228 # paid to create a net credit for this amount
1229 $credit_total += $total_to_refund;
1233 if ( $credit_total > 0 ) {
1235 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1236 $credit = $account->add_credit(
1238 amount => $credit_total,
1239 description => 'Item found ' . $self->itemnumber,
1240 type => 'LOST_FOUND',
1241 interface => C4::Context->interface,
1242 library_id => $branchcode,
1243 item_id => $self->itemnumber,
1244 issue_id => $lost_charge->issue_id
1248 $credit->apply( { debits => [$lost_charge] } );
1252 message => 'lost_refunded',
1253 payload => { credit_id => $credit->id }
1258 # Update the account status
1259 $lost_charge->status('FOUND');
1260 $lost_charge->store();
1262 # Reconcile balances if required
1263 if ( C4::Context->preference('AccountAutoReconcile') ) {
1264 $account->reconcile_balance;
1269 # possibly restore fine for lost book
1270 my $lost_overdue = Koha::Account::Lines->search(
1272 itemnumber => $self->itemnumber,
1273 debit_type_code => 'OVERDUE',
1277 order_by => { '-desc' => 'date' },
1281 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1283 my $patron = $lost_overdue->patron;
1285 my $account = $patron->account;
1287 # Update status of fine
1288 $lost_overdue->status('FOUND')->store();
1290 # Find related forgive credit
1291 my $refund = $lost_overdue->credits(
1293 credit_type_code => 'FORGIVEN',
1294 itemnumber => $self->itemnumber,
1295 status => [ { '!=' => 'VOID' }, undef ]
1297 { order_by => { '-desc' => 'date' }, rows => 1 }
1301 # Revert the forgive credit
1302 $refund->void({ interface => 'trigger' });
1306 message => 'lost_restored',
1307 payload => { refund_id => $refund->id }
1312 # Reconcile balances if required
1313 if ( C4::Context->preference('AccountAutoReconcile') ) {
1314 $account->reconcile_balance;
1318 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1322 message => 'lost_charge',
1328 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1330 if ( $processingreturn_policy ) {
1332 # refund processing charge made for lost book
1333 my $processing_charge = Koha::Account::Lines->search(
1335 itemnumber => $self->itemnumber,
1336 debit_type_code => 'PROCESSING',
1337 status => [ undef, { '<>' => 'FOUND' } ]
1340 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1345 if ( $processing_charge ) {
1347 my $patron = $processing_charge->patron;
1350 my $account = $patron->account;
1352 # Credit outstanding amount
1353 my $credit_total = $processing_charge->amountoutstanding;
1357 $processing_charge->amount > $processing_charge->amountoutstanding &&
1358 $processingreturn_policy ne "refund_unpaid"
1360 # some amount has been cancelled. collect the offsets that are not writeoffs
1361 # this works because the only way to subtract from this kind of a debt is
1362 # using the UI buttons 'Pay' and 'Write off'
1364 # We don't credit any payments if return policy is
1367 # In that case only unpaid/outstanding amount
1368 # will be credited which settles the debt without
1369 # creating extra credits
1371 my $credit_offsets = $processing_charge->debit_offsets(
1373 'credit_id' => { '!=' => undef },
1374 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1376 { join => 'credit' }
1379 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1380 # credits are negative on the DB
1381 $credit_offsets->total * -1 :
1383 # Credit the outstanding amount, then add what has been
1384 # paid to create a net credit for this amount
1385 $credit_total += $total_to_refund;
1389 if ( $credit_total > 0 ) {
1391 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1392 $credit = $account->add_credit(
1394 amount => $credit_total,
1395 description => 'Item found ' . $self->itemnumber,
1396 type => 'PROCESSING_FOUND',
1397 interface => C4::Context->interface,
1398 library_id => $branchcode,
1399 item_id => $self->itemnumber,
1400 issue_id => $processing_charge->issue_id
1404 $credit->apply( { debits => [$processing_charge] } );
1408 message => 'processing_refunded',
1409 payload => { credit_id => $credit->id }
1414 # Update the account status
1415 $processing_charge->status('FOUND');
1416 $processing_charge->store();
1418 # Reconcile balances if required
1419 if ( C4::Context->preference('AccountAutoReconcile') ) {
1420 $account->reconcile_balance;
1429 =head3 public_read_list
1431 This method returns the list of publicly readable database fields for both API and UI output purposes
1435 sub public_read_list {
1437 'itemnumber', 'biblionumber', 'homebranch',
1438 'holdingbranch', 'location', 'collectioncode',
1439 'itemcallnumber', 'copynumber', 'enumchron',
1440 'barcode', 'dateaccessioned', 'itemnotes',
1441 'onloan', 'uri', 'itype',
1442 'notforloan', 'damaged', 'itemlost',
1443 'withdrawn', 'restricted'
1449 Overloaded to_api method to ensure item-level itypes is adhered to.
1454 my ($self, $params) = @_;
1456 my $response = $self->SUPER::to_api($params);
1459 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1460 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1462 return { %$response, %$overrides };
1465 =head3 to_api_mapping
1467 This method returns the mapping for representing a Koha::Item object
1472 sub to_api_mapping {
1474 itemnumber => 'item_id',
1475 biblionumber => 'biblio_id',
1476 biblioitemnumber => undef,
1477 barcode => 'external_id',
1478 dateaccessioned => 'acquisition_date',
1479 booksellerid => 'acquisition_source',
1480 homebranch => 'home_library_id',
1481 price => 'purchase_price',
1482 replacementprice => 'replacement_price',
1483 replacementpricedate => 'replacement_price_date',
1484 datelastborrowed => 'last_checkout_date',
1485 datelastseen => 'last_seen_date',
1487 notforloan => 'not_for_loan_status',
1488 damaged => 'damaged_status',
1489 damaged_on => 'damaged_date',
1490 itemlost => 'lost_status',
1491 itemlost_on => 'lost_date',
1492 withdrawn => 'withdrawn',
1493 withdrawn_on => 'withdrawn_date',
1494 itemcallnumber => 'callnumber',
1495 coded_location_qualifier => 'coded_location_qualifier',
1496 issues => 'checkouts_count',
1497 renewals => 'renewals_count',
1498 reserves => 'holds_count',
1499 restricted => 'restricted_status',
1500 itemnotes => 'public_notes',
1501 itemnotes_nonpublic => 'internal_notes',
1502 holdingbranch => 'holding_library_id',
1503 timestamp => 'timestamp',
1504 location => 'location',
1505 permanent_location => 'permanent_location',
1506 onloan => 'checked_out_date',
1507 cn_source => 'call_number_source',
1508 cn_sort => 'call_number_sort',
1509 ccode => 'collection_code',
1510 materials => 'materials_notes',
1512 itype => 'item_type_id',
1513 more_subfields_xml => 'extended_subfields',
1514 enumchron => 'serial_issue_number',
1515 copynumber => 'copy_number',
1516 stocknumber => 'inventory_number',
1517 new_status => 'new_status',
1518 deleted_on => undef,
1524 my $itemtype = $item->itemtype;
1526 Returns Koha object for effective itemtype
1533 return Koha::ItemTypes->find( $self->effective_itemtype );
1538 my $orders = $item->orders();
1540 Returns a Koha::Acquisition::Orders object
1547 my $orders = $self->_result->item_orders;
1548 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1551 =head3 tracked_links
1553 my $tracked_links = $item->tracked_links();
1555 Returns a Koha::TrackedLinks object
1562 my $tracked_links = $self->_result->linktrackers;
1563 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1566 =head3 move_to_biblio
1568 $item->move_to_biblio($to_biblio[, $params]);
1570 Move the item to another biblio and update any references in other tables.
1572 The final optional parameter, C<$params>, is expected to contain the
1573 'skip_record_index' key, which is relayed down to Koha::Item->store.
1574 There it prevents calling index_records, which takes most of the
1575 time in batch adds/deletes. The caller must take care of calling
1576 index_records separately.
1579 skip_record_index => 1|0
1581 Returns undef if the move failed or the biblionumber of the destination record otherwise
1585 sub move_to_biblio {
1586 my ( $self, $to_biblio, $params ) = @_;
1590 return if $self->biblionumber == $to_biblio->biblionumber;
1592 my $from_biblionumber = $self->biblionumber;
1593 my $to_biblionumber = $to_biblio->biblionumber;
1595 # Own biblionumber and biblioitemnumber
1597 biblionumber => $to_biblionumber,
1598 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1599 })->store({ skip_record_index => $params->{skip_record_index} });
1601 unless ($params->{skip_record_index}) {
1602 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1603 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1606 # Acquisition orders
1607 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1610 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1612 # hold_fill_target (there's no Koha object available yet)
1613 my $hold_fill_target = $self->_result->hold_fill_target;
1614 if ($hold_fill_target) {
1615 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1618 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1619 # and can't even fake one since the significant columns are nullable.
1620 my $storage = $self->_result->result_source->storage;
1623 my ($storage, $dbh, @cols) = @_;
1625 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1630 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1632 return $to_biblionumber;
1637 my $bundle_items = $item->bundle_items;
1639 Returns the items associated with this bundle
1646 if ( !$self->{_bundle_items_cached} ) {
1647 my $bundle_items = Koha::Items->search(
1648 { 'item_bundles_item.host' => $self->itemnumber },
1649 { join => 'item_bundles_item' } );
1650 $self->{_bundle_items} = $bundle_items;
1651 $self->{_bundle_items_cached} = 1;
1654 return $self->{_bundle_items};
1659 my $is_bundle = $item->is_bundle;
1661 Returns whether the item is a bundle or not
1667 return $self->bundle_items->count ? 1 : 0;
1672 my $bundle = $item->bundle_host;
1674 Returns the bundle item this item is attached to
1681 my $bundle_items_rs = $self->_result->item_bundles_item;
1682 return unless $bundle_items_rs;
1683 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1688 my $in_bundle = $item->in_bundle;
1690 Returns whether this item is currently in a bundle
1696 return $self->bundle_host ? 1 : 0;
1699 =head3 add_to_bundle
1701 my $link = $item->add_to_bundle($bundle_item);
1703 Adds the bundle_item passed to this item
1708 my ( $self, $bundle_item, $options ) = @_;
1712 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1713 if ( $self->itemnumber eq $bundle_item->itemnumber
1714 || $bundle_item->is_bundle
1715 || $self->in_bundle );
1717 my $schema = Koha::Database->new->schema;
1719 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1724 my $checkout = $bundle_item->checkout;
1726 unless ($options->{force_checkin}) {
1727 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1730 my $branchcode = C4::Context->userenv->{'branch'};
1731 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1733 Koha::Exceptions::Checkin::FailedCheckin->throw();
1737 my $holds = $bundle_item->current_holds;
1738 if ($holds->count) {
1739 unless ($options->{ignore_holds}) {
1740 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1744 $self->_result->add_to_item_bundles_hosts(
1745 { item => $bundle_item->itemnumber } );
1747 $bundle_item->notforloan($BundleNotLoanValue)->store();
1753 # 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
1754 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1755 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1757 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1758 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1759 Koha::Exceptions::Object::FKConstraint->throw(
1760 error => 'Broken FK constraint',
1761 broken_fk => $+{column}
1766 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1768 Koha::Exceptions::Object::DuplicateID->throw(
1769 error => 'Duplicate ID',
1770 duplicate_id => $+{key}
1773 elsif ( $_->{msg} =~
1774 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1776 { # The optional \W in the regex might be a quote or backtick
1777 my $type = $+{type};
1778 my $value = $+{value};
1779 my $property = $+{property};
1780 $property =~ s/['`]//g;
1781 Koha::Exceptions::Object::BadValue->throw(
1784 property => $property =~ /(\w+\.\w+)$/
1787 , # results in table.column without quotes or backtics
1791 # Catch-all for foreign key breakages. It will help find other use cases
1800 =head3 remove_from_bundle
1802 Remove this item from any bundle it may have been attached to.
1806 sub remove_from_bundle {
1809 my $bundle_item_rs = $self->_result->item_bundles_item;
1810 if ( $bundle_item_rs ) {
1811 $bundle_item_rs->delete;
1812 $self->notforloan(0)->store();
1818 =head2 Internal methods
1820 =head3 _after_item_action_hooks
1822 Helper method that takes care of calling all plugin hooks
1826 sub _after_item_action_hooks {
1827 my ( $self, $params ) = @_;
1829 my $action = $params->{action};
1831 Koha::Plugins->call(
1832 'after_item_action',
1836 item_id => $self->itemnumber,
1843 my $recall = $item->recall;
1845 Return the relevant recall for this item
1851 my @recalls = Koha::Recalls->search(
1853 biblio_id => $self->biblionumber,
1856 { order_by => { -asc => 'created_date' } }
1858 foreach my $recall (@recalls) {
1859 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1863 # no item-level recall to return, so return earliest biblio-level
1864 # FIXME: eventually this will be based on priority
1868 =head3 can_be_recalled
1870 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1872 Does item-level checks and returns if items can be recalled by this borrower
1876 sub can_be_recalled {
1877 my ( $self, $params ) = @_;
1879 return 0 if !( C4::Context->preference('UseRecalls') );
1881 # check if this item is not for loan, withdrawn or lost
1882 return 0 if ( $self->notforloan != 0 );
1883 return 0 if ( $self->itemlost != 0 );
1884 return 0 if ( $self->withdrawn != 0 );
1886 # check if this item is not checked out - if not checked out, can't be recalled
1887 return 0 if ( !defined( $self->checkout ) );
1889 my $patron = $params->{patron};
1891 my $branchcode = C4::Context->userenv->{'branch'};
1893 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1896 # Check the circulation rule for each relevant itemtype for this item
1897 my $rule = Koha::CirculationRules->get_effective_rules({
1898 branchcode => $branchcode,
1899 categorycode => $patron ? $patron->categorycode : undef,
1900 itemtype => $self->effective_itemtype,
1903 'recalls_per_record',
1908 # check recalls allowed has been set and is not zero
1909 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1912 # check borrower has not reached open recalls allowed limit
1913 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1915 # check borrower has not reach open recalls allowed per record limit
1916 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1918 # check if this patron has already recalled this item
1919 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1921 # check if this patron has already checked out this item
1922 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1924 # check if this patron has already reserved this item
1925 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1928 # check item availability
1929 # items are unavailable for recall if they are lost, withdrawn or notforloan
1930 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1932 # if there are no available items at all, no recall can be placed
1933 return 0 if ( scalar @items == 0 );
1935 my $checked_out_count = 0;
1937 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1940 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1941 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1943 # can't recall if no items have been checked out
1944 return 0 if ( $checked_out_count == 0 );
1950 =head3 can_be_waiting_recall
1952 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1954 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1955 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1959 sub can_be_waiting_recall {
1962 return 0 if !( C4::Context->preference('UseRecalls') );
1964 # check if this item is not for loan, withdrawn or lost
1965 return 0 if ( $self->notforloan != 0 );
1966 return 0 if ( $self->itemlost != 0 );
1967 return 0 if ( $self->withdrawn != 0 );
1969 my $branchcode = $self->holdingbranch;
1970 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1971 $branchcode = C4::Context->userenv->{'branch'};
1973 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1976 # Check the circulation rule for each relevant itemtype for this item
1977 my $most_relevant_recall = $self->check_recalls;
1978 my $rule = Koha::CirculationRules->get_effective_rules(
1980 branchcode => $branchcode,
1981 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1982 itemtype => $self->effective_itemtype,
1983 rules => [ 'recalls_allowed', ],
1987 # check recalls allowed has been set and is not zero
1988 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1994 =head3 check_recalls
1996 my $recall = $item->check_recalls;
1998 Get the most relevant recall for this item.
2005 my @recalls = Koha::Recalls->search(
2006 { biblio_id => $self->biblionumber,
2007 item_id => [ $self->itemnumber, undef ]
2009 { order_by => { -asc => 'created_date' } }
2010 )->filter_by_current->as_list;
2013 # iterate through relevant recalls to find the best one.
2014 # if we come across a waiting recall, use this one.
2015 # 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.
2016 foreach my $r ( @recalls ) {
2017 if ( $r->waiting ) {
2022 unless ( defined $recall ) {
2023 $recall = $recalls[0];
2029 =head3 is_notforloan
2031 my $is_notforloan = $item->is_notforloan;
2033 Determine whether or not this item is "notforloan" based on
2034 the item's notforloan status or its item type
2040 my $is_notforloan = 0;
2042 if ( $self->notforloan ){
2046 my $itemtype = $self->itemtype;
2048 if ( $itemtype->notforloan ){
2054 return $is_notforloan;
2057 =head3 is_denied_renewal
2059 my $is_denied_renewal = $item->is_denied_renewal;
2061 Determine whether or not this item can be renewed based on the
2062 rules set in the ItemsDeniedRenewal system preference.
2066 sub is_denied_renewal {
2069 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
2070 return 0 unless $denyingrules;
2071 foreach my $field (keys %$denyingrules) {
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 ) = @_;
2102 my $columns_info = $self->_result->result_source->columns_info;
2103 my $frameworkcode = $self->biblio->frameworkcode;
2104 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
2105 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2107 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2109 # Hardcoded known 'authorised_value' values mapped to API codes
2110 my $code_to_type = {
2111 branches => 'library',
2112 cn_source => 'call_number_source',
2113 itemtypes => 'item_type',
2116 # Handle not null and default values for integers and dates
2119 foreach my $col ( keys %{$columns_info} ) {
2121 # By now, we are done with known columns, now check the framework for mappings
2122 my $field = $self->_result->result_source->name . '.' . $col;
2124 # Check there's an entry in the MARC subfield structure for the field
2125 if ( exists $mss->{$field}
2126 && scalar @{ $mss->{$field} } > 0
2127 && $mss->{$field}[0]->{authorised_value} )
2129 my $subfield = $mss->{$field}[0];
2130 my $code = $subfield->{authorised_value};
2132 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2133 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2134 $strings->{$col} = {
2137 ( $type eq 'av' ? ( category => $code ) : () ),
2155 Kyle M Hall <kyle@bywatersolutions.com>