3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
58 use base qw(Koha::Object);
62 Koha::Item - Koha Item object class
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
82 my $params = @_ ? shift : {};
84 my $log_action = $params->{log_action} // 1;
86 # We do not want to oblige callers to pass this value
87 # Dev conveniences vs performance?
88 unless ( $self->biblioitemnumber ) {
89 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
92 # See related changes from C4::Items::AddItem
93 unless ( $self->itype ) {
94 $self->itype($self->biblio->biblioitem->itemtype);
97 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
99 my $today = dt_from_string;
100 my $action = 'create';
102 unless ( $self->in_storage ) { #AddItem
104 unless ( $self->permanent_location ) {
105 $self->permanent_location($self->location);
108 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109 unless ( $self->location || !$default_location ) {
110 $self->permanent_location( $self->location || $default_location )
111 unless $self->permanent_location;
112 $self->location($default_location);
115 unless ( $self->replacementpricedate ) {
116 $self->replacementpricedate($today);
118 unless ( $self->datelastseen ) {
119 $self->datelastseen($today);
122 unless ( $self->dateaccessioned ) {
123 $self->dateaccessioned($today);
126 if ( $self->itemcallnumber
127 or $self->cn_source )
129 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130 $self->cn_sort($cn_sort);
137 my %updated_columns = $self->_result->get_dirty_columns;
138 return $self->SUPER::store unless %updated_columns;
140 # Retrieve the item for comparison if we need to
142 exists $updated_columns{itemlost}
143 or exists $updated_columns{withdrawn}
144 or exists $updated_columns{damaged}
145 ) ? $self->get_from_storage : undef;
147 # Update *_on fields if needed
148 # FIXME: Why not for AddItem as well?
149 my @fields = qw( itemlost withdrawn damaged );
150 for my $field (@fields) {
152 # If the field is defined but empty or 0, we are
153 # removing/unsetting and thus need to clear out
155 if ( exists $updated_columns{$field}
156 && defined( $self->$field )
159 my $field_on = "${field}_on";
160 $self->$field_on(undef);
162 # If the field has changed otherwise, we much update
164 elsif (exists $updated_columns{$field}
165 && $updated_columns{$field}
166 && !$pre_mod_item->$field )
168 my $field_on = "${field}_on";
169 $self->$field_on(dt_from_string);
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
210 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
212 biblio_ids => [ $self->biblionumber ]
214 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
225 my $params = @_ ? shift : {};
227 # FIXME check the item has no current issues
228 # i.e. raise the appropriate exception
230 # Get the item group so we can delete it later if it has no items left
231 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
233 my $result = $self->SUPER::delete;
235 # Delete the item gorup if it has no items left
236 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
251 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error //= "not_same_branch"
296 if defined C4::Context->userenv
297 and defined C4::Context->userenv->{number}
298 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
300 # check it doesn't have a waiting reserve
301 $error //= "book_reserved"
302 if $self->holds->filter_by_found->count;
304 $error //= "linked_analytics"
305 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
307 $error //= "last_item_for_hold"
308 if $self->biblio->items->count == 1
309 && $self->biblio->holds->search(
316 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
319 return Koha::Result::Boolean->new(1);
322 =head3 move_to_deleted
324 my $is_moved = $item->move_to_deleted;
326 Move an item to the deleteditems table.
327 This can be done before deleting an item, to make sure the data are not completely deleted.
331 sub move_to_deleted {
333 my $item_infos = $self->unblessed;
334 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
335 $item_infos->{deleted_on} = dt_from_string;
336 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
340 =head3 effective_itemtype
342 Returns the itemtype for the item based on whether item level itemtypes are set or not.
346 sub effective_itemtype {
349 return $self->_result()->effective_itemtype();
359 my $hb_rs = $self->_result->homebranch;
361 return Koha::Library->_new_from_dbic( $hb_rs );
364 =head3 holding_branch
371 my $hb_rs = $self->_result->holdingbranch;
373 return Koha::Library->_new_from_dbic( $hb_rs );
378 my $biblio = $item->biblio;
380 Return the bibliographic record of this item
386 my $biblio_rs = $self->_result->biblio;
387 return Koha::Biblio->_new_from_dbic( $biblio_rs );
392 my $biblioitem = $item->biblioitem;
394 Return the biblioitem record of this item
400 my $biblioitem_rs = $self->_result->biblioitem;
401 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
406 my $checkout = $item->checkout;
408 Return the checkout for this item
414 my $checkout_rs = $self->_result->issue;
415 return unless $checkout_rs;
416 return Koha::Checkout->_new_from_dbic( $checkout_rs );
421 my $item_group = $item->item_group;
423 Return the item group for this item
430 my $item_group_item = $self->_result->item_group_item;
431 return unless $item_group_item;
433 my $item_group_rs = $item_group_item->item_group;
434 return unless $item_group_rs;
436 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
442 my $return_claims = $item->return_claims;
444 Return any return_claims associated with this item
449 my ( $self, $params, $attrs ) = @_;
450 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
451 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
456 my $return_claim = $item->return_claim;
458 Returns the most recent unresolved return_claims associated with this item
465 $self->_result->return_claims->search( { resolution => undef },
466 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
467 return unless $claims_rs;
468 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
473 my $holds = $item->holds();
474 my $holds = $item->holds($params);
475 my $holds = $item->holds({ found => 'W'});
477 Return holds attached to an item, optionally accept a hashref of params to pass to search
482 my ( $self,$params ) = @_;
483 my $holds_rs = $self->_result->reserves->search($params);
484 return Koha::Holds->_new_from_dbic( $holds_rs );
487 =head3 request_transfer
489 my $transfer = $item->request_transfer(
493 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
497 Add a transfer request for this item to the given branch for the given reason.
499 An exception will be thrown if the BranchTransferLimits would prevent the requested
500 transfer, unless 'ignore_limits' is passed to override the limits.
502 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
503 The caller should catch such cases and retry the transfer request as appropriate passing
504 an appropriate override.
507 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
508 * replace - Used to replace the existing transfer request with your own.
512 sub request_transfer {
513 my ( $self, $params ) = @_;
515 # check for mandatory params
516 my @mandatory = ( 'to', 'reason' );
517 for my $param (@mandatory) {
518 unless ( defined( $params->{$param} ) ) {
519 Koha::Exceptions::MissingParameter->throw(
520 error => "The $param parameter is mandatory" );
524 Koha::Exceptions::Item::Transfer::Limit->throw()
525 unless ( $params->{ignore_limits}
526 || $self->can_be_transferred( { to => $params->{to} } ) );
528 my $request = $self->get_transfer;
529 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
530 if ( $request && !$params->{enqueue} && !$params->{replace} );
532 $request->cancel( { reason => $params->{reason}, force => 1 } )
533 if ( defined($request) && $params->{replace} );
535 my $transfer = Koha::Item::Transfer->new(
537 itemnumber => $self->itemnumber,
538 daterequested => dt_from_string,
539 frombranch => $self->holdingbranch,
540 tobranch => $params->{to}->branchcode,
541 reason => $params->{reason},
542 comments => $params->{comment}
551 my $transfer = $item->get_transfer;
553 Return the active transfer request or undef
555 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
556 whereby the most recently sent, but not received, transfer will be returned
557 if it exists, otherwise the oldest unsatisfied transfer will be returned.
559 This allows for transfers to queue, which is the case for stock rotation and
560 rotating collections where a manual transfer may need to take precedence but
561 we still expect the item to end up at a final location eventually.
568 my $transfer = $self->_result->current_branchtransfers->next;
569 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
574 my $transfer = $item->get_transfers;
576 Return the list of outstanding transfers (i.e requested but not yet cancelled
579 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
580 whereby the most recently sent, but not received, transfer will be returned
581 first if it exists, otherwise requests are in oldest to newest request order.
583 This allows for transfers to queue, which is the case for stock rotation and
584 rotating collections where a manual transfer may need to take precedence but
585 we still expect the item to end up at a final location eventually.
592 my $transfer_rs = $self->_result->current_branchtransfers;
594 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
597 =head3 last_returned_by
599 Gets and sets the last borrower to return an item.
601 Accepts and returns Koha::Patron objects
603 $item->last_returned_by( $borrowernumber );
605 $last_returned_by = $item->last_returned_by();
609 sub last_returned_by {
610 my ( $self, $borrower ) = @_;
612 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
615 return $items_last_returned_by_rs->update_or_create(
616 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
619 unless ( $self->{_last_returned_by} ) {
620 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
622 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
626 return $self->{_last_returned_by};
630 =head3 can_article_request
632 my $bool = $item->can_article_request( $borrower )
634 Returns true if item can be specifically requested
636 $borrower must be a Koha::Patron object
640 sub can_article_request {
641 my ( $self, $borrower ) = @_;
643 my $rule = $self->article_request_type($borrower);
645 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
649 =head3 hidden_in_opac
651 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
653 Returns true if item fields match the hidding criteria defined in $rules.
654 Returns false otherwise.
656 Takes HASHref that can have the following parameters:
658 $rules : { <field> => [ value_1, ... ], ... }
660 Note: $rules inherits its structure from the parsed YAML from reading
661 the I<OpacHiddenItems> system preference.
666 my ( $self, $params ) = @_;
668 my $rules = $params->{rules} // {};
671 if C4::Context->preference('hidelostitems') and
674 my $hidden_in_opac = 0;
676 foreach my $field ( keys %{$rules} ) {
678 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
684 return $hidden_in_opac;
687 =head3 can_be_transferred
689 $item->can_be_transferred({ to => $to_library, from => $from_library })
690 Checks if an item can be transferred to given library.
692 This feature is controlled by two system preferences:
693 UseBranchTransferLimits to enable / disable the feature
694 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
695 for setting the limitations
697 Takes HASHref that can have the following parameters:
698 MANDATORY PARAMETERS:
701 $from : Koha::Library # if not given, item holdingbranch
702 # will be used instead
704 Returns 1 if item can be transferred to $to_library, otherwise 0.
706 To find out whether at least one item of a Koha::Biblio can be transferred, please
707 see Koha::Biblio->can_be_transferred() instead of using this method for
708 multiple items of the same biblio.
712 sub can_be_transferred {
713 my ($self, $params) = @_;
715 my $to = $params->{to};
716 my $from = $params->{from};
718 $to = $to->branchcode;
719 $from = defined $from ? $from->branchcode : $self->holdingbranch;
721 return 1 if $from eq $to; # Transfer to current branch is allowed
722 return 1 unless C4::Context->preference('UseBranchTransferLimits');
724 my $limittype = C4::Context->preference('BranchTransferLimitsType');
725 return Koha::Item::Transfer::Limits->search({
728 $limittype => $limittype eq 'itemtype'
729 ? $self->effective_itemtype : $self->ccode
734 =head3 pickup_locations
736 my $pickup_locations = $item->pickup_locations({ patron => $patron })
738 Returns possible pickup locations for this item, according to patron's home library
739 and if item can be transferred to each pickup location.
741 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
746 sub pickup_locations {
747 my ($self, $params) = @_;
749 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
750 unless exists $params->{patron};
752 my $patron = $params->{patron};
754 my $circ_control_branch =
755 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
757 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
759 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
760 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 {
1052 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1053 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1054 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1056 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1059 for my $column ( @{$self->_columns}) {
1061 next if $column eq 'more_subfields_xml';
1063 my $value = $self->$column;
1064 # 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
1066 if ( not defined $value or $value eq "" ) {
1067 $values->{$column} = $value;
1072 exists $mss->{"items.$column"}
1073 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1076 $values->{$column} =
1078 ? $subfield->{authorised_value}
1079 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1080 $subfield->{tagsubfield}, $value, '', $tagslib )
1086 $self->more_subfields_xml
1087 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1092 my ( $field ) = $marc_more->fields;
1093 for my $sf ( $field->subfields ) {
1094 my $subfield_code = $sf->[0];
1095 my $value = $sf->[1];
1096 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1097 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1099 $subfield->{authorised_value}
1100 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1101 $subfield->{tagsubfield}, $value, '', $tagslib )
1104 push @{$more_values->{$subfield_code}}, $value;
1107 while ( my ( $k, $v ) = each %$more_values ) {
1108 $values->{$k} = join ' | ', @$v;
1115 =head3 additional_attributes
1117 my $attributes = $item->additional_attributes;
1118 $attributes->{k} = 'new k';
1119 $item->update({ more_subfields => $attributes->to_marcxml });
1121 Returns a Koha::Item::Attributes object that represents the non-mapped
1122 attributes for this item.
1126 sub additional_attributes {
1129 return Koha::Item::Attributes->new_from_marcxml(
1130 $self->more_subfields_xml,
1134 =head3 _set_found_trigger
1136 $self->_set_found_trigger
1138 Finds the most recent lost item charge for this item and refunds the patron
1139 appropriately, taking into account any payments or writeoffs already applied
1142 Internal function, not exported, called only by Koha::Item->store.
1146 sub _set_found_trigger {
1147 my ( $self, $pre_mod_item ) = @_;
1149 # Reverse any lost item charges if necessary.
1150 my $no_refund_after_days =
1151 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1152 if ($no_refund_after_days) {
1153 my $today = dt_from_string();
1154 my $lost_age_in_days =
1155 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1158 return $self unless $lost_age_in_days < $no_refund_after_days;
1161 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1164 return_branch => C4::Context->userenv
1165 ? C4::Context->userenv->{'branch'}
1169 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1171 if ( $lostreturn_policy ) {
1173 # refund charge made for lost book
1174 my $lost_charge = Koha::Account::Lines->search(
1176 itemnumber => $self->itemnumber,
1177 debit_type_code => 'LOST',
1178 status => [ undef, { '<>' => 'FOUND' } ]
1181 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1186 if ( $lost_charge ) {
1188 my $patron = $lost_charge->patron;
1191 my $account = $patron->account;
1193 # Credit outstanding amount
1194 my $credit_total = $lost_charge->amountoutstanding;
1198 $lost_charge->amount > $lost_charge->amountoutstanding &&
1199 $lostreturn_policy ne "refund_unpaid"
1201 # some amount has been cancelled. collect the offsets that are not writeoffs
1202 # this works because the only way to subtract from this kind of a debt is
1203 # using the UI buttons 'Pay' and 'Write off'
1205 # We don't credit any payments if return policy is
1208 # In that case only unpaid/outstanding amount
1209 # will be credited which settles the debt without
1210 # creating extra credits
1212 my $credit_offsets = $lost_charge->debit_offsets(
1214 'credit_id' => { '!=' => undef },
1215 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1217 { join => 'credit' }
1220 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1221 # credits are negative on the DB
1222 $credit_offsets->total * -1 :
1224 # Credit the outstanding amount, then add what has been
1225 # paid to create a net credit for this amount
1226 $credit_total += $total_to_refund;
1230 if ( $credit_total > 0 ) {
1232 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1233 $credit = $account->add_credit(
1235 amount => $credit_total,
1236 description => 'Item found ' . $self->itemnumber,
1237 type => 'LOST_FOUND',
1238 interface => C4::Context->interface,
1239 library_id => $branchcode,
1240 item_id => $self->itemnumber,
1241 issue_id => $lost_charge->issue_id
1245 $credit->apply( { debits => [$lost_charge] } );
1249 message => 'lost_refunded',
1250 payload => { credit_id => $credit->id }
1255 # Update the account status
1256 $lost_charge->status('FOUND');
1257 $lost_charge->store();
1259 # Reconcile balances if required
1260 if ( C4::Context->preference('AccountAutoReconcile') ) {
1261 $account->reconcile_balance;
1266 # possibly restore fine for lost book
1267 my $lost_overdue = Koha::Account::Lines->search(
1269 itemnumber => $self->itemnumber,
1270 debit_type_code => 'OVERDUE',
1274 order_by => { '-desc' => 'date' },
1278 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1280 my $patron = $lost_overdue->patron;
1282 my $account = $patron->account;
1284 # Update status of fine
1285 $lost_overdue->status('FOUND')->store();
1287 # Find related forgive credit
1288 my $refund = $lost_overdue->credits(
1290 credit_type_code => 'FORGIVEN',
1291 itemnumber => $self->itemnumber,
1292 status => [ { '!=' => 'VOID' }, undef ]
1294 { order_by => { '-desc' => 'date' }, rows => 1 }
1298 # Revert the forgive credit
1299 $refund->void({ interface => 'trigger' });
1303 message => 'lost_restored',
1304 payload => { refund_id => $refund->id }
1309 # Reconcile balances if required
1310 if ( C4::Context->preference('AccountAutoReconcile') ) {
1311 $account->reconcile_balance;
1315 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1319 message => 'lost_charge',
1325 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1327 if ( $processingreturn_policy ) {
1329 # refund processing charge made for lost book
1330 my $processing_charge = Koha::Account::Lines->search(
1332 itemnumber => $self->itemnumber,
1333 debit_type_code => 'PROCESSING',
1334 status => [ undef, { '<>' => 'FOUND' } ]
1337 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1342 if ( $processing_charge ) {
1344 my $patron = $processing_charge->patron;
1347 my $account = $patron->account;
1349 # Credit outstanding amount
1350 my $credit_total = $processing_charge->amountoutstanding;
1354 $processing_charge->amount > $processing_charge->amountoutstanding &&
1355 $processingreturn_policy ne "refund_unpaid"
1357 # some amount has been cancelled. collect the offsets that are not writeoffs
1358 # this works because the only way to subtract from this kind of a debt is
1359 # using the UI buttons 'Pay' and 'Write off'
1361 # We don't credit any payments if return policy is
1364 # In that case only unpaid/outstanding amount
1365 # will be credited which settles the debt without
1366 # creating extra credits
1368 my $credit_offsets = $processing_charge->debit_offsets(
1370 'credit_id' => { '!=' => undef },
1371 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1373 { join => 'credit' }
1376 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1377 # credits are negative on the DB
1378 $credit_offsets->total * -1 :
1380 # Credit the outstanding amount, then add what has been
1381 # paid to create a net credit for this amount
1382 $credit_total += $total_to_refund;
1386 if ( $credit_total > 0 ) {
1388 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1389 $credit = $account->add_credit(
1391 amount => $credit_total,
1392 description => 'Item found ' . $self->itemnumber,
1393 type => 'PROCESSING_FOUND',
1394 interface => C4::Context->interface,
1395 library_id => $branchcode,
1396 item_id => $self->itemnumber,
1397 issue_id => $processing_charge->issue_id
1401 $credit->apply( { debits => [$processing_charge] } );
1405 message => 'processing_refunded',
1406 payload => { credit_id => $credit->id }
1411 # Update the account status
1412 $processing_charge->status('FOUND');
1413 $processing_charge->store();
1415 # Reconcile balances if required
1416 if ( C4::Context->preference('AccountAutoReconcile') ) {
1417 $account->reconcile_balance;
1426 =head3 public_read_list
1428 This method returns the list of publicly readable database fields for both API and UI output purposes
1432 sub public_read_list {
1434 'itemnumber', 'biblionumber', 'homebranch',
1435 'holdingbranch', 'location', 'collectioncode',
1436 'itemcallnumber', 'copynumber', 'enumchron',
1437 'barcode', 'dateaccessioned', 'itemnotes',
1438 'onloan', 'uri', 'itype',
1439 'notforloan', 'damaged', 'itemlost',
1440 'withdrawn', 'restricted'
1446 Overloaded to_api method to ensure item-level itypes is adhered to.
1451 my ($self, $params) = @_;
1453 my $response = $self->SUPER::to_api($params);
1456 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1457 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1459 return { %$response, %$overrides };
1462 =head3 to_api_mapping
1464 This method returns the mapping for representing a Koha::Item object
1469 sub to_api_mapping {
1471 itemnumber => 'item_id',
1472 biblionumber => 'biblio_id',
1473 biblioitemnumber => undef,
1474 barcode => 'external_id',
1475 dateaccessioned => 'acquisition_date',
1476 booksellerid => 'acquisition_source',
1477 homebranch => 'home_library_id',
1478 price => 'purchase_price',
1479 replacementprice => 'replacement_price',
1480 replacementpricedate => 'replacement_price_date',
1481 datelastborrowed => 'last_checkout_date',
1482 datelastseen => 'last_seen_date',
1484 notforloan => 'not_for_loan_status',
1485 damaged => 'damaged_status',
1486 damaged_on => 'damaged_date',
1487 itemlost => 'lost_status',
1488 itemlost_on => 'lost_date',
1489 withdrawn => 'withdrawn',
1490 withdrawn_on => 'withdrawn_date',
1491 itemcallnumber => 'callnumber',
1492 coded_location_qualifier => 'coded_location_qualifier',
1493 issues => 'checkouts_count',
1494 renewals => 'renewals_count',
1495 reserves => 'holds_count',
1496 restricted => 'restricted_status',
1497 itemnotes => 'public_notes',
1498 itemnotes_nonpublic => 'internal_notes',
1499 holdingbranch => 'holding_library_id',
1500 timestamp => 'timestamp',
1501 location => 'location',
1502 permanent_location => 'permanent_location',
1503 onloan => 'checked_out_date',
1504 cn_source => 'call_number_source',
1505 cn_sort => 'call_number_sort',
1506 ccode => 'collection_code',
1507 materials => 'materials_notes',
1509 itype => 'item_type_id',
1510 more_subfields_xml => 'extended_subfields',
1511 enumchron => 'serial_issue_number',
1512 copynumber => 'copy_number',
1513 stocknumber => 'inventory_number',
1514 new_status => 'new_status',
1515 deleted_on => undef,
1521 my $itemtype = $item->itemtype;
1523 Returns Koha object for effective itemtype
1530 return Koha::ItemTypes->find( $self->effective_itemtype );
1535 my $orders = $item->orders();
1537 Returns a Koha::Acquisition::Orders object
1544 my $orders = $self->_result->item_orders;
1545 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1548 =head3 tracked_links
1550 my $tracked_links = $item->tracked_links();
1552 Returns a Koha::TrackedLinks object
1559 my $tracked_links = $self->_result->linktrackers;
1560 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1563 =head3 move_to_biblio
1565 $item->move_to_biblio($to_biblio[, $params]);
1567 Move the item to another biblio and update any references in other tables.
1569 The final optional parameter, C<$params>, is expected to contain the
1570 'skip_record_index' key, which is relayed down to Koha::Item->store.
1571 There it prevents calling index_records, which takes most of the
1572 time in batch adds/deletes. The caller must take care of calling
1573 index_records separately.
1576 skip_record_index => 1|0
1578 Returns undef if the move failed or the biblionumber of the destination record otherwise
1582 sub move_to_biblio {
1583 my ( $self, $to_biblio, $params ) = @_;
1587 return if $self->biblionumber == $to_biblio->biblionumber;
1589 my $from_biblionumber = $self->biblionumber;
1590 my $to_biblionumber = $to_biblio->biblionumber;
1592 # Own biblionumber and biblioitemnumber
1594 biblionumber => $to_biblionumber,
1595 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1596 })->store({ skip_record_index => $params->{skip_record_index} });
1598 unless ($params->{skip_record_index}) {
1599 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1600 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1603 # Acquisition orders
1604 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1607 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1609 # hold_fill_target (there's no Koha object available yet)
1610 my $hold_fill_target = $self->_result->hold_fill_target;
1611 if ($hold_fill_target) {
1612 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1615 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1616 # and can't even fake one since the significant columns are nullable.
1617 my $storage = $self->_result->result_source->storage;
1620 my ($storage, $dbh, @cols) = @_;
1622 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1627 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1629 return $to_biblionumber;
1634 my $bundle_items = $item->bundle_items;
1636 Returns the items associated with this bundle
1643 if ( !$self->{_bundle_items_cached} ) {
1644 my $bundle_items = Koha::Items->search(
1645 { 'item_bundles_item.host' => $self->itemnumber },
1646 { join => 'item_bundles_item' } );
1647 $self->{_bundle_items} = $bundle_items;
1648 $self->{_bundle_items_cached} = 1;
1651 return $self->{_bundle_items};
1656 my $is_bundle = $item->is_bundle;
1658 Returns whether the item is a bundle or not
1664 return $self->bundle_items->count ? 1 : 0;
1669 my $bundle = $item->bundle_host;
1671 Returns the bundle item this item is attached to
1678 my $bundle_items_rs = $self->_result->item_bundles_item;
1679 return unless $bundle_items_rs;
1680 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1685 my $in_bundle = $item->in_bundle;
1687 Returns whether this item is currently in a bundle
1693 return $self->bundle_host ? 1 : 0;
1696 =head3 add_to_bundle
1698 my $link = $item->add_to_bundle($bundle_item);
1700 Adds the bundle_item passed to this item
1705 my ( $self, $bundle_item, $options ) = @_;
1709 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1710 if ( $self->itemnumber eq $bundle_item->itemnumber
1711 || $bundle_item->is_bundle
1712 || $self->in_bundle );
1714 my $schema = Koha::Database->new->schema;
1716 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1721 my $checkout = $bundle_item->checkout;
1723 unless ($options->{force_checkin}) {
1724 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1727 my $branchcode = C4::Context->userenv->{'branch'};
1728 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1730 Koha::Exceptions::Checkin::FailedCheckin->throw();
1734 my $holds = $bundle_item->current_holds;
1735 if ($holds->count) {
1736 unless ($options->{ignore_holds}) {
1737 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1741 $self->_result->add_to_item_bundles_hosts(
1742 { item => $bundle_item->itemnumber } );
1744 $bundle_item->notforloan($BundleNotLoanValue)->store();
1750 # 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
1751 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1752 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1754 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1755 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1756 Koha::Exceptions::Object::FKConstraint->throw(
1757 error => 'Broken FK constraint',
1758 broken_fk => $+{column}
1763 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1765 Koha::Exceptions::Object::DuplicateID->throw(
1766 error => 'Duplicate ID',
1767 duplicate_id => $+{key}
1770 elsif ( $_->{msg} =~
1771 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1773 { # The optional \W in the regex might be a quote or backtick
1774 my $type = $+{type};
1775 my $value = $+{value};
1776 my $property = $+{property};
1777 $property =~ s/['`]//g;
1778 Koha::Exceptions::Object::BadValue->throw(
1781 property => $property =~ /(\w+\.\w+)$/
1784 , # results in table.column without quotes or backtics
1788 # Catch-all for foreign key breakages. It will help find other use cases
1797 =head3 remove_from_bundle
1799 Remove this item from any bundle it may have been attached to.
1803 sub remove_from_bundle {
1806 my $bundle_item_rs = $self->_result->item_bundles_item;
1807 if ( $bundle_item_rs ) {
1808 $bundle_item_rs->delete;
1809 $self->notforloan(0)->store();
1815 =head2 Internal methods
1817 =head3 _after_item_action_hooks
1819 Helper method that takes care of calling all plugin hooks
1823 sub _after_item_action_hooks {
1824 my ( $self, $params ) = @_;
1826 my $action = $params->{action};
1828 Koha::Plugins->call(
1829 'after_item_action',
1833 item_id => $self->itemnumber,
1840 my $recall = $item->recall;
1842 Return the relevant recall for this item
1848 my @recalls = Koha::Recalls->search(
1850 biblio_id => $self->biblionumber,
1853 { order_by => { -asc => 'created_date' } }
1855 foreach my $recall (@recalls) {
1856 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1860 # no item-level recall to return, so return earliest biblio-level
1861 # FIXME: eventually this will be based on priority
1865 =head3 can_be_recalled
1867 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1869 Does item-level checks and returns if items can be recalled by this borrower
1873 sub can_be_recalled {
1874 my ( $self, $params ) = @_;
1876 return 0 if !( C4::Context->preference('UseRecalls') );
1878 # check if this item is not for loan, withdrawn or lost
1879 return 0 if ( $self->notforloan != 0 );
1880 return 0 if ( $self->itemlost != 0 );
1881 return 0 if ( $self->withdrawn != 0 );
1883 # check if this item is not checked out - if not checked out, can't be recalled
1884 return 0 if ( !defined( $self->checkout ) );
1886 my $patron = $params->{patron};
1888 my $branchcode = C4::Context->userenv->{'branch'};
1890 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1893 # Check the circulation rule for each relevant itemtype for this item
1894 my $rule = Koha::CirculationRules->get_effective_rules({
1895 branchcode => $branchcode,
1896 categorycode => $patron ? $patron->categorycode : undef,
1897 itemtype => $self->effective_itemtype,
1900 'recalls_per_record',
1905 # check recalls allowed has been set and is not zero
1906 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1909 # check borrower has not reached open recalls allowed limit
1910 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1912 # check borrower has not reach open recalls allowed per record limit
1913 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1915 # check if this patron has already recalled this item
1916 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1918 # check if this patron has already checked out this item
1919 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1921 # check if this patron has already reserved this item
1922 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1925 # check item availability
1926 # items are unavailable for recall if they are lost, withdrawn or notforloan
1927 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1929 # if there are no available items at all, no recall can be placed
1930 return 0 if ( scalar @items == 0 );
1932 my $checked_out_count = 0;
1934 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1937 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1938 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1940 # can't recall if no items have been checked out
1941 return 0 if ( $checked_out_count == 0 );
1947 =head3 can_be_waiting_recall
1949 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1951 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1952 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1956 sub can_be_waiting_recall {
1959 return 0 if !( C4::Context->preference('UseRecalls') );
1961 # check if this item is not for loan, withdrawn or lost
1962 return 0 if ( $self->notforloan != 0 );
1963 return 0 if ( $self->itemlost != 0 );
1964 return 0 if ( $self->withdrawn != 0 );
1966 my $branchcode = $self->holdingbranch;
1967 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1968 $branchcode = C4::Context->userenv->{'branch'};
1970 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1973 # Check the circulation rule for each relevant itemtype for this item
1974 my $most_relevant_recall = $self->check_recalls;
1975 my $rule = Koha::CirculationRules->get_effective_rules(
1977 branchcode => $branchcode,
1978 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1979 itemtype => $self->effective_itemtype,
1980 rules => [ 'recalls_allowed', ],
1984 # check recalls allowed has been set and is not zero
1985 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1991 =head3 check_recalls
1993 my $recall = $item->check_recalls;
1995 Get the most relevant recall for this item.
2002 my @recalls = Koha::Recalls->search(
2003 { biblio_id => $self->biblionumber,
2004 item_id => [ $self->itemnumber, undef ]
2006 { order_by => { -asc => 'created_date' } }
2007 )->filter_by_current->as_list;
2010 # iterate through relevant recalls to find the best one.
2011 # if we come across a waiting recall, use this one.
2012 # 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.
2013 foreach my $r ( @recalls ) {
2014 if ( $r->waiting ) {
2019 unless ( defined $recall ) {
2020 $recall = $recalls[0];
2026 =head3 is_notforloan
2028 my $is_notforloan = $item->is_notforloan;
2030 Determine whether or not this item is "notforloan" based on
2031 the item's notforloan status or its item type
2037 my $is_notforloan = 0;
2039 if ( $self->notforloan ){
2043 my $itemtype = $self->itemtype;
2045 if ( $itemtype->notforloan ){
2051 return $is_notforloan;
2054 =head3 is_denied_renewal
2056 my $is_denied_renewal = $item->is_denied_renewal;
2058 Determine whether or not this item can be renewed based on the
2059 rules set in the ItemsDeniedRenewal system preference.
2063 sub is_denied_renewal {
2065 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2066 return 0 unless $denyingrules;
2067 foreach my $field (keys %$denyingrules) {
2068 # Silently ignore bad column names; TODO we should validate elsewhere
2069 next if !$self->_result->result_source->has_column($field);
2070 my $val = $self->$field;
2071 if( !defined $val) {
2072 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2075 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2076 # If the results matches the values in the syspref
2077 # We return true if match found
2086 Returns a map of column name to string representations including the string,
2087 the mapping type and the mapping category where appropriate.
2089 Currently handles authorised value mappings, library, callnumber and itemtype
2092 Accepts a param hashref where the 'public' key denotes whether we want the public
2093 or staff client strings.
2098 my ( $self, $params ) = @_;
2099 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2100 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2101 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2103 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2105 # Hardcoded known 'authorised_value' values mapped to API codes
2106 my $code_to_type = {
2107 branches => 'library',
2108 cn_source => 'call_number_source',
2109 itemtypes => 'item_type',
2112 # Handle not null and default values for integers and dates
2115 foreach my $col ( @{$self->_columns} ) {
2117 # By now, we are done with known columns, now check the framework for mappings
2118 my $field = $self->_result->result_source->name . '.' . $col;
2120 # Check there's an entry in the MARC subfield structure for the field
2121 if ( exists $mss->{$field}
2122 && scalar @{ $mss->{$field} } > 0
2123 && $mss->{$field}[0]->{authorised_value} )
2125 my $subfield = $mss->{$field}[0];
2126 my $code = $subfield->{authorised_value};
2128 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2129 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2130 $strings->{$col} = {
2133 ( $type eq 'av' ? ( category => $code ) : () ),
2151 Kyle M Hall <kyle@bywatersolutions.com>