3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
58 use base qw(Koha::Object);
62 Koha::Item - Koha Item object class
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
82 my $params = @_ ? shift : {};
84 my $log_action = $params->{log_action} // 1;
86 # We do not want to oblige callers to pass this value
87 # Dev conveniences vs performance?
88 unless ( $self->biblioitemnumber ) {
89 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
92 # See related changes from C4::Items::AddItem
93 unless ( $self->itype ) {
94 $self->itype($self->biblio->biblioitem->itemtype);
97 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
99 my $today = dt_from_string;
100 my $action = 'create';
102 unless ( $self->in_storage ) { #AddItem
104 unless ( $self->permanent_location ) {
105 $self->permanent_location($self->location);
108 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109 unless ( $self->location || !$default_location ) {
110 $self->permanent_location( $self->location || $default_location )
111 unless $self->permanent_location;
112 $self->location($default_location);
115 unless ( $self->replacementpricedate ) {
116 $self->replacementpricedate($today);
118 unless ( $self->datelastseen ) {
119 $self->datelastseen($today);
122 unless ( $self->dateaccessioned ) {
123 $self->dateaccessioned($today);
126 if ( $self->itemcallnumber
127 or $self->cn_source )
129 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130 $self->cn_sort($cn_sort);
137 my %updated_columns = $self->_result->get_dirty_columns;
138 return $self->SUPER::store unless %updated_columns;
140 # Retrieve the item for comparison if we need to
142 exists $updated_columns{itemlost}
143 or exists $updated_columns{withdrawn}
144 or exists $updated_columns{damaged}
145 ) ? $self->get_from_storage : undef;
147 # Update *_on fields if needed
148 # FIXME: Why not for AddItem as well?
149 my @fields = qw( itemlost withdrawn damaged );
150 for my $field (@fields) {
152 # If the field is defined but empty or 0, we are
153 # removing/unsetting and thus need to clear out
155 if ( exists $updated_columns{$field}
156 && defined( $self->$field )
159 my $field_on = "${field}_on";
160 $self->$field_on(undef);
162 # If the field has changed otherwise, we much update
164 elsif (exists $updated_columns{$field}
165 && $updated_columns{$field}
166 && !$pre_mod_item->$field )
168 my $field_on = "${field}_on";
169 $self->$field_on(dt_from_string);
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
210 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
212 biblio_ids => [ $self->biblionumber ]
214 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
225 my $params = @_ ? shift : {};
227 # FIXME check the item has no current issues
228 # i.e. raise the appropriate exception
230 # Get the item group so we can delete it later if it has no items left
231 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
233 my $result = $self->SUPER::delete;
235 # Delete the item gorup if it has no items left
236 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
251 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error //= "not_same_branch"
296 if defined C4::Context->userenv
297 && !C4::Context->IsSuperLibrarian()
298 && C4::Context->preference("IndependentBranches")
299 && ( C4::Context->userenv->{branch} ne $self->homebranch );
301 # check it doesn't have a waiting reserve
302 $error //= "book_reserved"
303 if $self->holds->filter_by_found->count;
305 $error //= "linked_analytics"
306 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
308 $error //= "last_item_for_hold"
309 if $self->biblio->items->count == 1
310 && $self->biblio->holds->search(
317 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
320 return Koha::Result::Boolean->new(1);
323 =head3 move_to_deleted
325 my $is_moved = $item->move_to_deleted;
327 Move an item to the deleteditems table.
328 This can be done before deleting an item, to make sure the data are not completely deleted.
332 sub move_to_deleted {
334 my $item_infos = $self->unblessed;
335 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
336 $item_infos->{deleted_on} = dt_from_string;
337 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
341 =head3 effective_itemtype
343 Returns the itemtype for the item based on whether item level itemtypes are set or not.
347 sub effective_itemtype {
350 return $self->_result()->effective_itemtype();
360 my $hb_rs = $self->_result->homebranch;
362 return Koha::Library->_new_from_dbic( $hb_rs );
365 =head3 holding_branch
372 my $hb_rs = $self->_result->holdingbranch;
374 return Koha::Library->_new_from_dbic( $hb_rs );
379 my $biblio = $item->biblio;
381 Return the bibliographic record of this item
387 my $biblio_rs = $self->_result->biblio;
388 return Koha::Biblio->_new_from_dbic( $biblio_rs );
393 my $biblioitem = $item->biblioitem;
395 Return the biblioitem record of this item
401 my $biblioitem_rs = $self->_result->biblioitem;
402 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
407 my $checkout = $item->checkout;
409 Return the checkout for this item
415 my $checkout_rs = $self->_result->issue;
416 return unless $checkout_rs;
417 return Koha::Checkout->_new_from_dbic( $checkout_rs );
422 my $item_group = $item->item_group;
424 Return the item group for this item
431 my $item_group_item = $self->_result->item_group_item;
432 return unless $item_group_item;
434 my $item_group_rs = $item_group_item->item_group;
435 return unless $item_group_rs;
437 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
443 my $return_claims = $item->return_claims;
445 Return any return_claims associated with this item
450 my ( $self, $params, $attrs ) = @_;
451 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
452 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
457 my $return_claim = $item->return_claim;
459 Returns the most recent unresolved return_claims associated with this item
466 $self->_result->return_claims->search( { resolution => undef },
467 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
468 return unless $claims_rs;
469 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
474 my $holds = $item->holds();
475 my $holds = $item->holds($params);
476 my $holds = $item->holds({ found => 'W'});
478 Return holds attached to an item, optionally accept a hashref of params to pass to search
483 my ( $self,$params ) = @_;
484 my $holds_rs = $self->_result->reserves->search($params);
485 return Koha::Holds->_new_from_dbic( $holds_rs );
488 =head3 request_transfer
490 my $transfer = $item->request_transfer(
494 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
498 Add a transfer request for this item to the given branch for the given reason.
500 An exception will be thrown if the BranchTransferLimits would prevent the requested
501 transfer, unless 'ignore_limits' is passed to override the limits.
503 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
504 The caller should catch such cases and retry the transfer request as appropriate passing
505 an appropriate override.
508 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
509 * replace - Used to replace the existing transfer request with your own.
513 sub request_transfer {
514 my ( $self, $params ) = @_;
516 # check for mandatory params
517 my @mandatory = ( 'to', 'reason' );
518 for my $param (@mandatory) {
519 unless ( defined( $params->{$param} ) ) {
520 Koha::Exceptions::MissingParameter->throw(
521 error => "The $param parameter is mandatory" );
525 Koha::Exceptions::Item::Transfer::Limit->throw()
526 unless ( $params->{ignore_limits}
527 || $self->can_be_transferred( { to => $params->{to} } ) );
529 my $request = $self->get_transfer;
530 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
531 if ( $request && !$params->{enqueue} && !$params->{replace} );
533 $request->cancel( { reason => $params->{reason}, force => 1 } )
534 if ( defined($request) && $params->{replace} );
536 my $transfer = Koha::Item::Transfer->new(
538 itemnumber => $self->itemnumber,
539 daterequested => dt_from_string,
540 frombranch => $self->holdingbranch,
541 tobranch => $params->{to}->branchcode,
542 reason => $params->{reason},
543 comments => $params->{comment}
552 my $transfer = $item->get_transfer;
554 Return the active transfer request or undef
556 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
557 whereby the most recently sent, but not received, transfer will be returned
558 if it exists, otherwise the oldest unsatisfied transfer will be returned.
560 This allows for transfers to queue, which is the case for stock rotation and
561 rotating collections where a manual transfer may need to take precedence but
562 we still expect the item to end up at a final location eventually.
569 return $self->get_transfers->search( {}, { rows => 1 } )->next;
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->branchtransfers;
594 return Koha::Item::Transfers
595 ->_new_from_dbic($transfer_rs)
597 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
600 =head3 last_returned_by
602 Gets and sets the last borrower to return an item.
604 Accepts and returns Koha::Patron objects
606 $item->last_returned_by( $borrowernumber );
608 $last_returned_by = $item->last_returned_by();
612 sub last_returned_by {
613 my ( $self, $borrower ) = @_;
615 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
618 return $items_last_returned_by_rs->update_or_create(
619 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
622 unless ( $self->{_last_returned_by} ) {
623 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
625 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
629 return $self->{_last_returned_by};
633 =head3 can_article_request
635 my $bool = $item->can_article_request( $borrower )
637 Returns true if item can be specifically requested
639 $borrower must be a Koha::Patron object
643 sub can_article_request {
644 my ( $self, $borrower ) = @_;
646 my $rule = $self->article_request_type($borrower);
648 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
652 =head3 hidden_in_opac
654 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
656 Returns true if item fields match the hidding criteria defined in $rules.
657 Returns false otherwise.
659 Takes HASHref that can have the following parameters:
661 $rules : { <field> => [ value_1, ... ], ... }
663 Note: $rules inherits its structure from the parsed YAML from reading
664 the I<OpacHiddenItems> system preference.
669 my ( $self, $params ) = @_;
671 my $rules = $params->{rules} // {};
674 if C4::Context->preference('hidelostitems') and
677 my $hidden_in_opac = 0;
679 foreach my $field ( keys %{$rules} ) {
681 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
687 return $hidden_in_opac;
690 =head3 can_be_transferred
692 $item->can_be_transferred({ to => $to_library, from => $from_library })
693 Checks if an item can be transferred to given library.
695 This feature is controlled by two system preferences:
696 UseBranchTransferLimits to enable / disable the feature
697 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
698 for setting the limitations
700 Takes HASHref that can have the following parameters:
701 MANDATORY PARAMETERS:
704 $from : Koha::Library # if not given, item holdingbranch
705 # will be used instead
707 Returns 1 if item can be transferred to $to_library, otherwise 0.
709 To find out whether at least one item of a Koha::Biblio can be transferred, please
710 see Koha::Biblio->can_be_transferred() instead of using this method for
711 multiple items of the same biblio.
715 sub can_be_transferred {
716 my ($self, $params) = @_;
718 my $to = $params->{to};
719 my $from = $params->{from};
721 $to = $to->branchcode;
722 $from = defined $from ? $from->branchcode : $self->holdingbranch;
724 return 1 if $from eq $to; # Transfer to current branch is allowed
725 return 1 unless C4::Context->preference('UseBranchTransferLimits');
727 my $limittype = C4::Context->preference('BranchTransferLimitsType');
728 return Koha::Item::Transfer::Limits->search({
731 $limittype => $limittype eq 'itemtype'
732 ? $self->effective_itemtype : $self->ccode
737 =head3 pickup_locations
739 my $pickup_locations = $item->pickup_locations({ patron => $patron })
741 Returns possible pickup locations for this item, according to patron's home library
742 and if item can be transferred to each pickup location.
744 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
749 sub pickup_locations {
750 my ($self, $params) = @_;
752 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
753 unless exists $params->{patron};
755 my $patron = $params->{patron};
757 my $circ_control_branch =
758 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
760 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
762 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
763 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
765 my $pickup_libraries = Koha::Libraries->search();
766 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
767 $pickup_libraries = $self->home_branch->get_hold_libraries;
768 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
769 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
770 $pickup_libraries = $plib->get_hold_libraries;
771 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
772 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
773 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
774 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
777 return $pickup_libraries->search(
782 order_by => ['branchname']
784 ) unless C4::Context->preference('UseBranchTransferLimits');
786 my $limittype = C4::Context->preference('BranchTransferLimitsType');
787 my ($ccode, $itype) = (undef, undef);
788 if( $limittype eq 'ccode' ){
789 $ccode = $self->ccode;
791 $itype = $self->itype;
793 my $limits = Koha::Item::Transfer::Limits->search(
795 fromBranch => $self->holdingbranch,
799 { columns => ['toBranch'] }
802 return $pickup_libraries->search(
804 pickup_location => 1,
806 '-not_in' => $limits->_resultset->as_query
810 order_by => ['branchname']
815 =head3 article_request_type
817 my $type = $item->article_request_type( $borrower )
819 returns 'yes', 'no', 'bib_only', or 'item_only'
821 $borrower must be a Koha::Patron object
825 sub article_request_type {
826 my ( $self, $borrower ) = @_;
828 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
830 $branch_control eq 'homebranch' ? $self->homebranch
831 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
833 my $borrowertype = $borrower->categorycode;
834 my $itemtype = $self->effective_itemtype();
835 my $rule = Koha::CirculationRules->get_effective_rule(
837 rule_name => 'article_requests',
838 categorycode => $borrowertype,
839 itemtype => $itemtype,
840 branchcode => $branchcode
844 return q{} unless $rule;
845 return $rule->rule_value || q{}
854 my $attributes = { order_by => 'priority' };
855 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
857 itemnumber => $self->itemnumber,
860 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
861 waitingdate => { '!=' => undef },
864 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
865 return Koha::Holds->_new_from_dbic($hold_rs);
868 =head3 stockrotationitem
870 my $sritem = Koha::Item->stockrotationitem;
872 Returns the stock rotation item associated with the current item.
876 sub stockrotationitem {
878 my $rs = $self->_result->stockrotationitem;
880 return Koha::StockRotationItem->_new_from_dbic( $rs );
885 my $item = $item->add_to_rota($rota_id);
887 Add this item to the rota identified by $ROTA_ID, which means associating it
888 with the first stage of that rota. Should this item already be associated
889 with a rota, then we will move it to the new rota.
894 my ( $self, $rota_id ) = @_;
895 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
899 =head3 has_pending_hold
901 my $is_pending_hold = $item->has_pending_hold();
903 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
907 sub has_pending_hold {
909 my $pending_hold = $self->_result->tmp_holdsqueues;
910 return $pending_hold->count ? 1: 0;
913 =head3 has_pending_recall {
915 my $has_pending_recall
917 Return if whether has pending recall of not.
921 sub has_pending_recall {
924 # FIXME Must be moved to $self->recalls
925 return Koha::Recalls->search(
927 item_id => $self->itemnumber,
935 my $field = $item->as_marc_field;
937 This method returns a MARC::Field object representing the Koha::Item object
938 with the current mappings configuration.
945 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
947 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
951 my $item_field = $tagslib->{$itemtag};
953 my $more_subfields = $self->additional_attributes->to_hashref;
954 foreach my $subfield (
956 $a->{display_order} <=> $b->{display_order}
957 || $a->{subfield} cmp $b->{subfield}
958 } grep { ref($_) && %$_ } values %$item_field
961 my $kohafield = $subfield->{kohafield};
962 my $tagsubfield = $subfield->{tagsubfield};
964 if ( defined $kohafield && $kohafield ne '' ) {
965 next if $kohafield !~ m{^items\.}; # That would be weird!
966 ( my $attribute = $kohafield ) =~ s|^items\.||;
967 $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
968 if defined $self->$attribute and $self->$attribute ne '';
970 $value = $more_subfields->{$tagsubfield}
973 next unless defined $value
976 if ( $subfield->{repeatable} ) {
977 my @values = split '\|', $value;
978 push @subfields, ( $tagsubfield => $_ ) for @values;
981 push @subfields, ( $tagsubfield => $value );
986 return unless @subfields;
988 return MARC::Field->new(
989 "$itemtag", ' ', ' ', @subfields
993 =head3 renewal_branchcode
995 Returns the branchcode to be recorded in statistics renewal of the item
999 sub renewal_branchcode {
1001 my ($self, $params ) = @_;
1003 my $interface = C4::Context->interface;
1005 if ( $interface eq 'opac' ){
1006 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1007 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1008 $branchcode = 'OPACRenew';
1010 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1011 $branchcode = $self->homebranch;
1013 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1014 $branchcode = $self->checkout->patron->branchcode;
1016 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1017 $branchcode = $self->checkout->branchcode;
1023 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1024 ? C4::Context->userenv->{branch} : $params->{branch};
1031 Return the cover images associated with this item.
1038 my $cover_image_rs = $self->_result->cover_images;
1039 return unless $cover_image_rs;
1040 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1043 =head3 columns_to_str
1045 my $values = $items->columns_to_str;
1047 Return a hashref with the string representation of the different attribute of the item.
1049 This is meant to be used for display purpose only.
1053 sub columns_to_str {
1056 my $frameworkcode = $self->biblio->frameworkcode;
1057 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1058 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1060 my $columns_info = $self->_result->result_source->columns_info;
1062 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1064 for my $column ( keys %$columns_info ) {
1066 next if $column eq 'more_subfields_xml';
1068 my $value = $self->$column;
1069 # 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
1071 if ( not defined $value or $value eq "" ) {
1072 $values->{$column} = $value;
1077 exists $mss->{"items.$column"}
1078 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1081 $values->{$column} =
1083 ? $subfield->{authorised_value}
1084 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1085 $subfield->{tagsubfield}, $value, '', $tagslib )
1091 $self->more_subfields_xml
1092 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1097 my ( $field ) = $marc_more->fields;
1098 for my $sf ( $field->subfields ) {
1099 my $subfield_code = $sf->[0];
1100 my $value = $sf->[1];
1101 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1102 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1104 $subfield->{authorised_value}
1105 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1106 $subfield->{tagsubfield}, $value, '', $tagslib )
1109 push @{$more_values->{$subfield_code}}, $value;
1112 while ( my ( $k, $v ) = each %$more_values ) {
1113 $values->{$k} = join ' | ', @$v;
1120 =head3 additional_attributes
1122 my $attributes = $item->additional_attributes;
1123 $attributes->{k} = 'new k';
1124 $item->update({ more_subfields => $attributes->to_marcxml });
1126 Returns a Koha::Item::Attributes object that represents the non-mapped
1127 attributes for this item.
1131 sub additional_attributes {
1134 return Koha::Item::Attributes->new_from_marcxml(
1135 $self->more_subfields_xml,
1139 =head3 _set_found_trigger
1141 $self->_set_found_trigger
1143 Finds the most recent lost item charge for this item and refunds the patron
1144 appropriately, taking into account any payments or writeoffs already applied
1147 Internal function, not exported, called only by Koha::Item->store.
1151 sub _set_found_trigger {
1152 my ( $self, $pre_mod_item ) = @_;
1154 # Reverse any lost item charges if necessary.
1155 my $no_refund_after_days =
1156 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1157 if ($no_refund_after_days) {
1158 my $today = dt_from_string();
1159 my $lost_age_in_days =
1160 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1163 return $self unless $lost_age_in_days < $no_refund_after_days;
1166 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1169 return_branch => C4::Context->userenv
1170 ? C4::Context->userenv->{'branch'}
1174 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1176 if ( $lostreturn_policy ) {
1178 # refund charge made for lost book
1179 my $lost_charge = Koha::Account::Lines->search(
1181 itemnumber => $self->itemnumber,
1182 debit_type_code => 'LOST',
1183 status => [ undef, { '<>' => 'FOUND' } ]
1186 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1191 if ( $lost_charge ) {
1193 my $patron = $lost_charge->patron;
1196 my $account = $patron->account;
1198 # Credit outstanding amount
1199 my $credit_total = $lost_charge->amountoutstanding;
1203 $lost_charge->amount > $lost_charge->amountoutstanding &&
1204 $lostreturn_policy ne "refund_unpaid"
1206 # some amount has been cancelled. collect the offsets that are not writeoffs
1207 # this works because the only way to subtract from this kind of a debt is
1208 # using the UI buttons 'Pay' and 'Write off'
1210 # We don't credit any payments if return policy is
1213 # In that case only unpaid/outstanding amount
1214 # will be credited which settles the debt without
1215 # creating extra credits
1217 my $credit_offsets = $lost_charge->debit_offsets(
1219 'credit_id' => { '!=' => undef },
1220 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1222 { join => 'credit' }
1225 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1226 # credits are negative on the DB
1227 $credit_offsets->total * -1 :
1229 # Credit the outstanding amount, then add what has been
1230 # paid to create a net credit for this amount
1231 $credit_total += $total_to_refund;
1235 if ( $credit_total > 0 ) {
1237 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1238 $credit = $account->add_credit(
1240 amount => $credit_total,
1241 description => 'Item found ' . $self->itemnumber,
1242 type => 'LOST_FOUND',
1243 interface => C4::Context->interface,
1244 library_id => $branchcode,
1245 item_id => $self->itemnumber,
1246 issue_id => $lost_charge->issue_id
1250 $credit->apply( { debits => [$lost_charge] } );
1254 message => 'lost_refunded',
1255 payload => { credit_id => $credit->id }
1260 # Update the account status
1261 $lost_charge->status('FOUND');
1262 $lost_charge->store();
1264 # Reconcile balances if required
1265 if ( C4::Context->preference('AccountAutoReconcile') ) {
1266 $account->reconcile_balance;
1271 # possibly restore fine for lost book
1272 my $lost_overdue = Koha::Account::Lines->search(
1274 itemnumber => $self->itemnumber,
1275 debit_type_code => 'OVERDUE',
1279 order_by => { '-desc' => 'date' },
1283 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1285 my $patron = $lost_overdue->patron;
1287 my $account = $patron->account;
1289 # Update status of fine
1290 $lost_overdue->status('FOUND')->store();
1292 # Find related forgive credit
1293 my $refund = $lost_overdue->credits(
1295 credit_type_code => 'FORGIVEN',
1296 itemnumber => $self->itemnumber,
1297 status => [ { '!=' => 'VOID' }, undef ]
1299 { order_by => { '-desc' => 'date' }, rows => 1 }
1303 # Revert the forgive credit
1304 $refund->void({ interface => 'trigger' });
1308 message => 'lost_restored',
1309 payload => { refund_id => $refund->id }
1314 # Reconcile balances if required
1315 if ( C4::Context->preference('AccountAutoReconcile') ) {
1316 $account->reconcile_balance;
1320 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1324 message => 'lost_charge',
1330 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1332 if ( $processingreturn_policy ) {
1334 # refund processing charge made for lost book
1335 my $processing_charge = Koha::Account::Lines->search(
1337 itemnumber => $self->itemnumber,
1338 debit_type_code => 'PROCESSING',
1339 status => [ undef, { '<>' => 'FOUND' } ]
1342 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1347 if ( $processing_charge ) {
1349 my $patron = $processing_charge->patron;
1352 my $account = $patron->account;
1354 # Credit outstanding amount
1355 my $credit_total = $processing_charge->amountoutstanding;
1359 $processing_charge->amount > $processing_charge->amountoutstanding &&
1360 $processingreturn_policy ne "refund_unpaid"
1362 # some amount has been cancelled. collect the offsets that are not writeoffs
1363 # this works because the only way to subtract from this kind of a debt is
1364 # using the UI buttons 'Pay' and 'Write off'
1366 # We don't credit any payments if return policy is
1369 # In that case only unpaid/outstanding amount
1370 # will be credited which settles the debt without
1371 # creating extra credits
1373 my $credit_offsets = $processing_charge->debit_offsets(
1375 'credit_id' => { '!=' => undef },
1376 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1378 { join => 'credit' }
1381 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1382 # credits are negative on the DB
1383 $credit_offsets->total * -1 :
1385 # Credit the outstanding amount, then add what has been
1386 # paid to create a net credit for this amount
1387 $credit_total += $total_to_refund;
1391 if ( $credit_total > 0 ) {
1393 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1394 $credit = $account->add_credit(
1396 amount => $credit_total,
1397 description => 'Item found ' . $self->itemnumber,
1398 type => 'PROCESSING_FOUND',
1399 interface => C4::Context->interface,
1400 library_id => $branchcode,
1401 item_id => $self->itemnumber,
1402 issue_id => $processing_charge->issue_id
1406 $credit->apply( { debits => [$processing_charge] } );
1410 message => 'processing_refunded',
1411 payload => { credit_id => $credit->id }
1416 # Update the account status
1417 $processing_charge->status('FOUND');
1418 $processing_charge->store();
1420 # Reconcile balances if required
1421 if ( C4::Context->preference('AccountAutoReconcile') ) {
1422 $account->reconcile_balance;
1431 =head3 public_read_list
1433 This method returns the list of publicly readable database fields for both API and UI output purposes
1437 sub public_read_list {
1439 'itemnumber', 'biblionumber', 'homebranch',
1440 'holdingbranch', 'location', 'collectioncode',
1441 'itemcallnumber', 'copynumber', 'enumchron',
1442 'barcode', 'dateaccessioned', 'itemnotes',
1443 'onloan', 'uri', 'itype',
1444 'notforloan', 'damaged', 'itemlost',
1445 'withdrawn', 'restricted'
1451 Overloaded to_api method to ensure item-level itypes is adhered to.
1456 my ($self, $params) = @_;
1458 my $response = $self->SUPER::to_api($params);
1461 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1462 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1464 return { %$response, %$overrides };
1467 =head3 to_api_mapping
1469 This method returns the mapping for representing a Koha::Item object
1474 sub to_api_mapping {
1476 itemnumber => 'item_id',
1477 biblionumber => 'biblio_id',
1478 biblioitemnumber => undef,
1479 barcode => 'external_id',
1480 dateaccessioned => 'acquisition_date',
1481 booksellerid => 'acquisition_source',
1482 homebranch => 'home_library_id',
1483 price => 'purchase_price',
1484 replacementprice => 'replacement_price',
1485 replacementpricedate => 'replacement_price_date',
1486 datelastborrowed => 'last_checkout_date',
1487 datelastseen => 'last_seen_date',
1489 notforloan => 'not_for_loan_status',
1490 damaged => 'damaged_status',
1491 damaged_on => 'damaged_date',
1492 itemlost => 'lost_status',
1493 itemlost_on => 'lost_date',
1494 withdrawn => 'withdrawn',
1495 withdrawn_on => 'withdrawn_date',
1496 itemcallnumber => 'callnumber',
1497 coded_location_qualifier => 'coded_location_qualifier',
1498 issues => 'checkouts_count',
1499 renewals => 'renewals_count',
1500 reserves => 'holds_count',
1501 restricted => 'restricted_status',
1502 itemnotes => 'public_notes',
1503 itemnotes_nonpublic => 'internal_notes',
1504 holdingbranch => 'holding_library_id',
1505 timestamp => 'timestamp',
1506 location => 'location',
1507 permanent_location => 'permanent_location',
1508 onloan => 'checked_out_date',
1509 cn_source => 'call_number_source',
1510 cn_sort => 'call_number_sort',
1511 ccode => 'collection_code',
1512 materials => 'materials_notes',
1514 itype => 'item_type_id',
1515 more_subfields_xml => 'extended_subfields',
1516 enumchron => 'serial_issue_number',
1517 copynumber => 'copy_number',
1518 stocknumber => 'inventory_number',
1519 new_status => 'new_status',
1520 deleted_on => undef,
1526 my $itemtype = $item->itemtype;
1528 Returns Koha object for effective itemtype
1535 return Koha::ItemTypes->find( $self->effective_itemtype );
1540 my $orders = $item->orders();
1542 Returns a Koha::Acquisition::Orders object
1549 my $orders = $self->_result->item_orders;
1550 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1553 =head3 tracked_links
1555 my $tracked_links = $item->tracked_links();
1557 Returns a Koha::TrackedLinks object
1564 my $tracked_links = $self->_result->linktrackers;
1565 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1568 =head3 move_to_biblio
1570 $item->move_to_biblio($to_biblio[, $params]);
1572 Move the item to another biblio and update any references in other tables.
1574 The final optional parameter, C<$params>, is expected to contain the
1575 'skip_record_index' key, which is relayed down to Koha::Item->store.
1576 There it prevents calling index_records, which takes most of the
1577 time in batch adds/deletes. The caller must take care of calling
1578 index_records separately.
1581 skip_record_index => 1|0
1583 Returns undef if the move failed or the biblionumber of the destination record otherwise
1587 sub move_to_biblio {
1588 my ( $self, $to_biblio, $params ) = @_;
1592 return if $self->biblionumber == $to_biblio->biblionumber;
1594 my $from_biblionumber = $self->biblionumber;
1595 my $to_biblionumber = $to_biblio->biblionumber;
1597 # Own biblionumber and biblioitemnumber
1599 biblionumber => $to_biblionumber,
1600 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1601 })->store({ skip_record_index => $params->{skip_record_index} });
1603 unless ($params->{skip_record_index}) {
1604 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1605 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1608 # Acquisition orders
1609 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1612 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1614 # hold_fill_target (there's no Koha object available yet)
1615 my $hold_fill_target = $self->_result->hold_fill_target;
1616 if ($hold_fill_target) {
1617 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1620 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1621 # and can't even fake one since the significant columns are nullable.
1622 my $storage = $self->_result->result_source->storage;
1625 my ($storage, $dbh, @cols) = @_;
1627 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1632 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1634 return $to_biblionumber;
1639 my $bundle_items = $item->bundle_items;
1641 Returns the items associated with this bundle
1648 if ( !$self->{_bundle_items_cached} ) {
1649 my $bundle_items = Koha::Items->search(
1650 { 'item_bundles_item.host' => $self->itemnumber },
1651 { join => 'item_bundles_item' } );
1652 $self->{_bundle_items} = $bundle_items;
1653 $self->{_bundle_items_cached} = 1;
1656 return $self->{_bundle_items};
1661 my $is_bundle = $item->is_bundle;
1663 Returns whether the item is a bundle or not
1669 return $self->bundle_items->count ? 1 : 0;
1674 my $bundle = $item->bundle_host;
1676 Returns the bundle item this item is attached to
1683 my $bundle_items_rs = $self->_result->item_bundles_item;
1684 return unless $bundle_items_rs;
1685 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1690 my $in_bundle = $item->in_bundle;
1692 Returns whether this item is currently in a bundle
1698 return $self->bundle_host ? 1 : 0;
1701 =head3 add_to_bundle
1703 my $link = $item->add_to_bundle($bundle_item);
1705 Adds the bundle_item passed to this item
1710 my ( $self, $bundle_item, $options ) = @_;
1714 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1715 if ( $self->itemnumber eq $bundle_item->itemnumber
1716 || $bundle_item->is_bundle
1717 || $self->in_bundle );
1719 my $schema = Koha::Database->new->schema;
1721 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1726 my $checkout = $bundle_item->checkout;
1728 unless ($options->{force_checkin}) {
1729 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1732 my $branchcode = C4::Context->userenv->{'branch'};
1733 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1735 Koha::Exceptions::Checkin::FailedCheckin->throw();
1739 $self->_result->add_to_item_bundles_hosts(
1740 { item => $bundle_item->itemnumber } );
1742 $bundle_item->notforloan($BundleNotLoanValue)->store();
1748 # 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
1749 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1750 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1752 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1753 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1754 Koha::Exceptions::Object::FKConstraint->throw(
1755 error => 'Broken FK constraint',
1756 broken_fk => $+{column}
1761 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1763 Koha::Exceptions::Object::DuplicateID->throw(
1764 error => 'Duplicate ID',
1765 duplicate_id => $+{key}
1768 elsif ( $_->{msg} =~
1769 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1771 { # The optional \W in the regex might be a quote or backtick
1772 my $type = $+{type};
1773 my $value = $+{value};
1774 my $property = $+{property};
1775 $property =~ s/['`]//g;
1776 Koha::Exceptions::Object::BadValue->throw(
1779 property => $property =~ /(\w+\.\w+)$/
1782 , # results in table.column without quotes or backtics
1786 # Catch-all for foreign key breakages. It will help find other use cases
1795 =head3 remove_from_bundle
1797 Remove this item from any bundle it may have been attached to.
1801 sub remove_from_bundle {
1804 my $bundle_item_rs = $self->_result->item_bundles_item;
1805 if ( $bundle_item_rs ) {
1806 $bundle_item_rs->delete;
1807 $self->notforloan(0)->store();
1813 =head2 Internal methods
1815 =head3 _after_item_action_hooks
1817 Helper method that takes care of calling all plugin hooks
1821 sub _after_item_action_hooks {
1822 my ( $self, $params ) = @_;
1824 my $action = $params->{action};
1826 Koha::Plugins->call(
1827 'after_item_action',
1831 item_id => $self->itemnumber,
1838 my $recall = $item->recall;
1840 Return the relevant recall for this item
1846 my @recalls = Koha::Recalls->search(
1848 biblio_id => $self->biblionumber,
1851 { order_by => { -asc => 'created_date' } }
1853 foreach my $recall (@recalls) {
1854 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1858 # no item-level recall to return, so return earliest biblio-level
1859 # FIXME: eventually this will be based on priority
1863 =head3 can_be_recalled
1865 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1867 Does item-level checks and returns if items can be recalled by this borrower
1871 sub can_be_recalled {
1872 my ( $self, $params ) = @_;
1874 return 0 if !( C4::Context->preference('UseRecalls') );
1876 # check if this item is not for loan, withdrawn or lost
1877 return 0 if ( $self->notforloan != 0 );
1878 return 0 if ( $self->itemlost != 0 );
1879 return 0 if ( $self->withdrawn != 0 );
1881 # check if this item is not checked out - if not checked out, can't be recalled
1882 return 0 if ( !defined( $self->checkout ) );
1884 my $patron = $params->{patron};
1886 my $branchcode = C4::Context->userenv->{'branch'};
1888 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1891 # Check the circulation rule for each relevant itemtype for this item
1892 my $rule = Koha::CirculationRules->get_effective_rules({
1893 branchcode => $branchcode,
1894 categorycode => $patron ? $patron->categorycode : undef,
1895 itemtype => $self->effective_itemtype,
1898 'recalls_per_record',
1903 # check recalls allowed has been set and is not zero
1904 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1907 # check borrower has not reached open recalls allowed limit
1908 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1910 # check borrower has not reach open recalls allowed per record limit
1911 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1913 # check if this patron has already recalled this item
1914 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1916 # check if this patron has already checked out this item
1917 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1919 # check if this patron has already reserved this item
1920 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1923 # check item availability
1924 # items are unavailable for recall if they are lost, withdrawn or notforloan
1925 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1927 # if there are no available items at all, no recall can be placed
1928 return 0 if ( scalar @items == 0 );
1930 my $checked_out_count = 0;
1932 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1935 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1936 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1938 # can't recall if no items have been checked out
1939 return 0 if ( $checked_out_count == 0 );
1945 =head3 can_be_waiting_recall
1947 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1949 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1950 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1954 sub can_be_waiting_recall {
1957 return 0 if !( C4::Context->preference('UseRecalls') );
1959 # check if this item is not for loan, withdrawn or lost
1960 return 0 if ( $self->notforloan != 0 );
1961 return 0 if ( $self->itemlost != 0 );
1962 return 0 if ( $self->withdrawn != 0 );
1964 my $branchcode = $self->holdingbranch;
1965 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1966 $branchcode = C4::Context->userenv->{'branch'};
1968 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1971 # Check the circulation rule for each relevant itemtype for this item
1972 my $rule = Koha::CirculationRules->get_effective_rules({
1973 branchcode => $branchcode,
1974 categorycode => $self->check_recalls ? $self->check_recalls->patron->categorycode : undef,
1975 itemtype => $self->effective_itemtype,
1981 # check recalls allowed has been set and is not zero
1982 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1988 =head3 check_recalls
1990 my $recall = $item->check_recalls;
1992 Get the most relevant recall for this item.
1999 my @recalls = Koha::Recalls->search(
2000 { biblio_id => $self->biblionumber,
2001 item_id => [ $self->itemnumber, undef ]
2003 { order_by => { -asc => 'created_date' } }
2004 )->filter_by_current->as_list;
2007 # iterate through relevant recalls to find the best one.
2008 # if we come across a waiting recall, use this one.
2009 # 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.
2010 foreach my $r ( @recalls ) {
2011 if ( $r->waiting ) {
2016 unless ( defined $recall ) {
2017 $recall = $recalls[0];
2023 =head3 is_notforloan
2025 my $is_notforloan = $item->is_notforloan;
2027 Determine whether or not this item is "notforloan" based on
2028 the item's notforloan status or its item type
2034 my $is_notforloan = 0;
2036 if ( $self->notforloan ){
2040 my $itemtype = $self->itemtype;
2042 if ( $itemtype->notforloan ){
2048 return $is_notforloan;
2051 =head3 is_denied_renewal
2053 my $is_denied_renewal = $item->is_denied_renewal;
2055 Determine whether or not this item can be renewed based on the
2056 rules set in the ItemsDeniedRenewal system preference.
2060 sub is_denied_renewal {
2063 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
2064 return 0 unless $denyingrules;
2065 foreach my $field (keys %$denyingrules) {
2066 my $val = $self->$field;
2067 if( !defined $val) {
2068 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2071 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2072 # If the results matches the values in the syspref
2073 # We return true if match found
2082 Returns a map of column name to string representations including the string,
2083 the mapping type and the mapping category where appropriate.
2085 Currently handles authorised value mappings, library, callnumber and itemtype
2088 Accepts a param hashref where the 'public' key denotes whether we want the public
2089 or staff client strings.
2094 my ( $self, $params ) = @_;
2096 my $columns_info = $self->_result->result_source->columns_info;
2097 my $frameworkcode = $self->biblio->frameworkcode;
2098 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
2099 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2101 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2103 # Hardcoded known 'authorised_value' values mapped to API codes
2104 my $code_to_type = {
2105 branches => 'library',
2106 cn_source => 'call_number_source',
2107 itemtypes => 'item_type',
2110 # Handle not null and default values for integers and dates
2113 foreach my $col ( keys %{$columns_info} ) {
2115 # By now, we are done with known columns, now check the framework for mappings
2116 my $field = $self->_result->result_source->name . '.' . $col;
2118 # Check there's an entry in the MARC subfield structure for the field
2119 if ( exists $mss->{$field}
2120 && scalar @{ $mss->{$field} } > 0
2121 && $mss->{$field}[0]->{authorised_value} )
2123 my $subfield = $mss->{$field}[0];
2124 my $code = $subfield->{authorised_value};
2126 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2127 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2128 $strings->{$col} = {
2131 ( $type eq 'av' ? ( category => $code ) : () ),
2149 Kyle M Hall <kyle@bywatersolutions.com>