3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::Exceptions::Checkin;
40 use Koha::Exceptions::Item::Bundle;
41 use Koha::Exceptions::Item::Transfer;
42 use Koha::Item::Attributes;
43 use Koha::Exceptions::Item::Bundle;
44 use Koha::Item::Transfer::Limits;
45 use Koha::Item::Transfers;
51 use Koha::Result::Boolean;
52 use Koha::SearchEngine::Indexer;
53 use Koha::StockRotationItem;
54 use Koha::StockRotationRotas;
55 use Koha::TrackedLinks;
57 use base qw(Koha::Object);
61 Koha::Item - Koha Item object class
73 $params can take an optional 'skip_record_index' parameter.
74 If set, the reindexation process will not happen (index_records not called)
75 You should not turn it on if you do not understand what it is doing exactly.
81 my $params = @_ ? shift : {};
83 my $log_action = $params->{log_action} // 1;
85 # We do not want to oblige callers to pass this value
86 # Dev conveniences vs performance?
87 unless ( $self->biblioitemnumber ) {
88 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
91 # See related changes from C4::Items::AddItem
92 unless ( $self->itype ) {
93 $self->itype($self->biblio->biblioitem->itemtype);
96 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
98 my $today = dt_from_string;
99 my $action = 'create';
101 unless ( $self->in_storage ) { #AddItem
103 unless ( $self->permanent_location ) {
104 $self->permanent_location($self->location);
107 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
108 unless ( $self->location || !$default_location ) {
109 $self->permanent_location( $self->location || $default_location )
110 unless $self->permanent_location;
111 $self->location($default_location);
114 unless ( $self->replacementpricedate ) {
115 $self->replacementpricedate($today);
117 unless ( $self->datelastseen ) {
118 $self->datelastseen($today);
121 unless ( $self->dateaccessioned ) {
122 $self->dateaccessioned($today);
125 if ( $self->itemcallnumber
126 or $self->cn_source )
128 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
129 $self->cn_sort($cn_sort);
136 my %updated_columns = $self->_result->get_dirty_columns;
137 return $self->SUPER::store unless %updated_columns;
139 # Retrieve the item for comparison if we need to
141 exists $updated_columns{itemlost}
142 or exists $updated_columns{withdrawn}
143 or exists $updated_columns{damaged}
144 ) ? $self->get_from_storage : undef;
146 # Update *_on fields if needed
147 # FIXME: Why not for AddItem as well?
148 my @fields = qw( itemlost withdrawn damaged );
149 for my $field (@fields) {
151 # If the field is defined but empty or 0, we are
152 # removing/unsetting and thus need to clear out
154 if ( exists $updated_columns{$field}
155 && defined( $self->$field )
158 my $field_on = "${field}_on";
159 $self->$field_on(undef);
161 # If the field has changed otherwise, we much update
163 elsif (exists $updated_columns{$field}
164 && $updated_columns{$field}
165 && !$pre_mod_item->$field )
167 my $field_on = "${field}_on";
168 $self->$field_on(dt_from_string);
172 if ( exists $updated_columns{itemcallnumber}
173 or exists $updated_columns{cn_source} )
175 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
176 $self->cn_sort($cn_sort);
180 if ( exists $updated_columns{location}
181 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
182 and not exists $updated_columns{permanent_location} )
184 $self->permanent_location( $self->location );
187 # If item was lost and has now been found,
188 # reverse any list item charges if necessary.
189 if ( exists $updated_columns{itemlost}
190 and $updated_columns{itemlost} <= 0
191 and $pre_mod_item->itemlost > 0 )
193 $self->_set_found_trigger($pre_mod_item);
198 my $result = $self->SUPER::store;
199 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
201 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
202 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
204 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
205 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
206 unless $params->{skip_record_index};
207 $self->get_from_storage->_after_item_action_hooks({ action => $action });
209 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
211 biblio_ids => [ $self->biblionumber ]
213 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
224 my $params = @_ ? shift : {};
226 # FIXME check the item has no current issues
227 # i.e. raise the appropriate exception
229 # Get the item group so we can delete it later if it has no items left
230 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
232 my $result = $self->SUPER::delete;
234 # Delete the item gorup if it has no items left
235 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
237 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
238 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
239 unless $params->{skip_record_index};
241 $self->_after_item_action_hooks({ action => 'delete' });
243 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
244 if C4::Context->preference("CataloguingLog");
246 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
248 biblio_ids => [ $self->biblionumber ]
250 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
261 my $params = @_ ? shift : {};
263 my $safe_to_delete = $self->safe_to_delete;
264 return $safe_to_delete unless $safe_to_delete;
266 $self->move_to_deleted;
268 return $self->delete($params);
271 =head3 safe_to_delete
273 returns 1 if the item is safe to delete,
275 "book_on_loan" if the item is checked out,
277 "not_same_branch" if the item is blocked by independent branches,
279 "book_reserved" if the there are holds aganst the item, or
281 "linked_analytics" if the item has linked analytic records.
283 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
292 $error = "book_on_loan" if $self->checkout;
294 $error //= "not_same_branch"
295 if defined C4::Context->userenv
296 and defined C4::Context->userenv->{number}
297 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
299 # check it doesn't have a waiting reserve
300 $error //= "book_reserved"
301 if $self->holds->filter_by_found->count;
303 $error //= "linked_analytics"
304 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
306 $error //= "last_item_for_hold"
307 if $self->biblio->items->count == 1
308 && $self->biblio->holds->search(
315 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
318 return Koha::Result::Boolean->new(1);
321 =head3 move_to_deleted
323 my $is_moved = $item->move_to_deleted;
325 Move an item to the deleteditems table.
326 This can be done before deleting an item, to make sure the data are not completely deleted.
330 sub move_to_deleted {
332 my $item_infos = $self->unblessed;
333 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
334 $item_infos->{deleted_on} = dt_from_string;
335 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
339 =head3 effective_itemtype
341 Returns the itemtype for the item based on whether item level itemtypes are set or not.
345 sub effective_itemtype {
348 return $self->_result()->effective_itemtype();
358 my $hb_rs = $self->_result->homebranch;
360 return Koha::Library->_new_from_dbic( $hb_rs );
363 =head3 holding_branch
370 my $hb_rs = $self->_result->holdingbranch;
372 return Koha::Library->_new_from_dbic( $hb_rs );
377 my $biblio = $item->biblio;
379 Return the bibliographic record of this item
385 my $biblio_rs = $self->_result->biblio;
386 return Koha::Biblio->_new_from_dbic( $biblio_rs );
391 my $biblioitem = $item->biblioitem;
393 Return the biblioitem record of this item
399 my $biblioitem_rs = $self->_result->biblioitem;
400 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
405 my $checkout = $item->checkout;
407 Return the checkout for this item
413 my $checkout_rs = $self->_result->issue;
414 return unless $checkout_rs;
415 return Koha::Checkout->_new_from_dbic( $checkout_rs );
420 my $item_group = $item->item_group;
422 Return the item group for this item
429 my $item_group_item = $self->_result->item_group_item;
430 return unless $item_group_item;
432 my $item_group_rs = $item_group_item->item_group;
433 return unless $item_group_rs;
435 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
441 my $return_claims = $item->return_claims;
443 Return any return_claims associated with this item
448 my ( $self, $params, $attrs ) = @_;
449 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
450 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
455 my $return_claim = $item->return_claim;
457 Returns the most recent unresolved return_claims associated with this item
464 $self->_result->return_claims->search( { resolution => undef },
465 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
466 return unless $claims_rs;
467 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
472 my $holds = $item->holds();
473 my $holds = $item->holds($params);
474 my $holds = $item->holds({ found => 'W'});
476 Return holds attached to an item, optionally accept a hashref of params to pass to search
481 my ( $self,$params ) = @_;
482 my $holds_rs = $self->_result->reserves->search($params);
483 return Koha::Holds->_new_from_dbic( $holds_rs );
486 =head3 request_transfer
488 my $transfer = $item->request_transfer(
492 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
496 Add a transfer request for this item to the given branch for the given reason.
498 An exception will be thrown if the BranchTransferLimits would prevent the requested
499 transfer, unless 'ignore_limits' is passed to override the limits.
501 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
502 The caller should catch such cases and retry the transfer request as appropriate passing
503 an appropriate override.
506 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
507 * replace - Used to replace the existing transfer request with your own.
511 sub request_transfer {
512 my ( $self, $params ) = @_;
514 # check for mandatory params
515 my @mandatory = ( 'to', 'reason' );
516 for my $param (@mandatory) {
517 unless ( defined( $params->{$param} ) ) {
518 Koha::Exceptions::MissingParameter->throw(
519 error => "The $param parameter is mandatory" );
523 Koha::Exceptions::Item::Transfer::Limit->throw()
524 unless ( $params->{ignore_limits}
525 || $self->can_be_transferred( { to => $params->{to} } ) );
527 my $request = $self->get_transfer;
528 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
529 if ( $request && !$params->{enqueue} && !$params->{replace} );
531 $request->cancel( { reason => $params->{reason}, force => 1 } )
532 if ( defined($request) && $params->{replace} );
534 my $transfer = Koha::Item::Transfer->new(
536 itemnumber => $self->itemnumber,
537 daterequested => dt_from_string,
538 frombranch => $self->holdingbranch,
539 tobranch => $params->{to}->branchcode,
540 reason => $params->{reason},
541 comments => $params->{comment}
550 my $transfer = $item->get_transfer;
552 Return the active transfer request or undef
554 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
555 whereby the most recently sent, but not received, transfer will be returned
556 if it exists, otherwise the oldest unsatisfied transfer will be returned.
558 This allows for transfers to queue, which is the case for stock rotation and
559 rotating collections where a manual transfer may need to take precedence but
560 we still expect the item to end up at a final location eventually.
567 return $self->get_transfers->search( {}, { rows => 1 } )->next;
572 my $transfer = $item->get_transfers;
574 Return the list of outstanding transfers (i.e requested but not yet cancelled
577 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
578 whereby the most recently sent, but not received, transfer will be returned
579 first if it exists, otherwise requests are in oldest to newest request order.
581 This allows for transfers to queue, which is the case for stock rotation and
582 rotating collections where a manual transfer may need to take precedence but
583 we still expect the item to end up at a final location eventually.
590 my $transfer_rs = $self->_result->branchtransfers;
592 return Koha::Item::Transfers
593 ->_new_from_dbic($transfer_rs)
595 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
598 =head3 last_returned_by
600 Gets and sets the last borrower to return an item.
602 Accepts and returns Koha::Patron objects
604 $item->last_returned_by( $borrowernumber );
606 $last_returned_by = $item->last_returned_by();
610 sub last_returned_by {
611 my ( $self, $borrower ) = @_;
613 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
616 return $items_last_returned_by_rs->update_or_create(
617 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
620 unless ( $self->{_last_returned_by} ) {
621 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
623 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
627 return $self->{_last_returned_by};
631 =head3 can_article_request
633 my $bool = $item->can_article_request( $borrower )
635 Returns true if item can be specifically requested
637 $borrower must be a Koha::Patron object
641 sub can_article_request {
642 my ( $self, $borrower ) = @_;
644 my $rule = $self->article_request_type($borrower);
646 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
650 =head3 hidden_in_opac
652 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
654 Returns true if item fields match the hidding criteria defined in $rules.
655 Returns false otherwise.
657 Takes HASHref that can have the following parameters:
659 $rules : { <field> => [ value_1, ... ], ... }
661 Note: $rules inherits its structure from the parsed YAML from reading
662 the I<OpacHiddenItems> system preference.
667 my ( $self, $params ) = @_;
669 my $rules = $params->{rules} // {};
672 if C4::Context->preference('hidelostitems') and
675 my $hidden_in_opac = 0;
677 foreach my $field ( keys %{$rules} ) {
679 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
685 return $hidden_in_opac;
688 =head3 can_be_transferred
690 $item->can_be_transferred({ to => $to_library, from => $from_library })
691 Checks if an item can be transferred to given library.
693 This feature is controlled by two system preferences:
694 UseBranchTransferLimits to enable / disable the feature
695 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
696 for setting the limitations
698 Takes HASHref that can have the following parameters:
699 MANDATORY PARAMETERS:
702 $from : Koha::Library # if not given, item holdingbranch
703 # will be used instead
705 Returns 1 if item can be transferred to $to_library, otherwise 0.
707 To find out whether at least one item of a Koha::Biblio can be transferred, please
708 see Koha::Biblio->can_be_transferred() instead of using this method for
709 multiple items of the same biblio.
713 sub can_be_transferred {
714 my ($self, $params) = @_;
716 my $to = $params->{to};
717 my $from = $params->{from};
719 $to = $to->branchcode;
720 $from = defined $from ? $from->branchcode : $self->holdingbranch;
722 return 1 if $from eq $to; # Transfer to current branch is allowed
723 return 1 unless C4::Context->preference('UseBranchTransferLimits');
725 my $limittype = C4::Context->preference('BranchTransferLimitsType');
726 return Koha::Item::Transfer::Limits->search({
729 $limittype => $limittype eq 'itemtype'
730 ? $self->effective_itemtype : $self->ccode
735 =head3 pickup_locations
737 $pickup_locations = $item->pickup_locations( {patron => $patron } )
739 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
740 and if item can be transferred to each pickup location.
744 sub pickup_locations {
745 my ($self, $params) = @_;
747 my $patron = $params->{patron};
749 my $circ_control_branch =
750 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
752 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
754 if(defined $patron) {
755 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
756 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
759 my $pickup_libraries = Koha::Libraries->search();
760 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
761 $pickup_libraries = $self->home_branch->get_hold_libraries;
762 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
763 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
764 $pickup_libraries = $plib->get_hold_libraries;
765 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
766 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
767 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
768 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
771 return $pickup_libraries->search(
776 order_by => ['branchname']
778 ) unless C4::Context->preference('UseBranchTransferLimits');
780 my $limittype = C4::Context->preference('BranchTransferLimitsType');
781 my ($ccode, $itype) = (undef, undef);
782 if( $limittype eq 'ccode' ){
783 $ccode = $self->ccode;
785 $itype = $self->itype;
787 my $limits = Koha::Item::Transfer::Limits->search(
789 fromBranch => $self->holdingbranch,
793 { columns => ['toBranch'] }
796 return $pickup_libraries->search(
798 pickup_location => 1,
800 '-not_in' => $limits->_resultset->as_query
804 order_by => ['branchname']
809 =head3 article_request_type
811 my $type = $item->article_request_type( $borrower )
813 returns 'yes', 'no', 'bib_only', or 'item_only'
815 $borrower must be a Koha::Patron object
819 sub article_request_type {
820 my ( $self, $borrower ) = @_;
822 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
824 $branch_control eq 'homebranch' ? $self->homebranch
825 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
827 my $borrowertype = $borrower->categorycode;
828 my $itemtype = $self->effective_itemtype();
829 my $rule = Koha::CirculationRules->get_effective_rule(
831 rule_name => 'article_requests',
832 categorycode => $borrowertype,
833 itemtype => $itemtype,
834 branchcode => $branchcode
838 return q{} unless $rule;
839 return $rule->rule_value || q{}
848 my $attributes = { order_by => 'priority' };
849 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
851 itemnumber => $self->itemnumber,
854 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
855 waitingdate => { '!=' => undef },
858 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
859 return Koha::Holds->_new_from_dbic($hold_rs);
862 =head3 stockrotationitem
864 my $sritem = Koha::Item->stockrotationitem;
866 Returns the stock rotation item associated with the current item.
870 sub stockrotationitem {
872 my $rs = $self->_result->stockrotationitem;
874 return Koha::StockRotationItem->_new_from_dbic( $rs );
879 my $item = $item->add_to_rota($rota_id);
881 Add this item to the rota identified by $ROTA_ID, which means associating it
882 with the first stage of that rota. Should this item already be associated
883 with a rota, then we will move it to the new rota.
888 my ( $self, $rota_id ) = @_;
889 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
893 =head3 has_pending_hold
895 my $is_pending_hold = $item->has_pending_hold();
897 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
901 sub has_pending_hold {
903 my $pending_hold = $self->_result->tmp_holdsqueues;
904 return $pending_hold->count ? 1: 0;
907 =head3 has_pending_recall {
909 my $has_pending_recall
911 Return if whether has pending recall of not.
915 sub has_pending_recall {
918 # FIXME Must be moved to $self->recalls
919 return Koha::Recalls->search(
921 item_id => $self->itemnumber,
929 my $field = $item->as_marc_field;
931 This method returns a MARC::Field object representing the Koha::Item object
932 with the current mappings configuration.
939 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
941 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
945 my $item_field = $tagslib->{$itemtag};
947 my $more_subfields = $self->additional_attributes->to_hashref;
948 foreach my $subfield (
950 $a->{display_order} <=> $b->{display_order}
951 || $a->{subfield} cmp $b->{subfield}
952 } grep { ref($_) && %$_ } values %$item_field
955 my $kohafield = $subfield->{kohafield};
956 my $tagsubfield = $subfield->{tagsubfield};
958 if ( defined $kohafield && $kohafield ne '' ) {
959 next if $kohafield !~ m{^items\.}; # That would be weird!
960 ( my $attribute = $kohafield ) =~ s|^items\.||;
961 $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
962 if defined $self->$attribute and $self->$attribute ne '';
964 $value = $more_subfields->{$tagsubfield}
967 next unless defined $value
970 if ( $subfield->{repeatable} ) {
971 my @values = split '\|', $value;
972 push @subfields, ( $tagsubfield => $_ ) for @values;
975 push @subfields, ( $tagsubfield => $value );
980 return unless @subfields;
982 return MARC::Field->new(
983 "$itemtag", ' ', ' ', @subfields
987 =head3 renewal_branchcode
989 Returns the branchcode to be recorded in statistics renewal of the item
993 sub renewal_branchcode {
995 my ($self, $params ) = @_;
997 my $interface = C4::Context->interface;
999 if ( $interface eq 'opac' ){
1000 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1001 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1002 $branchcode = 'OPACRenew';
1004 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1005 $branchcode = $self->homebranch;
1007 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1008 $branchcode = $self->checkout->patron->branchcode;
1010 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1011 $branchcode = $self->checkout->branchcode;
1017 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1018 ? C4::Context->userenv->{branch} : $params->{branch};
1025 Return the cover images associated with this item.
1032 my $cover_image_rs = $self->_result->cover_images;
1033 return unless $cover_image_rs;
1034 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1037 =head3 columns_to_str
1039 my $values = $items->columns_to_str;
1041 Return a hashref with the string representation of the different attribute of the item.
1043 This is meant to be used for display purpose only.
1047 sub columns_to_str {
1050 my $frameworkcode = $self->biblio->frameworkcode;
1051 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1052 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1054 my $columns_info = $self->_result->result_source->columns_info;
1056 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1058 for my $column ( keys %$columns_info ) {
1060 next if $column eq 'more_subfields_xml';
1062 my $value = $self->$column;
1063 # 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
1065 if ( not defined $value or $value eq "" ) {
1066 $values->{$column} = $value;
1071 exists $mss->{"items.$column"}
1072 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1075 $values->{$column} =
1077 ? $subfield->{authorised_value}
1078 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1079 $subfield->{tagsubfield}, $value, '', $tagslib )
1085 $self->more_subfields_xml
1086 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1091 my ( $field ) = $marc_more->fields;
1092 for my $sf ( $field->subfields ) {
1093 my $subfield_code = $sf->[0];
1094 my $value = $sf->[1];
1095 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1096 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1098 $subfield->{authorised_value}
1099 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1100 $subfield->{tagsubfield}, $value, '', $tagslib )
1103 push @{$more_values->{$subfield_code}}, $value;
1106 while ( my ( $k, $v ) = each %$more_values ) {
1107 $values->{$k} = join ' | ', @$v;
1114 =head3 additional_attributes
1116 my $attributes = $item->additional_attributes;
1117 $attributes->{k} = 'new k';
1118 $item->update({ more_subfields => $attributes->to_marcxml });
1120 Returns a Koha::Item::Attributes object that represents the non-mapped
1121 attributes for this item.
1125 sub additional_attributes {
1128 return Koha::Item::Attributes->new_from_marcxml(
1129 $self->more_subfields_xml,
1133 =head3 _set_found_trigger
1135 $self->_set_found_trigger
1137 Finds the most recent lost item charge for this item and refunds the patron
1138 appropriately, taking into account any payments or writeoffs already applied
1141 Internal function, not exported, called only by Koha::Item->store.
1145 sub _set_found_trigger {
1146 my ( $self, $pre_mod_item ) = @_;
1148 # Reverse any lost item charges if necessary.
1149 my $no_refund_after_days =
1150 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1151 if ($no_refund_after_days) {
1152 my $today = dt_from_string();
1153 my $lost_age_in_days =
1154 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1157 return $self unless $lost_age_in_days < $no_refund_after_days;
1160 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1163 return_branch => C4::Context->userenv
1164 ? C4::Context->userenv->{'branch'}
1168 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1170 if ( $lostreturn_policy ) {
1172 # refund charge made for lost book
1173 my $lost_charge = Koha::Account::Lines->search(
1175 itemnumber => $self->itemnumber,
1176 debit_type_code => 'LOST',
1177 status => [ undef, { '<>' => 'FOUND' } ]
1180 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1185 if ( $lost_charge ) {
1187 my $patron = $lost_charge->patron;
1190 my $account = $patron->account;
1192 # Credit outstanding amount
1193 my $credit_total = $lost_charge->amountoutstanding;
1197 $lost_charge->amount > $lost_charge->amountoutstanding &&
1198 $lostreturn_policy ne "refund_unpaid"
1200 # some amount has been cancelled. collect the offsets that are not writeoffs
1201 # this works because the only way to subtract from this kind of a debt is
1202 # using the UI buttons 'Pay' and 'Write off'
1204 # We don't credit any payments if return policy is
1207 # In that case only unpaid/outstanding amount
1208 # will be credited which settles the debt without
1209 # creating extra credits
1211 my $credit_offsets = $lost_charge->debit_offsets(
1213 'credit_id' => { '!=' => undef },
1214 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1216 { join => 'credit' }
1219 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1220 # credits are negative on the DB
1221 $credit_offsets->total * -1 :
1223 # Credit the outstanding amount, then add what has been
1224 # paid to create a net credit for this amount
1225 $credit_total += $total_to_refund;
1229 if ( $credit_total > 0 ) {
1231 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1232 $credit = $account->add_credit(
1234 amount => $credit_total,
1235 description => 'Item found ' . $self->itemnumber,
1236 type => 'LOST_FOUND',
1237 interface => C4::Context->interface,
1238 library_id => $branchcode,
1239 item_id => $self->itemnumber,
1240 issue_id => $lost_charge->issue_id
1244 $credit->apply( { debits => [$lost_charge] } );
1248 message => 'lost_refunded',
1249 payload => { credit_id => $credit->id }
1254 # Update the account status
1255 $lost_charge->status('FOUND');
1256 $lost_charge->store();
1258 # Reconcile balances if required
1259 if ( C4::Context->preference('AccountAutoReconcile') ) {
1260 $account->reconcile_balance;
1265 # possibly restore fine for lost book
1266 my $lost_overdue = Koha::Account::Lines->search(
1268 itemnumber => $self->itemnumber,
1269 debit_type_code => 'OVERDUE',
1273 order_by => { '-desc' => 'date' },
1277 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1279 my $patron = $lost_overdue->patron;
1281 my $account = $patron->account;
1283 # Update status of fine
1284 $lost_overdue->status('FOUND')->store();
1286 # Find related forgive credit
1287 my $refund = $lost_overdue->credits(
1289 credit_type_code => 'FORGIVEN',
1290 itemnumber => $self->itemnumber,
1291 status => [ { '!=' => 'VOID' }, undef ]
1293 { order_by => { '-desc' => 'date' }, rows => 1 }
1297 # Revert the forgive credit
1298 $refund->void({ interface => 'trigger' });
1302 message => 'lost_restored',
1303 payload => { refund_id => $refund->id }
1308 # Reconcile balances if required
1309 if ( C4::Context->preference('AccountAutoReconcile') ) {
1310 $account->reconcile_balance;
1314 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1318 message => 'lost_charge',
1324 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1326 if ( $processingreturn_policy ) {
1328 # refund processing charge made for lost book
1329 my $processing_charge = Koha::Account::Lines->search(
1331 itemnumber => $self->itemnumber,
1332 debit_type_code => 'PROCESSING',
1333 status => [ undef, { '<>' => 'FOUND' } ]
1336 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1341 if ( $processing_charge ) {
1343 my $patron = $processing_charge->patron;
1346 my $account = $patron->account;
1348 # Credit outstanding amount
1349 my $credit_total = $processing_charge->amountoutstanding;
1353 $processing_charge->amount > $processing_charge->amountoutstanding &&
1354 $processingreturn_policy ne "refund_unpaid"
1356 # some amount has been cancelled. collect the offsets that are not writeoffs
1357 # this works because the only way to subtract from this kind of a debt is
1358 # using the UI buttons 'Pay' and 'Write off'
1360 # We don't credit any payments if return policy is
1363 # In that case only unpaid/outstanding amount
1364 # will be credited which settles the debt without
1365 # creating extra credits
1367 my $credit_offsets = $processing_charge->debit_offsets(
1369 'credit_id' => { '!=' => undef },
1370 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1372 { join => 'credit' }
1375 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1376 # credits are negative on the DB
1377 $credit_offsets->total * -1 :
1379 # Credit the outstanding amount, then add what has been
1380 # paid to create a net credit for this amount
1381 $credit_total += $total_to_refund;
1385 if ( $credit_total > 0 ) {
1387 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1388 $credit = $account->add_credit(
1390 amount => $credit_total,
1391 description => 'Item found ' . $self->itemnumber,
1392 type => 'PROCESSING_FOUND',
1393 interface => C4::Context->interface,
1394 library_id => $branchcode,
1395 item_id => $self->itemnumber,
1396 issue_id => $processing_charge->issue_id
1400 $credit->apply( { debits => [$processing_charge] } );
1404 message => 'processing_refunded',
1405 payload => { credit_id => $credit->id }
1410 # Update the account status
1411 $processing_charge->status('FOUND');
1412 $processing_charge->store();
1414 # Reconcile balances if required
1415 if ( C4::Context->preference('AccountAutoReconcile') ) {
1416 $account->reconcile_balance;
1425 =head3 public_read_list
1427 This method returns the list of publicly readable database fields for both API and UI output purposes
1431 sub public_read_list {
1433 'itemnumber', 'biblionumber', 'homebranch',
1434 'holdingbranch', 'location', 'collectioncode',
1435 'itemcallnumber', 'copynumber', 'enumchron',
1436 'barcode', 'dateaccessioned', 'itemnotes',
1437 'onloan', 'uri', 'itype',
1438 'notforloan', 'damaged', 'itemlost',
1439 'withdrawn', 'restricted'
1445 Overloaded to_api method to ensure item-level itypes is adhered to.
1450 my ($self, $params) = @_;
1452 my $response = $self->SUPER::to_api($params);
1455 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1456 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1458 return { %$response, %$overrides };
1461 =head3 to_api_mapping
1463 This method returns the mapping for representing a Koha::Item object
1468 sub to_api_mapping {
1470 itemnumber => 'item_id',
1471 biblionumber => 'biblio_id',
1472 biblioitemnumber => undef,
1473 barcode => 'external_id',
1474 dateaccessioned => 'acquisition_date',
1475 booksellerid => 'acquisition_source',
1476 homebranch => 'home_library_id',
1477 price => 'purchase_price',
1478 replacementprice => 'replacement_price',
1479 replacementpricedate => 'replacement_price_date',
1480 datelastborrowed => 'last_checkout_date',
1481 datelastseen => 'last_seen_date',
1483 notforloan => 'not_for_loan_status',
1484 damaged => 'damaged_status',
1485 damaged_on => 'damaged_date',
1486 itemlost => 'lost_status',
1487 itemlost_on => 'lost_date',
1488 withdrawn => 'withdrawn',
1489 withdrawn_on => 'withdrawn_date',
1490 itemcallnumber => 'callnumber',
1491 coded_location_qualifier => 'coded_location_qualifier',
1492 issues => 'checkouts_count',
1493 renewals => 'renewals_count',
1494 reserves => 'holds_count',
1495 restricted => 'restricted_status',
1496 itemnotes => 'public_notes',
1497 itemnotes_nonpublic => 'internal_notes',
1498 holdingbranch => 'holding_library_id',
1499 timestamp => 'timestamp',
1500 location => 'location',
1501 permanent_location => 'permanent_location',
1502 onloan => 'checked_out_date',
1503 cn_source => 'call_number_source',
1504 cn_sort => 'call_number_sort',
1505 ccode => 'collection_code',
1506 materials => 'materials_notes',
1508 itype => 'item_type_id',
1509 more_subfields_xml => 'extended_subfields',
1510 enumchron => 'serial_issue_number',
1511 copynumber => 'copy_number',
1512 stocknumber => 'inventory_number',
1513 new_status => 'new_status',
1514 deleted_on => undef,
1520 my $itemtype = $item->itemtype;
1522 Returns Koha object for effective itemtype
1529 return Koha::ItemTypes->find( $self->effective_itemtype );
1534 my $orders = $item->orders();
1536 Returns a Koha::Acquisition::Orders object
1543 my $orders = $self->_result->item_orders;
1544 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1547 =head3 tracked_links
1549 my $tracked_links = $item->tracked_links();
1551 Returns a Koha::TrackedLinks object
1558 my $tracked_links = $self->_result->linktrackers;
1559 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1562 =head3 move_to_biblio
1564 $item->move_to_biblio($to_biblio[, $params]);
1566 Move the item to another biblio and update any references in other tables.
1568 The final optional parameter, C<$params>, is expected to contain the
1569 'skip_record_index' key, which is relayed down to Koha::Item->store.
1570 There it prevents calling index_records, which takes most of the
1571 time in batch adds/deletes. The caller must take care of calling
1572 index_records separately.
1575 skip_record_index => 1|0
1577 Returns undef if the move failed or the biblionumber of the destination record otherwise
1581 sub move_to_biblio {
1582 my ( $self, $to_biblio, $params ) = @_;
1586 return if $self->biblionumber == $to_biblio->biblionumber;
1588 my $from_biblionumber = $self->biblionumber;
1589 my $to_biblionumber = $to_biblio->biblionumber;
1591 # Own biblionumber and biblioitemnumber
1593 biblionumber => $to_biblionumber,
1594 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1595 })->store({ skip_record_index => $params->{skip_record_index} });
1597 unless ($params->{skip_record_index}) {
1598 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1599 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1602 # Acquisition orders
1603 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1606 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1608 # hold_fill_target (there's no Koha object available yet)
1609 my $hold_fill_target = $self->_result->hold_fill_target;
1610 if ($hold_fill_target) {
1611 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1614 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1615 # and can't even fake one since the significant columns are nullable.
1616 my $storage = $self->_result->result_source->storage;
1619 my ($storage, $dbh, @cols) = @_;
1621 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1626 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1628 return $to_biblionumber;
1633 my $bundle_items = $item->bundle_items;
1635 Returns the items associated with this bundle
1642 if ( !$self->{_bundle_items_cached} ) {
1643 my $bundle_items = Koha::Items->search(
1644 { 'item_bundles_item.host' => $self->itemnumber },
1645 { join => 'item_bundles_item' } );
1646 $self->{_bundle_items} = $bundle_items;
1647 $self->{_bundle_items_cached} = 1;
1650 return $self->{_bundle_items};
1655 my $is_bundle = $item->is_bundle;
1657 Returns whether the item is a bundle or not
1663 return $self->bundle_items->count ? 1 : 0;
1668 my $bundle = $item->bundle_host;
1670 Returns the bundle item this item is attached to
1677 my $bundle_items_rs = $self->_result->item_bundles_item;
1678 return unless $bundle_items_rs;
1679 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1684 my $in_bundle = $item->in_bundle;
1686 Returns whether this item is currently in a bundle
1692 return $self->bundle_host ? 1 : 0;
1695 =head3 add_to_bundle
1697 my $link = $item->add_to_bundle($bundle_item);
1699 Adds the bundle_item passed to this item
1704 my ( $self, $bundle_item, $options ) = @_;
1708 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1709 if ( $self->itemnumber eq $bundle_item->itemnumber
1710 || $bundle_item->is_bundle
1711 || $self->in_bundle );
1713 my $schema = Koha::Database->new->schema;
1715 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1720 my $checkout = $bundle_item->checkout;
1722 unless ($options->{force_checkin}) {
1723 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1726 my $branchcode = C4::Context->userenv->{'branch'};
1727 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1729 Koha::Exceptions::Checkin::FailedCheckin->throw();
1733 my $holds = $bundle_item->current_holds;
1734 if ($holds->count) {
1735 unless ($options->{ignore_holds}) {
1736 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1740 $self->_result->add_to_item_bundles_hosts(
1741 { item => $bundle_item->itemnumber } );
1743 $bundle_item->notforloan($BundleNotLoanValue)->store();
1749 # 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
1750 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1751 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1753 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1754 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1755 Koha::Exceptions::Object::FKConstraint->throw(
1756 error => 'Broken FK constraint',
1757 broken_fk => $+{column}
1762 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1764 Koha::Exceptions::Object::DuplicateID->throw(
1765 error => 'Duplicate ID',
1766 duplicate_id => $+{key}
1769 elsif ( $_->{msg} =~
1770 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1772 { # The optional \W in the regex might be a quote or backtick
1773 my $type = $+{type};
1774 my $value = $+{value};
1775 my $property = $+{property};
1776 $property =~ s/['`]//g;
1777 Koha::Exceptions::Object::BadValue->throw(
1780 property => $property =~ /(\w+\.\w+)$/
1783 , # results in table.column without quotes or backtics
1787 # Catch-all for foreign key breakages. It will help find other use cases
1796 =head3 remove_from_bundle
1798 Remove this item from any bundle it may have been attached to.
1802 sub remove_from_bundle {
1805 my $bundle_item_rs = $self->_result->item_bundles_item;
1806 if ( $bundle_item_rs ) {
1807 $bundle_item_rs->delete;
1808 $self->notforloan(0)->store();
1814 =head2 Internal methods
1816 =head3 _after_item_action_hooks
1818 Helper method that takes care of calling all plugin hooks
1822 sub _after_item_action_hooks {
1823 my ( $self, $params ) = @_;
1825 my $action = $params->{action};
1827 Koha::Plugins->call(
1828 'after_item_action',
1832 item_id => $self->itemnumber,
1839 my $recall = $item->recall;
1841 Return the relevant recall for this item
1847 my @recalls = Koha::Recalls->search(
1849 biblio_id => $self->biblionumber,
1852 { order_by => { -asc => 'created_date' } }
1854 foreach my $recall (@recalls) {
1855 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1859 # no item-level recall to return, so return earliest biblio-level
1860 # FIXME: eventually this will be based on priority
1864 =head3 can_be_recalled
1866 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1868 Does item-level checks and returns if items can be recalled by this borrower
1872 sub can_be_recalled {
1873 my ( $self, $params ) = @_;
1875 return 0 if !( C4::Context->preference('UseRecalls') );
1877 # check if this item is not for loan, withdrawn or lost
1878 return 0 if ( $self->notforloan != 0 );
1879 return 0 if ( $self->itemlost != 0 );
1880 return 0 if ( $self->withdrawn != 0 );
1882 # check if this item is not checked out - if not checked out, can't be recalled
1883 return 0 if ( !defined( $self->checkout ) );
1885 my $patron = $params->{patron};
1887 my $branchcode = C4::Context->userenv->{'branch'};
1889 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1892 # Check the circulation rule for each relevant itemtype for this item
1893 my $rule = Koha::CirculationRules->get_effective_rules({
1894 branchcode => $branchcode,
1895 categorycode => $patron ? $patron->categorycode : undef,
1896 itemtype => $self->effective_itemtype,
1899 'recalls_per_record',
1904 # check recalls allowed has been set and is not zero
1905 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1908 # check borrower has not reached open recalls allowed limit
1909 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1911 # check borrower has not reach open recalls allowed per record limit
1912 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1914 # check if this patron has already recalled this item
1915 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1917 # check if this patron has already checked out this item
1918 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1920 # check if this patron has already reserved this item
1921 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1924 # check item availability
1925 # items are unavailable for recall if they are lost, withdrawn or notforloan
1926 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1928 # if there are no available items at all, no recall can be placed
1929 return 0 if ( scalar @items == 0 );
1931 my $checked_out_count = 0;
1933 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1936 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1937 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1939 # can't recall if no items have been checked out
1940 return 0 if ( $checked_out_count == 0 );
1946 =head3 can_be_waiting_recall
1948 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1950 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1951 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1955 sub can_be_waiting_recall {
1958 return 0 if !( C4::Context->preference('UseRecalls') );
1960 # check if this item is not for loan, withdrawn or lost
1961 return 0 if ( $self->notforloan != 0 );
1962 return 0 if ( $self->itemlost != 0 );
1963 return 0 if ( $self->withdrawn != 0 );
1965 my $branchcode = $self->holdingbranch;
1966 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1967 $branchcode = C4::Context->userenv->{'branch'};
1969 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1972 # Check the circulation rule for each relevant itemtype for this item
1973 my $rule = Koha::CirculationRules->get_effective_rules({
1974 branchcode => $branchcode,
1975 categorycode => undef,
1976 itemtype => $self->effective_itemtype,
1982 # check recalls allowed has been set and is not zero
1983 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1989 =head3 check_recalls
1991 my $recall = $item->check_recalls;
1993 Get the most relevant recall for this item.
2000 my @recalls = Koha::Recalls->search(
2001 { biblio_id => $self->biblionumber,
2002 item_id => [ $self->itemnumber, undef ]
2004 { order_by => { -asc => 'created_date' } }
2005 )->filter_by_current->as_list;
2008 # iterate through relevant recalls to find the best one.
2009 # if we come across a waiting recall, use this one.
2010 # 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.
2011 foreach my $r ( @recalls ) {
2012 if ( $r->waiting ) {
2017 unless ( defined $recall ) {
2018 $recall = $recalls[0];
2024 =head3 is_notforloan
2026 my $is_notforloan = $item->is_notforloan;
2028 Determine whether or not this item is "notforloan" based on
2029 the item's notforloan status or its item type
2035 my $is_notforloan = 0;
2037 if ( $self->notforloan ){
2041 my $itemtype = $self->itemtype;
2043 if ( $itemtype->notforloan ){
2049 return $is_notforloan;
2052 =head3 is_denied_renewal
2054 my $is_denied_renewal = $item->is_denied_renewal;
2056 Determine whether or not this item can be renewed based on the
2057 rules set in the ItemsDeniedRenewal system preference.
2061 sub is_denied_renewal {
2064 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
2065 return 0 unless $denyingrules;
2066 foreach my $field (keys %$denyingrules) {
2067 my $val = $self->$field;
2068 if( !defined $val) {
2069 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2072 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2073 # If the results matches the values in the syspref
2074 # We return true if match found
2083 Returns a map of column name to string representations including the string,
2084 the mapping type and the mapping category where appropriate.
2086 Currently handles authorised value mappings, library, callnumber and itemtype
2089 Accepts a param hashref where the 'public' key denotes whether we want the public
2090 or staff client strings.
2095 my ( $self, $params ) = @_;
2097 my $columns_info = $self->_result->result_source->columns_info;
2098 my $frameworkcode = $self->biblio->frameworkcode;
2099 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
2100 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2102 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2104 # Hardcoded known 'authorised_value' values mapped to API codes
2105 my $code_to_type = {
2106 branches => 'library',
2107 cn_source => 'call_number_source',
2108 itemtypes => 'item_type',
2111 # Handle not null and default values for integers and dates
2114 foreach my $col ( keys %{$columns_info} ) {
2116 # By now, we are done with known columns, now check the framework for mappings
2117 my $field = $self->_result->result_source->name . '.' . $col;
2119 # Check there's an entry in the MARC subfield structure for the field
2120 if ( exists $mss->{$field}
2121 && scalar @{ $mss->{$field} } > 0
2122 && $mss->{$field}[0]->{authorised_value} )
2124 my $subfield = $mss->{$field}[0];
2125 my $code = $subfield->{authorised_value};
2127 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2128 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2129 $strings->{$col} = {
2132 ( $type eq 'av' ? ( category => $code ) : () ),
2150 Kyle M Hall <kyle@bywatersolutions.com>