3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::Exceptions::Item::Transfer;
40 use Koha::Item::Attributes;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Item::Transfer::Limits;
43 use Koha::Item::Transfers;
49 use Koha::Result::Boolean;
50 use Koha::SearchEngine::Indexer;
51 use Koha::StockRotationItem;
52 use Koha::StockRotationRotas;
53 use Koha::TrackedLinks;
55 use base qw(Koha::Object);
59 Koha::Item - Koha Item object class
71 $params can take an optional 'skip_record_index' parameter.
72 If set, the reindexation process will not happen (index_records not called)
74 NOTE: This is a temporary fix to answer a performance issue when lot of items
75 are added (or modified) at the same time.
76 The correct way to fix this is to make the ES reindexation process async.
77 You should not turn it on if you do not understand what it is doing exactly.
83 my $params = @_ ? shift : {};
85 my $log_action = $params->{log_action} // 1;
87 # We do not want to oblige callers to pass this value
88 # Dev conveniences vs performance?
89 unless ( $self->biblioitemnumber ) {
90 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
93 # See related changes from C4::Items::AddItem
94 unless ( $self->itype ) {
95 $self->itype($self->biblio->biblioitem->itemtype);
98 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
100 my $today = dt_from_string;
101 my $action = 'create';
103 unless ( $self->in_storage ) { #AddItem
105 unless ( $self->permanent_location ) {
106 $self->permanent_location($self->location);
109 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
110 unless ( $self->location || !$default_location ) {
111 $self->permanent_location( $self->location || $default_location )
112 unless $self->permanent_location;
113 $self->location($default_location);
116 unless ( $self->replacementpricedate ) {
117 $self->replacementpricedate($today);
119 unless ( $self->datelastseen ) {
120 $self->datelastseen($today);
123 unless ( $self->dateaccessioned ) {
124 $self->dateaccessioned($today);
127 if ( $self->itemcallnumber
128 or $self->cn_source )
130 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
131 $self->cn_sort($cn_sort);
138 my %updated_columns = $self->_result->get_dirty_columns;
139 return $self->SUPER::store unless %updated_columns;
141 # Retrieve the item for comparison if we need to
143 exists $updated_columns{itemlost}
144 or exists $updated_columns{withdrawn}
145 or exists $updated_columns{damaged}
146 ) ? $self->get_from_storage : undef;
148 # Update *_on fields if needed
149 # FIXME: Why not for AddItem as well?
150 my @fields = qw( itemlost withdrawn damaged );
151 for my $field (@fields) {
153 # If the field is defined but empty or 0, we are
154 # removing/unsetting and thus need to clear out
156 if ( exists $updated_columns{$field}
157 && defined( $self->$field )
160 my $field_on = "${field}_on";
161 $self->$field_on(undef);
163 # If the field has changed otherwise, we much update
165 elsif (exists $updated_columns{$field}
166 && $updated_columns{$field}
167 && !$pre_mod_item->$field )
169 my $field_on = "${field}_on";
170 $self->$field_on(dt_from_string);
174 if ( exists $updated_columns{itemcallnumber}
175 or exists $updated_columns{cn_source} )
177 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
178 $self->cn_sort($cn_sort);
182 if ( exists $updated_columns{location}
183 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
184 and not exists $updated_columns{permanent_location} )
186 $self->permanent_location( $self->location );
189 # If item was lost and has now been found,
190 # reverse any list item charges if necessary.
191 if ( exists $updated_columns{itemlost}
192 and $updated_columns{itemlost} <= 0
193 and $pre_mod_item->itemlost > 0 )
195 $self->_set_found_trigger($pre_mod_item);
200 my $result = $self->SUPER::store;
201 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
203 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
204 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
206 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
207 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
208 unless $params->{skip_record_index};
209 $self->get_from_storage->_after_item_action_hooks({ action => $action });
211 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
213 biblio_ids => [ $self->biblionumber ]
215 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
226 my $params = @_ ? shift : {};
228 # FIXME check the item has no current issues
229 # i.e. raise the appropriate exception
231 # Get the item group so we can delete it later if it has no items left
232 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
234 my $result = $self->SUPER::delete;
236 # Delete the item gorup if it has no items left
237 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
239 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
240 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
241 unless $params->{skip_record_index};
243 $self->_after_item_action_hooks({ action => 'delete' });
245 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
246 if C4::Context->preference("CataloguingLog");
248 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
250 biblio_ids => [ $self->biblionumber ]
252 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
263 my $params = @_ ? shift : {};
265 my $safe_to_delete = $self->safe_to_delete;
266 return $safe_to_delete unless $safe_to_delete;
268 $self->move_to_deleted;
270 return $self->delete($params);
273 =head3 safe_to_delete
275 returns 1 if the item is safe to delete,
277 "book_on_loan" if the item is checked out,
279 "not_same_branch" if the item is blocked by independent branches,
281 "book_reserved" if the there are holds aganst the item, or
283 "linked_analytics" if the item has linked analytic records.
285 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
294 $error = "book_on_loan" if $self->checkout;
296 $error = "not_same_branch"
297 if defined C4::Context->userenv
298 and !C4::Context->IsSuperLibrarian()
299 and C4::Context->preference("IndependentBranches")
300 and ( C4::Context->userenv->{branch} ne $self->homebranch );
302 # check it doesn't have a waiting reserve
303 $error = "book_reserved"
304 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
306 $error = "linked_analytics"
307 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
309 $error = "last_item_for_hold"
310 if $self->biblio->items->count == 1
311 && $self->biblio->holds->search(
318 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
321 return Koha::Result::Boolean->new(1);
324 =head3 move_to_deleted
326 my $is_moved = $item->move_to_deleted;
328 Move an item to the deleteditems table.
329 This can be done before deleting an item, to make sure the data are not completely deleted.
333 sub move_to_deleted {
335 my $item_infos = $self->unblessed;
336 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
337 $item_infos->{deleted_on} = dt_from_string;
338 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
342 =head3 effective_itemtype
344 Returns the itemtype for the item based on whether item level itemtypes are set or not.
348 sub effective_itemtype {
351 return $self->_result()->effective_itemtype();
361 my $hb_rs = $self->_result->homebranch;
363 return Koha::Library->_new_from_dbic( $hb_rs );
366 =head3 holding_branch
373 my $hb_rs = $self->_result->holdingbranch;
375 return Koha::Library->_new_from_dbic( $hb_rs );
380 my $biblio = $item->biblio;
382 Return the bibliographic record of this item
388 my $biblio_rs = $self->_result->biblio;
389 return Koha::Biblio->_new_from_dbic( $biblio_rs );
394 my $biblioitem = $item->biblioitem;
396 Return the biblioitem record of this item
402 my $biblioitem_rs = $self->_result->biblioitem;
403 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
408 my $checkout = $item->checkout;
410 Return the checkout for this item
416 my $checkout_rs = $self->_result->issue;
417 return unless $checkout_rs;
418 return Koha::Checkout->_new_from_dbic( $checkout_rs );
423 my $item_group = $item->item_group;
425 Return the item group for this item
432 my $item_group_item = $self->_result->item_group_item;
433 return unless $item_group_item;
435 my $item_group_rs = $item_group_item->item_group;
436 return unless $item_group_rs;
438 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
444 my $return_claims = $item->return_claims;
446 Return any return_claims associated with this item
451 my ( $self, $params, $attrs ) = @_;
452 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
453 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
458 my $return_claim = $item->return_claim;
460 Returns the most recent unresolved return_claims associated with this item
467 $self->_result->return_claims->search( { resolution => undef },
468 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
469 return unless $claims_rs;
470 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
475 my $holds = $item->holds();
476 my $holds = $item->holds($params);
477 my $holds = $item->holds({ found => 'W'});
479 Return holds attached to an item, optionally accept a hashref of params to pass to search
484 my ( $self,$params ) = @_;
485 my $holds_rs = $self->_result->reserves->search($params);
486 return Koha::Holds->_new_from_dbic( $holds_rs );
489 =head3 request_transfer
491 my $transfer = $item->request_transfer(
495 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
499 Add a transfer request for this item to the given branch for the given reason.
501 An exception will be thrown if the BranchTransferLimits would prevent the requested
502 transfer, unless 'ignore_limits' is passed to override the limits.
504 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
505 The caller should catch such cases and retry the transfer request as appropriate passing
506 an appropriate override.
509 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
510 * replace - Used to replace the existing transfer request with your own.
514 sub request_transfer {
515 my ( $self, $params ) = @_;
517 # check for mandatory params
518 my @mandatory = ( 'to', 'reason' );
519 for my $param (@mandatory) {
520 unless ( defined( $params->{$param} ) ) {
521 Koha::Exceptions::MissingParameter->throw(
522 error => "The $param parameter is mandatory" );
526 Koha::Exceptions::Item::Transfer::Limit->throw()
527 unless ( $params->{ignore_limits}
528 || $self->can_be_transferred( { to => $params->{to} } ) );
530 my $request = $self->get_transfer;
531 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
532 if ( $request && !$params->{enqueue} && !$params->{replace} );
534 $request->cancel( { reason => $params->{reason}, force => 1 } )
535 if ( defined($request) && $params->{replace} );
537 my $transfer = Koha::Item::Transfer->new(
539 itemnumber => $self->itemnumber,
540 daterequested => dt_from_string,
541 frombranch => $self->holdingbranch,
542 tobranch => $params->{to}->branchcode,
543 reason => $params->{reason},
544 comments => $params->{comment}
553 my $transfer = $item->get_transfer;
555 Return the active transfer request or undef
557 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
558 whereby the most recently sent, but not received, transfer will be returned
559 if it exists, otherwise the oldest unsatisfied transfer will be returned.
561 This allows for transfers to queue, which is the case for stock rotation and
562 rotating collections where a manual transfer may need to take precedence but
563 we still expect the item to end up at a final location eventually.
570 return $self->get_transfers->search( {}, { rows => 1 } )->next;
575 my $transfer = $item->get_transfers;
577 Return the list of outstanding transfers (i.e requested but not yet cancelled
580 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
581 whereby the most recently sent, but not received, transfer will be returned
582 first if it exists, otherwise requests are in oldest to newest request order.
584 This allows for transfers to queue, which is the case for stock rotation and
585 rotating collections where a manual transfer may need to take precedence but
586 we still expect the item to end up at a final location eventually.
593 my $transfer_rs = $self->_result->branchtransfers;
595 return Koha::Item::Transfers
596 ->_new_from_dbic($transfer_rs)
598 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
601 =head3 last_returned_by
603 Gets and sets the last borrower to return an item.
605 Accepts and returns Koha::Patron objects
607 $item->last_returned_by( $borrowernumber );
609 $last_returned_by = $item->last_returned_by();
613 sub last_returned_by {
614 my ( $self, $borrower ) = @_;
616 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
619 return $items_last_returned_by_rs->update_or_create(
620 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
623 unless ( $self->{_last_returned_by} ) {
624 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
626 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
630 return $self->{_last_returned_by};
634 =head3 can_article_request
636 my $bool = $item->can_article_request( $borrower )
638 Returns true if item can be specifically requested
640 $borrower must be a Koha::Patron object
644 sub can_article_request {
645 my ( $self, $borrower ) = @_;
647 my $rule = $self->article_request_type($borrower);
649 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
653 =head3 hidden_in_opac
655 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
657 Returns true if item fields match the hidding criteria defined in $rules.
658 Returns false otherwise.
660 Takes HASHref that can have the following parameters:
662 $rules : { <field> => [ value_1, ... ], ... }
664 Note: $rules inherits its structure from the parsed YAML from reading
665 the I<OpacHiddenItems> system preference.
670 my ( $self, $params ) = @_;
672 my $rules = $params->{rules} // {};
675 if C4::Context->preference('hidelostitems') and
678 my $hidden_in_opac = 0;
680 foreach my $field ( keys %{$rules} ) {
682 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
688 return $hidden_in_opac;
691 =head3 can_be_transferred
693 $item->can_be_transferred({ to => $to_library, from => $from_library })
694 Checks if an item can be transferred to given library.
696 This feature is controlled by two system preferences:
697 UseBranchTransferLimits to enable / disable the feature
698 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
699 for setting the limitations
701 Takes HASHref that can have the following parameters:
702 MANDATORY PARAMETERS:
705 $from : Koha::Library # if not given, item holdingbranch
706 # will be used instead
708 Returns 1 if item can be transferred to $to_library, otherwise 0.
710 To find out whether at least one item of a Koha::Biblio can be transferred, please
711 see Koha::Biblio->can_be_transferred() instead of using this method for
712 multiple items of the same biblio.
716 sub can_be_transferred {
717 my ($self, $params) = @_;
719 my $to = $params->{to};
720 my $from = $params->{from};
722 $to = $to->branchcode;
723 $from = defined $from ? $from->branchcode : $self->holdingbranch;
725 return 1 if $from eq $to; # Transfer to current branch is allowed
726 return 1 unless C4::Context->preference('UseBranchTransferLimits');
728 my $limittype = C4::Context->preference('BranchTransferLimitsType');
729 return Koha::Item::Transfer::Limits->search({
732 $limittype => $limittype eq 'itemtype'
733 ? $self->effective_itemtype : $self->ccode
738 =head3 pickup_locations
740 $pickup_locations = $item->pickup_locations( {patron => $patron } )
742 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
743 and if item can be transferred to each pickup location.
747 sub pickup_locations {
748 my ($self, $params) = @_;
750 my $patron = $params->{patron};
752 my $circ_control_branch =
753 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
755 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
757 if(defined $patron) {
758 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
759 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
762 my $pickup_libraries = Koha::Libraries->search();
763 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
764 $pickup_libraries = $self->home_branch->get_hold_libraries;
765 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
766 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
767 $pickup_libraries = $plib->get_hold_libraries;
768 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
769 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
770 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
771 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
774 return $pickup_libraries->search(
779 order_by => ['branchname']
781 ) unless C4::Context->preference('UseBranchTransferLimits');
783 my $limittype = C4::Context->preference('BranchTransferLimitsType');
784 my ($ccode, $itype) = (undef, undef);
785 if( $limittype eq 'ccode' ){
786 $ccode = $self->ccode;
788 $itype = $self->itype;
790 my $limits = Koha::Item::Transfer::Limits->search(
792 fromBranch => $self->holdingbranch,
796 { columns => ['toBranch'] }
799 return $pickup_libraries->search(
801 pickup_location => 1,
803 '-not_in' => $limits->_resultset->as_query
807 order_by => ['branchname']
812 =head3 article_request_type
814 my $type = $item->article_request_type( $borrower )
816 returns 'yes', 'no', 'bib_only', or 'item_only'
818 $borrower must be a Koha::Patron object
822 sub article_request_type {
823 my ( $self, $borrower ) = @_;
825 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
827 $branch_control eq 'homebranch' ? $self->homebranch
828 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
830 my $borrowertype = $borrower->categorycode;
831 my $itemtype = $self->effective_itemtype();
832 my $rule = Koha::CirculationRules->get_effective_rule(
834 rule_name => 'article_requests',
835 categorycode => $borrowertype,
836 itemtype => $itemtype,
837 branchcode => $branchcode
841 return q{} unless $rule;
842 return $rule->rule_value || q{}
851 my $attributes = { order_by => 'priority' };
852 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
854 itemnumber => $self->itemnumber,
857 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
858 waitingdate => { '!=' => undef },
861 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
862 return Koha::Holds->_new_from_dbic($hold_rs);
865 =head3 stockrotationitem
867 my $sritem = Koha::Item->stockrotationitem;
869 Returns the stock rotation item associated with the current item.
873 sub stockrotationitem {
875 my $rs = $self->_result->stockrotationitem;
877 return Koha::StockRotationItem->_new_from_dbic( $rs );
882 my $item = $item->add_to_rota($rota_id);
884 Add this item to the rota identified by $ROTA_ID, which means associating it
885 with the first stage of that rota. Should this item already be associated
886 with a rota, then we will move it to the new rota.
891 my ( $self, $rota_id ) = @_;
892 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
896 =head3 has_pending_hold
898 my $is_pending_hold = $item->has_pending_hold();
900 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
904 sub has_pending_hold {
906 my $pending_hold = $self->_result->tmp_holdsqueues;
907 return $pending_hold->count ? 1: 0;
910 =head3 has_pending_recall {
912 my $has_pending_recall
914 Return if whether has pending recall of not.
918 sub has_pending_recall {
921 # FIXME Must be moved to $self->recalls
922 return Koha::Recalls->search(
924 item_id => $self->itemnumber,
932 my $field = $item->as_marc_field;
934 This method returns a MARC::Field object representing the Koha::Item object
935 with the current mappings configuration.
942 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
944 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
948 my $item_field = $tagslib->{$itemtag};
950 my $more_subfields = $self->additional_attributes->to_hashref;
951 foreach my $subfield (
953 $a->{display_order} <=> $b->{display_order}
954 || $a->{subfield} cmp $b->{subfield}
955 } grep { ref($_) && %$_ } values %$item_field
958 my $kohafield = $subfield->{kohafield};
959 my $tagsubfield = $subfield->{tagsubfield};
961 if ( defined $kohafield && $kohafield ne '' ) {
962 next if $kohafield !~ m{^items\.}; # That would be weird!
963 ( my $attribute = $kohafield ) =~ s|^items\.||;
964 $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
965 if defined $self->$attribute and $self->$attribute ne '';
967 $value = $more_subfields->{$tagsubfield}
970 next unless defined $value
973 if ( $subfield->{repeatable} ) {
974 my @values = split '\|', $value;
975 push @subfields, ( $tagsubfield => $_ ) for @values;
978 push @subfields, ( $tagsubfield => $value );
983 return unless @subfields;
985 return MARC::Field->new(
986 "$itemtag", ' ', ' ', @subfields
990 =head3 renewal_branchcode
992 Returns the branchcode to be recorded in statistics renewal of the item
996 sub renewal_branchcode {
998 my ($self, $params ) = @_;
1000 my $interface = C4::Context->interface;
1002 if ( $interface eq 'opac' ){
1003 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1004 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1005 $branchcode = 'OPACRenew';
1007 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1008 $branchcode = $self->homebranch;
1010 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1011 $branchcode = $self->checkout->patron->branchcode;
1013 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1014 $branchcode = $self->checkout->branchcode;
1020 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1021 ? C4::Context->userenv->{branch} : $params->{branch};
1028 Return the cover images associated with this item.
1035 my $cover_image_rs = $self->_result->cover_images;
1036 return unless $cover_image_rs;
1037 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1040 =head3 columns_to_str
1042 my $values = $items->columns_to_str;
1044 Return a hashref with the string representation of the different attribute of the item.
1046 This is meant to be used for display purpose only.
1050 sub columns_to_str {
1053 my $frameworkcode = $self->biblio->frameworkcode;
1054 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1055 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1057 my $columns_info = $self->_result->result_source->columns_info;
1059 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1061 for my $column ( keys %$columns_info ) {
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 # restore fine for lost book
1269 if ( $lostreturn_policy eq 'restore' ) {
1270 my $lost_overdue = Koha::Account::Lines->search(
1272 itemnumber => $self->itemnumber,
1273 debit_type_code => 'OVERDUE',
1277 order_by => { '-desc' => 'date' },
1282 if ( $lost_overdue ) {
1284 my $patron = $lost_overdue->patron;
1286 my $account = $patron->account;
1288 # Update status of fine
1289 $lost_overdue->status('FOUND')->store();
1291 # Find related forgive credit
1292 my $refund = $lost_overdue->credits(
1294 credit_type_code => 'FORGIVEN',
1295 itemnumber => $self->itemnumber,
1296 status => [ { '!=' => 'VOID' }, undef ]
1298 { order_by => { '-desc' => 'date' }, rows => 1 }
1302 # Revert the forgive credit
1303 $refund->void({ interface => 'trigger' });
1307 message => 'lost_restored',
1308 payload => { refund_id => $refund->id }
1313 # Reconcile balances if required
1314 if ( C4::Context->preference('AccountAutoReconcile') ) {
1315 $account->reconcile_balance;
1319 } elsif ( $lostreturn_policy eq 'charge' ) {
1323 message => 'lost_charge',
1329 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1331 if ( $processingreturn_policy ) {
1333 # refund processing charge made for lost book
1334 my $processing_charge = Koha::Account::Lines->search(
1336 itemnumber => $self->itemnumber,
1337 debit_type_code => 'PROCESSING',
1338 status => [ undef, { '<>' => 'FOUND' } ]
1341 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1346 if ( $processing_charge ) {
1348 my $patron = $processing_charge->patron;
1351 my $account = $patron->account;
1353 # Credit outstanding amount
1354 my $credit_total = $processing_charge->amountoutstanding;
1358 $processing_charge->amount > $processing_charge->amountoutstanding &&
1359 $processingreturn_policy ne "refund_unpaid"
1361 # some amount has been cancelled. collect the offsets that are not writeoffs
1362 # this works because the only way to subtract from this kind of a debt is
1363 # using the UI buttons 'Pay' and 'Write off'
1365 # We don't credit any payments if return policy is
1368 # In that case only unpaid/outstanding amount
1369 # will be credited which settles the debt without
1370 # creating extra credits
1372 my $credit_offsets = $processing_charge->debit_offsets(
1374 'credit_id' => { '!=' => undef },
1375 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1377 { join => 'credit' }
1380 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1381 # credits are negative on the DB
1382 $credit_offsets->total * -1 :
1384 # Credit the outstanding amount, then add what has been
1385 # paid to create a net credit for this amount
1386 $credit_total += $total_to_refund;
1390 if ( $credit_total > 0 ) {
1392 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1393 $credit = $account->add_credit(
1395 amount => $credit_total,
1396 description => 'Item found ' . $self->itemnumber,
1397 type => 'PROCESSING_FOUND',
1398 interface => C4::Context->interface,
1399 library_id => $branchcode,
1400 item_id => $self->itemnumber,
1401 issue_id => $processing_charge->issue_id
1405 $credit->apply( { debits => [$processing_charge] } );
1409 message => 'processing_refunded',
1410 payload => { credit_id => $credit->id }
1415 # Update the account status
1416 $processing_charge->status('FOUND');
1417 $processing_charge->store();
1419 # Reconcile balances if required
1420 if ( C4::Context->preference('AccountAutoReconcile') ) {
1421 $account->reconcile_balance;
1430 =head3 public_read_list
1432 This method returns the list of publicly readable database fields for both API and UI output purposes
1436 sub public_read_list {
1438 'itemnumber', 'biblionumber', 'homebranch',
1439 'holdingbranch', 'location', 'collectioncode',
1440 'itemcallnumber', 'copynumber', 'enumchron',
1441 'barcode', 'dateaccessioned', 'itemnotes',
1442 'onloan', 'uri', 'itype',
1443 'notforloan', 'damaged', 'itemlost',
1444 'withdrawn', 'restricted'
1450 Overloaded to_api method to ensure item-level itypes is adhered to.
1455 my ($self, $params) = @_;
1457 my $response = $self->SUPER::to_api($params);
1460 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1461 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1463 return { %$response, %$overrides };
1466 =head3 to_api_mapping
1468 This method returns the mapping for representing a Koha::Item object
1473 sub to_api_mapping {
1475 itemnumber => 'item_id',
1476 biblionumber => 'biblio_id',
1477 biblioitemnumber => undef,
1478 barcode => 'external_id',
1479 dateaccessioned => 'acquisition_date',
1480 booksellerid => 'acquisition_source',
1481 homebranch => 'home_library_id',
1482 price => 'purchase_price',
1483 replacementprice => 'replacement_price',
1484 replacementpricedate => 'replacement_price_date',
1485 datelastborrowed => 'last_checkout_date',
1486 datelastseen => 'last_seen_date',
1488 notforloan => 'not_for_loan_status',
1489 damaged => 'damaged_status',
1490 damaged_on => 'damaged_date',
1491 itemlost => 'lost_status',
1492 itemlost_on => 'lost_date',
1493 withdrawn => 'withdrawn',
1494 withdrawn_on => 'withdrawn_date',
1495 itemcallnumber => 'callnumber',
1496 coded_location_qualifier => 'coded_location_qualifier',
1497 issues => 'checkouts_count',
1498 renewals => 'renewals_count',
1499 reserves => 'holds_count',
1500 restricted => 'restricted_status',
1501 itemnotes => 'public_notes',
1502 itemnotes_nonpublic => 'internal_notes',
1503 holdingbranch => 'holding_library_id',
1504 timestamp => 'timestamp',
1505 location => 'location',
1506 permanent_location => 'permanent_location',
1507 onloan => 'checked_out_date',
1508 cn_source => 'call_number_source',
1509 cn_sort => 'call_number_sort',
1510 ccode => 'collection_code',
1511 materials => 'materials_notes',
1513 itype => 'item_type_id',
1514 more_subfields_xml => 'extended_subfields',
1515 enumchron => 'serial_issue_number',
1516 copynumber => 'copy_number',
1517 stocknumber => 'inventory_number',
1518 new_status => 'new_status',
1519 deleted_on => undef,
1525 my $itemtype = $item->itemtype;
1527 Returns Koha object for effective itemtype
1534 return Koha::ItemTypes->find( $self->effective_itemtype );
1539 my $orders = $item->orders();
1541 Returns a Koha::Acquisition::Orders object
1548 my $orders = $self->_result->item_orders;
1549 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1552 =head3 tracked_links
1554 my $tracked_links = $item->tracked_links();
1556 Returns a Koha::TrackedLinks object
1563 my $tracked_links = $self->_result->linktrackers;
1564 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1567 =head3 move_to_biblio
1569 $item->move_to_biblio($to_biblio[, $params]);
1571 Move the item to another biblio and update any references in other tables.
1573 The final optional parameter, C<$params>, is expected to contain the
1574 'skip_record_index' key, which is relayed down to Koha::Item->store.
1575 There it prevents calling index_records, which takes most of the
1576 time in batch adds/deletes. The caller must take care of calling
1577 index_records separately.
1580 skip_record_index => 1|0
1582 Returns undef if the move failed or the biblionumber of the destination record otherwise
1586 sub move_to_biblio {
1587 my ( $self, $to_biblio, $params ) = @_;
1591 return if $self->biblionumber == $to_biblio->biblionumber;
1593 my $from_biblionumber = $self->biblionumber;
1594 my $to_biblionumber = $to_biblio->biblionumber;
1596 # Own biblionumber and biblioitemnumber
1598 biblionumber => $to_biblionumber,
1599 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1600 })->store({ skip_record_index => $params->{skip_record_index} });
1602 unless ($params->{skip_record_index}) {
1603 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1604 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1607 # Acquisition orders
1608 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1611 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1613 # hold_fill_target (there's no Koha object available yet)
1614 my $hold_fill_target = $self->_result->hold_fill_target;
1615 if ($hold_fill_target) {
1616 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1619 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1620 # and can't even fake one since the significant columns are nullable.
1621 my $storage = $self->_result->result_source->storage;
1624 my ($storage, $dbh, @cols) = @_;
1626 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1631 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1633 return $to_biblionumber;
1638 my $bundle_items = $item->bundle_items;
1640 Returns the items associated with this bundle
1647 if ( !$self->{_bundle_items_cached} ) {
1648 my $bundle_items = Koha::Items->search(
1649 { 'item_bundles_item.host' => $self->itemnumber },
1650 { join => 'item_bundles_item' } );
1651 $self->{_bundle_items} = $bundle_items;
1652 $self->{_bundle_items_cached} = 1;
1655 return $self->{_bundle_items};
1660 my $is_bundle = $item->is_bundle;
1662 Returns whether the item is a bundle or not
1668 return $self->bundle_items->count ? 1 : 0;
1673 my $bundle = $item->bundle_host;
1675 Returns the bundle item this item is attached to
1682 my $bundle_items_rs = $self->_result->item_bundles_item;
1683 return unless $bundle_items_rs;
1684 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1689 my $in_bundle = $item->in_bundle;
1691 Returns whether this item is currently in a bundle
1697 return $self->bundle_host ? 1 : 0;
1700 =head3 add_to_bundle
1702 my $link = $item->add_to_bundle($bundle_item);
1704 Adds the bundle_item passed to this item
1709 my ( $self, $bundle_item ) = @_;
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 $self->_result->add_to_item_bundles_hosts(
1724 { item => $bundle_item->itemnumber } );
1726 $bundle_item->notforloan($BundleNotLoanValue)->store();
1732 # 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
1733 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1734 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1736 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1737 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1738 Koha::Exceptions::Object::FKConstraint->throw(
1739 error => 'Broken FK constraint',
1740 broken_fk => $+{column}
1745 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1747 Koha::Exceptions::Object::DuplicateID->throw(
1748 error => 'Duplicate ID',
1749 duplicate_id => $+{key}
1752 elsif ( $_->{msg} =~
1753 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1755 { # The optional \W in the regex might be a quote or backtick
1756 my $type = $+{type};
1757 my $value = $+{value};
1758 my $property = $+{property};
1759 $property =~ s/['`]//g;
1760 Koha::Exceptions::Object::BadValue->throw(
1763 property => $property =~ /(\w+\.\w+)$/
1766 , # results in table.column without quotes or backtics
1770 # Catch-all for foreign key breakages. It will help find other use cases
1779 =head3 remove_from_bundle
1781 Remove this item from any bundle it may have been attached to.
1785 sub remove_from_bundle {
1788 my $bundle_item_rs = $self->_result->item_bundles_item;
1789 if ( $bundle_item_rs ) {
1790 $bundle_item_rs->delete;
1791 $self->notforloan(0)->store();
1797 =head2 Internal methods
1799 =head3 _after_item_action_hooks
1801 Helper method that takes care of calling all plugin hooks
1805 sub _after_item_action_hooks {
1806 my ( $self, $params ) = @_;
1808 my $action = $params->{action};
1810 Koha::Plugins->call(
1811 'after_item_action',
1815 item_id => $self->itemnumber,
1822 my $recall = $item->recall;
1824 Return the relevant recall for this item
1830 my @recalls = Koha::Recalls->search(
1832 biblio_id => $self->biblionumber,
1835 { order_by => { -asc => 'created_date' } }
1837 foreach my $recall (@recalls) {
1838 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1842 # no item-level recall to return, so return earliest biblio-level
1843 # FIXME: eventually this will be based on priority
1847 =head3 can_be_recalled
1849 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1851 Does item-level checks and returns if items can be recalled by this borrower
1855 sub can_be_recalled {
1856 my ( $self, $params ) = @_;
1858 return 0 if !( C4::Context->preference('UseRecalls') );
1860 # check if this item is not for loan, withdrawn or lost
1861 return 0 if ( $self->notforloan != 0 );
1862 return 0 if ( $self->itemlost != 0 );
1863 return 0 if ( $self->withdrawn != 0 );
1865 # check if this item is not checked out - if not checked out, can't be recalled
1866 return 0 if ( !defined( $self->checkout ) );
1868 my $patron = $params->{patron};
1870 my $branchcode = C4::Context->userenv->{'branch'};
1872 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1875 # Check the circulation rule for each relevant itemtype for this item
1876 my $rule = Koha::CirculationRules->get_effective_rules({
1877 branchcode => $branchcode,
1878 categorycode => $patron ? $patron->categorycode : undef,
1879 itemtype => $self->effective_itemtype,
1882 'recalls_per_record',
1887 # check recalls allowed has been set and is not zero
1888 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1891 # check borrower has not reached open recalls allowed limit
1892 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1894 # check borrower has not reach open recalls allowed per record limit
1895 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1897 # check if this patron has already recalled this item
1898 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1900 # check if this patron has already checked out this item
1901 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1903 # check if this patron has already reserved this item
1904 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1907 # check item availability
1908 # items are unavailable for recall if they are lost, withdrawn or notforloan
1909 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1911 # if there are no available items at all, no recall can be placed
1912 return 0 if ( scalar @items == 0 );
1914 my $checked_out_count = 0;
1916 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1919 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1920 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1922 # can't recall if no items have been checked out
1923 return 0 if ( $checked_out_count == 0 );
1929 =head3 can_be_waiting_recall
1931 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1933 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1934 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1938 sub can_be_waiting_recall {
1941 return 0 if !( C4::Context->preference('UseRecalls') );
1943 # check if this item is not for loan, withdrawn or lost
1944 return 0 if ( $self->notforloan != 0 );
1945 return 0 if ( $self->itemlost != 0 );
1946 return 0 if ( $self->withdrawn != 0 );
1948 my $branchcode = $self->holdingbranch;
1949 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1950 $branchcode = C4::Context->userenv->{'branch'};
1952 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1955 # Check the circulation rule for each relevant itemtype for this item
1956 my $rule = Koha::CirculationRules->get_effective_rules({
1957 branchcode => $branchcode,
1958 categorycode => undef,
1959 itemtype => $self->effective_itemtype,
1965 # check recalls allowed has been set and is not zero
1966 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1972 =head3 check_recalls
1974 my $recall = $item->check_recalls;
1976 Get the most relevant recall for this item.
1983 my @recalls = Koha::Recalls->search(
1984 { biblio_id => $self->biblionumber,
1985 item_id => [ $self->itemnumber, undef ]
1987 { order_by => { -asc => 'created_date' } }
1988 )->filter_by_current->as_list;
1991 # iterate through relevant recalls to find the best one.
1992 # if we come across a waiting recall, use this one.
1993 # 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.
1994 foreach my $r ( @recalls ) {
1995 if ( $r->waiting ) {
2000 unless ( defined $recall ) {
2001 $recall = $recalls[0];
2007 =head3 is_notforloan
2009 my $is_notforloan = $item->is_notforloan;
2011 Determine whether or not this item is "notforloan" based on
2012 the item's notforloan status or its item type
2018 my $is_notforloan = 0;
2020 if ( $self->notforloan ){
2024 my $itemtype = $self->itemtype;
2026 if ( $itemtype->notforloan ){
2032 return $is_notforloan;
2035 =head3 is_denied_renewal
2037 my $is_denied_renewal = $item->is_denied_renewal;
2039 Determine whether or not this item can be renewed based on the
2040 rules set in the ItemsDeniedRenewal system preference.
2044 sub is_denied_renewal {
2047 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
2048 return 0 unless $denyingrules;
2049 foreach my $field (keys %$denyingrules) {
2050 my $val = $self->$field;
2051 if( !defined $val) {
2052 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2055 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2056 # If the results matches the values in the syspref
2057 # We return true if match found
2074 Kyle M Hall <kyle@bywatersolutions.com>