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 group 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->next;
569 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
574 my $transfer = $item->get_transfers;
576 Return the list of outstanding transfers (i.e requested but not yet cancelled
579 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
580 whereby the most recently sent, but not received, transfer will be returned
581 first if it exists, otherwise requests are in oldest to newest request order.
583 This allows for transfers to queue, which is the case for stock rotation and
584 rotating collections where a manual transfer may need to take precedence but
585 we still expect the item to end up at a final location eventually.
592 my $transfer_rs = $self->_result->current_branchtransfers;
594 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
597 =head3 last_returned_by
599 Gets and sets the last patron to return an item.
601 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
603 $item->last_returned_by( $borrowernumber );
605 my $patron = $item->last_returned_by();
609 sub last_returned_by {
610 my ( $self, $borrowernumber ) = @_;
611 if ( $borrowernumber ) {
612 $self->_result->update_or_create_related('last_returned_by',
613 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
615 my $rs = $self->_result->last_returned_by;
617 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
620 =head3 can_article_request
622 my $bool = $item->can_article_request( $borrower )
624 Returns true if item can be specifically requested
626 $borrower must be a Koha::Patron object
630 sub can_article_request {
631 my ( $self, $borrower ) = @_;
633 my $rule = $self->article_request_type($borrower);
635 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
639 =head3 hidden_in_opac
641 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
643 Returns true if item fields match the hidding criteria defined in $rules.
644 Returns false otherwise.
646 Takes HASHref that can have the following parameters:
648 $rules : { <field> => [ value_1, ... ], ... }
650 Note: $rules inherits its structure from the parsed YAML from reading
651 the I<OpacHiddenItems> system preference.
656 my ( $self, $params ) = @_;
658 my $rules = $params->{rules} // {};
661 if C4::Context->preference('hidelostitems') and
664 my $hidden_in_opac = 0;
666 foreach my $field ( keys %{$rules} ) {
668 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
674 return $hidden_in_opac;
677 =head3 can_be_transferred
679 $item->can_be_transferred({ to => $to_library, from => $from_library })
680 Checks if an item can be transferred to given library.
682 This feature is controlled by two system preferences:
683 UseBranchTransferLimits to enable / disable the feature
684 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
685 for setting the limitations
687 Takes HASHref that can have the following parameters:
688 MANDATORY PARAMETERS:
691 $from : Koha::Library # if not given, item holdingbranch
692 # will be used instead
694 Returns 1 if item can be transferred to $to_library, otherwise 0.
696 To find out whether at least one item of a Koha::Biblio can be transferred, please
697 see Koha::Biblio->can_be_transferred() instead of using this method for
698 multiple items of the same biblio.
702 sub can_be_transferred {
703 my ($self, $params) = @_;
705 my $to = $params->{to};
706 my $from = $params->{from};
708 $to = $to->branchcode;
709 $from = defined $from ? $from->branchcode : $self->holdingbranch;
711 return 1 if $from eq $to; # Transfer to current branch is allowed
712 return 1 unless C4::Context->preference('UseBranchTransferLimits');
714 my $limittype = C4::Context->preference('BranchTransferLimitsType');
715 return Koha::Item::Transfer::Limits->search({
718 $limittype => $limittype eq 'itemtype'
719 ? $self->effective_itemtype : $self->ccode
724 =head3 pickup_locations
726 my $pickup_locations = $item->pickup_locations({ patron => $patron })
728 Returns possible pickup locations for this item, according to patron's home library
729 and if item can be transferred to each pickup location.
731 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
736 sub pickup_locations {
737 my ($self, $params) = @_;
739 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
740 unless exists $params->{patron};
742 my $patron = $params->{patron};
744 my $circ_control_branch =
745 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
747 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
749 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
750 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
752 my $pickup_libraries = Koha::Libraries->search();
753 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
754 $pickup_libraries = $self->home_branch->get_hold_libraries;
755 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
756 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
757 $pickup_libraries = $plib->get_hold_libraries;
758 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
759 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
760 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
761 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
764 return $pickup_libraries->search(
769 order_by => ['branchname']
771 ) unless C4::Context->preference('UseBranchTransferLimits');
773 my $limittype = C4::Context->preference('BranchTransferLimitsType');
774 my ($ccode, $itype) = (undef, undef);
775 if( $limittype eq 'ccode' ){
776 $ccode = $self->ccode;
778 $itype = $self->itype;
780 my $limits = Koha::Item::Transfer::Limits->search(
782 fromBranch => $self->holdingbranch,
786 { columns => ['toBranch'] }
789 return $pickup_libraries->search(
791 pickup_location => 1,
793 '-not_in' => $limits->_resultset->as_query
797 order_by => ['branchname']
802 =head3 article_request_type
804 my $type = $item->article_request_type( $borrower )
806 returns 'yes', 'no', 'bib_only', or 'item_only'
808 $borrower must be a Koha::Patron object
812 sub article_request_type {
813 my ( $self, $borrower ) = @_;
815 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
817 $branch_control eq 'homebranch' ? $self->homebranch
818 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
820 my $borrowertype = $borrower->categorycode;
821 my $itemtype = $self->effective_itemtype();
822 my $rule = Koha::CirculationRules->get_effective_rule(
824 rule_name => 'article_requests',
825 categorycode => $borrowertype,
826 itemtype => $itemtype,
827 branchcode => $branchcode
831 return q{} unless $rule;
832 return $rule->rule_value || q{}
841 my $attributes = { order_by => 'priority' };
842 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
844 itemnumber => $self->itemnumber,
847 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
848 waitingdate => { '!=' => undef },
851 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
852 return Koha::Holds->_new_from_dbic($hold_rs);
855 =head3 stockrotationitem
857 my $sritem = Koha::Item->stockrotationitem;
859 Returns the stock rotation item associated with the current item.
863 sub stockrotationitem {
865 my $rs = $self->_result->stockrotationitem;
867 return Koha::StockRotationItem->_new_from_dbic( $rs );
872 my $item = $item->add_to_rota($rota_id);
874 Add this item to the rota identified by $ROTA_ID, which means associating it
875 with the first stage of that rota. Should this item already be associated
876 with a rota, then we will move it to the new rota.
881 my ( $self, $rota_id ) = @_;
882 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
886 =head3 has_pending_hold
888 my $is_pending_hold = $item->has_pending_hold();
890 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
894 sub has_pending_hold {
896 my $pending_hold = $self->_result->tmp_holdsqueues;
897 return $pending_hold->count ? 1: 0;
900 =head3 has_pending_recall {
902 my $has_pending_recall
904 Return if whether has pending recall of not.
908 sub has_pending_recall {
911 # FIXME Must be moved to $self->recalls
912 return Koha::Recalls->search(
914 item_id => $self->itemnumber,
922 my $field = $item->as_marc_field;
924 This method returns a MARC::Field object representing the Koha::Item object
925 with the current mappings configuration.
932 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
934 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
938 my $item_field = $tagslib->{$itemtag};
940 my $more_subfields = $self->additional_attributes->to_hashref;
941 foreach my $subfield (
943 $a->{display_order} <=> $b->{display_order}
944 || $a->{subfield} cmp $b->{subfield}
945 } grep { ref($_) && %$_ } values %$item_field
948 my $kohafield = $subfield->{kohafield};
949 my $tagsubfield = $subfield->{tagsubfield};
951 if ( defined $kohafield && $kohafield ne '' ) {
952 next if $kohafield !~ m{^items\.}; # That would be weird!
953 ( my $attribute = $kohafield ) =~ s|^items\.||;
954 $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
955 if defined $self->$attribute and $self->$attribute ne '';
957 $value = $more_subfields->{$tagsubfield}
960 next unless defined $value
963 if ( $subfield->{repeatable} ) {
964 my @values = split '\|', $value;
965 push @subfields, ( $tagsubfield => $_ ) for @values;
968 push @subfields, ( $tagsubfield => $value );
973 return unless @subfields;
975 return MARC::Field->new(
976 "$itemtag", ' ', ' ', @subfields
980 =head3 renewal_branchcode
982 Returns the branchcode to be recorded in statistics renewal of the item
986 sub renewal_branchcode {
988 my ($self, $params ) = @_;
990 my $interface = C4::Context->interface;
992 if ( $interface eq 'opac' ){
993 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
994 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
995 $branchcode = 'OPACRenew';
997 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
998 $branchcode = $self->homebranch;
1000 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1001 $branchcode = $self->checkout->patron->branchcode;
1003 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1004 $branchcode = $self->checkout->branchcode;
1010 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1011 ? C4::Context->userenv->{branch} : $params->{branch};
1018 Return the cover images associated with this item.
1025 my $cover_image_rs = $self->_result->cover_images;
1026 return unless $cover_image_rs;
1027 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1030 =head3 columns_to_str
1032 my $values = $items->columns_to_str;
1034 Return a hashref with the string representation of the different attribute of the item.
1036 This is meant to be used for display purpose only.
1040 sub columns_to_str {
1042 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1043 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1044 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1046 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1049 for my $column ( @{$self->_columns}) {
1051 next if $column eq 'more_subfields_xml';
1053 my $value = $self->$column;
1054 # 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
1056 if ( not defined $value or $value eq "" ) {
1057 $values->{$column} = $value;
1062 exists $mss->{"items.$column"}
1063 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1066 $values->{$column} =
1068 ? $subfield->{authorised_value}
1069 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1070 $subfield->{tagsubfield}, $value, '', $tagslib )
1076 $self->more_subfields_xml
1077 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1082 my ( $field ) = $marc_more->fields;
1083 for my $sf ( $field->subfields ) {
1084 my $subfield_code = $sf->[0];
1085 my $value = $sf->[1];
1086 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1087 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1089 $subfield->{authorised_value}
1090 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1091 $subfield->{tagsubfield}, $value, '', $tagslib )
1094 push @{$more_values->{$subfield_code}}, $value;
1097 while ( my ( $k, $v ) = each %$more_values ) {
1098 $values->{$k} = join ' | ', @$v;
1105 =head3 additional_attributes
1107 my $attributes = $item->additional_attributes;
1108 $attributes->{k} = 'new k';
1109 $item->update({ more_subfields => $attributes->to_marcxml });
1111 Returns a Koha::Item::Attributes object that represents the non-mapped
1112 attributes for this item.
1116 sub additional_attributes {
1119 return Koha::Item::Attributes->new_from_marcxml(
1120 $self->more_subfields_xml,
1124 =head3 _set_found_trigger
1126 $self->_set_found_trigger
1128 Finds the most recent lost item charge for this item and refunds the patron
1129 appropriately, taking into account any payments or writeoffs already applied
1132 Internal function, not exported, called only by Koha::Item->store.
1136 sub _set_found_trigger {
1137 my ( $self, $pre_mod_item ) = @_;
1139 # Reverse any lost item charges if necessary.
1140 my $no_refund_after_days =
1141 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1142 if ($no_refund_after_days) {
1143 my $today = dt_from_string();
1144 my $lost_age_in_days =
1145 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1148 return $self unless $lost_age_in_days < $no_refund_after_days;
1151 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1154 return_branch => C4::Context->userenv
1155 ? C4::Context->userenv->{'branch'}
1159 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1161 if ( $lostreturn_policy ) {
1163 # refund charge made for lost book
1164 my $lost_charge = Koha::Account::Lines->search(
1166 itemnumber => $self->itemnumber,
1167 debit_type_code => 'LOST',
1168 status => [ undef, { '<>' => 'FOUND' } ]
1171 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1176 if ( $lost_charge ) {
1178 my $patron = $lost_charge->patron;
1181 my $account = $patron->account;
1183 # Credit outstanding amount
1184 my $credit_total = $lost_charge->amountoutstanding;
1188 $lost_charge->amount > $lost_charge->amountoutstanding &&
1189 $lostreturn_policy ne "refund_unpaid"
1191 # some amount has been cancelled. collect the offsets that are not writeoffs
1192 # this works because the only way to subtract from this kind of a debt is
1193 # using the UI buttons 'Pay' and 'Write off'
1195 # We don't credit any payments if return policy is
1198 # In that case only unpaid/outstanding amount
1199 # will be credited which settles the debt without
1200 # creating extra credits
1202 my $credit_offsets = $lost_charge->debit_offsets(
1204 'credit_id' => { '!=' => undef },
1205 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1207 { join => 'credit' }
1210 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1211 # credits are negative on the DB
1212 $credit_offsets->total * -1 :
1214 # Credit the outstanding amount, then add what has been
1215 # paid to create a net credit for this amount
1216 $credit_total += $total_to_refund;
1220 if ( $credit_total > 0 ) {
1222 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1223 $credit = $account->add_credit(
1225 amount => $credit_total,
1226 description => 'Item found ' . $self->itemnumber,
1227 type => 'LOST_FOUND',
1228 interface => C4::Context->interface,
1229 library_id => $branchcode,
1230 item_id => $self->itemnumber,
1231 issue_id => $lost_charge->issue_id
1235 $credit->apply( { debits => [$lost_charge] } );
1239 message => 'lost_refunded',
1240 payload => { credit_id => $credit->id }
1245 # Update the account status
1246 $lost_charge->status('FOUND');
1247 $lost_charge->store();
1249 # Reconcile balances if required
1250 if ( C4::Context->preference('AccountAutoReconcile') ) {
1251 $account->reconcile_balance;
1256 # possibly restore fine for lost book
1257 my $lost_overdue = Koha::Account::Lines->search(
1259 itemnumber => $self->itemnumber,
1260 debit_type_code => 'OVERDUE',
1264 order_by => { '-desc' => 'date' },
1268 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1270 my $patron = $lost_overdue->patron;
1272 my $account = $patron->account;
1274 # Update status of fine
1275 $lost_overdue->status('FOUND')->store();
1277 # Find related forgive credit
1278 my $refund = $lost_overdue->credits(
1280 credit_type_code => 'FORGIVEN',
1281 itemnumber => $self->itemnumber,
1282 status => [ { '!=' => 'VOID' }, undef ]
1284 { order_by => { '-desc' => 'date' }, rows => 1 }
1288 # Revert the forgive credit
1289 $refund->void({ interface => 'trigger' });
1293 message => 'lost_restored',
1294 payload => { refund_id => $refund->id }
1299 # Reconcile balances if required
1300 if ( C4::Context->preference('AccountAutoReconcile') ) {
1301 $account->reconcile_balance;
1305 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1309 message => 'lost_charge',
1315 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1317 if ( $processingreturn_policy ) {
1319 # refund processing charge made for lost book
1320 my $processing_charge = Koha::Account::Lines->search(
1322 itemnumber => $self->itemnumber,
1323 debit_type_code => 'PROCESSING',
1324 status => [ undef, { '<>' => 'FOUND' } ]
1327 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1332 if ( $processing_charge ) {
1334 my $patron = $processing_charge->patron;
1337 my $account = $patron->account;
1339 # Credit outstanding amount
1340 my $credit_total = $processing_charge->amountoutstanding;
1344 $processing_charge->amount > $processing_charge->amountoutstanding &&
1345 $processingreturn_policy ne "refund_unpaid"
1347 # some amount has been cancelled. collect the offsets that are not writeoffs
1348 # this works because the only way to subtract from this kind of a debt is
1349 # using the UI buttons 'Pay' and 'Write off'
1351 # We don't credit any payments if return policy is
1354 # In that case only unpaid/outstanding amount
1355 # will be credited which settles the debt without
1356 # creating extra credits
1358 my $credit_offsets = $processing_charge->debit_offsets(
1360 'credit_id' => { '!=' => undef },
1361 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1363 { join => 'credit' }
1366 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1367 # credits are negative on the DB
1368 $credit_offsets->total * -1 :
1370 # Credit the outstanding amount, then add what has been
1371 # paid to create a net credit for this amount
1372 $credit_total += $total_to_refund;
1376 if ( $credit_total > 0 ) {
1378 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1379 $credit = $account->add_credit(
1381 amount => $credit_total,
1382 description => 'Item found ' . $self->itemnumber,
1383 type => 'PROCESSING_FOUND',
1384 interface => C4::Context->interface,
1385 library_id => $branchcode,
1386 item_id => $self->itemnumber,
1387 issue_id => $processing_charge->issue_id
1391 $credit->apply( { debits => [$processing_charge] } );
1395 message => 'processing_refunded',
1396 payload => { credit_id => $credit->id }
1401 # Update the account status
1402 $processing_charge->status('FOUND');
1403 $processing_charge->store();
1405 # Reconcile balances if required
1406 if ( C4::Context->preference('AccountAutoReconcile') ) {
1407 $account->reconcile_balance;
1416 =head3 public_read_list
1418 This method returns the list of publicly readable database fields for both API and UI output purposes
1422 sub public_read_list {
1424 'itemnumber', 'biblionumber', 'homebranch',
1425 'holdingbranch', 'location', 'collectioncode',
1426 'itemcallnumber', 'copynumber', 'enumchron',
1427 'barcode', 'dateaccessioned', 'itemnotes',
1428 'onloan', 'uri', 'itype',
1429 'notforloan', 'damaged', 'itemlost',
1430 'withdrawn', 'restricted'
1436 Overloaded to_api method to ensure item-level itypes is adhered to.
1441 my ($self, $params) = @_;
1443 my $response = $self->SUPER::to_api($params);
1446 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1448 my $itype_notforloan = $self->itemtype->notforloan;
1449 $overrides->{effective_not_for_loan_status} =
1450 ( defined $itype_notforloan && !$self->notforloan ) ? $itype_notforloan : $self->notforloan;
1452 return { %$response, %$overrides };
1455 =head3 to_api_mapping
1457 This method returns the mapping for representing a Koha::Item object
1462 sub to_api_mapping {
1464 itemnumber => 'item_id',
1465 biblionumber => 'biblio_id',
1466 biblioitemnumber => undef,
1467 barcode => 'external_id',
1468 dateaccessioned => 'acquisition_date',
1469 booksellerid => 'acquisition_source',
1470 homebranch => 'home_library_id',
1471 price => 'purchase_price',
1472 replacementprice => 'replacement_price',
1473 replacementpricedate => 'replacement_price_date',
1474 datelastborrowed => 'last_checkout_date',
1475 datelastseen => 'last_seen_date',
1477 notforloan => 'not_for_loan_status',
1478 damaged => 'damaged_status',
1479 damaged_on => 'damaged_date',
1480 itemlost => 'lost_status',
1481 itemlost_on => 'lost_date',
1482 withdrawn => 'withdrawn',
1483 withdrawn_on => 'withdrawn_date',
1484 itemcallnumber => 'callnumber',
1485 coded_location_qualifier => 'coded_location_qualifier',
1486 issues => 'checkouts_count',
1487 renewals => 'renewals_count',
1488 reserves => 'holds_count',
1489 restricted => 'restricted_status',
1490 itemnotes => 'public_notes',
1491 itemnotes_nonpublic => 'internal_notes',
1492 holdingbranch => 'holding_library_id',
1493 timestamp => 'timestamp',
1494 location => 'location',
1495 permanent_location => 'permanent_location',
1496 onloan => 'checked_out_date',
1497 cn_source => 'call_number_source',
1498 cn_sort => 'call_number_sort',
1499 ccode => 'collection_code',
1500 materials => 'materials_notes',
1502 itype => 'item_type_id',
1503 more_subfields_xml => 'extended_subfields',
1504 enumchron => 'serial_issue_number',
1505 copynumber => 'copy_number',
1506 stocknumber => 'inventory_number',
1507 new_status => 'new_status',
1508 deleted_on => undef,
1514 my $itemtype = $item->itemtype;
1516 Returns Koha object for effective itemtype
1523 return Koha::ItemTypes->find( $self->effective_itemtype );
1528 my $orders = $item->orders();
1530 Returns a Koha::Acquisition::Orders object
1537 my $orders = $self->_result->item_orders;
1538 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1541 =head3 tracked_links
1543 my $tracked_links = $item->tracked_links();
1545 Returns a Koha::TrackedLinks object
1552 my $tracked_links = $self->_result->linktrackers;
1553 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1556 =head3 move_to_biblio
1558 $item->move_to_biblio($to_biblio[, $params]);
1560 Move the item to another biblio and update any references in other tables.
1562 The final optional parameter, C<$params>, is expected to contain the
1563 'skip_record_index' key, which is relayed down to Koha::Item->store.
1564 There it prevents calling index_records, which takes most of the
1565 time in batch adds/deletes. The caller must take care of calling
1566 index_records separately.
1569 skip_record_index => 1|0
1571 Returns undef if the move failed or the biblionumber of the destination record otherwise
1575 sub move_to_biblio {
1576 my ( $self, $to_biblio, $params ) = @_;
1580 return if $self->biblionumber == $to_biblio->biblionumber;
1582 my $from_biblionumber = $self->biblionumber;
1583 my $to_biblionumber = $to_biblio->biblionumber;
1585 # Own biblionumber and biblioitemnumber
1587 biblionumber => $to_biblionumber,
1588 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1589 })->store({ skip_record_index => $params->{skip_record_index} });
1591 unless ($params->{skip_record_index}) {
1592 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1593 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1596 # Acquisition orders
1597 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1600 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1602 # hold_fill_target (there's no Koha object available yet)
1603 my $hold_fill_target = $self->_result->hold_fill_target;
1604 if ($hold_fill_target) {
1605 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1608 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1609 # and can't even fake one since the significant columns are nullable.
1610 my $storage = $self->_result->result_source->storage;
1613 my ($storage, $dbh, @cols) = @_;
1615 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1620 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1622 return $to_biblionumber;
1627 my $bundle_items = $item->bundle_items;
1629 Returns the items associated with this bundle
1636 my $rs = $self->_result->bundle_items;
1637 return Koha::Items->_new_from_dbic($rs);
1642 my $is_bundle = $item->is_bundle;
1644 Returns whether the item is a bundle or not
1650 return $self->bundle_items->count ? 1 : 0;
1655 my $bundle = $item->bundle_host;
1657 Returns the bundle item this item is attached to
1664 my $bundle_items_rs = $self->_result->item_bundles_item;
1665 return unless $bundle_items_rs;
1666 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1671 my $in_bundle = $item->in_bundle;
1673 Returns whether this item is currently in a bundle
1679 return $self->bundle_host ? 1 : 0;
1682 =head3 add_to_bundle
1684 my $link = $item->add_to_bundle($bundle_item);
1686 Adds the bundle_item passed to this item
1691 my ( $self, $bundle_item, $options ) = @_;
1695 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1696 if ( $self->itemnumber eq $bundle_item->itemnumber
1697 || $bundle_item->is_bundle
1698 || $self->in_bundle );
1700 my $schema = Koha::Database->new->schema;
1702 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1708 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
1710 my $checkout = $bundle_item->checkout;
1712 unless ($options->{force_checkin}) {
1713 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1716 my $branchcode = C4::Context->userenv->{'branch'};
1717 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1719 Koha::Exceptions::Checkin::FailedCheckin->throw();
1723 my $holds = $bundle_item->current_holds;
1724 if ($holds->count) {
1725 unless ($options->{ignore_holds}) {
1726 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1730 $self->_result->add_to_item_bundles_hosts(
1731 { item => $bundle_item->itemnumber } );
1733 $bundle_item->notforloan($BundleNotLoanValue)->store();
1739 # 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
1740 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1741 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1743 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1744 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1745 Koha::Exceptions::Object::FKConstraint->throw(
1746 error => 'Broken FK constraint',
1747 broken_fk => $+{column}
1752 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1754 Koha::Exceptions::Object::DuplicateID->throw(
1755 error => 'Duplicate ID',
1756 duplicate_id => $+{key}
1759 elsif ( $_->{msg} =~
1760 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1762 { # The optional \W in the regex might be a quote or backtick
1763 my $type = $+{type};
1764 my $value = $+{value};
1765 my $property = $+{property};
1766 $property =~ s/['`]//g;
1767 Koha::Exceptions::Object::BadValue->throw(
1770 property => $property =~ /(\w+\.\w+)$/
1773 , # results in table.column without quotes or backtics
1777 # Catch-all for foreign key breakages. It will help find other use cases
1786 =head3 remove_from_bundle
1788 Remove this item from any bundle it may have been attached to.
1792 sub remove_from_bundle {
1795 my $bundle_host = $self->bundle_host;
1797 return 0 unless $bundle_host; # Should not we raise an exception here?
1799 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $bundle_host->checkout;
1801 my $bundle_item_rs = $self->_result->item_bundles_item;
1802 if ( $bundle_item_rs ) {
1803 $bundle_item_rs->delete;
1804 $self->notforloan(0)->store();
1810 =head2 Internal methods
1812 =head3 _after_item_action_hooks
1814 Helper method that takes care of calling all plugin hooks
1818 sub _after_item_action_hooks {
1819 my ( $self, $params ) = @_;
1821 my $action = $params->{action};
1823 Koha::Plugins->call(
1824 'after_item_action',
1828 item_id => $self->itemnumber,
1835 my $recall = $item->recall;
1837 Return the relevant recall for this item
1843 my @recalls = Koha::Recalls->search(
1845 biblio_id => $self->biblionumber,
1848 { order_by => { -asc => 'created_date' } }
1851 my $item_level_recall;
1852 foreach my $recall (@recalls) {
1853 if ( $recall->item_level ) {
1854 $item_level_recall = 1;
1855 if ( $recall->item_id == $self->itemnumber ) {
1860 if ($item_level_recall) {
1862 # recall needs to be filled be a specific item only
1863 # no other item is relevant to return
1867 # no item-level recall to return, so return earliest biblio-level
1868 # FIXME: eventually this will be based on priority
1872 =head3 can_be_recalled
1874 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1876 Does item-level checks and returns if items can be recalled by this borrower
1880 sub can_be_recalled {
1881 my ( $self, $params ) = @_;
1883 return 0 if !( C4::Context->preference('UseRecalls') );
1885 # check if this item is not for loan, withdrawn or lost
1886 return 0 if ( $self->notforloan != 0 );
1887 return 0 if ( $self->itemlost != 0 );
1888 return 0 if ( $self->withdrawn != 0 );
1890 # check if this item is not checked out - if not checked out, can't be recalled
1891 return 0 if ( !defined( $self->checkout ) );
1893 my $patron = $params->{patron};
1895 my $branchcode = C4::Context->userenv->{'branch'};
1897 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1900 # Check the circulation rule for each relevant itemtype for this item
1901 my $rule = Koha::CirculationRules->get_effective_rules({
1902 branchcode => $branchcode,
1903 categorycode => $patron ? $patron->categorycode : undef,
1904 itemtype => $self->effective_itemtype,
1907 'recalls_per_record',
1912 # check recalls allowed has been set and is not zero
1913 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1916 # check borrower has not reached open recalls allowed limit
1917 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1919 # check borrower has not reach open recalls allowed per record limit
1920 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1922 # check if this patron has already recalled this item
1923 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1925 # check if this patron has already checked out this item
1926 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1928 # check if this patron has already reserved this item
1929 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1932 # check item availability
1933 # items are unavailable for recall if they are lost, withdrawn or notforloan
1934 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1936 # if there are no available items at all, no recall can be placed
1937 return 0 if ( scalar @items == 0 );
1939 my $checked_out_count = 0;
1941 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1944 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1945 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1947 # can't recall if no items have been checked out
1948 return 0 if ( $checked_out_count == 0 );
1954 =head3 can_be_waiting_recall
1956 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1958 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1959 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1963 sub can_be_waiting_recall {
1966 return 0 if !( C4::Context->preference('UseRecalls') );
1968 # check if this item is not for loan, withdrawn or lost
1969 return 0 if ( $self->notforloan != 0 );
1970 return 0 if ( $self->itemlost != 0 );
1971 return 0 if ( $self->withdrawn != 0 );
1973 my $branchcode = $self->holdingbranch;
1974 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1975 $branchcode = C4::Context->userenv->{'branch'};
1977 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1980 # Check the circulation rule for each relevant itemtype for this item
1981 my $most_relevant_recall = $self->check_recalls;
1982 my $rule = Koha::CirculationRules->get_effective_rules(
1984 branchcode => $branchcode,
1985 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1986 itemtype => $self->effective_itemtype,
1987 rules => [ 'recalls_allowed', ],
1991 # check recalls allowed has been set and is not zero
1992 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1998 =head3 check_recalls
2000 my $recall = $item->check_recalls;
2002 Get the most relevant recall for this item.
2009 my @recalls = Koha::Recalls->search(
2010 { biblio_id => $self->biblionumber,
2011 item_id => [ $self->itemnumber, undef ]
2013 { order_by => { -asc => 'created_date' } }
2014 )->filter_by_current->as_list;
2017 # iterate through relevant recalls to find the best one.
2018 # if we come across a waiting recall, use this one.
2019 # 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.
2020 foreach my $r ( @recalls ) {
2021 if ( $r->waiting ) {
2026 unless ( defined $recall ) {
2027 $recall = $recalls[0];
2033 =head3 is_notforloan
2035 my $is_notforloan = $item->is_notforloan;
2037 Determine whether or not this item is "notforloan" based on
2038 the item's notforloan status or its item type
2044 my $is_notforloan = 0;
2046 if ( $self->notforloan ){
2050 my $itemtype = $self->itemtype;
2052 if ( $itemtype->notforloan ){
2058 return $is_notforloan;
2061 =head3 is_denied_renewal
2063 my $is_denied_renewal = $item->is_denied_renewal;
2065 Determine whether or not this item can be renewed based on the
2066 rules set in the ItemsDeniedRenewal system preference.
2070 sub is_denied_renewal {
2072 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2073 return 0 unless $denyingrules;
2074 foreach my $field (keys %$denyingrules) {
2075 # Silently ignore bad column names; TODO we should validate elsewhere
2076 next if !$self->_result->result_source->has_column($field);
2077 my $val = $self->$field;
2078 if( !defined $val) {
2079 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2082 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2083 # If the results matches the values in the syspref
2084 # We return true if match found
2093 Returns a map of column name to string representations including the string,
2094 the mapping type and the mapping category where appropriate.
2096 Currently handles authorised value mappings, library, callnumber and itemtype
2099 Accepts a param hashref where the 'public' key denotes whether we want the public
2100 or staff client strings.
2105 my ( $self, $params ) = @_;
2106 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2107 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2108 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2110 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2112 # Hardcoded known 'authorised_value' values mapped to API codes
2113 my $code_to_type = {
2114 branches => 'library',
2115 cn_source => 'call_number_source',
2116 itemtypes => 'item_type',
2119 # Handle not null and default values for integers and dates
2122 foreach my $col ( @{$self->_columns} ) {
2124 # By now, we are done with known columns, now check the framework for mappings
2125 my $field = $self->_result->result_source->name . '.' . $col;
2127 # Check there's an entry in the MARC subfield structure for the field
2128 if ( exists $mss->{$field}
2129 && scalar @{ $mss->{$field} } > 0
2130 && $mss->{$field}[0]->{authorised_value} )
2132 my $subfield = $mss->{$field}[0];
2133 my $code = $subfield->{authorised_value};
2135 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2136 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2137 $strings->{$col} = {
2140 ( $type eq 'av' ? ( category => $code ) : () ),
2158 Kyle M Hall <kyle@bywatersolutions.com>