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 return $self->get_transfers->search( {}, { rows => 1 } )->next;
573 my $transfer = $item->get_transfers;
575 Return the list of outstanding transfers (i.e requested but not yet cancelled
578 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
579 whereby the most recently sent, but not received, transfer will be returned
580 first if it exists, otherwise requests are in oldest to newest request order.
582 This allows for transfers to queue, which is the case for stock rotation and
583 rotating collections where a manual transfer may need to take precedence but
584 we still expect the item to end up at a final location eventually.
591 my $transfer_rs = $self->_result->branchtransfers;
593 return Koha::Item::Transfers
594 ->_new_from_dbic($transfer_rs)
596 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
599 =head3 last_returned_by
601 Gets and sets the last borrower to return an item.
603 Accepts and returns Koha::Patron objects
605 $item->last_returned_by( $borrowernumber );
607 $last_returned_by = $item->last_returned_by();
611 sub last_returned_by {
612 my ( $self, $borrower ) = @_;
614 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
617 return $items_last_returned_by_rs->update_or_create(
618 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
621 unless ( $self->{_last_returned_by} ) {
622 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
624 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
628 return $self->{_last_returned_by};
632 =head3 can_article_request
634 my $bool = $item->can_article_request( $borrower )
636 Returns true if item can be specifically requested
638 $borrower must be a Koha::Patron object
642 sub can_article_request {
643 my ( $self, $borrower ) = @_;
645 my $rule = $self->article_request_type($borrower);
647 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
651 =head3 hidden_in_opac
653 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
655 Returns true if item fields match the hidding criteria defined in $rules.
656 Returns false otherwise.
658 Takes HASHref that can have the following parameters:
660 $rules : { <field> => [ value_1, ... ], ... }
662 Note: $rules inherits its structure from the parsed YAML from reading
663 the I<OpacHiddenItems> system preference.
668 my ( $self, $params ) = @_;
670 my $rules = $params->{rules} // {};
673 if C4::Context->preference('hidelostitems') and
676 my $hidden_in_opac = 0;
678 foreach my $field ( keys %{$rules} ) {
680 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
686 return $hidden_in_opac;
689 =head3 can_be_transferred
691 $item->can_be_transferred({ to => $to_library, from => $from_library })
692 Checks if an item can be transferred to given library.
694 This feature is controlled by two system preferences:
695 UseBranchTransferLimits to enable / disable the feature
696 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
697 for setting the limitations
699 Takes HASHref that can have the following parameters:
700 MANDATORY PARAMETERS:
703 $from : Koha::Library # if not given, item holdingbranch
704 # will be used instead
706 Returns 1 if item can be transferred to $to_library, otherwise 0.
708 To find out whether at least one item of a Koha::Biblio can be transferred, please
709 see Koha::Biblio->can_be_transferred() instead of using this method for
710 multiple items of the same biblio.
714 sub can_be_transferred {
715 my ($self, $params) = @_;
717 my $to = $params->{to};
718 my $from = $params->{from};
720 $to = $to->branchcode;
721 $from = defined $from ? $from->branchcode : $self->holdingbranch;
723 return 1 if $from eq $to; # Transfer to current branch is allowed
724 return 1 unless C4::Context->preference('UseBranchTransferLimits');
726 my $limittype = C4::Context->preference('BranchTransferLimitsType');
727 return Koha::Item::Transfer::Limits->search({
730 $limittype => $limittype eq 'itemtype'
731 ? $self->effective_itemtype : $self->ccode
736 =head3 pickup_locations
738 my $pickup_locations = $item->pickup_locations({ patron => $patron })
740 Returns possible pickup locations for this item, according to patron's home library
741 and if item can be transferred to each pickup location.
743 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
748 sub pickup_locations {
749 my ($self, $params) = @_;
751 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
752 unless exists $params->{patron};
754 my $patron = $params->{patron};
756 my $circ_control_branch =
757 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
759 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
761 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
762 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
764 my $pickup_libraries = Koha::Libraries->search();
765 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
766 $pickup_libraries = $self->home_branch->get_hold_libraries;
767 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
768 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
769 $pickup_libraries = $plib->get_hold_libraries;
770 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
771 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
772 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
773 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
776 return $pickup_libraries->search(
781 order_by => ['branchname']
783 ) unless C4::Context->preference('UseBranchTransferLimits');
785 my $limittype = C4::Context->preference('BranchTransferLimitsType');
786 my ($ccode, $itype) = (undef, undef);
787 if( $limittype eq 'ccode' ){
788 $ccode = $self->ccode;
790 $itype = $self->itype;
792 my $limits = Koha::Item::Transfer::Limits->search(
794 fromBranch => $self->holdingbranch,
798 { columns => ['toBranch'] }
801 return $pickup_libraries->search(
803 pickup_location => 1,
805 '-not_in' => $limits->_resultset->as_query
809 order_by => ['branchname']
814 =head3 article_request_type
816 my $type = $item->article_request_type( $borrower )
818 returns 'yes', 'no', 'bib_only', or 'item_only'
820 $borrower must be a Koha::Patron object
824 sub article_request_type {
825 my ( $self, $borrower ) = @_;
827 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
829 $branch_control eq 'homebranch' ? $self->homebranch
830 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
832 my $borrowertype = $borrower->categorycode;
833 my $itemtype = $self->effective_itemtype();
834 my $rule = Koha::CirculationRules->get_effective_rule(
836 rule_name => 'article_requests',
837 categorycode => $borrowertype,
838 itemtype => $itemtype,
839 branchcode => $branchcode
843 return q{} unless $rule;
844 return $rule->rule_value || q{}
853 my $attributes = { order_by => 'priority' };
854 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
856 itemnumber => $self->itemnumber,
859 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
860 waitingdate => { '!=' => undef },
863 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
864 return Koha::Holds->_new_from_dbic($hold_rs);
867 =head3 stockrotationitem
869 my $sritem = Koha::Item->stockrotationitem;
871 Returns the stock rotation item associated with the current item.
875 sub stockrotationitem {
877 my $rs = $self->_result->stockrotationitem;
879 return Koha::StockRotationItem->_new_from_dbic( $rs );
884 my $item = $item->add_to_rota($rota_id);
886 Add this item to the rota identified by $ROTA_ID, which means associating it
887 with the first stage of that rota. Should this item already be associated
888 with a rota, then we will move it to the new rota.
893 my ( $self, $rota_id ) = @_;
894 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
898 =head3 has_pending_hold
900 my $is_pending_hold = $item->has_pending_hold();
902 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
906 sub has_pending_hold {
908 my $pending_hold = $self->_result->tmp_holdsqueues;
909 return $pending_hold->count ? 1: 0;
912 =head3 has_pending_recall {
914 my $has_pending_recall
916 Return if whether has pending recall of not.
920 sub has_pending_recall {
923 # FIXME Must be moved to $self->recalls
924 return Koha::Recalls->search(
926 item_id => $self->itemnumber,
934 my $field = $item->as_marc_field;
936 This method returns a MARC::Field object representing the Koha::Item object
937 with the current mappings configuration.
944 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
946 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
950 my $item_field = $tagslib->{$itemtag};
952 my $more_subfields = $self->additional_attributes->to_hashref;
953 foreach my $subfield (
955 $a->{display_order} <=> $b->{display_order}
956 || $a->{subfield} cmp $b->{subfield}
957 } grep { ref($_) && %$_ } values %$item_field
960 my $kohafield = $subfield->{kohafield};
961 my $tagsubfield = $subfield->{tagsubfield};
963 if ( defined $kohafield && $kohafield ne '' ) {
964 next if $kohafield !~ m{^items\.}; # That would be weird!
965 ( my $attribute = $kohafield ) =~ s|^items\.||;
966 $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
967 if defined $self->$attribute and $self->$attribute ne '';
969 $value = $more_subfields->{$tagsubfield}
972 next unless defined $value
975 if ( $subfield->{repeatable} ) {
976 my @values = split '\|', $value;
977 push @subfields, ( $tagsubfield => $_ ) for @values;
980 push @subfields, ( $tagsubfield => $value );
985 return unless @subfields;
987 return MARC::Field->new(
988 "$itemtag", ' ', ' ', @subfields
992 =head3 renewal_branchcode
994 Returns the branchcode to be recorded in statistics renewal of the item
998 sub renewal_branchcode {
1000 my ($self, $params ) = @_;
1002 my $interface = C4::Context->interface;
1004 if ( $interface eq 'opac' ){
1005 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1006 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1007 $branchcode = 'OPACRenew';
1009 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1010 $branchcode = $self->homebranch;
1012 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1013 $branchcode = $self->checkout->patron->branchcode;
1015 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1016 $branchcode = $self->checkout->branchcode;
1022 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1023 ? C4::Context->userenv->{branch} : $params->{branch};
1030 Return the cover images associated with this item.
1037 my $cover_image_rs = $self->_result->cover_images;
1038 return unless $cover_image_rs;
1039 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1042 =head3 columns_to_str
1044 my $values = $items->columns_to_str;
1046 Return a hashref with the string representation of the different attribute of the item.
1048 This is meant to be used for display purpose only.
1052 sub columns_to_str {
1054 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1055 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1056 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1058 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1061 for my $column ( @{$self->_columns}) {
1063 next if $column eq 'more_subfields_xml';
1065 my $value = $self->$column;
1066 # 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
1068 if ( not defined $value or $value eq "" ) {
1069 $values->{$column} = $value;
1074 exists $mss->{"items.$column"}
1075 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1078 $values->{$column} =
1080 ? $subfield->{authorised_value}
1081 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1082 $subfield->{tagsubfield}, $value, '', $tagslib )
1088 $self->more_subfields_xml
1089 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1094 my ( $field ) = $marc_more->fields;
1095 for my $sf ( $field->subfields ) {
1096 my $subfield_code = $sf->[0];
1097 my $value = $sf->[1];
1098 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1099 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1101 $subfield->{authorised_value}
1102 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1103 $subfield->{tagsubfield}, $value, '', $tagslib )
1106 push @{$more_values->{$subfield_code}}, $value;
1109 while ( my ( $k, $v ) = each %$more_values ) {
1110 $values->{$k} = join ' | ', @$v;
1117 =head3 additional_attributes
1119 my $attributes = $item->additional_attributes;
1120 $attributes->{k} = 'new k';
1121 $item->update({ more_subfields => $attributes->to_marcxml });
1123 Returns a Koha::Item::Attributes object that represents the non-mapped
1124 attributes for this item.
1128 sub additional_attributes {
1131 return Koha::Item::Attributes->new_from_marcxml(
1132 $self->more_subfields_xml,
1136 =head3 _set_found_trigger
1138 $self->_set_found_trigger
1140 Finds the most recent lost item charge for this item and refunds the patron
1141 appropriately, taking into account any payments or writeoffs already applied
1144 Internal function, not exported, called only by Koha::Item->store.
1148 sub _set_found_trigger {
1149 my ( $self, $pre_mod_item ) = @_;
1151 # Reverse any lost item charges if necessary.
1152 my $no_refund_after_days =
1153 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1154 if ($no_refund_after_days) {
1155 my $today = dt_from_string();
1156 my $lost_age_in_days =
1157 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1160 return $self unless $lost_age_in_days < $no_refund_after_days;
1163 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1166 return_branch => C4::Context->userenv
1167 ? C4::Context->userenv->{'branch'}
1171 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1173 if ( $lostreturn_policy ) {
1175 # refund charge made for lost book
1176 my $lost_charge = Koha::Account::Lines->search(
1178 itemnumber => $self->itemnumber,
1179 debit_type_code => 'LOST',
1180 status => [ undef, { '<>' => 'FOUND' } ]
1183 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1188 if ( $lost_charge ) {
1190 my $patron = $lost_charge->patron;
1193 my $account = $patron->account;
1195 # Credit outstanding amount
1196 my $credit_total = $lost_charge->amountoutstanding;
1200 $lost_charge->amount > $lost_charge->amountoutstanding &&
1201 $lostreturn_policy ne "refund_unpaid"
1203 # some amount has been cancelled. collect the offsets that are not writeoffs
1204 # this works because the only way to subtract from this kind of a debt is
1205 # using the UI buttons 'Pay' and 'Write off'
1207 # We don't credit any payments if return policy is
1210 # In that case only unpaid/outstanding amount
1211 # will be credited which settles the debt without
1212 # creating extra credits
1214 my $credit_offsets = $lost_charge->debit_offsets(
1216 'credit_id' => { '!=' => undef },
1217 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1219 { join => 'credit' }
1222 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1223 # credits are negative on the DB
1224 $credit_offsets->total * -1 :
1226 # Credit the outstanding amount, then add what has been
1227 # paid to create a net credit for this amount
1228 $credit_total += $total_to_refund;
1232 if ( $credit_total > 0 ) {
1234 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1235 $credit = $account->add_credit(
1237 amount => $credit_total,
1238 description => 'Item found ' . $self->itemnumber,
1239 type => 'LOST_FOUND',
1240 interface => C4::Context->interface,
1241 library_id => $branchcode,
1242 item_id => $self->itemnumber,
1243 issue_id => $lost_charge->issue_id
1247 $credit->apply( { debits => [$lost_charge] } );
1251 message => 'lost_refunded',
1252 payload => { credit_id => $credit->id }
1257 # Update the account status
1258 $lost_charge->status('FOUND');
1259 $lost_charge->store();
1261 # Reconcile balances if required
1262 if ( C4::Context->preference('AccountAutoReconcile') ) {
1263 $account->reconcile_balance;
1268 # possibly restore fine for lost book
1269 my $lost_overdue = Koha::Account::Lines->search(
1271 itemnumber => $self->itemnumber,
1272 debit_type_code => 'OVERDUE',
1276 order_by => { '-desc' => 'date' },
1280 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1282 my $patron = $lost_overdue->patron;
1284 my $account = $patron->account;
1286 # Update status of fine
1287 $lost_overdue->status('FOUND')->store();
1289 # Find related forgive credit
1290 my $refund = $lost_overdue->credits(
1292 credit_type_code => 'FORGIVEN',
1293 itemnumber => $self->itemnumber,
1294 status => [ { '!=' => 'VOID' }, undef ]
1296 { order_by => { '-desc' => 'date' }, rows => 1 }
1300 # Revert the forgive credit
1301 $refund->void({ interface => 'trigger' });
1305 message => 'lost_restored',
1306 payload => { refund_id => $refund->id }
1311 # Reconcile balances if required
1312 if ( C4::Context->preference('AccountAutoReconcile') ) {
1313 $account->reconcile_balance;
1317 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1321 message => 'lost_charge',
1327 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1329 if ( $processingreturn_policy ) {
1331 # refund processing charge made for lost book
1332 my $processing_charge = Koha::Account::Lines->search(
1334 itemnumber => $self->itemnumber,
1335 debit_type_code => 'PROCESSING',
1336 status => [ undef, { '<>' => 'FOUND' } ]
1339 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1344 if ( $processing_charge ) {
1346 my $patron = $processing_charge->patron;
1349 my $account = $patron->account;
1351 # Credit outstanding amount
1352 my $credit_total = $processing_charge->amountoutstanding;
1356 $processing_charge->amount > $processing_charge->amountoutstanding &&
1357 $processingreturn_policy ne "refund_unpaid"
1359 # some amount has been cancelled. collect the offsets that are not writeoffs
1360 # this works because the only way to subtract from this kind of a debt is
1361 # using the UI buttons 'Pay' and 'Write off'
1363 # We don't credit any payments if return policy is
1366 # In that case only unpaid/outstanding amount
1367 # will be credited which settles the debt without
1368 # creating extra credits
1370 my $credit_offsets = $processing_charge->debit_offsets(
1372 'credit_id' => { '!=' => undef },
1373 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1375 { join => 'credit' }
1378 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1379 # credits are negative on the DB
1380 $credit_offsets->total * -1 :
1382 # Credit the outstanding amount, then add what has been
1383 # paid to create a net credit for this amount
1384 $credit_total += $total_to_refund;
1388 if ( $credit_total > 0 ) {
1390 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1391 $credit = $account->add_credit(
1393 amount => $credit_total,
1394 description => 'Item found ' . $self->itemnumber,
1395 type => 'PROCESSING_FOUND',
1396 interface => C4::Context->interface,
1397 library_id => $branchcode,
1398 item_id => $self->itemnumber,
1399 issue_id => $processing_charge->issue_id
1403 $credit->apply( { debits => [$processing_charge] } );
1407 message => 'processing_refunded',
1408 payload => { credit_id => $credit->id }
1413 # Update the account status
1414 $processing_charge->status('FOUND');
1415 $processing_charge->store();
1417 # Reconcile balances if required
1418 if ( C4::Context->preference('AccountAutoReconcile') ) {
1419 $account->reconcile_balance;
1428 =head3 public_read_list
1430 This method returns the list of publicly readable database fields for both API and UI output purposes
1434 sub public_read_list {
1436 'itemnumber', 'biblionumber', 'homebranch',
1437 'holdingbranch', 'location', 'collectioncode',
1438 'itemcallnumber', 'copynumber', 'enumchron',
1439 'barcode', 'dateaccessioned', 'itemnotes',
1440 'onloan', 'uri', 'itype',
1441 'notforloan', 'damaged', 'itemlost',
1442 'withdrawn', 'restricted'
1448 Overloaded to_api method to ensure item-level itypes is adhered to.
1453 my ($self, $params) = @_;
1455 my $response = $self->SUPER::to_api($params);
1458 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1459 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1461 return { %$response, %$overrides };
1464 =head3 to_api_mapping
1466 This method returns the mapping for representing a Koha::Item object
1471 sub to_api_mapping {
1473 itemnumber => 'item_id',
1474 biblionumber => 'biblio_id',
1475 biblioitemnumber => undef,
1476 barcode => 'external_id',
1477 dateaccessioned => 'acquisition_date',
1478 booksellerid => 'acquisition_source',
1479 homebranch => 'home_library_id',
1480 price => 'purchase_price',
1481 replacementprice => 'replacement_price',
1482 replacementpricedate => 'replacement_price_date',
1483 datelastborrowed => 'last_checkout_date',
1484 datelastseen => 'last_seen_date',
1486 notforloan => 'not_for_loan_status',
1487 damaged => 'damaged_status',
1488 damaged_on => 'damaged_date',
1489 itemlost => 'lost_status',
1490 itemlost_on => 'lost_date',
1491 withdrawn => 'withdrawn',
1492 withdrawn_on => 'withdrawn_date',
1493 itemcallnumber => 'callnumber',
1494 coded_location_qualifier => 'coded_location_qualifier',
1495 issues => 'checkouts_count',
1496 renewals => 'renewals_count',
1497 reserves => 'holds_count',
1498 restricted => 'restricted_status',
1499 itemnotes => 'public_notes',
1500 itemnotes_nonpublic => 'internal_notes',
1501 holdingbranch => 'holding_library_id',
1502 timestamp => 'timestamp',
1503 location => 'location',
1504 permanent_location => 'permanent_location',
1505 onloan => 'checked_out_date',
1506 cn_source => 'call_number_source',
1507 cn_sort => 'call_number_sort',
1508 ccode => 'collection_code',
1509 materials => 'materials_notes',
1511 itype => 'item_type_id',
1512 more_subfields_xml => 'extended_subfields',
1513 enumchron => 'serial_issue_number',
1514 copynumber => 'copy_number',
1515 stocknumber => 'inventory_number',
1516 new_status => 'new_status',
1517 deleted_on => undef,
1523 my $itemtype = $item->itemtype;
1525 Returns Koha object for effective itemtype
1532 return Koha::ItemTypes->find( $self->effective_itemtype );
1537 my $orders = $item->orders();
1539 Returns a Koha::Acquisition::Orders object
1546 my $orders = $self->_result->item_orders;
1547 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1550 =head3 tracked_links
1552 my $tracked_links = $item->tracked_links();
1554 Returns a Koha::TrackedLinks object
1561 my $tracked_links = $self->_result->linktrackers;
1562 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1565 =head3 move_to_biblio
1567 $item->move_to_biblio($to_biblio[, $params]);
1569 Move the item to another biblio and update any references in other tables.
1571 The final optional parameter, C<$params>, is expected to contain the
1572 'skip_record_index' key, which is relayed down to Koha::Item->store.
1573 There it prevents calling index_records, which takes most of the
1574 time in batch adds/deletes. The caller must take care of calling
1575 index_records separately.
1578 skip_record_index => 1|0
1580 Returns undef if the move failed or the biblionumber of the destination record otherwise
1584 sub move_to_biblio {
1585 my ( $self, $to_biblio, $params ) = @_;
1589 return if $self->biblionumber == $to_biblio->biblionumber;
1591 my $from_biblionumber = $self->biblionumber;
1592 my $to_biblionumber = $to_biblio->biblionumber;
1594 # Own biblionumber and biblioitemnumber
1596 biblionumber => $to_biblionumber,
1597 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1598 })->store({ skip_record_index => $params->{skip_record_index} });
1600 unless ($params->{skip_record_index}) {
1601 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1602 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1605 # Acquisition orders
1606 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1609 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1611 # hold_fill_target (there's no Koha object available yet)
1612 my $hold_fill_target = $self->_result->hold_fill_target;
1613 if ($hold_fill_target) {
1614 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1617 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1618 # and can't even fake one since the significant columns are nullable.
1619 my $storage = $self->_result->result_source->storage;
1622 my ($storage, $dbh, @cols) = @_;
1624 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1629 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1631 return $to_biblionumber;
1636 my $bundle_items = $item->bundle_items;
1638 Returns the items associated with this bundle
1645 if ( !$self->{_bundle_items_cached} ) {
1646 my $bundle_items = Koha::Items->search(
1647 { 'item_bundles_item.host' => $self->itemnumber },
1648 { join => 'item_bundles_item' } );
1649 $self->{_bundle_items} = $bundle_items;
1650 $self->{_bundle_items_cached} = 1;
1653 return $self->{_bundle_items};
1658 my $is_bundle = $item->is_bundle;
1660 Returns whether the item is a bundle or not
1666 return $self->bundle_items->count ? 1 : 0;
1671 my $bundle = $item->bundle_host;
1673 Returns the bundle item this item is attached to
1680 my $bundle_items_rs = $self->_result->item_bundles_item;
1681 return unless $bundle_items_rs;
1682 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1687 my $in_bundle = $item->in_bundle;
1689 Returns whether this item is currently in a bundle
1695 return $self->bundle_host ? 1 : 0;
1698 =head3 add_to_bundle
1700 my $link = $item->add_to_bundle($bundle_item);
1702 Adds the bundle_item passed to this item
1707 my ( $self, $bundle_item, $options ) = @_;
1711 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1712 if ( $self->itemnumber eq $bundle_item->itemnumber
1713 || $bundle_item->is_bundle
1714 || $self->in_bundle );
1716 my $schema = Koha::Database->new->schema;
1718 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1723 my $checkout = $bundle_item->checkout;
1725 unless ($options->{force_checkin}) {
1726 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1729 my $branchcode = C4::Context->userenv->{'branch'};
1730 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1732 Koha::Exceptions::Checkin::FailedCheckin->throw();
1736 my $holds = $bundle_item->current_holds;
1737 if ($holds->count) {
1738 unless ($options->{ignore_holds}) {
1739 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1743 $self->_result->add_to_item_bundles_hosts(
1744 { item => $bundle_item->itemnumber } );
1746 $bundle_item->notforloan($BundleNotLoanValue)->store();
1752 # 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
1753 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1754 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1756 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1757 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1758 Koha::Exceptions::Object::FKConstraint->throw(
1759 error => 'Broken FK constraint',
1760 broken_fk => $+{column}
1765 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1767 Koha::Exceptions::Object::DuplicateID->throw(
1768 error => 'Duplicate ID',
1769 duplicate_id => $+{key}
1772 elsif ( $_->{msg} =~
1773 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1775 { # The optional \W in the regex might be a quote or backtick
1776 my $type = $+{type};
1777 my $value = $+{value};
1778 my $property = $+{property};
1779 $property =~ s/['`]//g;
1780 Koha::Exceptions::Object::BadValue->throw(
1783 property => $property =~ /(\w+\.\w+)$/
1786 , # results in table.column without quotes or backtics
1790 # Catch-all for foreign key breakages. It will help find other use cases
1799 =head3 remove_from_bundle
1801 Remove this item from any bundle it may have been attached to.
1805 sub remove_from_bundle {
1808 my $bundle_item_rs = $self->_result->item_bundles_item;
1809 if ( $bundle_item_rs ) {
1810 $bundle_item_rs->delete;
1811 $self->notforloan(0)->store();
1817 =head2 Internal methods
1819 =head3 _after_item_action_hooks
1821 Helper method that takes care of calling all plugin hooks
1825 sub _after_item_action_hooks {
1826 my ( $self, $params ) = @_;
1828 my $action = $params->{action};
1830 Koha::Plugins->call(
1831 'after_item_action',
1835 item_id => $self->itemnumber,
1842 my $recall = $item->recall;
1844 Return the relevant recall for this item
1850 my @recalls = Koha::Recalls->search(
1852 biblio_id => $self->biblionumber,
1855 { order_by => { -asc => 'created_date' } }
1857 foreach my $recall (@recalls) {
1858 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
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->unblessed, $patron->unblessed );
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 my $val = $self->$field;
2071 if( !defined $val) {
2072 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2075 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2076 # If the results matches the values in the syspref
2077 # We return true if match found
2086 Returns a map of column name to string representations including the string,
2087 the mapping type and the mapping category where appropriate.
2089 Currently handles authorised value mappings, library, callnumber and itemtype
2092 Accepts a param hashref where the 'public' key denotes whether we want the public
2093 or staff client strings.
2098 my ( $self, $params ) = @_;
2099 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2100 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2101 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2103 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2105 # Hardcoded known 'authorised_value' values mapped to API codes
2106 my $code_to_type = {
2107 branches => 'library',
2108 cn_source => 'call_number_source',
2109 itemtypes => 'item_type',
2112 # Handle not null and default values for integers and dates
2115 foreach my $col ( @{$self->_columns} ) {
2117 # By now, we are done with known columns, now check the framework for mappings
2118 my $field = $self->_result->result_source->name . '.' . $col;
2120 # Check there's an entry in the MARC subfield structure for the field
2121 if ( exists $mss->{$field}
2122 && scalar @{ $mss->{$field} } > 0
2123 && $mss->{$field}[0]->{authorised_value} )
2125 my $subfield = $mss->{$field}[0];
2126 my $code = $subfield->{authorised_value};
2128 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2129 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2130 $strings->{$col} = {
2133 ( $type eq 'av' ? ( category => $code ) : () ),
2151 Kyle M Hall <kyle@bywatersolutions.com>