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 branch => C4::Context->userenv ? C4::Context->userenv->{branch} : undef,
237 borrowernumber => undef,
238 categorycode => undef,
239 itemnumber => $self->itemnumber,
240 ccode => $self->ccode,
241 itemtype => $self->effective_itemtype,
242 location => $self->location,
252 my $params = @_ ? shift : {};
254 # FIXME check the item has no current issues
255 # i.e. raise the appropriate exception
257 # Get the item group so we can delete it later if it has no items left
258 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
260 my $result = $self->SUPER::delete;
262 # Delete the item group if it has no items left
263 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
265 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
266 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
267 unless $params->{skip_record_index};
269 $self->_after_item_action_hooks({ action => 'delete' });
271 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
272 if C4::Context->preference("CataloguingLog");
274 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
276 biblio_ids => [ $self->biblionumber ]
278 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
289 my $params = @_ ? shift : {};
291 my $safe_to_delete = $self->safe_to_delete;
292 return $safe_to_delete unless $safe_to_delete;
294 $self->move_to_deleted;
296 return $self->delete($params);
299 =head3 safe_to_delete
301 returns 1 if the item is safe to delete,
303 "book_on_loan" if the item is checked out,
305 "not_same_branch" if the item is blocked by independent branches,
307 "book_reserved" if the there are holds aganst the item, or
309 "linked_analytics" if the item has linked analytic records.
311 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
320 $error = "book_on_loan" if $self->checkout;
322 $error //= "not_same_branch"
323 if defined C4::Context->userenv
324 and defined C4::Context->userenv->{number}
325 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
327 # check it doesn't have a waiting reserve
328 $error //= "book_reserved"
329 if $self->holds->filter_by_found->count;
331 $error //= "linked_analytics"
332 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
334 $error //= "last_item_for_hold"
335 if $self->biblio->items->count == 1
336 && $self->biblio->holds->search(
343 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
346 return Koha::Result::Boolean->new(1);
349 =head3 move_to_deleted
351 my $is_moved = $item->move_to_deleted;
353 Move an item to the deleteditems table.
354 This can be done before deleting an item, to make sure the data are not completely deleted.
358 sub move_to_deleted {
360 my $item_infos = $self->unblessed;
361 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
362 $item_infos->{deleted_on} = dt_from_string;
363 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
366 =head3 effective_itemtype
368 Returns the itemtype for the item based on whether item level itemtypes are set or not.
372 sub effective_itemtype {
375 return $self->_result()->effective_itemtype();
385 my $hb_rs = $self->_result->homebranch;
387 return Koha::Library->_new_from_dbic( $hb_rs );
390 =head3 holding_branch
397 my $hb_rs = $self->_result->holdingbranch;
399 return Koha::Library->_new_from_dbic( $hb_rs );
404 my $biblio = $item->biblio;
406 Return the bibliographic record of this item
412 my $biblio_rs = $self->_result->biblio;
413 return Koha::Biblio->_new_from_dbic( $biblio_rs );
418 my $biblioitem = $item->biblioitem;
420 Return the biblioitem record of this item
426 my $biblioitem_rs = $self->_result->biblioitem;
427 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
432 my $checkout = $item->checkout;
434 Return the checkout for this item
440 my $checkout_rs = $self->_result->issue;
441 return unless $checkout_rs;
442 return Koha::Checkout->_new_from_dbic( $checkout_rs );
447 my $item_group = $item->item_group;
449 Return the item group for this item
456 my $item_group_item = $self->_result->item_group_item;
457 return unless $item_group_item;
459 my $item_group_rs = $item_group_item->item_group;
460 return unless $item_group_rs;
462 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
468 my $return_claims = $item->return_claims;
470 Return any return_claims associated with this item
475 my ( $self, $params, $attrs ) = @_;
476 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
477 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
482 my $return_claim = $item->return_claim;
484 Returns the most recent unresolved return_claims associated with this item
491 $self->_result->return_claims->search( { resolution => undef },
492 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
493 return unless $claims_rs;
494 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
499 my $holds = $item->holds();
500 my $holds = $item->holds($params);
501 my $holds = $item->holds({ found => 'W'});
503 Return holds attached to an item, optionally accept a hashref of params to pass to search
508 my ( $self,$params ) = @_;
509 my $holds_rs = $self->_result->reserves->search($params);
510 return Koha::Holds->_new_from_dbic( $holds_rs );
513 =head3 request_transfer
515 my $transfer = $item->request_transfer(
519 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
523 Add a transfer request for this item to the given branch for the given reason.
525 An exception will be thrown if the BranchTransferLimits would prevent the requested
526 transfer, unless 'ignore_limits' is passed to override the limits.
528 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
529 The caller should catch such cases and retry the transfer request as appropriate passing
530 an appropriate override.
533 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
534 * replace - Used to replace the existing transfer request with your own.
538 sub request_transfer {
539 my ( $self, $params ) = @_;
541 # check for mandatory params
542 my @mandatory = ( 'to', 'reason' );
543 for my $param (@mandatory) {
544 unless ( defined( $params->{$param} ) ) {
545 Koha::Exceptions::MissingParameter->throw(
546 error => "The $param parameter is mandatory" );
550 Koha::Exceptions::Item::Transfer::Limit->throw()
551 unless ( $params->{ignore_limits}
552 || $self->can_be_transferred( { to => $params->{to} } ) );
554 my $request = $self->get_transfer;
555 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
556 if ( $request && !$params->{enqueue} && !$params->{replace} );
558 $request->cancel( { reason => $params->{reason}, force => 1 } )
559 if ( defined($request) && $params->{replace} );
561 my $transfer = Koha::Item::Transfer->new(
563 itemnumber => $self->itemnumber,
564 daterequested => dt_from_string,
565 frombranch => $self->holdingbranch,
566 tobranch => $params->{to}->branchcode,
567 reason => $params->{reason},
568 comments => $params->{comment}
577 my $transfer = $item->get_transfer;
579 Return the active transfer request or undef
581 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
582 whereby the most recently sent, but not received, transfer will be returned
583 if it exists, otherwise the oldest unsatisfied transfer will be returned.
585 This allows for transfers to queue, which is the case for stock rotation and
586 rotating collections where a manual transfer may need to take precedence but
587 we still expect the item to end up at a final location eventually.
594 my $transfer = $self->_result->current_branchtransfers->next;
595 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
600 my $transfer = $item->get_transfers;
602 Return the list of outstanding transfers (i.e requested but not yet cancelled
605 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
606 whereby the most recently sent, but not received, transfer will be returned
607 first if it exists, otherwise requests are in oldest to newest request order.
609 This allows for transfers to queue, which is the case for stock rotation and
610 rotating collections where a manual transfer may need to take precedence but
611 we still expect the item to end up at a final location eventually.
618 my $transfer_rs = $self->_result->current_branchtransfers;
620 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
623 =head3 last_returned_by
625 Gets and sets the last patron to return an item.
627 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
629 $item->last_returned_by( $borrowernumber );
631 my $patron = $item->last_returned_by();
635 sub last_returned_by {
636 my ( $self, $borrowernumber ) = @_;
637 if ( $borrowernumber ) {
638 $self->_result->update_or_create_related('last_returned_by',
639 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
641 my $rs = $self->_result->last_returned_by;
643 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
646 =head3 can_article_request
648 my $bool = $item->can_article_request( $borrower )
650 Returns true if item can be specifically requested
652 $borrower must be a Koha::Patron object
656 sub can_article_request {
657 my ( $self, $borrower ) = @_;
659 my $rule = $self->article_request_type($borrower);
661 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
665 =head3 hidden_in_opac
667 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
669 Returns true if item fields match the hidding criteria defined in $rules.
670 Returns false otherwise.
672 Takes HASHref that can have the following parameters:
674 $rules : { <field> => [ value_1, ... ], ... }
676 Note: $rules inherits its structure from the parsed YAML from reading
677 the I<OpacHiddenItems> system preference.
682 my ( $self, $params ) = @_;
684 my $rules = $params->{rules} // {};
687 if C4::Context->preference('hidelostitems') and
690 my $hidden_in_opac = 0;
692 foreach my $field ( keys %{$rules} ) {
694 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
700 return $hidden_in_opac;
703 =head3 can_be_transferred
705 $item->can_be_transferred({ to => $to_library, from => $from_library })
706 Checks if an item can be transferred to given library.
708 This feature is controlled by two system preferences:
709 UseBranchTransferLimits to enable / disable the feature
710 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
711 for setting the limitations
713 Takes HASHref that can have the following parameters:
714 MANDATORY PARAMETERS:
717 $from : Koha::Library # if not given, item holdingbranch
718 # will be used instead
720 Returns 1 if item can be transferred to $to_library, otherwise 0.
722 To find out whether at least one item of a Koha::Biblio can be transferred, please
723 see Koha::Biblio->can_be_transferred() instead of using this method for
724 multiple items of the same biblio.
728 sub can_be_transferred {
729 my ($self, $params) = @_;
731 my $to = $params->{to};
732 my $from = $params->{from};
734 $to = $to->branchcode;
735 $from = defined $from ? $from->branchcode : $self->holdingbranch;
737 return 1 if $from eq $to; # Transfer to current branch is allowed
738 return 1 unless C4::Context->preference('UseBranchTransferLimits');
740 my $limittype = C4::Context->preference('BranchTransferLimitsType');
741 return Koha::Item::Transfer::Limits->search({
744 $limittype => $limittype eq 'itemtype'
745 ? $self->effective_itemtype : $self->ccode
750 =head3 pickup_locations
752 my $pickup_locations = $item->pickup_locations({ patron => $patron })
754 Returns possible pickup locations for this item, according to patron's home library
755 and if item can be transferred to each pickup location.
757 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
762 sub pickup_locations {
763 my ($self, $params) = @_;
765 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
766 unless exists $params->{patron};
768 my $patron = $params->{patron};
770 my $circ_control_branch = Koha::Policy::Holds->holds_control_library( $self, $patron );
772 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
774 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
775 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
777 my $pickup_libraries = Koha::Libraries->search();
778 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
779 $pickup_libraries = $self->home_branch->get_hold_libraries;
780 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
781 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
782 $pickup_libraries = $plib->get_hold_libraries;
783 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
784 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
785 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
786 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
789 return $pickup_libraries->search(
794 order_by => ['branchname']
796 ) unless C4::Context->preference('UseBranchTransferLimits');
798 my $limittype = C4::Context->preference('BranchTransferLimitsType');
799 my ($ccode, $itype) = (undef, undef);
800 if( $limittype eq 'ccode' ){
801 $ccode = $self->ccode;
803 $itype = $self->itype;
805 my $limits = Koha::Item::Transfer::Limits->search(
807 fromBranch => $self->holdingbranch,
811 { columns => ['toBranch'] }
814 return $pickup_libraries->search(
816 pickup_location => 1,
818 '-not_in' => $limits->_resultset->as_query
822 order_by => ['branchname']
827 =head3 article_request_type
829 my $type = $item->article_request_type( $borrower )
831 returns 'yes', 'no', 'bib_only', or 'item_only'
833 $borrower must be a Koha::Patron object
837 sub article_request_type {
838 my ( $self, $borrower ) = @_;
840 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
842 $branch_control eq 'homebranch' ? $self->homebranch
843 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
845 my $borrowertype = $borrower->categorycode;
846 my $itemtype = $self->effective_itemtype();
847 my $rule = Koha::CirculationRules->get_effective_rule(
849 rule_name => 'article_requests',
850 categorycode => $borrowertype,
851 itemtype => $itemtype,
852 branchcode => $branchcode
856 return q{} unless $rule;
857 return $rule->rule_value || q{}
866 my $attributes = { order_by => 'priority' };
867 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
869 itemnumber => $self->itemnumber,
872 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
873 waitingdate => { '!=' => undef },
876 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
877 return Koha::Holds->_new_from_dbic($hold_rs);
880 =head3 stockrotationitem
882 my $sritem = Koha::Item->stockrotationitem;
884 Returns the stock rotation item associated with the current item.
888 sub stockrotationitem {
890 my $rs = $self->_result->stockrotationitem;
892 return Koha::StockRotationItem->_new_from_dbic( $rs );
897 my $item = $item->add_to_rota($rota_id);
899 Add this item to the rota identified by $ROTA_ID, which means associating it
900 with the first stage of that rota. Should this item already be associated
901 with a rota, then we will move it to the new rota.
906 my ( $self, $rota_id ) = @_;
907 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
911 =head3 has_pending_hold
913 my $is_pending_hold = $item->has_pending_hold();
915 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
919 sub has_pending_hold {
921 return $self->_result->tmp_holdsqueue ? 1 : 0;
924 =head3 has_pending_recall {
926 my $has_pending_recall
928 Return if whether has pending recall of not.
932 sub has_pending_recall {
935 # FIXME Must be moved to $self->recalls
936 return Koha::Recalls->search(
938 item_id => $self->itemnumber,
946 my $field = $item->as_marc_field;
948 This method returns a MARC::Field object representing the Koha::Item object
949 with the current mappings configuration.
956 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
958 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
962 my $item_field = $tagslib->{$itemtag};
964 my $more_subfields = $self->additional_attributes->to_hashref;
965 foreach my $subfield (
967 $a->{display_order} <=> $b->{display_order}
968 || $a->{subfield} cmp $b->{subfield}
969 } grep { ref($_) && %$_ } values %$item_field
972 my $kohafield = $subfield->{kohafield};
973 my $tagsubfield = $subfield->{tagsubfield};
975 if ( defined $kohafield && $kohafield ne '' ) {
976 next if $kohafield !~ m{^items\.}; # That would be weird!
977 ( my $attribute = $kohafield ) =~ s|^items\.||;
978 $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
979 if defined $self->$attribute and $self->$attribute ne '';
981 $value = $more_subfields->{$tagsubfield}
984 next unless defined $value
987 if ( $subfield->{repeatable} ) {
988 my @values = split '\|', $value;
989 push @subfields, ( $tagsubfield => $_ ) for @values;
992 push @subfields, ( $tagsubfield => $value );
997 return unless @subfields;
999 return MARC::Field->new(
1000 "$itemtag", ' ', ' ', @subfields
1004 =head3 renewal_branchcode
1006 Returns the branchcode to be recorded in statistics renewal of the item
1010 sub renewal_branchcode {
1012 my ($self, $params ) = @_;
1014 my $interface = C4::Context->interface;
1016 if ( $interface eq 'opac' ){
1017 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1018 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1019 $branchcode = 'OPACRenew';
1021 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1022 $branchcode = $self->homebranch;
1024 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1025 $branchcode = $self->checkout->patron->branchcode;
1027 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1028 $branchcode = $self->checkout->branchcode;
1034 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1035 ? C4::Context->userenv->{branch} : $params->{branch};
1042 Return the cover images associated with this item.
1049 my $cover_image_rs = $self->_result->cover_images;
1050 return unless $cover_image_rs;
1051 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1054 =head3 columns_to_str
1056 my $values = $items->columns_to_str;
1058 Return a hashref with the string representation of the different attribute of the item.
1060 This is meant to be used for display purpose only.
1064 sub columns_to_str {
1066 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1067 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1068 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1070 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1073 for my $column ( @{$self->_columns}) {
1075 next if $column eq 'more_subfields_xml';
1077 my $value = $self->$column;
1078 # 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
1080 if ( not defined $value or $value eq "" ) {
1081 $values->{$column} = $value;
1086 exists $mss->{"items.$column"}
1087 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1090 $values->{$column} =
1092 ? $subfield->{authorised_value}
1093 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1094 $subfield->{tagsubfield}, $value, '', $tagslib )
1100 $self->more_subfields_xml
1101 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1106 my ( $field ) = $marc_more->fields;
1107 for my $sf ( $field->subfields ) {
1108 my $subfield_code = $sf->[0];
1109 my $value = $sf->[1];
1110 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1111 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1113 $subfield->{authorised_value}
1114 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1115 $subfield->{tagsubfield}, $value, '', $tagslib )
1118 push @{$more_values->{$subfield_code}}, $value;
1121 while ( my ( $k, $v ) = each %$more_values ) {
1122 $values->{$k} = join ' | ', @$v;
1129 =head3 additional_attributes
1131 my $attributes = $item->additional_attributes;
1132 $attributes->{k} = 'new k';
1133 $item->update({ more_subfields => $attributes->to_marcxml });
1135 Returns a Koha::Item::Attributes object that represents the non-mapped
1136 attributes for this item.
1140 sub additional_attributes {
1143 return Koha::Item::Attributes->new_from_marcxml(
1144 $self->more_subfields_xml,
1148 =head3 _set_found_trigger
1150 $self->_set_found_trigger
1152 Finds the most recent lost item charge for this item and refunds the patron
1153 appropriately, taking into account any payments or writeoffs already applied
1156 Internal function, not exported, called only by Koha::Item->store.
1160 sub _set_found_trigger {
1161 my ( $self, $pre_mod_item ) = @_;
1163 # Reverse any lost item charges if necessary.
1164 my $no_refund_after_days =
1165 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1166 if ($no_refund_after_days) {
1167 my $today = dt_from_string();
1168 my $lost_age_in_days =
1169 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1172 return $self unless $lost_age_in_days < $no_refund_after_days;
1175 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1178 return_branch => C4::Context->userenv
1179 ? C4::Context->userenv->{'branch'}
1183 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1185 if ( $lostreturn_policy ) {
1187 # refund charge made for lost book
1188 my $lost_charge = Koha::Account::Lines->search(
1190 itemnumber => $self->itemnumber,
1191 debit_type_code => 'LOST',
1192 status => [ undef, { '<>' => 'FOUND' } ]
1195 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1200 if ( $lost_charge ) {
1202 my $patron = $lost_charge->patron;
1205 my $account = $patron->account;
1207 # Credit outstanding amount
1208 my $credit_total = $lost_charge->amountoutstanding;
1212 $lost_charge->amount > $lost_charge->amountoutstanding &&
1213 $lostreturn_policy ne "refund_unpaid"
1215 # some amount has been cancelled. collect the offsets that are not writeoffs
1216 # this works because the only way to subtract from this kind of a debt is
1217 # using the UI buttons 'Pay' and 'Write off'
1219 # We don't credit any payments if return policy is
1222 # In that case only unpaid/outstanding amount
1223 # will be credited which settles the debt without
1224 # creating extra credits
1226 my $credit_offsets = $lost_charge->debit_offsets(
1228 'credit_id' => { '!=' => undef },
1229 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1231 { join => 'credit' }
1234 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1235 # credits are negative on the DB
1236 $credit_offsets->total * -1 :
1238 # Credit the outstanding amount, then add what has been
1239 # paid to create a net credit for this amount
1240 $credit_total += $total_to_refund;
1244 if ( $credit_total > 0 ) {
1246 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1247 $credit = $account->add_credit(
1249 amount => $credit_total,
1250 description => 'Item found ' . $self->itemnumber,
1251 type => 'LOST_FOUND',
1252 interface => C4::Context->interface,
1253 library_id => $branchcode,
1254 item_id => $self->itemnumber,
1255 issue_id => $lost_charge->issue_id
1259 $credit->apply( { debits => [$lost_charge] } );
1263 message => 'lost_refunded',
1264 payload => { credit_id => $credit->id }
1269 # Update the account status
1270 $lost_charge->status('FOUND');
1271 $lost_charge->store();
1273 # Reconcile balances if required
1274 if ( C4::Context->preference('AccountAutoReconcile') ) {
1275 $account->reconcile_balance;
1280 # possibly restore fine for lost book
1281 my $lost_overdue = Koha::Account::Lines->search(
1283 itemnumber => $self->itemnumber,
1284 debit_type_code => 'OVERDUE',
1288 order_by => { '-desc' => 'date' },
1292 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1294 my $patron = $lost_overdue->patron;
1296 my $account = $patron->account;
1298 # Update status of fine
1299 $lost_overdue->status('FOUND')->store();
1301 # Find related forgive credit
1302 my $refund = $lost_overdue->credits(
1304 credit_type_code => 'FORGIVEN',
1305 itemnumber => $self->itemnumber,
1306 status => [ { '!=' => 'VOID' }, undef ]
1308 { order_by => { '-desc' => 'date' }, rows => 1 }
1312 # Revert the forgive credit
1313 $refund->void({ interface => 'trigger' });
1317 message => 'lost_restored',
1318 payload => { refund_id => $refund->id }
1323 # Reconcile balances if required
1324 if ( C4::Context->preference('AccountAutoReconcile') ) {
1325 $account->reconcile_balance;
1329 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1333 message => 'lost_charge',
1339 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1341 if ( $processingreturn_policy ) {
1343 # refund processing charge made for lost book
1344 my $processing_charge = Koha::Account::Lines->search(
1346 itemnumber => $self->itemnumber,
1347 debit_type_code => 'PROCESSING',
1348 status => [ undef, { '<>' => 'FOUND' } ]
1351 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1356 if ( $processing_charge ) {
1358 my $patron = $processing_charge->patron;
1361 my $account = $patron->account;
1363 # Credit outstanding amount
1364 my $credit_total = $processing_charge->amountoutstanding;
1368 $processing_charge->amount > $processing_charge->amountoutstanding &&
1369 $processingreturn_policy ne "refund_unpaid"
1371 # some amount has been cancelled. collect the offsets that are not writeoffs
1372 # this works because the only way to subtract from this kind of a debt is
1373 # using the UI buttons 'Pay' and 'Write off'
1375 # We don't credit any payments if return policy is
1378 # In that case only unpaid/outstanding amount
1379 # will be credited which settles the debt without
1380 # creating extra credits
1382 my $credit_offsets = $processing_charge->debit_offsets(
1384 'credit_id' => { '!=' => undef },
1385 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1387 { join => 'credit' }
1390 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1391 # credits are negative on the DB
1392 $credit_offsets->total * -1 :
1394 # Credit the outstanding amount, then add what has been
1395 # paid to create a net credit for this amount
1396 $credit_total += $total_to_refund;
1400 if ( $credit_total > 0 ) {
1402 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1403 $credit = $account->add_credit(
1405 amount => $credit_total,
1406 description => 'Item found ' . $self->itemnumber,
1407 type => 'PROCESSING_FOUND',
1408 interface => C4::Context->interface,
1409 library_id => $branchcode,
1410 item_id => $self->itemnumber,
1411 issue_id => $processing_charge->issue_id
1415 $credit->apply( { debits => [$processing_charge] } );
1419 message => 'processing_refunded',
1420 payload => { credit_id => $credit->id }
1425 # Update the account status
1426 $processing_charge->status('FOUND');
1427 $processing_charge->store();
1429 # Reconcile balances if required
1430 if ( C4::Context->preference('AccountAutoReconcile') ) {
1431 $account->reconcile_balance;
1440 =head3 public_read_list
1442 This method returns the list of publicly readable database fields for both API and UI output purposes
1446 sub public_read_list {
1448 'itemnumber', 'biblionumber', 'homebranch',
1449 'holdingbranch', 'location', 'collectioncode',
1450 'itemcallnumber', 'copynumber', 'enumchron',
1451 'barcode', 'dateaccessioned', 'itemnotes',
1452 'onloan', 'uri', 'itype',
1453 'notforloan', 'damaged', 'itemlost',
1454 'withdrawn', 'restricted'
1460 Overloaded to_api method to ensure item-level itypes is adhered to.
1465 my ($self, $params) = @_;
1467 my $response = $self->SUPER::to_api($params);
1470 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1471 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1473 return { %$response, %$overrides };
1476 =head3 to_api_mapping
1478 This method returns the mapping for representing a Koha::Item object
1483 sub to_api_mapping {
1485 itemnumber => 'item_id',
1486 biblionumber => 'biblio_id',
1487 biblioitemnumber => undef,
1488 barcode => 'external_id',
1489 dateaccessioned => 'acquisition_date',
1490 booksellerid => 'acquisition_source',
1491 homebranch => 'home_library_id',
1492 price => 'purchase_price',
1493 replacementprice => 'replacement_price',
1494 replacementpricedate => 'replacement_price_date',
1495 datelastborrowed => 'last_checkout_date',
1496 datelastseen => 'last_seen_date',
1498 notforloan => 'not_for_loan_status',
1499 damaged => 'damaged_status',
1500 damaged_on => 'damaged_date',
1501 itemlost => 'lost_status',
1502 itemlost_on => 'lost_date',
1503 withdrawn => 'withdrawn',
1504 withdrawn_on => 'withdrawn_date',
1505 itemcallnumber => 'callnumber',
1506 coded_location_qualifier => 'coded_location_qualifier',
1507 issues => 'checkouts_count',
1508 renewals => 'renewals_count',
1509 reserves => 'holds_count',
1510 restricted => 'restricted_status',
1511 itemnotes => 'public_notes',
1512 itemnotes_nonpublic => 'internal_notes',
1513 holdingbranch => 'holding_library_id',
1514 timestamp => 'timestamp',
1515 location => 'location',
1516 permanent_location => 'permanent_location',
1517 onloan => 'checked_out_date',
1518 cn_source => 'call_number_source',
1519 cn_sort => 'call_number_sort',
1520 ccode => 'collection_code',
1521 materials => 'materials_notes',
1523 itype => 'item_type_id',
1524 more_subfields_xml => 'extended_subfields',
1525 enumchron => 'serial_issue_number',
1526 copynumber => 'copy_number',
1527 stocknumber => 'inventory_number',
1528 new_status => 'new_status',
1529 deleted_on => undef,
1535 my $itemtype = $item->itemtype;
1537 Returns Koha object for effective itemtype
1544 return Koha::ItemTypes->find( $self->effective_itemtype );
1549 my $orders = $item->orders();
1551 Returns a Koha::Acquisition::Orders object
1558 my $orders = $self->_result->item_orders;
1559 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1562 =head3 tracked_links
1564 my $tracked_links = $item->tracked_links();
1566 Returns a Koha::TrackedLinks object
1573 my $tracked_links = $self->_result->linktrackers;
1574 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1577 =head3 move_to_biblio
1579 $item->move_to_biblio($to_biblio[, $params]);
1581 Move the item to another biblio and update any references in other tables.
1583 The final optional parameter, C<$params>, is expected to contain the
1584 'skip_record_index' key, which is relayed down to Koha::Item->store.
1585 There it prevents calling index_records, which takes most of the
1586 time in batch adds/deletes. The caller must take care of calling
1587 index_records separately.
1590 skip_record_index => 1|0
1592 Returns undef if the move failed or the biblionumber of the destination record otherwise
1596 sub move_to_biblio {
1597 my ( $self, $to_biblio, $params ) = @_;
1601 return if $self->biblionumber == $to_biblio->biblionumber;
1603 my $from_biblionumber = $self->biblionumber;
1604 my $to_biblionumber = $to_biblio->biblionumber;
1606 # Own biblionumber and biblioitemnumber
1608 biblionumber => $to_biblionumber,
1609 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1610 })->store({ skip_record_index => $params->{skip_record_index} });
1612 unless ($params->{skip_record_index}) {
1613 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1614 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1617 # Acquisition orders
1618 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1621 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1623 # hold_fill_target (there's no Koha object available yet)
1624 my $hold_fill_target = $self->_result->hold_fill_target;
1625 if ($hold_fill_target) {
1626 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1629 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1630 # and can't even fake one since the significant columns are nullable.
1631 my $storage = $self->_result->result_source->storage;
1634 my ($storage, $dbh, @cols) = @_;
1636 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1641 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1643 return $to_biblionumber;
1648 my $bundle_items = $item->bundle_items;
1650 Returns the items associated with this bundle
1657 my $rs = $self->_result->bundle_items;
1658 return Koha::Items->_new_from_dbic($rs);
1663 my $is_bundle = $item->is_bundle;
1665 Returns whether the item is a bundle or not
1671 return $self->bundle_items->count ? 1 : 0;
1676 my $bundle = $item->bundle_host;
1678 Returns the bundle item this item is attached to
1685 my $bundle_items_rs = $self->_result->item_bundles_item;
1686 return unless $bundle_items_rs;
1687 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1692 my $in_bundle = $item->in_bundle;
1694 Returns whether this item is currently in a bundle
1700 return $self->bundle_host ? 1 : 0;
1703 =head3 add_to_bundle
1705 my $link = $item->add_to_bundle($bundle_item);
1707 Adds the bundle_item passed to this item
1712 my ( $self, $bundle_item, $options ) = @_;
1716 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1717 if ( $self->itemnumber eq $bundle_item->itemnumber
1718 || $bundle_item->is_bundle
1719 || $self->in_bundle );
1721 my $schema = Koha::Database->new->schema;
1723 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1729 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
1731 my $checkout = $bundle_item->checkout;
1733 unless ($options->{force_checkin}) {
1734 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1737 my $branchcode = C4::Context->userenv->{'branch'};
1738 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1740 Koha::Exceptions::Checkin::FailedCheckin->throw();
1744 my $holds = $bundle_item->current_holds;
1745 if ($holds->count) {
1746 unless ($options->{ignore_holds}) {
1747 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1751 $self->_result->add_to_item_bundles_hosts(
1752 { item => $bundle_item->itemnumber } );
1754 $bundle_item->notforloan($BundleNotLoanValue)->store();
1760 # 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
1761 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1762 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1764 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1765 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1766 Koha::Exceptions::Object::FKConstraint->throw(
1767 error => 'Broken FK constraint',
1768 broken_fk => $+{column}
1773 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1775 Koha::Exceptions::Object::DuplicateID->throw(
1776 error => 'Duplicate ID',
1777 duplicate_id => $+{key}
1780 elsif ( $_->{msg} =~
1781 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1783 { # The optional \W in the regex might be a quote or backtick
1784 my $type = $+{type};
1785 my $value = $+{value};
1786 my $property = $+{property};
1787 $property =~ s/['`]//g;
1788 Koha::Exceptions::Object::BadValue->throw(
1791 property => $property =~ /(\w+\.\w+)$/
1794 , # results in table.column without quotes or backtics
1798 # Catch-all for foreign key breakages. It will help find other use cases
1807 =head3 remove_from_bundle
1809 Remove this item from any bundle it may have been attached to.
1813 sub remove_from_bundle {
1816 my $bundle_host = $self->bundle_host;
1818 return 0 unless $bundle_host; # Should not we raise an exception here?
1820 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $bundle_host->checkout;
1822 my $bundle_item_rs = $self->_result->item_bundles_item;
1823 if ( $bundle_item_rs ) {
1824 $bundle_item_rs->delete;
1825 $self->notforloan(0)->store();
1831 =head2 Internal methods
1833 =head3 _after_item_action_hooks
1835 Helper method that takes care of calling all plugin hooks
1839 sub _after_item_action_hooks {
1840 my ( $self, $params ) = @_;
1842 my $action = $params->{action};
1844 Koha::Plugins->call(
1845 'after_item_action',
1849 item_id => $self->itemnumber,
1856 my $recall = $item->recall;
1858 Return the relevant recall for this item
1864 my @recalls = Koha::Recalls->search(
1866 biblio_id => $self->biblionumber,
1869 { order_by => { -asc => 'created_date' } }
1872 my $item_level_recall;
1873 foreach my $recall (@recalls) {
1874 if ( $recall->item_level ) {
1875 $item_level_recall = 1;
1876 if ( $recall->item_id == $self->itemnumber ) {
1881 if ($item_level_recall) {
1883 # recall needs to be filled be a specific item only
1884 # no other item is relevant to return
1888 # no item-level recall to return, so return earliest biblio-level
1889 # FIXME: eventually this will be based on priority
1893 =head3 can_be_recalled
1895 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1897 Does item-level checks and returns if items can be recalled by this borrower
1901 sub can_be_recalled {
1902 my ( $self, $params ) = @_;
1904 return 0 if !( C4::Context->preference('UseRecalls') );
1906 # check if this item is not for loan, withdrawn or lost
1907 return 0 if ( $self->notforloan != 0 );
1908 return 0 if ( $self->itemlost != 0 );
1909 return 0 if ( $self->withdrawn != 0 );
1911 # check if this item is not checked out - if not checked out, can't be recalled
1912 return 0 if ( !defined( $self->checkout ) );
1914 my $patron = $params->{patron};
1916 my $branchcode = C4::Context->userenv->{'branch'};
1918 $branchcode = C4::Circulation::_GetCircControlBranch( $self, $patron );
1921 # Check the circulation rule for each relevant itemtype for this item
1922 my $rule = Koha::CirculationRules->get_effective_rules({
1923 branchcode => $branchcode,
1924 categorycode => $patron ? $patron->categorycode : undef,
1925 itemtype => $self->effective_itemtype,
1928 'recalls_per_record',
1933 # check recalls allowed has been set and is not zero
1934 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1937 # check borrower has not reached open recalls allowed limit
1938 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1940 # check borrower has not reach open recalls allowed per record limit
1941 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1943 # check if this patron has already recalled this item
1944 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1946 # check if this patron has already checked out this item
1947 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1949 # check if this patron has already reserved this item
1950 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1953 # check item availability
1954 # items are unavailable for recall if they are lost, withdrawn or notforloan
1955 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1957 # if there are no available items at all, no recall can be placed
1958 return 0 if ( scalar @items == 0 );
1960 my $checked_out_count = 0;
1962 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1965 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1966 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1968 # can't recall if no items have been checked out
1969 return 0 if ( $checked_out_count == 0 );
1975 =head3 can_be_waiting_recall
1977 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1979 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1980 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1984 sub can_be_waiting_recall {
1987 return 0 if !( C4::Context->preference('UseRecalls') );
1989 # check if this item is not for loan, withdrawn or lost
1990 return 0 if ( $self->notforloan != 0 );
1991 return 0 if ( $self->itemlost != 0 );
1992 return 0 if ( $self->withdrawn != 0 );
1994 my $branchcode = $self->holdingbranch;
1995 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1996 $branchcode = C4::Context->userenv->{'branch'};
1998 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
2001 # Check the circulation rule for each relevant itemtype for this item
2002 my $most_relevant_recall = $self->check_recalls;
2003 my $rule = Koha::CirculationRules->get_effective_rules(
2005 branchcode => $branchcode,
2006 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
2007 itemtype => $self->effective_itemtype,
2008 rules => [ 'recalls_allowed', ],
2012 # check recalls allowed has been set and is not zero
2013 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
2019 =head3 check_recalls
2021 my $recall = $item->check_recalls;
2023 Get the most relevant recall for this item.
2030 my @recalls = Koha::Recalls->search(
2031 { biblio_id => $self->biblionumber,
2032 item_id => [ $self->itemnumber, undef ]
2034 { order_by => { -asc => 'created_date' } }
2035 )->filter_by_current->as_list;
2038 # iterate through relevant recalls to find the best one.
2039 # if we come across a waiting recall, use this one.
2040 # 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.
2041 foreach my $r ( @recalls ) {
2042 if ( $r->waiting ) {
2047 unless ( defined $recall ) {
2048 $recall = $recalls[0];
2054 =head3 is_notforloan
2056 my $is_notforloan = $item->is_notforloan;
2058 Determine whether or not this item is "notforloan" based on
2059 the item's notforloan status or its item type
2065 my $is_notforloan = 0;
2067 if ( $self->notforloan ){
2071 my $itemtype = $self->itemtype;
2073 if ( $itemtype->notforloan ){
2079 return $is_notforloan;
2082 =head3 is_denied_renewal
2084 my $is_denied_renewal = $item->is_denied_renewal;
2086 Determine whether or not this item can be renewed based on the
2087 rules set in the ItemsDeniedRenewal system preference.
2091 sub is_denied_renewal {
2093 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2094 return 0 unless $denyingrules;
2095 foreach my $field (keys %$denyingrules) {
2096 # Silently ignore bad column names; TODO we should validate elsewhere
2097 next if !$self->_result->result_source->has_column($field);
2098 my $val = $self->$field;
2099 if( !defined $val) {
2100 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2103 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2104 # If the results matches the values in the syspref
2105 # We return true if match found
2114 Returns a map of column name to string representations including the string,
2115 the mapping type and the mapping category where appropriate.
2117 Currently handles authorised value mappings, library, callnumber and itemtype
2120 Accepts a param hashref where the 'public' key denotes whether we want the public
2121 or staff client strings.
2126 my ( $self, $params ) = @_;
2127 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2128 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2129 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2131 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2133 # Hardcoded known 'authorised_value' values mapped to API codes
2134 my $code_to_type = {
2135 branches => 'library',
2136 cn_source => 'call_number_source',
2137 itemtypes => 'item_type',
2140 # Handle not null and default values for integers and dates
2143 foreach my $col ( @{$self->_columns} ) {
2145 # By now, we are done with known columns, now check the framework for mappings
2146 my $field = $self->_result->result_source->name . '.' . $col;
2148 # Check there's an entry in the MARC subfield structure for the field
2149 if ( exists $mss->{$field}
2150 && scalar @{ $mss->{$field} } > 0
2151 && $mss->{$field}[0]->{authorised_value} )
2153 my $subfield = $mss->{$field}[0];
2154 my $code = $subfield->{authorised_value};
2156 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2157 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2158 $strings->{$col} = {
2161 ( $type eq 'av' ? ( category => $code ) : () ),
2179 Kyle M Hall <kyle@bywatersolutions.com>