3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
58 use base qw(Koha::Object);
62 Koha::Item - Koha Item object class
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
82 my $params = @_ ? shift : {};
84 my $log_action = $params->{log_action} // 1;
86 # We do not want to oblige callers to pass this value
87 # Dev conveniences vs performance?
88 unless ( $self->biblioitemnumber ) {
89 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
92 # See related changes from C4::Items::AddItem
93 unless ( $self->itype ) {
94 $self->itype($self->biblio->biblioitem->itemtype);
97 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
99 my $today = dt_from_string;
100 my $action = 'create';
102 unless ( $self->in_storage ) { #AddItem
104 unless ( $self->permanent_location ) {
105 $self->permanent_location($self->location);
108 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109 unless ( $self->location || !$default_location ) {
110 $self->permanent_location( $self->location || $default_location )
111 unless $self->permanent_location;
112 $self->location($default_location);
115 unless ( $self->replacementpricedate ) {
116 $self->replacementpricedate($today);
118 unless ( $self->datelastseen ) {
119 $self->datelastseen($today);
122 unless ( $self->dateaccessioned ) {
123 $self->dateaccessioned($today);
126 if ( $self->itemcallnumber
127 or $self->cn_source )
129 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130 $self->cn_sort($cn_sort);
137 my %updated_columns = $self->_result->get_dirty_columns;
138 return $self->SUPER::store unless %updated_columns;
140 # Retrieve the item for comparison if we need to
142 exists $updated_columns{itemlost}
143 or exists $updated_columns{withdrawn}
144 or exists $updated_columns{damaged}
145 ) ? $self->get_from_storage : undef;
147 # Update *_on fields if needed
148 # FIXME: Why not for AddItem as well?
149 my @fields = qw( itemlost withdrawn damaged );
150 for my $field (@fields) {
152 # If the field is defined but empty or 0, we are
153 # removing/unsetting and thus need to clear out
155 if ( exists $updated_columns{$field}
156 && defined( $self->$field )
159 my $field_on = "${field}_on";
160 $self->$field_on(undef);
162 # If the field has changed otherwise, we much update
164 elsif (exists $updated_columns{$field}
165 && $updated_columns{$field}
166 && !$pre_mod_item->$field )
168 my $field_on = "${field}_on";
169 $self->$field_on(dt_from_string);
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
210 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
212 biblio_ids => [ $self->biblionumber ]
214 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
225 my $params = @_ ? shift : {};
227 # FIXME check the item has no current issues
228 # i.e. raise the appropriate exception
230 # Get the item group so we can delete it later if it has no items left
231 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
233 my $result = $self->SUPER::delete;
235 # Delete the item gorup if it has no items left
236 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
251 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error //= "not_same_branch"
296 if defined C4::Context->userenv
297 and defined C4::Context->userenv->{number}
298 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
300 # check it doesn't have a waiting reserve
301 $error //= "book_reserved"
302 if $self->holds->filter_by_found->count;
304 $error //= "linked_analytics"
305 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
307 $error //= "last_item_for_hold"
308 if $self->biblio->items->count == 1
309 && $self->biblio->holds->search(
316 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
319 return Koha::Result::Boolean->new(1);
322 =head3 move_to_deleted
324 my $is_moved = $item->move_to_deleted;
326 Move an item to the deleteditems table.
327 This can be done before deleting an item, to make sure the data are not completely deleted.
331 sub move_to_deleted {
333 my $item_infos = $self->unblessed;
334 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
335 $item_infos->{deleted_on} = dt_from_string;
336 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
340 =head3 effective_itemtype
342 Returns the itemtype for the item based on whether item level itemtypes are set or not.
346 sub effective_itemtype {
349 return $self->_result()->effective_itemtype();
359 my $hb_rs = $self->_result->homebranch;
361 return Koha::Library->_new_from_dbic( $hb_rs );
364 =head3 holding_branch
371 my $hb_rs = $self->_result->holdingbranch;
373 return Koha::Library->_new_from_dbic( $hb_rs );
378 my $biblio = $item->biblio;
380 Return the bibliographic record of this item
386 my $biblio_rs = $self->_result->biblio;
387 return Koha::Biblio->_new_from_dbic( $biblio_rs );
392 my $biblioitem = $item->biblioitem;
394 Return the biblioitem record of this item
400 my $biblioitem_rs = $self->_result->biblioitem;
401 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
406 my $checkout = $item->checkout;
408 Return the checkout for this item
414 my $checkout_rs = $self->_result->issue;
415 return unless $checkout_rs;
416 return Koha::Checkout->_new_from_dbic( $checkout_rs );
421 my $item_group = $item->item_group;
423 Return the item group for this item
430 my $item_group_item = $self->_result->item_group_item;
431 return unless $item_group_item;
433 my $item_group_rs = $item_group_item->item_group;
434 return unless $item_group_rs;
436 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
442 my $return_claims = $item->return_claims;
444 Return any return_claims associated with this item
449 my ( $self, $params, $attrs ) = @_;
450 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
451 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
456 my $return_claim = $item->return_claim;
458 Returns the most recent unresolved return_claims associated with this item
465 $self->_result->return_claims->search( { resolution => undef },
466 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
467 return unless $claims_rs;
468 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
473 my $holds = $item->holds();
474 my $holds = $item->holds($params);
475 my $holds = $item->holds({ found => 'W'});
477 Return holds attached to an item, optionally accept a hashref of params to pass to search
482 my ( $self,$params ) = @_;
483 my $holds_rs = $self->_result->reserves->search($params);
484 return Koha::Holds->_new_from_dbic( $holds_rs );
487 =head3 request_transfer
489 my $transfer = $item->request_transfer(
493 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
497 Add a transfer request for this item to the given branch for the given reason.
499 An exception will be thrown if the BranchTransferLimits would prevent the requested
500 transfer, unless 'ignore_limits' is passed to override the limits.
502 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
503 The caller should catch such cases and retry the transfer request as appropriate passing
504 an appropriate override.
507 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
508 * replace - Used to replace the existing transfer request with your own.
512 sub request_transfer {
513 my ( $self, $params ) = @_;
515 # check for mandatory params
516 my @mandatory = ( 'to', 'reason' );
517 for my $param (@mandatory) {
518 unless ( defined( $params->{$param} ) ) {
519 Koha::Exceptions::MissingParameter->throw(
520 error => "The $param parameter is mandatory" );
524 Koha::Exceptions::Item::Transfer::Limit->throw()
525 unless ( $params->{ignore_limits}
526 || $self->can_be_transferred( { to => $params->{to} } ) );
528 my $request = $self->get_transfer;
529 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
530 if ( $request && !$params->{enqueue} && !$params->{replace} );
532 $request->cancel( { reason => $params->{reason}, force => 1 } )
533 if ( defined($request) && $params->{replace} );
535 my $transfer = Koha::Item::Transfer->new(
537 itemnumber => $self->itemnumber,
538 daterequested => dt_from_string,
539 frombranch => $self->holdingbranch,
540 tobranch => $params->{to}->branchcode,
541 reason => $params->{reason},
542 comments => $params->{comment}
551 my $transfer = $item->get_transfer;
553 Return the active transfer request or undef
555 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
556 whereby the most recently sent, but not received, transfer will be returned
557 if it exists, otherwise the oldest unsatisfied transfer will be returned.
559 This allows for transfers to queue, which is the case for stock rotation and
560 rotating collections where a manual transfer may need to take precedence but
561 we still expect the item to end up at a final location eventually.
568 my $transfer = $self->_result->current_branchtransfers->next;
569 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
574 my $transfer = $item->get_transfers;
576 Return the list of outstanding transfers (i.e requested but not yet cancelled
579 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
580 whereby the most recently sent, but not received, transfer will be returned
581 first if it exists, otherwise requests are in oldest to newest request order.
583 This allows for transfers to queue, which is the case for stock rotation and
584 rotating collections where a manual transfer may need to take precedence but
585 we still expect the item to end up at a final location eventually.
592 my $transfer_rs = $self->_result->current_branchtransfers;
594 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
597 =head3 last_returned_by
599 Gets the last borrower to return an item.
601 $last_returned_by = $item->last_returned_by();
605 sub last_returned_by {
607 my $rs = $self->_result->last_returned_by;
609 return Koha::Patron->_new_from_dbic($rs);
612 =head3 can_article_request
614 my $bool = $item->can_article_request( $borrower )
616 Returns true if item can be specifically requested
618 $borrower must be a Koha::Patron object
622 sub can_article_request {
623 my ( $self, $borrower ) = @_;
625 my $rule = $self->article_request_type($borrower);
627 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
631 =head3 hidden_in_opac
633 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
635 Returns true if item fields match the hidding criteria defined in $rules.
636 Returns false otherwise.
638 Takes HASHref that can have the following parameters:
640 $rules : { <field> => [ value_1, ... ], ... }
642 Note: $rules inherits its structure from the parsed YAML from reading
643 the I<OpacHiddenItems> system preference.
648 my ( $self, $params ) = @_;
650 my $rules = $params->{rules} // {};
653 if C4::Context->preference('hidelostitems') and
656 my $hidden_in_opac = 0;
658 foreach my $field ( keys %{$rules} ) {
660 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
666 return $hidden_in_opac;
669 =head3 can_be_transferred
671 $item->can_be_transferred({ to => $to_library, from => $from_library })
672 Checks if an item can be transferred to given library.
674 This feature is controlled by two system preferences:
675 UseBranchTransferLimits to enable / disable the feature
676 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
677 for setting the limitations
679 Takes HASHref that can have the following parameters:
680 MANDATORY PARAMETERS:
683 $from : Koha::Library # if not given, item holdingbranch
684 # will be used instead
686 Returns 1 if item can be transferred to $to_library, otherwise 0.
688 To find out whether at least one item of a Koha::Biblio can be transferred, please
689 see Koha::Biblio->can_be_transferred() instead of using this method for
690 multiple items of the same biblio.
694 sub can_be_transferred {
695 my ($self, $params) = @_;
697 my $to = $params->{to};
698 my $from = $params->{from};
700 $to = $to->branchcode;
701 $from = defined $from ? $from->branchcode : $self->holdingbranch;
703 return 1 if $from eq $to; # Transfer to current branch is allowed
704 return 1 unless C4::Context->preference('UseBranchTransferLimits');
706 my $limittype = C4::Context->preference('BranchTransferLimitsType');
707 return Koha::Item::Transfer::Limits->search({
710 $limittype => $limittype eq 'itemtype'
711 ? $self->effective_itemtype : $self->ccode
716 =head3 pickup_locations
718 my $pickup_locations = $item->pickup_locations({ patron => $patron })
720 Returns possible pickup locations for this item, according to patron's home library
721 and if item can be transferred to each pickup location.
723 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
728 sub pickup_locations {
729 my ($self, $params) = @_;
731 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
732 unless exists $params->{patron};
734 my $patron = $params->{patron};
736 my $circ_control_branch =
737 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
739 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
741 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
742 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
744 my $pickup_libraries = Koha::Libraries->search();
745 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
746 $pickup_libraries = $self->home_branch->get_hold_libraries;
747 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
748 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
749 $pickup_libraries = $plib->get_hold_libraries;
750 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
751 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
752 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
753 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
756 return $pickup_libraries->search(
761 order_by => ['branchname']
763 ) unless C4::Context->preference('UseBranchTransferLimits');
765 my $limittype = C4::Context->preference('BranchTransferLimitsType');
766 my ($ccode, $itype) = (undef, undef);
767 if( $limittype eq 'ccode' ){
768 $ccode = $self->ccode;
770 $itype = $self->itype;
772 my $limits = Koha::Item::Transfer::Limits->search(
774 fromBranch => $self->holdingbranch,
778 { columns => ['toBranch'] }
781 return $pickup_libraries->search(
783 pickup_location => 1,
785 '-not_in' => $limits->_resultset->as_query
789 order_by => ['branchname']
794 =head3 article_request_type
796 my $type = $item->article_request_type( $borrower )
798 returns 'yes', 'no', 'bib_only', or 'item_only'
800 $borrower must be a Koha::Patron object
804 sub article_request_type {
805 my ( $self, $borrower ) = @_;
807 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
809 $branch_control eq 'homebranch' ? $self->homebranch
810 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
812 my $borrowertype = $borrower->categorycode;
813 my $itemtype = $self->effective_itemtype();
814 my $rule = Koha::CirculationRules->get_effective_rule(
816 rule_name => 'article_requests',
817 categorycode => $borrowertype,
818 itemtype => $itemtype,
819 branchcode => $branchcode
823 return q{} unless $rule;
824 return $rule->rule_value || q{}
833 my $attributes = { order_by => 'priority' };
834 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
836 itemnumber => $self->itemnumber,
839 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
840 waitingdate => { '!=' => undef },
843 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
844 return Koha::Holds->_new_from_dbic($hold_rs);
847 =head3 stockrotationitem
849 my $sritem = Koha::Item->stockrotationitem;
851 Returns the stock rotation item associated with the current item.
855 sub stockrotationitem {
857 my $rs = $self->_result->stockrotationitem;
859 return Koha::StockRotationItem->_new_from_dbic( $rs );
864 my $item = $item->add_to_rota($rota_id);
866 Add this item to the rota identified by $ROTA_ID, which means associating it
867 with the first stage of that rota. Should this item already be associated
868 with a rota, then we will move it to the new rota.
873 my ( $self, $rota_id ) = @_;
874 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
878 =head3 has_pending_hold
880 my $is_pending_hold = $item->has_pending_hold();
882 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
886 sub has_pending_hold {
888 my $pending_hold = $self->_result->tmp_holdsqueues;
889 return $pending_hold->count ? 1: 0;
892 =head3 has_pending_recall {
894 my $has_pending_recall
896 Return if whether has pending recall of not.
900 sub has_pending_recall {
903 # FIXME Must be moved to $self->recalls
904 return Koha::Recalls->search(
906 item_id => $self->itemnumber,
914 my $field = $item->as_marc_field;
916 This method returns a MARC::Field object representing the Koha::Item object
917 with the current mappings configuration.
924 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
926 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
930 my $item_field = $tagslib->{$itemtag};
932 my $more_subfields = $self->additional_attributes->to_hashref;
933 foreach my $subfield (
935 $a->{display_order} <=> $b->{display_order}
936 || $a->{subfield} cmp $b->{subfield}
937 } grep { ref($_) && %$_ } values %$item_field
940 my $kohafield = $subfield->{kohafield};
941 my $tagsubfield = $subfield->{tagsubfield};
943 if ( defined $kohafield && $kohafield ne '' ) {
944 next if $kohafield !~ m{^items\.}; # That would be weird!
945 ( my $attribute = $kohafield ) =~ s|^items\.||;
946 $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
947 if defined $self->$attribute and $self->$attribute ne '';
949 $value = $more_subfields->{$tagsubfield}
952 next unless defined $value
955 if ( $subfield->{repeatable} ) {
956 my @values = split '\|', $value;
957 push @subfields, ( $tagsubfield => $_ ) for @values;
960 push @subfields, ( $tagsubfield => $value );
965 return unless @subfields;
967 return MARC::Field->new(
968 "$itemtag", ' ', ' ', @subfields
972 =head3 renewal_branchcode
974 Returns the branchcode to be recorded in statistics renewal of the item
978 sub renewal_branchcode {
980 my ($self, $params ) = @_;
982 my $interface = C4::Context->interface;
984 if ( $interface eq 'opac' ){
985 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
986 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
987 $branchcode = 'OPACRenew';
989 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
990 $branchcode = $self->homebranch;
992 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
993 $branchcode = $self->checkout->patron->branchcode;
995 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
996 $branchcode = $self->checkout->branchcode;
1002 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1003 ? C4::Context->userenv->{branch} : $params->{branch};
1010 Return the cover images associated with this item.
1017 my $cover_image_rs = $self->_result->cover_images;
1018 return unless $cover_image_rs;
1019 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1022 =head3 columns_to_str
1024 my $values = $items->columns_to_str;
1026 Return a hashref with the string representation of the different attribute of the item.
1028 This is meant to be used for display purpose only.
1032 sub columns_to_str {
1034 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1035 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1036 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1038 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1041 for my $column ( @{$self->_columns}) {
1043 next if $column eq 'more_subfields_xml';
1045 my $value = $self->$column;
1046 # 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
1048 if ( not defined $value or $value eq "" ) {
1049 $values->{$column} = $value;
1054 exists $mss->{"items.$column"}
1055 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1058 $values->{$column} =
1060 ? $subfield->{authorised_value}
1061 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1062 $subfield->{tagsubfield}, $value, '', $tagslib )
1068 $self->more_subfields_xml
1069 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1074 my ( $field ) = $marc_more->fields;
1075 for my $sf ( $field->subfields ) {
1076 my $subfield_code = $sf->[0];
1077 my $value = $sf->[1];
1078 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1079 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1081 $subfield->{authorised_value}
1082 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1083 $subfield->{tagsubfield}, $value, '', $tagslib )
1086 push @{$more_values->{$subfield_code}}, $value;
1089 while ( my ( $k, $v ) = each %$more_values ) {
1090 $values->{$k} = join ' | ', @$v;
1097 =head3 additional_attributes
1099 my $attributes = $item->additional_attributes;
1100 $attributes->{k} = 'new k';
1101 $item->update({ more_subfields => $attributes->to_marcxml });
1103 Returns a Koha::Item::Attributes object that represents the non-mapped
1104 attributes for this item.
1108 sub additional_attributes {
1111 return Koha::Item::Attributes->new_from_marcxml(
1112 $self->more_subfields_xml,
1116 =head3 _set_found_trigger
1118 $self->_set_found_trigger
1120 Finds the most recent lost item charge for this item and refunds the patron
1121 appropriately, taking into account any payments or writeoffs already applied
1124 Internal function, not exported, called only by Koha::Item->store.
1128 sub _set_found_trigger {
1129 my ( $self, $pre_mod_item ) = @_;
1131 # Reverse any lost item charges if necessary.
1132 my $no_refund_after_days =
1133 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1134 if ($no_refund_after_days) {
1135 my $today = dt_from_string();
1136 my $lost_age_in_days =
1137 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1140 return $self unless $lost_age_in_days < $no_refund_after_days;
1143 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1146 return_branch => C4::Context->userenv
1147 ? C4::Context->userenv->{'branch'}
1151 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1153 if ( $lostreturn_policy ) {
1155 # refund charge made for lost book
1156 my $lost_charge = Koha::Account::Lines->search(
1158 itemnumber => $self->itemnumber,
1159 debit_type_code => 'LOST',
1160 status => [ undef, { '<>' => 'FOUND' } ]
1163 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1168 if ( $lost_charge ) {
1170 my $patron = $lost_charge->patron;
1173 my $account = $patron->account;
1175 # Credit outstanding amount
1176 my $credit_total = $lost_charge->amountoutstanding;
1180 $lost_charge->amount > $lost_charge->amountoutstanding &&
1181 $lostreturn_policy ne "refund_unpaid"
1183 # some amount has been cancelled. collect the offsets that are not writeoffs
1184 # this works because the only way to subtract from this kind of a debt is
1185 # using the UI buttons 'Pay' and 'Write off'
1187 # We don't credit any payments if return policy is
1190 # In that case only unpaid/outstanding amount
1191 # will be credited which settles the debt without
1192 # creating extra credits
1194 my $credit_offsets = $lost_charge->debit_offsets(
1196 'credit_id' => { '!=' => undef },
1197 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1199 { join => 'credit' }
1202 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1203 # credits are negative on the DB
1204 $credit_offsets->total * -1 :
1206 # Credit the outstanding amount, then add what has been
1207 # paid to create a net credit for this amount
1208 $credit_total += $total_to_refund;
1212 if ( $credit_total > 0 ) {
1214 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1215 $credit = $account->add_credit(
1217 amount => $credit_total,
1218 description => 'Item found ' . $self->itemnumber,
1219 type => 'LOST_FOUND',
1220 interface => C4::Context->interface,
1221 library_id => $branchcode,
1222 item_id => $self->itemnumber,
1223 issue_id => $lost_charge->issue_id
1227 $credit->apply( { debits => [$lost_charge] } );
1231 message => 'lost_refunded',
1232 payload => { credit_id => $credit->id }
1237 # Update the account status
1238 $lost_charge->status('FOUND');
1239 $lost_charge->store();
1241 # Reconcile balances if required
1242 if ( C4::Context->preference('AccountAutoReconcile') ) {
1243 $account->reconcile_balance;
1248 # possibly restore fine for lost book
1249 my $lost_overdue = Koha::Account::Lines->search(
1251 itemnumber => $self->itemnumber,
1252 debit_type_code => 'OVERDUE',
1256 order_by => { '-desc' => 'date' },
1260 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1262 my $patron = $lost_overdue->patron;
1264 my $account = $patron->account;
1266 # Update status of fine
1267 $lost_overdue->status('FOUND')->store();
1269 # Find related forgive credit
1270 my $refund = $lost_overdue->credits(
1272 credit_type_code => 'FORGIVEN',
1273 itemnumber => $self->itemnumber,
1274 status => [ { '!=' => 'VOID' }, undef ]
1276 { order_by => { '-desc' => 'date' }, rows => 1 }
1280 # Revert the forgive credit
1281 $refund->void({ interface => 'trigger' });
1285 message => 'lost_restored',
1286 payload => { refund_id => $refund->id }
1291 # Reconcile balances if required
1292 if ( C4::Context->preference('AccountAutoReconcile') ) {
1293 $account->reconcile_balance;
1297 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1301 message => 'lost_charge',
1307 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1309 if ( $processingreturn_policy ) {
1311 # refund processing charge made for lost book
1312 my $processing_charge = Koha::Account::Lines->search(
1314 itemnumber => $self->itemnumber,
1315 debit_type_code => 'PROCESSING',
1316 status => [ undef, { '<>' => 'FOUND' } ]
1319 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1324 if ( $processing_charge ) {
1326 my $patron = $processing_charge->patron;
1329 my $account = $patron->account;
1331 # Credit outstanding amount
1332 my $credit_total = $processing_charge->amountoutstanding;
1336 $processing_charge->amount > $processing_charge->amountoutstanding &&
1337 $processingreturn_policy ne "refund_unpaid"
1339 # some amount has been cancelled. collect the offsets that are not writeoffs
1340 # this works because the only way to subtract from this kind of a debt is
1341 # using the UI buttons 'Pay' and 'Write off'
1343 # We don't credit any payments if return policy is
1346 # In that case only unpaid/outstanding amount
1347 # will be credited which settles the debt without
1348 # creating extra credits
1350 my $credit_offsets = $processing_charge->debit_offsets(
1352 'credit_id' => { '!=' => undef },
1353 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1355 { join => 'credit' }
1358 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1359 # credits are negative on the DB
1360 $credit_offsets->total * -1 :
1362 # Credit the outstanding amount, then add what has been
1363 # paid to create a net credit for this amount
1364 $credit_total += $total_to_refund;
1368 if ( $credit_total > 0 ) {
1370 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1371 $credit = $account->add_credit(
1373 amount => $credit_total,
1374 description => 'Item found ' . $self->itemnumber,
1375 type => 'PROCESSING_FOUND',
1376 interface => C4::Context->interface,
1377 library_id => $branchcode,
1378 item_id => $self->itemnumber,
1379 issue_id => $processing_charge->issue_id
1383 $credit->apply( { debits => [$processing_charge] } );
1387 message => 'processing_refunded',
1388 payload => { credit_id => $credit->id }
1393 # Update the account status
1394 $processing_charge->status('FOUND');
1395 $processing_charge->store();
1397 # Reconcile balances if required
1398 if ( C4::Context->preference('AccountAutoReconcile') ) {
1399 $account->reconcile_balance;
1408 =head3 public_read_list
1410 This method returns the list of publicly readable database fields for both API and UI output purposes
1414 sub public_read_list {
1416 'itemnumber', 'biblionumber', 'homebranch',
1417 'holdingbranch', 'location', 'collectioncode',
1418 'itemcallnumber', 'copynumber', 'enumchron',
1419 'barcode', 'dateaccessioned', 'itemnotes',
1420 'onloan', 'uri', 'itype',
1421 'notforloan', 'damaged', 'itemlost',
1422 'withdrawn', 'restricted'
1428 Overloaded to_api method to ensure item-level itypes is adhered to.
1433 my ($self, $params) = @_;
1435 my $response = $self->SUPER::to_api($params);
1438 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1439 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1441 return { %$response, %$overrides };
1444 =head3 to_api_mapping
1446 This method returns the mapping for representing a Koha::Item object
1451 sub to_api_mapping {
1453 itemnumber => 'item_id',
1454 biblionumber => 'biblio_id',
1455 biblioitemnumber => undef,
1456 barcode => 'external_id',
1457 dateaccessioned => 'acquisition_date',
1458 booksellerid => 'acquisition_source',
1459 homebranch => 'home_library_id',
1460 price => 'purchase_price',
1461 replacementprice => 'replacement_price',
1462 replacementpricedate => 'replacement_price_date',
1463 datelastborrowed => 'last_checkout_date',
1464 datelastseen => 'last_seen_date',
1466 notforloan => 'not_for_loan_status',
1467 damaged => 'damaged_status',
1468 damaged_on => 'damaged_date',
1469 itemlost => 'lost_status',
1470 itemlost_on => 'lost_date',
1471 withdrawn => 'withdrawn',
1472 withdrawn_on => 'withdrawn_date',
1473 itemcallnumber => 'callnumber',
1474 coded_location_qualifier => 'coded_location_qualifier',
1475 issues => 'checkouts_count',
1476 renewals => 'renewals_count',
1477 reserves => 'holds_count',
1478 restricted => 'restricted_status',
1479 itemnotes => 'public_notes',
1480 itemnotes_nonpublic => 'internal_notes',
1481 holdingbranch => 'holding_library_id',
1482 timestamp => 'timestamp',
1483 location => 'location',
1484 permanent_location => 'permanent_location',
1485 onloan => 'checked_out_date',
1486 cn_source => 'call_number_source',
1487 cn_sort => 'call_number_sort',
1488 ccode => 'collection_code',
1489 materials => 'materials_notes',
1491 itype => 'item_type_id',
1492 more_subfields_xml => 'extended_subfields',
1493 enumchron => 'serial_issue_number',
1494 copynumber => 'copy_number',
1495 stocknumber => 'inventory_number',
1496 new_status => 'new_status',
1497 deleted_on => undef,
1503 my $itemtype = $item->itemtype;
1505 Returns Koha object for effective itemtype
1512 return Koha::ItemTypes->find( $self->effective_itemtype );
1517 my $orders = $item->orders();
1519 Returns a Koha::Acquisition::Orders object
1526 my $orders = $self->_result->item_orders;
1527 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1530 =head3 tracked_links
1532 my $tracked_links = $item->tracked_links();
1534 Returns a Koha::TrackedLinks object
1541 my $tracked_links = $self->_result->linktrackers;
1542 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1545 =head3 move_to_biblio
1547 $item->move_to_biblio($to_biblio[, $params]);
1549 Move the item to another biblio and update any references in other tables.
1551 The final optional parameter, C<$params>, is expected to contain the
1552 'skip_record_index' key, which is relayed down to Koha::Item->store.
1553 There it prevents calling index_records, which takes most of the
1554 time in batch adds/deletes. The caller must take care of calling
1555 index_records separately.
1558 skip_record_index => 1|0
1560 Returns undef if the move failed or the biblionumber of the destination record otherwise
1564 sub move_to_biblio {
1565 my ( $self, $to_biblio, $params ) = @_;
1569 return if $self->biblionumber == $to_biblio->biblionumber;
1571 my $from_biblionumber = $self->biblionumber;
1572 my $to_biblionumber = $to_biblio->biblionumber;
1574 # Own biblionumber and biblioitemnumber
1576 biblionumber => $to_biblionumber,
1577 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1578 })->store({ skip_record_index => $params->{skip_record_index} });
1580 unless ($params->{skip_record_index}) {
1581 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1582 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1585 # Acquisition orders
1586 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1589 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1591 # hold_fill_target (there's no Koha object available yet)
1592 my $hold_fill_target = $self->_result->hold_fill_target;
1593 if ($hold_fill_target) {
1594 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1597 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1598 # and can't even fake one since the significant columns are nullable.
1599 my $storage = $self->_result->result_source->storage;
1602 my ($storage, $dbh, @cols) = @_;
1604 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1609 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1611 return $to_biblionumber;
1616 my $bundle_items = $item->bundle_items;
1618 Returns the items associated with this bundle
1625 my $rs = $self->_result->bundle_items;
1626 return Koha::Items->_new_from_dbic($rs);
1631 my $is_bundle = $item->is_bundle;
1633 Returns whether the item is a bundle or not
1639 return $self->bundle_items->count ? 1 : 0;
1644 my $bundle = $item->bundle_host;
1646 Returns the bundle item this item is attached to
1653 my $bundle_items_rs = $self->_result->item_bundles_item;
1654 return unless $bundle_items_rs;
1655 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1660 my $in_bundle = $item->in_bundle;
1662 Returns whether this item is currently in a bundle
1668 return $self->bundle_host ? 1 : 0;
1671 =head3 add_to_bundle
1673 my $link = $item->add_to_bundle($bundle_item);
1675 Adds the bundle_item passed to this item
1680 my ( $self, $bundle_item, $options ) = @_;
1684 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1685 if ( $self->itemnumber eq $bundle_item->itemnumber
1686 || $bundle_item->is_bundle
1687 || $self->in_bundle );
1689 my $schema = Koha::Database->new->schema;
1691 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1696 my $checkout = $bundle_item->checkout;
1698 unless ($options->{force_checkin}) {
1699 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1702 my $branchcode = C4::Context->userenv->{'branch'};
1703 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1705 Koha::Exceptions::Checkin::FailedCheckin->throw();
1709 my $holds = $bundle_item->current_holds;
1710 if ($holds->count) {
1711 unless ($options->{ignore_holds}) {
1712 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1716 $self->_result->add_to_item_bundles_hosts(
1717 { item => $bundle_item->itemnumber } );
1719 $bundle_item->notforloan($BundleNotLoanValue)->store();
1725 # 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
1726 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1727 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1729 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1730 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1731 Koha::Exceptions::Object::FKConstraint->throw(
1732 error => 'Broken FK constraint',
1733 broken_fk => $+{column}
1738 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1740 Koha::Exceptions::Object::DuplicateID->throw(
1741 error => 'Duplicate ID',
1742 duplicate_id => $+{key}
1745 elsif ( $_->{msg} =~
1746 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1748 { # The optional \W in the regex might be a quote or backtick
1749 my $type = $+{type};
1750 my $value = $+{value};
1751 my $property = $+{property};
1752 $property =~ s/['`]//g;
1753 Koha::Exceptions::Object::BadValue->throw(
1756 property => $property =~ /(\w+\.\w+)$/
1759 , # results in table.column without quotes or backtics
1763 # Catch-all for foreign key breakages. It will help find other use cases
1772 =head3 remove_from_bundle
1774 Remove this item from any bundle it may have been attached to.
1778 sub remove_from_bundle {
1781 my $bundle_item_rs = $self->_result->item_bundles_item;
1782 if ( $bundle_item_rs ) {
1783 $bundle_item_rs->delete;
1784 $self->notforloan(0)->store();
1790 =head2 Internal methods
1792 =head3 _after_item_action_hooks
1794 Helper method that takes care of calling all plugin hooks
1798 sub _after_item_action_hooks {
1799 my ( $self, $params ) = @_;
1801 my $action = $params->{action};
1803 Koha::Plugins->call(
1804 'after_item_action',
1808 item_id => $self->itemnumber,
1815 my $recall = $item->recall;
1817 Return the relevant recall for this item
1823 my @recalls = Koha::Recalls->search(
1825 biblio_id => $self->biblionumber,
1828 { order_by => { -asc => 'created_date' } }
1830 foreach my $recall (@recalls) {
1831 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1835 # no item-level recall to return, so return earliest biblio-level
1836 # FIXME: eventually this will be based on priority
1840 =head3 can_be_recalled
1842 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1844 Does item-level checks and returns if items can be recalled by this borrower
1848 sub can_be_recalled {
1849 my ( $self, $params ) = @_;
1851 return 0 if !( C4::Context->preference('UseRecalls') );
1853 # check if this item is not for loan, withdrawn or lost
1854 return 0 if ( $self->notforloan != 0 );
1855 return 0 if ( $self->itemlost != 0 );
1856 return 0 if ( $self->withdrawn != 0 );
1858 # check if this item is not checked out - if not checked out, can't be recalled
1859 return 0 if ( !defined( $self->checkout ) );
1861 my $patron = $params->{patron};
1863 my $branchcode = C4::Context->userenv->{'branch'};
1865 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1868 # Check the circulation rule for each relevant itemtype for this item
1869 my $rule = Koha::CirculationRules->get_effective_rules({
1870 branchcode => $branchcode,
1871 categorycode => $patron ? $patron->categorycode : undef,
1872 itemtype => $self->effective_itemtype,
1875 'recalls_per_record',
1880 # check recalls allowed has been set and is not zero
1881 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1884 # check borrower has not reached open recalls allowed limit
1885 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1887 # check borrower has not reach open recalls allowed per record limit
1888 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1890 # check if this patron has already recalled this item
1891 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1893 # check if this patron has already checked out this item
1894 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1896 # check if this patron has already reserved this item
1897 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1900 # check item availability
1901 # items are unavailable for recall if they are lost, withdrawn or notforloan
1902 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1904 # if there are no available items at all, no recall can be placed
1905 return 0 if ( scalar @items == 0 );
1907 my $checked_out_count = 0;
1909 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1912 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1913 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1915 # can't recall if no items have been checked out
1916 return 0 if ( $checked_out_count == 0 );
1922 =head3 can_be_waiting_recall
1924 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1926 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1927 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1931 sub can_be_waiting_recall {
1934 return 0 if !( C4::Context->preference('UseRecalls') );
1936 # check if this item is not for loan, withdrawn or lost
1937 return 0 if ( $self->notforloan != 0 );
1938 return 0 if ( $self->itemlost != 0 );
1939 return 0 if ( $self->withdrawn != 0 );
1941 my $branchcode = $self->holdingbranch;
1942 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1943 $branchcode = C4::Context->userenv->{'branch'};
1945 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1948 # Check the circulation rule for each relevant itemtype for this item
1949 my $most_relevant_recall = $self->check_recalls;
1950 my $rule = Koha::CirculationRules->get_effective_rules(
1952 branchcode => $branchcode,
1953 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1954 itemtype => $self->effective_itemtype,
1955 rules => [ 'recalls_allowed', ],
1959 # check recalls allowed has been set and is not zero
1960 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1966 =head3 check_recalls
1968 my $recall = $item->check_recalls;
1970 Get the most relevant recall for this item.
1977 my @recalls = Koha::Recalls->search(
1978 { biblio_id => $self->biblionumber,
1979 item_id => [ $self->itemnumber, undef ]
1981 { order_by => { -asc => 'created_date' } }
1982 )->filter_by_current->as_list;
1985 # iterate through relevant recalls to find the best one.
1986 # if we come across a waiting recall, use this one.
1987 # 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.
1988 foreach my $r ( @recalls ) {
1989 if ( $r->waiting ) {
1994 unless ( defined $recall ) {
1995 $recall = $recalls[0];
2001 =head3 is_notforloan
2003 my $is_notforloan = $item->is_notforloan;
2005 Determine whether or not this item is "notforloan" based on
2006 the item's notforloan status or its item type
2012 my $is_notforloan = 0;
2014 if ( $self->notforloan ){
2018 my $itemtype = $self->itemtype;
2020 if ( $itemtype->notforloan ){
2026 return $is_notforloan;
2029 =head3 is_denied_renewal
2031 my $is_denied_renewal = $item->is_denied_renewal;
2033 Determine whether or not this item can be renewed based on the
2034 rules set in the ItemsDeniedRenewal system preference.
2038 sub is_denied_renewal {
2040 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2041 return 0 unless $denyingrules;
2042 foreach my $field (keys %$denyingrules) {
2043 # Silently ignore bad column names; TODO we should validate elsewhere
2044 next if !$self->_result->result_source->has_column($field);
2045 my $val = $self->$field;
2046 if( !defined $val) {
2047 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2050 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2051 # If the results matches the values in the syspref
2052 # We return true if match found
2061 Returns a map of column name to string representations including the string,
2062 the mapping type and the mapping category where appropriate.
2064 Currently handles authorised value mappings, library, callnumber and itemtype
2067 Accepts a param hashref where the 'public' key denotes whether we want the public
2068 or staff client strings.
2073 my ( $self, $params ) = @_;
2074 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2075 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2076 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2078 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2080 # Hardcoded known 'authorised_value' values mapped to API codes
2081 my $code_to_type = {
2082 branches => 'library',
2083 cn_source => 'call_number_source',
2084 itemtypes => 'item_type',
2087 # Handle not null and default values for integers and dates
2090 foreach my $col ( @{$self->_columns} ) {
2092 # By now, we are done with known columns, now check the framework for mappings
2093 my $field = $self->_result->result_source->name . '.' . $col;
2095 # Check there's an entry in the MARC subfield structure for the field
2096 if ( exists $mss->{$field}
2097 && scalar @{ $mss->{$field} } > 0
2098 && $mss->{$field}[0]->{authorised_value} )
2100 my $subfield = $mss->{$field}[0];
2101 my $code = $subfield->{authorised_value};
2103 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2104 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2105 $strings->{$col} = {
2108 ( $type eq 'av' ? ( category => $code ) : () ),
2126 Kyle M Hall <kyle@bywatersolutions.com>