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;
57 use Koha::Policy::Holds;
59 use base qw(Koha::Object);
63 Koha::Item - Koha Item object class
75 $params can take an optional 'skip_record_index' parameter.
76 If set, the reindexation process will not happen (index_records not called)
77 You should not turn it on if you do not understand what it is doing exactly.
83 my $params = @_ ? shift : {};
85 my $log_action = $params->{log_action} // 1;
87 # We do not want to oblige callers to pass this value
88 # Dev conveniences vs performance?
89 unless ( $self->biblioitemnumber ) {
90 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
93 # See related changes from C4::Items::AddItem
94 unless ( $self->itype ) {
95 $self->itype($self->biblio->biblioitem->itemtype);
98 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
100 my $today = dt_from_string;
101 my $action = 'create';
103 unless ( $self->in_storage ) { #AddItem
105 unless ( $self->permanent_location ) {
106 $self->permanent_location($self->location);
109 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
110 unless ( $self->location || !$default_location ) {
111 $self->permanent_location( $self->location || $default_location )
112 unless $self->permanent_location;
113 $self->location($default_location);
116 unless ( $self->replacementpricedate ) {
117 $self->replacementpricedate($today);
119 unless ( $self->datelastseen ) {
120 $self->datelastseen($today);
123 unless ( $self->dateaccessioned ) {
124 $self->dateaccessioned($today);
127 if ( $self->itemcallnumber
128 or $self->cn_source )
130 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
131 $self->cn_sort($cn_sort);
134 # should be quite rare when adding item
135 if ( $self->itemlost && $self->itemlost > 0 ) { # TODO BZ34308
136 $self->_add_statistic('item_lost');
143 my %updated_columns = $self->_result->get_dirty_columns;
144 return $self->SUPER::store unless %updated_columns;
146 # Retrieve the item for comparison if we need to
148 exists $updated_columns{itemlost}
149 or exists $updated_columns{withdrawn}
150 or exists $updated_columns{damaged}
151 ) ? $self->get_from_storage : undef;
153 # Update *_on fields if needed
154 # FIXME: Why not for AddItem as well?
155 my @fields = qw( itemlost withdrawn damaged );
156 for my $field (@fields) {
158 # If the field is defined but empty or 0, we are
159 # removing/unsetting and thus need to clear out
161 if ( exists $updated_columns{$field}
162 && defined( $self->$field )
165 my $field_on = "${field}_on";
166 $self->$field_on(undef);
168 # If the field has changed otherwise, we much update
170 elsif (exists $updated_columns{$field}
171 && $updated_columns{$field}
172 && !$pre_mod_item->$field )
174 my $field_on = "${field}_on";
175 $self->$field_on(dt_from_string);
179 if ( exists $updated_columns{itemcallnumber}
180 or exists $updated_columns{cn_source} )
182 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
183 $self->cn_sort($cn_sort);
187 if ( exists $updated_columns{location}
188 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
189 and not exists $updated_columns{permanent_location} )
191 $self->permanent_location( $self->location );
194 # TODO BZ 34308 (gt zero checks)
195 if ( exists $updated_columns{itemlost}
196 && ( !$updated_columns{itemlost} || $updated_columns{itemlost} <= 0 )
197 && ( $pre_mod_item->itemlost && $pre_mod_item->itemlost > 0 ) )
200 # reverse any list item charges if necessary
201 $self->_set_found_trigger($pre_mod_item);
202 $self->_add_statistic('item_found');
203 } elsif ( exists $updated_columns{itemlost}
204 && ( $updated_columns{itemlost} && $updated_columns{itemlost} > 0 )
205 && ( !$pre_mod_item->itemlost || $pre_mod_item->itemlost <= 0 ) )
208 $self->_add_statistic('item_lost');
212 my $result = $self->SUPER::store;
213 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
215 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
216 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
218 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
219 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
220 unless $params->{skip_record_index};
221 $self->get_from_storage->_after_item_action_hooks({ action => $action });
223 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
225 biblio_ids => [ $self->biblionumber ]
227 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
233 my ( $self, $type ) = @_;
234 C4::Stats::UpdateStats(
236 borrowernumber => undef,
237 branch => C4::Context->userenv ? C4::Context->userenv->{branch} : undef,
238 categorycode => undef,
239 ccode => $self->ccode,
240 itemnumber => $self->itemnumber,
241 itemtype => $self->effective_itemtype,
242 location => $self->location,
254 my $params = @_ ? shift : {};
256 # FIXME check the item has no current issues
257 # i.e. raise the appropriate exception
259 # Get the item group so we can delete it later if it has no items left
260 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
262 my $result = $self->SUPER::delete;
264 # Delete the item group if it has no items left
265 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
267 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
268 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
269 unless $params->{skip_record_index};
271 $self->_after_item_action_hooks({ action => 'delete' });
273 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
274 if C4::Context->preference("CataloguingLog");
276 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
278 biblio_ids => [ $self->biblionumber ]
280 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
291 my $params = @_ ? shift : {};
293 my $safe_to_delete = $self->safe_to_delete;
294 return $safe_to_delete unless $safe_to_delete;
296 $self->move_to_deleted;
298 return $self->delete($params);
301 =head3 safe_to_delete
303 returns 1 if the item is safe to delete,
305 "book_on_loan" if the item is checked out,
307 "not_same_branch" if the item is blocked by independent branches,
309 "book_reserved" if the there are holds aganst the item, or
311 "linked_analytics" if the item has linked analytic records.
313 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
322 $error = "book_on_loan" if $self->checkout;
324 $error //= "not_same_branch"
325 if defined C4::Context->userenv
326 and defined C4::Context->userenv->{number}
327 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
329 # check it doesn't have a waiting reserve
330 $error //= "book_reserved"
331 if $self->holds->filter_by_found->count;
333 $error //= "linked_analytics"
334 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
336 $error //= "last_item_for_hold"
337 if $self->biblio->items->count == 1
338 && $self->biblio->holds->search(
345 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
348 return Koha::Result::Boolean->new(1);
351 =head3 move_to_deleted
353 my $is_moved = $item->move_to_deleted;
355 Move an item to the deleteditems table.
356 This can be done before deleting an item, to make sure the data are not completely deleted.
360 sub move_to_deleted {
362 my $item_infos = $self->unblessed;
363 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
364 $item_infos->{deleted_on} = dt_from_string;
365 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
368 =head3 effective_itemtype
370 Returns the itemtype for the item based on whether item level itemtypes are set or not.
374 sub effective_itemtype {
377 return $self->_result()->effective_itemtype();
387 my $hb_rs = $self->_result->homebranch;
389 return Koha::Library->_new_from_dbic( $hb_rs );
392 =head3 holding_branch
399 my $hb_rs = $self->_result->holdingbranch;
401 return Koha::Library->_new_from_dbic( $hb_rs );
406 my $biblio = $item->biblio;
408 Return the bibliographic record of this item
414 my $biblio_rs = $self->_result->biblio;
415 return Koha::Biblio->_new_from_dbic( $biblio_rs );
420 my $biblioitem = $item->biblioitem;
422 Return the biblioitem record of this item
428 my $biblioitem_rs = $self->_result->biblioitem;
429 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
434 my $checkout = $item->checkout;
436 Return the checkout for this item
442 my $checkout_rs = $self->_result->issue;
443 return unless $checkout_rs;
444 return Koha::Checkout->_new_from_dbic( $checkout_rs );
449 my $item_group = $item->item_group;
451 Return the item group for this item
458 my $item_group_item = $self->_result->item_group_item;
459 return unless $item_group_item;
461 my $item_group_rs = $item_group_item->item_group;
462 return unless $item_group_rs;
464 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
470 my $return_claims = $item->return_claims;
472 Return any return_claims associated with this item
477 my ( $self, $params, $attrs ) = @_;
478 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
479 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
484 my $return_claim = $item->return_claim;
486 Returns the most recent unresolved return_claims associated with this item
493 $self->_result->return_claims->search( { resolution => undef },
494 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
495 return unless $claims_rs;
496 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
501 my $holds = $item->holds();
502 my $holds = $item->holds($params);
503 my $holds = $item->holds({ found => 'W'});
505 Return holds attached to an item, optionally accept a hashref of params to pass to search
510 my ( $self,$params ) = @_;
511 my $holds_rs = $self->_result->reserves->search($params);
512 return Koha::Holds->_new_from_dbic( $holds_rs );
515 =head3 request_transfer
517 my $transfer = $item->request_transfer(
521 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
525 Add a transfer request for this item to the given branch for the given reason.
527 An exception will be thrown if the BranchTransferLimits would prevent the requested
528 transfer, unless 'ignore_limits' is passed to override the limits.
530 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
531 The caller should catch such cases and retry the transfer request as appropriate passing
532 an appropriate override.
535 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
536 * replace - Used to replace the existing transfer request with your own.
540 sub request_transfer {
541 my ( $self, $params ) = @_;
543 # check for mandatory params
544 my @mandatory = ( 'to', 'reason' );
545 for my $param (@mandatory) {
546 unless ( defined( $params->{$param} ) ) {
547 Koha::Exceptions::MissingParameter->throw(
548 error => "The $param parameter is mandatory" );
552 Koha::Exceptions::Item::Transfer::Limit->throw()
553 unless ( $params->{ignore_limits}
554 || $self->can_be_transferred( { to => $params->{to} } ) );
556 my $request = $self->get_transfer;
557 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
558 if ( $request && !$params->{enqueue} && !$params->{replace} );
560 $request->cancel( { reason => $params->{reason}, force => 1 } )
561 if ( defined($request) && $params->{replace} );
563 my $transfer = Koha::Item::Transfer->new(
565 itemnumber => $self->itemnumber,
566 daterequested => dt_from_string,
567 frombranch => $self->holdingbranch,
568 tobranch => $params->{to}->branchcode,
569 reason => $params->{reason},
570 comments => $params->{comment}
579 my $transfer = $item->get_transfer;
581 Return the active transfer request or undef
583 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
584 whereby the most recently sent, but not received, transfer will be returned
585 if it exists, otherwise the oldest unsatisfied transfer will be returned.
587 This allows for transfers to queue, which is the case for stock rotation and
588 rotating collections where a manual transfer may need to take precedence but
589 we still expect the item to end up at a final location eventually.
596 my $transfer = $self->_result->current_branchtransfers->next;
597 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
602 my $transfer = $item->get_transfers;
604 Return the list of outstanding transfers (i.e requested but not yet cancelled
607 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
608 whereby the most recently sent, but not received, transfer will be returned
609 first if it exists, otherwise requests are in oldest to newest request order.
611 This allows for transfers to queue, which is the case for stock rotation and
612 rotating collections where a manual transfer may need to take precedence but
613 we still expect the item to end up at a final location eventually.
620 my $transfer_rs = $self->_result->current_branchtransfers;
622 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
625 =head3 last_returned_by
627 Gets and sets the last patron to return an item.
629 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
631 $item->last_returned_by( $borrowernumber );
633 my $patron = $item->last_returned_by();
637 sub last_returned_by {
638 my ( $self, $borrowernumber ) = @_;
639 if ( $borrowernumber ) {
640 $self->_result->update_or_create_related('last_returned_by',
641 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
643 my $rs = $self->_result->last_returned_by;
645 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
648 =head3 can_article_request
650 my $bool = $item->can_article_request( $borrower )
652 Returns true if item can be specifically requested
654 $borrower must be a Koha::Patron object
658 sub can_article_request {
659 my ( $self, $borrower ) = @_;
661 my $rule = $self->article_request_type($borrower);
663 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
667 =head3 hidden_in_opac
669 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
671 Returns true if item fields match the hidding criteria defined in $rules.
672 Returns false otherwise.
674 Takes HASHref that can have the following parameters:
676 $rules : { <field> => [ value_1, ... ], ... }
678 Note: $rules inherits its structure from the parsed YAML from reading
679 the I<OpacHiddenItems> system preference.
684 my ( $self, $params ) = @_;
686 my $rules = $params->{rules} // {};
689 if C4::Context->preference('hidelostitems') and
692 my $hidden_in_opac = 0;
694 foreach my $field ( keys %{$rules} ) {
696 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
702 return $hidden_in_opac;
705 =head3 can_be_transferred
707 $item->can_be_transferred({ to => $to_library, from => $from_library })
708 Checks if an item can be transferred to given library.
710 This feature is controlled by two system preferences:
711 UseBranchTransferLimits to enable / disable the feature
712 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
713 for setting the limitations
715 Takes HASHref that can have the following parameters:
716 MANDATORY PARAMETERS:
719 $from : Koha::Library # if not given, item holdingbranch
720 # will be used instead
722 Returns 1 if item can be transferred to $to_library, otherwise 0.
724 To find out whether at least one item of a Koha::Biblio can be transferred, please
725 see Koha::Biblio->can_be_transferred() instead of using this method for
726 multiple items of the same biblio.
730 sub can_be_transferred {
731 my ($self, $params) = @_;
733 my $to = $params->{to};
734 my $from = $params->{from};
736 $to = $to->branchcode;
737 $from = defined $from ? $from->branchcode : $self->holdingbranch;
739 return 1 if $from eq $to; # Transfer to current branch is allowed
740 return 1 unless C4::Context->preference('UseBranchTransferLimits');
742 my $limittype = C4::Context->preference('BranchTransferLimitsType');
743 return Koha::Item::Transfer::Limits->search({
746 $limittype => $limittype eq 'itemtype'
747 ? $self->effective_itemtype : $self->ccode
752 =head3 pickup_locations
754 my $pickup_locations = $item->pickup_locations({ patron => $patron })
756 Returns possible pickup locations for this item, according to patron's home library
757 and if item can be transferred to each pickup location.
759 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
764 sub pickup_locations {
765 my ($self, $params) = @_;
767 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
768 unless exists $params->{patron};
770 my $patron = $params->{patron};
772 my $circ_control_branch = Koha::Policy::Holds->holds_control_library( $self, $patron );
774 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
776 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
777 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
779 my $pickup_libraries = Koha::Libraries->search();
780 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
781 $pickup_libraries = $self->home_branch->get_hold_libraries;
782 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
783 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
784 $pickup_libraries = $plib->get_hold_libraries;
785 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
786 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
787 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
788 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
791 return $pickup_libraries->search(
796 order_by => ['branchname']
798 ) unless C4::Context->preference('UseBranchTransferLimits');
800 my $limittype = C4::Context->preference('BranchTransferLimitsType');
801 my ($ccode, $itype) = (undef, undef);
802 if( $limittype eq 'ccode' ){
803 $ccode = $self->ccode;
805 $itype = $self->itype;
807 my $limits = Koha::Item::Transfer::Limits->search(
809 fromBranch => $self->holdingbranch,
813 { columns => ['toBranch'] }
816 return $pickup_libraries->search(
818 pickup_location => 1,
820 '-not_in' => $limits->_resultset->as_query
824 order_by => ['branchname']
829 =head3 article_request_type
831 my $type = $item->article_request_type( $borrower )
833 returns 'yes', 'no', 'bib_only', or 'item_only'
835 $borrower must be a Koha::Patron object
839 sub article_request_type {
840 my ( $self, $borrower ) = @_;
842 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
844 $branch_control eq 'homebranch' ? $self->homebranch
845 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
847 my $borrowertype = $borrower->categorycode;
848 my $itemtype = $self->effective_itemtype();
849 my $rule = Koha::CirculationRules->get_effective_rule(
851 rule_name => 'article_requests',
852 categorycode => $borrowertype,
853 itemtype => $itemtype,
854 branchcode => $branchcode
858 return q{} unless $rule;
859 return $rule->rule_value || q{}
868 my $attributes = { order_by => 'priority' };
869 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
871 itemnumber => $self->itemnumber,
874 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
875 waitingdate => { '!=' => undef },
878 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
879 return Koha::Holds->_new_from_dbic($hold_rs);
882 =head3 stockrotationitem
884 my $sritem = Koha::Item->stockrotationitem;
886 Returns the stock rotation item associated with the current item.
890 sub stockrotationitem {
892 my $rs = $self->_result->stockrotationitem;
894 return Koha::StockRotationItem->_new_from_dbic( $rs );
899 my $item = $item->add_to_rota($rota_id);
901 Add this item to the rota identified by $ROTA_ID, which means associating it
902 with the first stage of that rota. Should this item already be associated
903 with a rota, then we will move it to the new rota.
908 my ( $self, $rota_id ) = @_;
909 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
913 =head3 has_pending_hold
915 my $is_pending_hold = $item->has_pending_hold();
917 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
921 sub has_pending_hold {
923 return $self->_result->tmp_holdsqueue ? 1 : 0;
926 =head3 has_pending_recall {
928 my $has_pending_recall
930 Return if whether has pending recall of not.
934 sub has_pending_recall {
937 # FIXME Must be moved to $self->recalls
938 return Koha::Recalls->search(
940 item_id => $self->itemnumber,
948 my $field = $item->as_marc_field;
950 This method returns a MARC::Field object representing the Koha::Item object
951 with the current mappings configuration.
958 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
960 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
964 my $item_field = $tagslib->{$itemtag};
966 my $more_subfields = $self->additional_attributes->to_hashref;
967 foreach my $subfield (
969 $a->{display_order} <=> $b->{display_order}
970 || $a->{subfield} cmp $b->{subfield}
971 } grep { ref($_) && %$_ } values %$item_field
974 my $kohafield = $subfield->{kohafield};
975 my $tagsubfield = $subfield->{tagsubfield};
977 if ( defined $kohafield && $kohafield ne '' ) {
978 next if $kohafield !~ m{^items\.}; # That would be weird!
979 ( my $attribute = $kohafield ) =~ s|^items\.||;
980 $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
981 if defined $self->$attribute and $self->$attribute ne '';
983 $value = $more_subfields->{$tagsubfield}
986 next unless defined $value
989 if ( $subfield->{repeatable} ) {
990 my @values = split '\|', $value;
991 push @subfields, ( $tagsubfield => $_ ) for @values;
994 push @subfields, ( $tagsubfield => $value );
999 return unless @subfields;
1001 return MARC::Field->new(
1002 "$itemtag", ' ', ' ', @subfields
1006 =head3 renewal_branchcode
1008 Returns the branchcode to be recorded in statistics renewal of the item
1012 sub renewal_branchcode {
1014 my ($self, $params ) = @_;
1016 my $interface = C4::Context->interface;
1018 if ( $interface eq 'opac' ){
1019 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1020 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1021 $branchcode = 'OPACRenew';
1023 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1024 $branchcode = $self->homebranch;
1026 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1027 $branchcode = $self->checkout->patron->branchcode;
1029 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1030 $branchcode = $self->checkout->branchcode;
1036 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1037 ? C4::Context->userenv->{branch} : $params->{branch};
1044 Return the cover images associated with this item.
1051 my $cover_image_rs = $self->_result->cover_images;
1052 return unless $cover_image_rs;
1053 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1056 =head3 columns_to_str
1058 my $values = $items->columns_to_str;
1060 Return a hashref with the string representation of the different attribute of the item.
1062 This is meant to be used for display purpose only.
1066 sub columns_to_str {
1068 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1069 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1070 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1072 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1075 for my $column ( @{$self->_columns}) {
1077 next if $column eq 'more_subfields_xml';
1079 my $value = $self->$column;
1080 # 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
1082 if ( not defined $value or $value eq "" ) {
1083 $values->{$column} = $value;
1088 exists $mss->{"items.$column"}
1089 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1092 $values->{$column} =
1094 ? $subfield->{authorised_value}
1095 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1096 $subfield->{tagsubfield}, $value, '', $tagslib )
1102 $self->more_subfields_xml
1103 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1108 my ( $field ) = $marc_more->fields;
1109 for my $sf ( $field->subfields ) {
1110 my $subfield_code = $sf->[0];
1111 my $value = $sf->[1];
1112 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1113 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1115 $subfield->{authorised_value}
1116 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1117 $subfield->{tagsubfield}, $value, '', $tagslib )
1120 push @{$more_values->{$subfield_code}}, $value;
1123 while ( my ( $k, $v ) = each %$more_values ) {
1124 $values->{$k} = join ' | ', @$v;
1131 =head3 additional_attributes
1133 my $attributes = $item->additional_attributes;
1134 $attributes->{k} = 'new k';
1135 $item->update({ more_subfields => $attributes->to_marcxml });
1137 Returns a Koha::Item::Attributes object that represents the non-mapped
1138 attributes for this item.
1142 sub additional_attributes {
1145 return Koha::Item::Attributes->new_from_marcxml(
1146 $self->more_subfields_xml,
1150 =head3 _set_found_trigger
1152 $self->_set_found_trigger
1154 Finds the most recent lost item charge for this item and refunds the patron
1155 appropriately, taking into account any payments or writeoffs already applied
1158 Internal function, not exported, called only by Koha::Item->store.
1162 sub _set_found_trigger {
1163 my ( $self, $pre_mod_item ) = @_;
1165 # Reverse any lost item charges if necessary.
1166 my $no_refund_after_days =
1167 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1168 if ($no_refund_after_days) {
1169 my $today = dt_from_string();
1170 my $lost_age_in_days =
1171 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1174 return $self unless $lost_age_in_days < $no_refund_after_days;
1177 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1180 return_branch => C4::Context->userenv
1181 ? C4::Context->userenv->{'branch'}
1185 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1187 if ( $lostreturn_policy ) {
1189 # refund charge made for lost book
1190 my $lost_charge = Koha::Account::Lines->search(
1192 itemnumber => $self->itemnumber,
1193 debit_type_code => 'LOST',
1194 status => [ undef, { '<>' => 'FOUND' } ]
1197 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1202 if ( $lost_charge ) {
1204 my $patron = $lost_charge->patron;
1207 my $account = $patron->account;
1209 # Credit outstanding amount
1210 my $credit_total = $lost_charge->amountoutstanding;
1214 $lost_charge->amount > $lost_charge->amountoutstanding &&
1215 $lostreturn_policy ne "refund_unpaid"
1217 # some amount has been cancelled. collect the offsets that are not writeoffs
1218 # this works because the only way to subtract from this kind of a debt is
1219 # using the UI buttons 'Pay' and 'Write off'
1221 # We don't credit any payments if return policy is
1224 # In that case only unpaid/outstanding amount
1225 # will be credited which settles the debt without
1226 # creating extra credits
1228 my $credit_offsets = $lost_charge->debit_offsets(
1230 'credit_id' => { '!=' => undef },
1231 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1233 { join => 'credit' }
1236 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1237 # credits are negative on the DB
1238 $credit_offsets->total * -1 :
1240 # Credit the outstanding amount, then add what has been
1241 # paid to create a net credit for this amount
1242 $credit_total += $total_to_refund;
1246 if ( $credit_total > 0 ) {
1248 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1249 $credit = $account->add_credit(
1251 amount => $credit_total,
1252 description => 'Item found ' . $self->itemnumber,
1253 type => 'LOST_FOUND',
1254 interface => C4::Context->interface,
1255 library_id => $branchcode,
1256 item_id => $self->itemnumber,
1257 issue_id => $lost_charge->issue_id
1261 $credit->apply( { debits => [$lost_charge] } );
1265 message => 'lost_refunded',
1266 payload => { credit_id => $credit->id }
1271 # Update the account status
1272 $lost_charge->status('FOUND');
1273 $lost_charge->store();
1275 # Reconcile balances if required
1276 if ( C4::Context->preference('AccountAutoReconcile') ) {
1277 $account->reconcile_balance;
1282 # possibly restore fine for lost book
1283 my $lost_overdue = Koha::Account::Lines->search(
1285 itemnumber => $self->itemnumber,
1286 debit_type_code => 'OVERDUE',
1290 order_by => { '-desc' => 'date' },
1294 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1296 my $patron = $lost_overdue->patron;
1298 my $account = $patron->account;
1300 # Update status of fine
1301 $lost_overdue->status('FOUND')->store();
1303 # Find related forgive credit
1304 my $refund = $lost_overdue->credits(
1306 credit_type_code => 'FORGIVEN',
1307 itemnumber => $self->itemnumber,
1308 status => [ { '!=' => 'VOID' }, undef ]
1310 { order_by => { '-desc' => 'date' }, rows => 1 }
1314 # Revert the forgive credit
1315 $refund->void({ interface => 'trigger' });
1319 message => 'lost_restored',
1320 payload => { refund_id => $refund->id }
1325 # Reconcile balances if required
1326 if ( C4::Context->preference('AccountAutoReconcile') ) {
1327 $account->reconcile_balance;
1331 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1335 message => 'lost_charge',
1341 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1343 if ( $processingreturn_policy ) {
1345 # refund processing charge made for lost book
1346 my $processing_charge = Koha::Account::Lines->search(
1348 itemnumber => $self->itemnumber,
1349 debit_type_code => 'PROCESSING',
1350 status => [ undef, { '<>' => 'FOUND' } ]
1353 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1358 if ( $processing_charge ) {
1360 my $patron = $processing_charge->patron;
1363 my $account = $patron->account;
1365 # Credit outstanding amount
1366 my $credit_total = $processing_charge->amountoutstanding;
1370 $processing_charge->amount > $processing_charge->amountoutstanding &&
1371 $processingreturn_policy ne "refund_unpaid"
1373 # some amount has been cancelled. collect the offsets that are not writeoffs
1374 # this works because the only way to subtract from this kind of a debt is
1375 # using the UI buttons 'Pay' and 'Write off'
1377 # We don't credit any payments if return policy is
1380 # In that case only unpaid/outstanding amount
1381 # will be credited which settles the debt without
1382 # creating extra credits
1384 my $credit_offsets = $processing_charge->debit_offsets(
1386 'credit_id' => { '!=' => undef },
1387 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1389 { join => 'credit' }
1392 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1393 # credits are negative on the DB
1394 $credit_offsets->total * -1 :
1396 # Credit the outstanding amount, then add what has been
1397 # paid to create a net credit for this amount
1398 $credit_total += $total_to_refund;
1402 if ( $credit_total > 0 ) {
1404 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1405 $credit = $account->add_credit(
1407 amount => $credit_total,
1408 description => 'Item found ' . $self->itemnumber,
1409 type => 'PROCESSING_FOUND',
1410 interface => C4::Context->interface,
1411 library_id => $branchcode,
1412 item_id => $self->itemnumber,
1413 issue_id => $processing_charge->issue_id
1417 $credit->apply( { debits => [$processing_charge] } );
1421 message => 'processing_refunded',
1422 payload => { credit_id => $credit->id }
1427 # Update the account status
1428 $processing_charge->status('FOUND');
1429 $processing_charge->store();
1431 # Reconcile balances if required
1432 if ( C4::Context->preference('AccountAutoReconcile') ) {
1433 $account->reconcile_balance;
1442 =head3 public_read_list
1444 This method returns the list of publicly readable database fields for both API and UI output purposes
1448 sub public_read_list {
1450 'itemnumber', 'biblionumber', 'homebranch',
1451 'holdingbranch', 'location', 'collectioncode',
1452 'itemcallnumber', 'copynumber', 'enumchron',
1453 'barcode', 'dateaccessioned', 'itemnotes',
1454 'onloan', 'uri', 'itype',
1455 'notforloan', 'damaged', 'itemlost',
1456 'withdrawn', 'restricted'
1462 Overloaded to_api method to ensure item-level itypes is adhered to.
1467 my ($self, $params) = @_;
1469 my $response = $self->SUPER::to_api($params);
1472 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1474 my $itype_notforloan = $self->itemtype->notforloan;
1475 $overrides->{effective_not_for_loan_status} =
1476 ( defined $itype_notforloan && !$self->notforloan ) ? $itype_notforloan : $self->notforloan;
1478 return { %$response, %$overrides };
1481 =head3 to_api_mapping
1483 This method returns the mapping for representing a Koha::Item object
1488 sub to_api_mapping {
1490 itemnumber => 'item_id',
1491 biblionumber => 'biblio_id',
1492 biblioitemnumber => undef,
1493 barcode => 'external_id',
1494 dateaccessioned => 'acquisition_date',
1495 booksellerid => 'acquisition_source',
1496 homebranch => 'home_library_id',
1497 price => 'purchase_price',
1498 replacementprice => 'replacement_price',
1499 replacementpricedate => 'replacement_price_date',
1500 datelastborrowed => 'last_checkout_date',
1501 datelastseen => 'last_seen_date',
1503 notforloan => 'not_for_loan_status',
1504 damaged => 'damaged_status',
1505 damaged_on => 'damaged_date',
1506 itemlost => 'lost_status',
1507 itemlost_on => 'lost_date',
1508 withdrawn => 'withdrawn',
1509 withdrawn_on => 'withdrawn_date',
1510 itemcallnumber => 'callnumber',
1511 coded_location_qualifier => 'coded_location_qualifier',
1512 issues => 'checkouts_count',
1513 renewals => 'renewals_count',
1514 reserves => 'holds_count',
1515 restricted => 'restricted_status',
1516 itemnotes => 'public_notes',
1517 itemnotes_nonpublic => 'internal_notes',
1518 holdingbranch => 'holding_library_id',
1519 timestamp => 'timestamp',
1520 location => 'location',
1521 permanent_location => 'permanent_location',
1522 onloan => 'checked_out_date',
1523 cn_source => 'call_number_source',
1524 cn_sort => 'call_number_sort',
1525 ccode => 'collection_code',
1526 materials => 'materials_notes',
1528 itype => 'item_type_id',
1529 more_subfields_xml => 'extended_subfields',
1530 enumchron => 'serial_issue_number',
1531 copynumber => 'copy_number',
1532 stocknumber => 'inventory_number',
1533 new_status => 'new_status',
1534 deleted_on => undef,
1540 my $itemtype = $item->itemtype;
1542 Returns Koha object for effective itemtype
1549 return Koha::ItemTypes->find( $self->effective_itemtype );
1554 my $orders = $item->orders();
1556 Returns a Koha::Acquisition::Orders object
1563 my $orders = $self->_result->item_orders;
1564 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1567 =head3 tracked_links
1569 my $tracked_links = $item->tracked_links();
1571 Returns a Koha::TrackedLinks object
1578 my $tracked_links = $self->_result->linktrackers;
1579 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1582 =head3 move_to_biblio
1584 $item->move_to_biblio($to_biblio[, $params]);
1586 Move the item to another biblio and update any references in other tables.
1588 The final optional parameter, C<$params>, is expected to contain the
1589 'skip_record_index' key, which is relayed down to Koha::Item->store.
1590 There it prevents calling index_records, which takes most of the
1591 time in batch adds/deletes. The caller must take care of calling
1592 index_records separately.
1595 skip_record_index => 1|0
1597 Returns undef if the move failed or the biblionumber of the destination record otherwise
1601 sub move_to_biblio {
1602 my ( $self, $to_biblio, $params ) = @_;
1606 return if $self->biblionumber == $to_biblio->biblionumber;
1608 my $from_biblionumber = $self->biblionumber;
1609 my $to_biblionumber = $to_biblio->biblionumber;
1611 # Own biblionumber and biblioitemnumber
1613 biblionumber => $to_biblionumber,
1614 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1615 })->store({ skip_record_index => $params->{skip_record_index} });
1617 unless ($params->{skip_record_index}) {
1618 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1619 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1622 # Acquisition orders
1623 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1626 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1628 # hold_fill_target (there's no Koha object available yet)
1629 my $hold_fill_target = $self->_result->hold_fill_target;
1630 if ($hold_fill_target) {
1631 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1634 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1635 # and can't even fake one since the significant columns are nullable.
1636 my $storage = $self->_result->result_source->storage;
1639 my ($storage, $dbh, @cols) = @_;
1641 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1646 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1648 return $to_biblionumber;
1653 my $bundle_items = $item->bundle_items;
1655 Returns the items associated with this bundle
1662 my $rs = $self->_result->bundle_items;
1663 return Koha::Items->_new_from_dbic($rs);
1668 my $is_bundle = $item->is_bundle;
1670 Returns whether the item is a bundle or not
1676 return $self->bundle_items->count ? 1 : 0;
1681 my $bundle = $item->bundle_host;
1683 Returns the bundle item this item is attached to
1690 my $bundle_items_rs = $self->_result->item_bundles_item;
1691 return unless $bundle_items_rs;
1692 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1697 my $in_bundle = $item->in_bundle;
1699 Returns whether this item is currently in a bundle
1705 return $self->bundle_host ? 1 : 0;
1708 =head3 add_to_bundle
1710 my $link = $item->add_to_bundle($bundle_item);
1712 Adds the bundle_item passed to this item
1717 my ( $self, $bundle_item, $options ) = @_;
1721 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1722 if ( $self->itemnumber eq $bundle_item->itemnumber
1723 || $bundle_item->is_bundle
1724 || $self->in_bundle );
1726 my $schema = Koha::Database->new->schema;
1728 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1734 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
1736 my $checkout = $bundle_item->checkout;
1738 unless ($options->{force_checkin}) {
1739 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1742 my $branchcode = C4::Context->userenv->{'branch'};
1743 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1745 Koha::Exceptions::Checkin::FailedCheckin->throw();
1749 my $holds = $bundle_item->current_holds;
1750 if ($holds->count) {
1751 unless ($options->{ignore_holds}) {
1752 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1756 $self->_result->add_to_item_bundles_hosts(
1757 { item => $bundle_item->itemnumber } );
1759 $bundle_item->notforloan($BundleNotLoanValue)->store();
1765 # 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
1766 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1767 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1769 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1770 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1771 Koha::Exceptions::Object::FKConstraint->throw(
1772 error => 'Broken FK constraint',
1773 broken_fk => $+{column}
1778 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1780 Koha::Exceptions::Object::DuplicateID->throw(
1781 error => 'Duplicate ID',
1782 duplicate_id => $+{key}
1785 elsif ( $_->{msg} =~
1786 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1788 { # The optional \W in the regex might be a quote or backtick
1789 my $type = $+{type};
1790 my $value = $+{value};
1791 my $property = $+{property};
1792 $property =~ s/['`]//g;
1793 Koha::Exceptions::Object::BadValue->throw(
1796 property => $property =~ /(\w+\.\w+)$/
1799 , # results in table.column without quotes or backtics
1803 # Catch-all for foreign key breakages. It will help find other use cases
1812 =head3 remove_from_bundle
1814 Remove this item from any bundle it may have been attached to.
1818 sub remove_from_bundle {
1821 my $bundle_host = $self->bundle_host;
1823 return 0 unless $bundle_host; # Should not we raise an exception here?
1825 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $bundle_host->checkout;
1827 my $bundle_item_rs = $self->_result->item_bundles_item;
1828 if ( $bundle_item_rs ) {
1829 $bundle_item_rs->delete;
1830 $self->notforloan(0)->store();
1836 =head2 Internal methods
1838 =head3 _after_item_action_hooks
1840 Helper method that takes care of calling all plugin hooks
1844 sub _after_item_action_hooks {
1845 my ( $self, $params ) = @_;
1847 my $action = $params->{action};
1849 Koha::Plugins->call(
1850 'after_item_action',
1854 item_id => $self->itemnumber,
1861 my $recall = $item->recall;
1863 Return the relevant recall for this item
1869 my @recalls = Koha::Recalls->search(
1871 biblio_id => $self->biblionumber,
1874 { order_by => { -asc => 'created_date' } }
1877 my $item_level_recall;
1878 foreach my $recall (@recalls) {
1879 if ( $recall->item_level ) {
1880 $item_level_recall = 1;
1881 if ( $recall->item_id == $self->itemnumber ) {
1886 if ($item_level_recall) {
1888 # recall needs to be filled be a specific item only
1889 # no other item is relevant to return
1893 # no item-level recall to return, so return earliest biblio-level
1894 # FIXME: eventually this will be based on priority
1898 =head3 can_be_recalled
1900 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1902 Does item-level checks and returns if items can be recalled by this borrower
1906 sub can_be_recalled {
1907 my ( $self, $params ) = @_;
1909 return 0 if !( C4::Context->preference('UseRecalls') );
1911 # check if this item is not for loan, withdrawn or lost
1912 return 0 if ( $self->notforloan != 0 );
1913 return 0 if ( $self->itemlost != 0 );
1914 return 0 if ( $self->withdrawn != 0 );
1916 # check if this item is not checked out - if not checked out, can't be recalled
1917 return 0 if ( !defined( $self->checkout ) );
1919 my $patron = $params->{patron};
1921 my $branchcode = C4::Context->userenv->{'branch'};
1923 $branchcode = C4::Circulation::_GetCircControlBranch( $self, $patron );
1926 # Check the circulation rule for each relevant itemtype for this item
1927 my $rule = Koha::CirculationRules->get_effective_rules({
1928 branchcode => $branchcode,
1929 categorycode => $patron ? $patron->categorycode : undef,
1930 itemtype => $self->effective_itemtype,
1933 'recalls_per_record',
1938 # check recalls allowed has been set and is not zero
1939 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1942 # check borrower has not reached open recalls allowed limit
1943 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1945 # check borrower has not reach open recalls allowed per record limit
1946 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1948 # check if this patron has already recalled this item
1949 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1951 # check if this patron has already checked out this item
1952 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1954 # check if this patron has already reserved this item
1955 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1958 # check item availability
1959 # items are unavailable for recall if they are lost, withdrawn or notforloan
1960 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1962 # if there are no available items at all, no recall can be placed
1963 return 0 if ( scalar @items == 0 );
1965 my $checked_out_count = 0;
1967 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1970 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1971 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1973 # can't recall if no items have been checked out
1974 return 0 if ( $checked_out_count == 0 );
1980 =head3 can_be_waiting_recall
1982 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1984 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1985 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1989 sub can_be_waiting_recall {
1992 return 0 if !( C4::Context->preference('UseRecalls') );
1994 # check if this item is not for loan, withdrawn or lost
1995 return 0 if ( $self->notforloan != 0 );
1996 return 0 if ( $self->itemlost != 0 );
1997 return 0 if ( $self->withdrawn != 0 );
1999 my $branchcode = $self->holdingbranch;
2000 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
2001 $branchcode = C4::Context->userenv->{'branch'};
2003 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
2006 # Check the circulation rule for each relevant itemtype for this item
2007 my $most_relevant_recall = $self->check_recalls;
2008 my $rule = Koha::CirculationRules->get_effective_rules(
2010 branchcode => $branchcode,
2011 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
2012 itemtype => $self->effective_itemtype,
2013 rules => [ 'recalls_allowed', ],
2017 # check recalls allowed has been set and is not zero
2018 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
2024 =head3 check_recalls
2026 my $recall = $item->check_recalls;
2028 Get the most relevant recall for this item.
2035 my @recalls = Koha::Recalls->search(
2036 { biblio_id => $self->biblionumber,
2037 item_id => [ $self->itemnumber, undef ]
2039 { order_by => { -asc => 'created_date' } }
2040 )->filter_by_current->as_list;
2043 # iterate through relevant recalls to find the best one.
2044 # if we come across a waiting recall, use this one.
2045 # 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.
2046 foreach my $r ( @recalls ) {
2047 if ( $r->waiting ) {
2052 unless ( defined $recall ) {
2053 $recall = $recalls[0];
2059 =head3 is_notforloan
2061 my $is_notforloan = $item->is_notforloan;
2063 Determine whether or not this item is "notforloan" based on
2064 the item's notforloan status or its item type
2070 my $is_notforloan = 0;
2072 if ( $self->notforloan ){
2076 my $itemtype = $self->itemtype;
2078 if ( $itemtype->notforloan ){
2084 return $is_notforloan;
2087 =head3 is_denied_renewal
2089 my $is_denied_renewal = $item->is_denied_renewal;
2091 Determine whether or not this item can be renewed based on the
2092 rules set in the ItemsDeniedRenewal system preference.
2096 sub is_denied_renewal {
2098 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2099 return 0 unless $denyingrules;
2100 foreach my $field (keys %$denyingrules) {
2101 # Silently ignore bad column names; TODO we should validate elsewhere
2102 next if !$self->_result->result_source->has_column($field);
2103 my $val = $self->$field;
2104 if( !defined $val) {
2105 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2108 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2109 # If the results matches the values in the syspref
2110 # We return true if match found
2119 Returns a map of column name to string representations including the string,
2120 the mapping type and the mapping category where appropriate.
2122 Currently handles authorised value mappings, library, callnumber and itemtype
2125 Accepts a param hashref where the 'public' key denotes whether we want the public
2126 or staff client strings.
2131 my ( $self, $params ) = @_;
2132 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2133 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2134 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2136 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2138 # Hardcoded known 'authorised_value' values mapped to API codes
2139 my $code_to_type = {
2140 branches => 'library',
2141 cn_source => 'call_number_source',
2142 itemtypes => 'item_type',
2145 # Handle not null and default values for integers and dates
2148 foreach my $col ( @{$self->_columns} ) {
2150 # By now, we are done with known columns, now check the framework for mappings
2151 my $field = $self->_result->result_source->name . '.' . $col;
2153 # Check there's an entry in the MARC subfield structure for the field
2154 if ( exists $mss->{$field}
2155 && scalar @{ $mss->{$field} } > 0
2156 && $mss->{$field}[0]->{authorised_value} )
2158 my $subfield = $mss->{$field}[0];
2159 my $code = $subfield->{authorised_value};
2161 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2162 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2163 $strings->{$col} = {
2166 ( $type eq 'av' ? ( category => $code ) : () ),
2184 Kyle M Hall <kyle@bywatersolutions.com>