3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
58 use base qw(Koha::Object);
62 Koha::Item - Koha Item object class
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
82 my $params = @_ ? shift : {};
84 my $log_action = $params->{log_action} // 1;
86 # We do not want to oblige callers to pass this value
87 # Dev conveniences vs performance?
88 unless ( $self->biblioitemnumber ) {
89 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
92 # See related changes from C4::Items::AddItem
93 unless ( $self->itype ) {
94 $self->itype($self->biblio->biblioitem->itemtype);
97 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
99 my $today = dt_from_string;
100 my $action = 'create';
102 unless ( $self->in_storage ) { #AddItem
104 unless ( $self->permanent_location ) {
105 $self->permanent_location($self->location);
108 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109 unless ( $self->location || !$default_location ) {
110 $self->permanent_location( $self->location || $default_location )
111 unless $self->permanent_location;
112 $self->location($default_location);
115 unless ( $self->replacementpricedate ) {
116 $self->replacementpricedate($today);
118 unless ( $self->datelastseen ) {
119 $self->datelastseen($today);
122 unless ( $self->dateaccessioned ) {
123 $self->dateaccessioned($today);
126 if ( $self->itemcallnumber
127 or $self->cn_source )
129 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130 $self->cn_sort($cn_sort);
137 my %updated_columns = $self->_result->get_dirty_columns;
138 return $self->SUPER::store unless %updated_columns;
140 # Retrieve the item for comparison if we need to
142 exists $updated_columns{itemlost}
143 or exists $updated_columns{withdrawn}
144 or exists $updated_columns{damaged}
145 ) ? $self->get_from_storage : undef;
147 # Update *_on fields if needed
148 # FIXME: Why not for AddItem as well?
149 my @fields = qw( itemlost withdrawn damaged );
150 for my $field (@fields) {
152 # If the field is defined but empty or 0, we are
153 # removing/unsetting and thus need to clear out
155 if ( exists $updated_columns{$field}
156 && defined( $self->$field )
159 my $field_on = "${field}_on";
160 $self->$field_on(undef);
162 # If the field has changed otherwise, we much update
164 elsif (exists $updated_columns{$field}
165 && $updated_columns{$field}
166 && !$pre_mod_item->$field )
168 my $field_on = "${field}_on";
169 $self->$field_on(dt_from_string);
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
210 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
212 biblio_ids => [ $self->biblionumber ]
214 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
225 my $params = @_ ? shift : {};
227 # FIXME check the item has no current issues
228 # i.e. raise the appropriate exception
230 # Get the item group so we can delete it later if it has no items left
231 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
233 my $result = $self->SUPER::delete;
235 # Delete the item gorup if it has no items left
236 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
251 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error //= "not_same_branch"
296 if defined C4::Context->userenv
297 && !C4::Context->IsSuperLibrarian()
298 && C4::Context->preference("IndependentBranches")
299 && ( C4::Context->userenv->{branch} ne $self->homebranch );
301 # check it doesn't have a waiting reserve
302 $error //= "book_reserved"
303 if $self->holds->filter_by_found->count;
305 $error //= "linked_analytics"
306 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
308 $error //= "last_item_for_hold"
309 if $self->biblio->items->count == 1
310 && $self->biblio->holds->search(
317 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
320 return Koha::Result::Boolean->new(1);
323 =head3 move_to_deleted
325 my $is_moved = $item->move_to_deleted;
327 Move an item to the deleteditems table.
328 This can be done before deleting an item, to make sure the data are not completely deleted.
332 sub move_to_deleted {
334 my $item_infos = $self->unblessed;
335 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
336 $item_infos->{deleted_on} = dt_from_string;
337 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
341 =head3 effective_itemtype
343 Returns the itemtype for the item based on whether item level itemtypes are set or not.
347 sub effective_itemtype {
350 return $self->_result()->effective_itemtype();
360 my $hb_rs = $self->_result->homebranch;
362 return Koha::Library->_new_from_dbic( $hb_rs );
365 =head3 holding_branch
372 my $hb_rs = $self->_result->holdingbranch;
374 return Koha::Library->_new_from_dbic( $hb_rs );
379 my $biblio = $item->biblio;
381 Return the bibliographic record of this item
387 my $biblio_rs = $self->_result->biblio;
388 return Koha::Biblio->_new_from_dbic( $biblio_rs );
393 my $biblioitem = $item->biblioitem;
395 Return the biblioitem record of this item
401 my $biblioitem_rs = $self->_result->biblioitem;
402 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
407 my $checkout = $item->checkout;
409 Return the checkout for this item
415 my $checkout_rs = $self->_result->issue;
416 return unless $checkout_rs;
417 return Koha::Checkout->_new_from_dbic( $checkout_rs );
422 my $item_group = $item->item_group;
424 Return the item group for this item
431 my $item_group_item = $self->_result->item_group_item;
432 return unless $item_group_item;
434 my $item_group_rs = $item_group_item->item_group;
435 return unless $item_group_rs;
437 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
443 my $return_claims = $item->return_claims;
445 Return any return_claims associated with this item
450 my ( $self, $params, $attrs ) = @_;
451 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
452 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
457 my $return_claim = $item->return_claim;
459 Returns the most recent unresolved return_claims associated with this item
466 $self->_result->return_claims->search( { resolution => undef },
467 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
468 return unless $claims_rs;
469 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
474 my $holds = $item->holds();
475 my $holds = $item->holds($params);
476 my $holds = $item->holds({ found => 'W'});
478 Return holds attached to an item, optionally accept a hashref of params to pass to search
483 my ( $self,$params ) = @_;
484 my $holds_rs = $self->_result->reserves->search($params);
485 return Koha::Holds->_new_from_dbic( $holds_rs );
488 =head3 request_transfer
490 my $transfer = $item->request_transfer(
494 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
498 Add a transfer request for this item to the given branch for the given reason.
500 An exception will be thrown if the BranchTransferLimits would prevent the requested
501 transfer, unless 'ignore_limits' is passed to override the limits.
503 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
504 The caller should catch such cases and retry the transfer request as appropriate passing
505 an appropriate override.
508 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
509 * replace - Used to replace the existing transfer request with your own.
513 sub request_transfer {
514 my ( $self, $params ) = @_;
516 # check for mandatory params
517 my @mandatory = ( 'to', 'reason' );
518 for my $param (@mandatory) {
519 unless ( defined( $params->{$param} ) ) {
520 Koha::Exceptions::MissingParameter->throw(
521 error => "The $param parameter is mandatory" );
525 Koha::Exceptions::Item::Transfer::Limit->throw()
526 unless ( $params->{ignore_limits}
527 || $self->can_be_transferred( { to => $params->{to} } ) );
529 my $request = $self->get_transfer;
530 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
531 if ( $request && !$params->{enqueue} && !$params->{replace} );
533 $request->cancel( { reason => $params->{reason}, force => 1 } )
534 if ( defined($request) && $params->{replace} );
536 my $transfer = Koha::Item::Transfer->new(
538 itemnumber => $self->itemnumber,
539 daterequested => dt_from_string,
540 frombranch => $self->holdingbranch,
541 tobranch => $params->{to}->branchcode,
542 reason => $params->{reason},
543 comments => $params->{comment}
552 my $transfer = $item->get_transfer;
554 Return the active transfer request or undef
556 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
557 whereby the most recently sent, but not received, transfer will be returned
558 if it exists, otherwise the oldest unsatisfied transfer will be returned.
560 This allows for transfers to queue, which is the case for stock rotation and
561 rotating collections where a manual transfer may need to take precedence but
562 we still expect the item to end up at a final location eventually.
569 return $self->get_transfers->search( {}, { rows => 1 } )->next;
574 my $transfer = $item->get_transfers;
576 Return the list of outstanding transfers (i.e requested but not yet cancelled
579 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
580 whereby the most recently sent, but not received, transfer will be returned
581 first if it exists, otherwise requests are in oldest to newest request order.
583 This allows for transfers to queue, which is the case for stock rotation and
584 rotating collections where a manual transfer may need to take precedence but
585 we still expect the item to end up at a final location eventually.
592 my $transfer_rs = $self->_result->branchtransfers;
594 return Koha::Item::Transfers
595 ->_new_from_dbic($transfer_rs)
597 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
600 =head3 last_returned_by
602 Gets and sets the last patron to return an item.
604 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
606 $item->last_returned_by( $borrowernumber );
608 my $patron = $item->last_returned_by();
612 sub last_returned_by {
613 my ( $self, $borrowernumber ) = @_;
614 if ( $borrowernumber ) {
615 $self->_result->update_or_create_related('last_returned_by',
616 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
618 my $rs = $self->_result->last_returned_by;
620 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
623 =head3 can_article_request
625 my $bool = $item->can_article_request( $borrower )
627 Returns true if item can be specifically requested
629 $borrower must be a Koha::Patron object
633 sub can_article_request {
634 my ( $self, $borrower ) = @_;
636 my $rule = $self->article_request_type($borrower);
638 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
642 =head3 hidden_in_opac
644 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
646 Returns true if item fields match the hidding criteria defined in $rules.
647 Returns false otherwise.
649 Takes HASHref that can have the following parameters:
651 $rules : { <field> => [ value_1, ... ], ... }
653 Note: $rules inherits its structure from the parsed YAML from reading
654 the I<OpacHiddenItems> system preference.
659 my ( $self, $params ) = @_;
661 my $rules = $params->{rules} // {};
664 if C4::Context->preference('hidelostitems') and
667 my $hidden_in_opac = 0;
669 foreach my $field ( keys %{$rules} ) {
671 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
677 return $hidden_in_opac;
680 =head3 can_be_transferred
682 $item->can_be_transferred({ to => $to_library, from => $from_library })
683 Checks if an item can be transferred to given library.
685 This feature is controlled by two system preferences:
686 UseBranchTransferLimits to enable / disable the feature
687 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
688 for setting the limitations
690 Takes HASHref that can have the following parameters:
691 MANDATORY PARAMETERS:
694 $from : Koha::Library # if not given, item holdingbranch
695 # will be used instead
697 Returns 1 if item can be transferred to $to_library, otherwise 0.
699 To find out whether at least one item of a Koha::Biblio can be transferred, please
700 see Koha::Biblio->can_be_transferred() instead of using this method for
701 multiple items of the same biblio.
705 sub can_be_transferred {
706 my ($self, $params) = @_;
708 my $to = $params->{to};
709 my $from = $params->{from};
711 $to = $to->branchcode;
712 $from = defined $from ? $from->branchcode : $self->holdingbranch;
714 return 1 if $from eq $to; # Transfer to current branch is allowed
715 return 1 unless C4::Context->preference('UseBranchTransferLimits');
717 my $limittype = C4::Context->preference('BranchTransferLimitsType');
718 return Koha::Item::Transfer::Limits->search({
721 $limittype => $limittype eq 'itemtype'
722 ? $self->effective_itemtype : $self->ccode
727 =head3 pickup_locations
729 my $pickup_locations = $item->pickup_locations({ patron => $patron })
731 Returns possible pickup locations for this item, according to patron's home library
732 and if item can be transferred to each pickup location.
734 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
739 sub pickup_locations {
740 my ($self, $params) = @_;
742 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
743 unless exists $params->{patron};
745 my $patron = $params->{patron};
747 my $circ_control_branch =
748 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
750 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
752 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
753 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
755 my $pickup_libraries = Koha::Libraries->search();
756 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
757 $pickup_libraries = $self->home_branch->get_hold_libraries;
758 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
759 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
760 $pickup_libraries = $plib->get_hold_libraries;
761 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
762 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
763 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
764 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
767 return $pickup_libraries->search(
772 order_by => ['branchname']
774 ) unless C4::Context->preference('UseBranchTransferLimits');
776 my $limittype = C4::Context->preference('BranchTransferLimitsType');
777 my ($ccode, $itype) = (undef, undef);
778 if( $limittype eq 'ccode' ){
779 $ccode = $self->ccode;
781 $itype = $self->itype;
783 my $limits = Koha::Item::Transfer::Limits->search(
785 fromBranch => $self->holdingbranch,
789 { columns => ['toBranch'] }
792 return $pickup_libraries->search(
794 pickup_location => 1,
796 '-not_in' => $limits->_resultset->as_query
800 order_by => ['branchname']
805 =head3 article_request_type
807 my $type = $item->article_request_type( $borrower )
809 returns 'yes', 'no', 'bib_only', or 'item_only'
811 $borrower must be a Koha::Patron object
815 sub article_request_type {
816 my ( $self, $borrower ) = @_;
818 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
820 $branch_control eq 'homebranch' ? $self->homebranch
821 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
823 my $borrowertype = $borrower->categorycode;
824 my $itemtype = $self->effective_itemtype();
825 my $rule = Koha::CirculationRules->get_effective_rule(
827 rule_name => 'article_requests',
828 categorycode => $borrowertype,
829 itemtype => $itemtype,
830 branchcode => $branchcode
834 return q{} unless $rule;
835 return $rule->rule_value || q{}
844 my $attributes = { order_by => 'priority' };
845 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
847 itemnumber => $self->itemnumber,
850 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
851 waitingdate => { '!=' => undef },
854 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
855 return Koha::Holds->_new_from_dbic($hold_rs);
858 =head3 stockrotationitem
860 my $sritem = Koha::Item->stockrotationitem;
862 Returns the stock rotation item associated with the current item.
866 sub stockrotationitem {
868 my $rs = $self->_result->stockrotationitem;
870 return Koha::StockRotationItem->_new_from_dbic( $rs );
875 my $item = $item->add_to_rota($rota_id);
877 Add this item to the rota identified by $ROTA_ID, which means associating it
878 with the first stage of that rota. Should this item already be associated
879 with a rota, then we will move it to the new rota.
884 my ( $self, $rota_id ) = @_;
885 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
889 =head3 has_pending_hold
891 my $is_pending_hold = $item->has_pending_hold();
893 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
897 sub has_pending_hold {
899 my $pending_hold = $self->_result->tmp_holdsqueues;
900 return $pending_hold->count ? 1: 0;
903 =head3 has_pending_recall {
905 my $has_pending_recall
907 Return if whether has pending recall of not.
911 sub has_pending_recall {
914 # FIXME Must be moved to $self->recalls
915 return Koha::Recalls->search(
917 item_id => $self->itemnumber,
925 my $field = $item->as_marc_field;
927 This method returns a MARC::Field object representing the Koha::Item object
928 with the current mappings configuration.
935 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
937 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
941 my $item_field = $tagslib->{$itemtag};
943 my $more_subfields = $self->additional_attributes->to_hashref;
944 foreach my $subfield (
946 $a->{display_order} <=> $b->{display_order}
947 || $a->{subfield} cmp $b->{subfield}
948 } grep { ref($_) && %$_ } values %$item_field
951 my $kohafield = $subfield->{kohafield};
952 my $tagsubfield = $subfield->{tagsubfield};
954 if ( defined $kohafield && $kohafield ne '' ) {
955 next if $kohafield !~ m{^items\.}; # That would be weird!
956 ( my $attribute = $kohafield ) =~ s|^items\.||;
957 $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
958 if defined $self->$attribute and $self->$attribute ne '';
960 $value = $more_subfields->{$tagsubfield}
963 next unless defined $value
966 if ( $subfield->{repeatable} ) {
967 my @values = split '\|', $value;
968 push @subfields, ( $tagsubfield => $_ ) for @values;
971 push @subfields, ( $tagsubfield => $value );
976 return unless @subfields;
978 return MARC::Field->new(
979 "$itemtag", ' ', ' ', @subfields
983 =head3 renewal_branchcode
985 Returns the branchcode to be recorded in statistics renewal of the item
989 sub renewal_branchcode {
991 my ($self, $params ) = @_;
993 my $interface = C4::Context->interface;
995 if ( $interface eq 'opac' ){
996 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
997 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
998 $branchcode = 'OPACRenew';
1000 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1001 $branchcode = $self->homebranch;
1003 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1004 $branchcode = $self->checkout->patron->branchcode;
1006 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1007 $branchcode = $self->checkout->branchcode;
1013 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1014 ? C4::Context->userenv->{branch} : $params->{branch};
1021 Return the cover images associated with this item.
1028 my $cover_image_rs = $self->_result->cover_images;
1029 return unless $cover_image_rs;
1030 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1033 =head3 columns_to_str
1035 my $values = $items->columns_to_str;
1037 Return a hashref with the string representation of the different attribute of the item.
1039 This is meant to be used for display purpose only.
1043 sub columns_to_str {
1045 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1046 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1047 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1049 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1052 for my $column ( @{$self->_columns}) {
1054 next if $column eq 'more_subfields_xml';
1056 my $value = $self->$column;
1057 # 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
1059 if ( not defined $value or $value eq "" ) {
1060 $values->{$column} = $value;
1065 exists $mss->{"items.$column"}
1066 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1069 $values->{$column} =
1071 ? $subfield->{authorised_value}
1072 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1073 $subfield->{tagsubfield}, $value, '', $tagslib )
1079 $self->more_subfields_xml
1080 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1085 my ( $field ) = $marc_more->fields;
1086 for my $sf ( $field->subfields ) {
1087 my $subfield_code = $sf->[0];
1088 my $value = $sf->[1];
1089 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1090 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1092 $subfield->{authorised_value}
1093 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1094 $subfield->{tagsubfield}, $value, '', $tagslib )
1097 push @{$more_values->{$subfield_code}}, $value;
1100 while ( my ( $k, $v ) = each %$more_values ) {
1101 $values->{$k} = join ' | ', @$v;
1108 =head3 additional_attributes
1110 my $attributes = $item->additional_attributes;
1111 $attributes->{k} = 'new k';
1112 $item->update({ more_subfields => $attributes->to_marcxml });
1114 Returns a Koha::Item::Attributes object that represents the non-mapped
1115 attributes for this item.
1119 sub additional_attributes {
1122 return Koha::Item::Attributes->new_from_marcxml(
1123 $self->more_subfields_xml,
1127 =head3 _set_found_trigger
1129 $self->_set_found_trigger
1131 Finds the most recent lost item charge for this item and refunds the patron
1132 appropriately, taking into account any payments or writeoffs already applied
1135 Internal function, not exported, called only by Koha::Item->store.
1139 sub _set_found_trigger {
1140 my ( $self, $pre_mod_item ) = @_;
1142 # Reverse any lost item charges if necessary.
1143 my $no_refund_after_days =
1144 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1145 if ($no_refund_after_days) {
1146 my $today = dt_from_string();
1147 my $lost_age_in_days =
1148 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1151 return $self unless $lost_age_in_days < $no_refund_after_days;
1154 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1157 return_branch => C4::Context->userenv
1158 ? C4::Context->userenv->{'branch'}
1162 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1164 if ( $lostreturn_policy ) {
1166 # refund charge made for lost book
1167 my $lost_charge = Koha::Account::Lines->search(
1169 itemnumber => $self->itemnumber,
1170 debit_type_code => 'LOST',
1171 status => [ undef, { '<>' => 'FOUND' } ]
1174 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1179 if ( $lost_charge ) {
1181 my $patron = $lost_charge->patron;
1184 my $account = $patron->account;
1186 # Credit outstanding amount
1187 my $credit_total = $lost_charge->amountoutstanding;
1191 $lost_charge->amount > $lost_charge->amountoutstanding &&
1192 $lostreturn_policy ne "refund_unpaid"
1194 # some amount has been cancelled. collect the offsets that are not writeoffs
1195 # this works because the only way to subtract from this kind of a debt is
1196 # using the UI buttons 'Pay' and 'Write off'
1198 # We don't credit any payments if return policy is
1201 # In that case only unpaid/outstanding amount
1202 # will be credited which settles the debt without
1203 # creating extra credits
1205 my $credit_offsets = $lost_charge->debit_offsets(
1207 'credit_id' => { '!=' => undef },
1208 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1210 { join => 'credit' }
1213 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1214 # credits are negative on the DB
1215 $credit_offsets->total * -1 :
1217 # Credit the outstanding amount, then add what has been
1218 # paid to create a net credit for this amount
1219 $credit_total += $total_to_refund;
1223 if ( $credit_total > 0 ) {
1225 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1226 $credit = $account->add_credit(
1228 amount => $credit_total,
1229 description => 'Item found ' . $self->itemnumber,
1230 type => 'LOST_FOUND',
1231 interface => C4::Context->interface,
1232 library_id => $branchcode,
1233 item_id => $self->itemnumber,
1234 issue_id => $lost_charge->issue_id
1238 $credit->apply( { debits => [$lost_charge] } );
1242 message => 'lost_refunded',
1243 payload => { credit_id => $credit->id }
1248 # Update the account status
1249 $lost_charge->status('FOUND');
1250 $lost_charge->store();
1252 # Reconcile balances if required
1253 if ( C4::Context->preference('AccountAutoReconcile') ) {
1254 $account->reconcile_balance;
1259 # possibly restore fine for lost book
1260 my $lost_overdue = Koha::Account::Lines->search(
1262 itemnumber => $self->itemnumber,
1263 debit_type_code => 'OVERDUE',
1267 order_by => { '-desc' => 'date' },
1271 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1273 my $patron = $lost_overdue->patron;
1275 my $account = $patron->account;
1277 # Update status of fine
1278 $lost_overdue->status('FOUND')->store();
1280 # Find related forgive credit
1281 my $refund = $lost_overdue->credits(
1283 credit_type_code => 'FORGIVEN',
1284 itemnumber => $self->itemnumber,
1285 status => [ { '!=' => 'VOID' }, undef ]
1287 { order_by => { '-desc' => 'date' }, rows => 1 }
1291 # Revert the forgive credit
1292 $refund->void({ interface => 'trigger' });
1296 message => 'lost_restored',
1297 payload => { refund_id => $refund->id }
1302 # Reconcile balances if required
1303 if ( C4::Context->preference('AccountAutoReconcile') ) {
1304 $account->reconcile_balance;
1308 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1312 message => 'lost_charge',
1318 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1320 if ( $processingreturn_policy ) {
1322 # refund processing charge made for lost book
1323 my $processing_charge = Koha::Account::Lines->search(
1325 itemnumber => $self->itemnumber,
1326 debit_type_code => 'PROCESSING',
1327 status => [ undef, { '<>' => 'FOUND' } ]
1330 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1335 if ( $processing_charge ) {
1337 my $patron = $processing_charge->patron;
1340 my $account = $patron->account;
1342 # Credit outstanding amount
1343 my $credit_total = $processing_charge->amountoutstanding;
1347 $processing_charge->amount > $processing_charge->amountoutstanding &&
1348 $processingreturn_policy ne "refund_unpaid"
1350 # some amount has been cancelled. collect the offsets that are not writeoffs
1351 # this works because the only way to subtract from this kind of a debt is
1352 # using the UI buttons 'Pay' and 'Write off'
1354 # We don't credit any payments if return policy is
1357 # In that case only unpaid/outstanding amount
1358 # will be credited which settles the debt without
1359 # creating extra credits
1361 my $credit_offsets = $processing_charge->debit_offsets(
1363 'credit_id' => { '!=' => undef },
1364 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1366 { join => 'credit' }
1369 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1370 # credits are negative on the DB
1371 $credit_offsets->total * -1 :
1373 # Credit the outstanding amount, then add what has been
1374 # paid to create a net credit for this amount
1375 $credit_total += $total_to_refund;
1379 if ( $credit_total > 0 ) {
1381 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1382 $credit = $account->add_credit(
1384 amount => $credit_total,
1385 description => 'Item found ' . $self->itemnumber,
1386 type => 'PROCESSING_FOUND',
1387 interface => C4::Context->interface,
1388 library_id => $branchcode,
1389 item_id => $self->itemnumber,
1390 issue_id => $processing_charge->issue_id
1394 $credit->apply( { debits => [$processing_charge] } );
1398 message => 'processing_refunded',
1399 payload => { credit_id => $credit->id }
1404 # Update the account status
1405 $processing_charge->status('FOUND');
1406 $processing_charge->store();
1408 # Reconcile balances if required
1409 if ( C4::Context->preference('AccountAutoReconcile') ) {
1410 $account->reconcile_balance;
1419 =head3 public_read_list
1421 This method returns the list of publicly readable database fields for both API and UI output purposes
1425 sub public_read_list {
1427 'itemnumber', 'biblionumber', 'homebranch',
1428 'holdingbranch', 'location', 'collectioncode',
1429 'itemcallnumber', 'copynumber', 'enumchron',
1430 'barcode', 'dateaccessioned', 'itemnotes',
1431 'onloan', 'uri', 'itype',
1432 'notforloan', 'damaged', 'itemlost',
1433 'withdrawn', 'restricted'
1439 Overloaded to_api method to ensure item-level itypes is adhered to.
1444 my ($self, $params) = @_;
1446 my $response = $self->SUPER::to_api($params);
1449 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1450 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1452 return { %$response, %$overrides };
1455 =head3 to_api_mapping
1457 This method returns the mapping for representing a Koha::Item object
1462 sub to_api_mapping {
1464 itemnumber => 'item_id',
1465 biblionumber => 'biblio_id',
1466 biblioitemnumber => undef,
1467 barcode => 'external_id',
1468 dateaccessioned => 'acquisition_date',
1469 booksellerid => 'acquisition_source',
1470 homebranch => 'home_library_id',
1471 price => 'purchase_price',
1472 replacementprice => 'replacement_price',
1473 replacementpricedate => 'replacement_price_date',
1474 datelastborrowed => 'last_checkout_date',
1475 datelastseen => 'last_seen_date',
1477 notforloan => 'not_for_loan_status',
1478 damaged => 'damaged_status',
1479 damaged_on => 'damaged_date',
1480 itemlost => 'lost_status',
1481 itemlost_on => 'lost_date',
1482 withdrawn => 'withdrawn',
1483 withdrawn_on => 'withdrawn_date',
1484 itemcallnumber => 'callnumber',
1485 coded_location_qualifier => 'coded_location_qualifier',
1486 issues => 'checkouts_count',
1487 renewals => 'renewals_count',
1488 reserves => 'holds_count',
1489 restricted => 'restricted_status',
1490 itemnotes => 'public_notes',
1491 itemnotes_nonpublic => 'internal_notes',
1492 holdingbranch => 'holding_library_id',
1493 timestamp => 'timestamp',
1494 location => 'location',
1495 permanent_location => 'permanent_location',
1496 onloan => 'checked_out_date',
1497 cn_source => 'call_number_source',
1498 cn_sort => 'call_number_sort',
1499 ccode => 'collection_code',
1500 materials => 'materials_notes',
1502 itype => 'item_type_id',
1503 more_subfields_xml => 'extended_subfields',
1504 enumchron => 'serial_issue_number',
1505 copynumber => 'copy_number',
1506 stocknumber => 'inventory_number',
1507 new_status => 'new_status',
1508 deleted_on => undef,
1514 my $itemtype = $item->itemtype;
1516 Returns Koha object for effective itemtype
1523 return Koha::ItemTypes->find( $self->effective_itemtype );
1528 my $orders = $item->orders();
1530 Returns a Koha::Acquisition::Orders object
1537 my $orders = $self->_result->item_orders;
1538 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1541 =head3 tracked_links
1543 my $tracked_links = $item->tracked_links();
1545 Returns a Koha::TrackedLinks object
1552 my $tracked_links = $self->_result->linktrackers;
1553 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1556 =head3 move_to_biblio
1558 $item->move_to_biblio($to_biblio[, $params]);
1560 Move the item to another biblio and update any references in other tables.
1562 The final optional parameter, C<$params>, is expected to contain the
1563 'skip_record_index' key, which is relayed down to Koha::Item->store.
1564 There it prevents calling index_records, which takes most of the
1565 time in batch adds/deletes. The caller must take care of calling
1566 index_records separately.
1569 skip_record_index => 1|0
1571 Returns undef if the move failed or the biblionumber of the destination record otherwise
1575 sub move_to_biblio {
1576 my ( $self, $to_biblio, $params ) = @_;
1580 return if $self->biblionumber == $to_biblio->biblionumber;
1582 my $from_biblionumber = $self->biblionumber;
1583 my $to_biblionumber = $to_biblio->biblionumber;
1585 # Own biblionumber and biblioitemnumber
1587 biblionumber => $to_biblionumber,
1588 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1589 })->store({ skip_record_index => $params->{skip_record_index} });
1591 unless ($params->{skip_record_index}) {
1592 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1593 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1596 # Acquisition orders
1597 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1600 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1602 # hold_fill_target (there's no Koha object available yet)
1603 my $hold_fill_target = $self->_result->hold_fill_target;
1604 if ($hold_fill_target) {
1605 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1608 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1609 # and can't even fake one since the significant columns are nullable.
1610 my $storage = $self->_result->result_source->storage;
1613 my ($storage, $dbh, @cols) = @_;
1615 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1620 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1622 return $to_biblionumber;
1627 my $bundle_items = $item->bundle_items;
1629 Returns the items associated with this bundle
1636 my $rs = $self->_result->bundle_items;
1637 return Koha::Items->_new_from_dbic($rs);
1642 my $is_bundle = $item->is_bundle;
1644 Returns whether the item is a bundle or not
1650 return $self->bundle_items->count ? 1 : 0;
1655 my $bundle = $item->bundle_host;
1657 Returns the bundle item this item is attached to
1664 my $bundle_items_rs = $self->_result->item_bundles_item;
1665 return unless $bundle_items_rs;
1666 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1671 my $in_bundle = $item->in_bundle;
1673 Returns whether this item is currently in a bundle
1679 return $self->bundle_host ? 1 : 0;
1682 =head3 add_to_bundle
1684 my $link = $item->add_to_bundle($bundle_item);
1686 Adds the bundle_item passed to this item
1691 my ( $self, $bundle_item, $options ) = @_;
1695 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1696 if ( $self->itemnumber eq $bundle_item->itemnumber
1697 || $bundle_item->is_bundle
1698 || $self->in_bundle );
1700 my $schema = Koha::Database->new->schema;
1702 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1707 my $checkout = $bundle_item->checkout;
1709 unless ($options->{force_checkin}) {
1710 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1713 my $branchcode = C4::Context->userenv->{'branch'};
1714 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1716 Koha::Exceptions::Checkin::FailedCheckin->throw();
1720 $self->_result->add_to_item_bundles_hosts(
1721 { item => $bundle_item->itemnumber } );
1723 $bundle_item->notforloan($BundleNotLoanValue)->store();
1729 # 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
1730 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1731 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1733 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1734 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1735 Koha::Exceptions::Object::FKConstraint->throw(
1736 error => 'Broken FK constraint',
1737 broken_fk => $+{column}
1742 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1744 Koha::Exceptions::Object::DuplicateID->throw(
1745 error => 'Duplicate ID',
1746 duplicate_id => $+{key}
1749 elsif ( $_->{msg} =~
1750 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1752 { # The optional \W in the regex might be a quote or backtick
1753 my $type = $+{type};
1754 my $value = $+{value};
1755 my $property = $+{property};
1756 $property =~ s/['`]//g;
1757 Koha::Exceptions::Object::BadValue->throw(
1760 property => $property =~ /(\w+\.\w+)$/
1763 , # results in table.column without quotes or backtics
1767 # Catch-all for foreign key breakages. It will help find other use cases
1776 =head3 remove_from_bundle
1778 Remove this item from any bundle it may have been attached to.
1782 sub remove_from_bundle {
1785 my $bundle_item_rs = $self->_result->item_bundles_item;
1786 if ( $bundle_item_rs ) {
1787 $bundle_item_rs->delete;
1788 $self->notforloan(0)->store();
1794 =head2 Internal methods
1796 =head3 _after_item_action_hooks
1798 Helper method that takes care of calling all plugin hooks
1802 sub _after_item_action_hooks {
1803 my ( $self, $params ) = @_;
1805 my $action = $params->{action};
1807 Koha::Plugins->call(
1808 'after_item_action',
1812 item_id => $self->itemnumber,
1819 my $recall = $item->recall;
1821 Return the relevant recall for this item
1827 my @recalls = Koha::Recalls->search(
1829 biblio_id => $self->biblionumber,
1832 { order_by => { -asc => 'created_date' } }
1834 foreach my $recall (@recalls) {
1835 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1839 # no item-level recall to return, so return earliest biblio-level
1840 # FIXME: eventually this will be based on priority
1844 =head3 can_be_recalled
1846 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1848 Does item-level checks and returns if items can be recalled by this borrower
1852 sub can_be_recalled {
1853 my ( $self, $params ) = @_;
1855 return 0 if !( C4::Context->preference('UseRecalls') );
1857 # check if this item is not for loan, withdrawn or lost
1858 return 0 if ( $self->notforloan != 0 );
1859 return 0 if ( $self->itemlost != 0 );
1860 return 0 if ( $self->withdrawn != 0 );
1862 # check if this item is not checked out - if not checked out, can't be recalled
1863 return 0 if ( !defined( $self->checkout ) );
1865 my $patron = $params->{patron};
1867 my $branchcode = C4::Context->userenv->{'branch'};
1869 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1872 # Check the circulation rule for each relevant itemtype for this item
1873 my $rule = Koha::CirculationRules->get_effective_rules({
1874 branchcode => $branchcode,
1875 categorycode => $patron ? $patron->categorycode : undef,
1876 itemtype => $self->effective_itemtype,
1879 'recalls_per_record',
1884 # check recalls allowed has been set and is not zero
1885 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1888 # check borrower has not reached open recalls allowed limit
1889 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1891 # check borrower has not reach open recalls allowed per record limit
1892 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1894 # check if this patron has already recalled this item
1895 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1897 # check if this patron has already checked out this item
1898 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1900 # check if this patron has already reserved this item
1901 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1904 # check item availability
1905 # items are unavailable for recall if they are lost, withdrawn or notforloan
1906 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1908 # if there are no available items at all, no recall can be placed
1909 return 0 if ( scalar @items == 0 );
1911 my $checked_out_count = 0;
1913 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1916 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1917 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1919 # can't recall if no items have been checked out
1920 return 0 if ( $checked_out_count == 0 );
1926 =head3 can_be_waiting_recall
1928 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1930 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1931 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1935 sub can_be_waiting_recall {
1938 return 0 if !( C4::Context->preference('UseRecalls') );
1940 # check if this item is not for loan, withdrawn or lost
1941 return 0 if ( $self->notforloan != 0 );
1942 return 0 if ( $self->itemlost != 0 );
1943 return 0 if ( $self->withdrawn != 0 );
1945 my $branchcode = $self->holdingbranch;
1946 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1947 $branchcode = C4::Context->userenv->{'branch'};
1949 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1952 # Check the circulation rule for each relevant itemtype for this item
1953 my $most_relevant_recall = $self->check_recalls;
1954 my $rule = Koha::CirculationRules->get_effective_rules(
1956 branchcode => $branchcode,
1957 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1958 itemtype => $self->effective_itemtype,
1959 rules => [ 'recalls_allowed', ],
1963 # check recalls allowed has been set and is not zero
1964 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1970 =head3 check_recalls
1972 my $recall = $item->check_recalls;
1974 Get the most relevant recall for this item.
1981 my @recalls = Koha::Recalls->search(
1982 { biblio_id => $self->biblionumber,
1983 item_id => [ $self->itemnumber, undef ]
1985 { order_by => { -asc => 'created_date' } }
1986 )->filter_by_current->as_list;
1989 # iterate through relevant recalls to find the best one.
1990 # if we come across a waiting recall, use this one.
1991 # 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.
1992 foreach my $r ( @recalls ) {
1993 if ( $r->waiting ) {
1998 unless ( defined $recall ) {
1999 $recall = $recalls[0];
2005 =head3 is_notforloan
2007 my $is_notforloan = $item->is_notforloan;
2009 Determine whether or not this item is "notforloan" based on
2010 the item's notforloan status or its item type
2016 my $is_notforloan = 0;
2018 if ( $self->notforloan ){
2022 my $itemtype = $self->itemtype;
2024 if ( $itemtype->notforloan ){
2030 return $is_notforloan;
2033 =head3 is_denied_renewal
2035 my $is_denied_renewal = $item->is_denied_renewal;
2037 Determine whether or not this item can be renewed based on the
2038 rules set in the ItemsDeniedRenewal system preference.
2042 sub is_denied_renewal {
2044 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2045 return 0 unless $denyingrules;
2046 foreach my $field (keys %$denyingrules) {
2047 # Silently ignore bad column names; TODO we should validate elsewhere
2048 next if !$self->_result->result_source->has_column($field);
2049 my $val = $self->$field;
2050 if( !defined $val) {
2051 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2054 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2055 # If the results matches the values in the syspref
2056 # We return true if match found
2065 Returns a map of column name to string representations including the string,
2066 the mapping type and the mapping category where appropriate.
2068 Currently handles authorised value mappings, library, callnumber and itemtype
2071 Accepts a param hashref where the 'public' key denotes whether we want the public
2072 or staff client strings.
2077 my ( $self, $params ) = @_;
2078 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2079 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2080 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2082 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2084 # Hardcoded known 'authorised_value' values mapped to API codes
2085 my $code_to_type = {
2086 branches => 'library',
2087 cn_source => 'call_number_source',
2088 itemtypes => 'item_type',
2091 # Handle not null and default values for integers and dates
2094 foreach my $col ( @{$self->_columns} ) {
2096 # By now, we are done with known columns, now check the framework for mappings
2097 my $field = $self->_result->result_source->name . '.' . $col;
2099 # Check there's an entry in the MARC subfield structure for the field
2100 if ( exists $mss->{$field}
2101 && scalar @{ $mss->{$field} } > 0
2102 && $mss->{$field}[0]->{authorised_value} )
2104 my $subfield = $mss->{$field}[0];
2105 my $code = $subfield->{authorised_value};
2107 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2108 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2109 $strings->{$col} = {
2112 ( $type eq 'av' ? ( category => $code ) : () ),
2130 Kyle M Hall <kyle@bywatersolutions.com>