3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::Exceptions::Item::Transfer;
40 use Koha::Item::Attributes;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Item::Transfer::Limits;
43 use Koha::Item::Transfers;
49 use Koha::Result::Boolean;
50 use Koha::SearchEngine::Indexer;
51 use Koha::StockRotationItem;
52 use Koha::StockRotationRotas;
53 use Koha::TrackedLinks;
55 use base qw(Koha::Object);
59 Koha::Item - Koha Item object class
71 $params can take an optional 'skip_record_index' parameter.
72 If set, the reindexation process will not happen (index_records not called)
74 NOTE: This is a temporary fix to answer a performance issue when lot of items
75 are added (or modified) at the same time.
76 The correct way to fix this is to make the ES reindexation process async.
77 You should not turn it on if you do not understand what it is doing exactly.
83 my $params = @_ ? shift : {};
85 my $log_action = $params->{log_action} // 1;
87 # We do not want to oblige callers to pass this value
88 # Dev conveniences vs performance?
89 unless ( $self->biblioitemnumber ) {
90 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
93 # See related changes from C4::Items::AddItem
94 unless ( $self->itype ) {
95 $self->itype($self->biblio->biblioitem->itemtype);
98 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
100 my $today = dt_from_string;
101 my $action = 'create';
103 unless ( $self->in_storage ) { #AddItem
105 unless ( $self->permanent_location ) {
106 $self->permanent_location($self->location);
109 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
110 unless ( $self->location || !$default_location ) {
111 $self->permanent_location( $self->location || $default_location )
112 unless $self->permanent_location;
113 $self->location($default_location);
116 unless ( $self->replacementpricedate ) {
117 $self->replacementpricedate($today);
119 unless ( $self->datelastseen ) {
120 $self->datelastseen($today);
123 unless ( $self->dateaccessioned ) {
124 $self->dateaccessioned($today);
127 if ( $self->itemcallnumber
128 or $self->cn_source )
130 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
131 $self->cn_sort($cn_sort);
138 my %updated_columns = $self->_result->get_dirty_columns;
139 return $self->SUPER::store unless %updated_columns;
141 # Retrieve the item for comparison if we need to
143 exists $updated_columns{itemlost}
144 or exists $updated_columns{withdrawn}
145 or exists $updated_columns{damaged}
146 ) ? $self->get_from_storage : undef;
148 # Update *_on fields if needed
149 # FIXME: Why not for AddItem as well?
150 my @fields = qw( itemlost withdrawn damaged );
151 for my $field (@fields) {
153 # If the field is defined but empty or 0, we are
154 # removing/unsetting and thus need to clear out
156 if ( exists $updated_columns{$field}
157 && defined( $self->$field )
160 my $field_on = "${field}_on";
161 $self->$field_on(undef);
163 # If the field has changed otherwise, we much update
165 elsif (exists $updated_columns{$field}
166 && $updated_columns{$field}
167 && !$pre_mod_item->$field )
169 my $field_on = "${field}_on";
170 $self->$field_on(dt_from_string);
174 if ( exists $updated_columns{itemcallnumber}
175 or exists $updated_columns{cn_source} )
177 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
178 $self->cn_sort($cn_sort);
182 if ( exists $updated_columns{location}
183 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
184 and not exists $updated_columns{permanent_location} )
186 $self->permanent_location( $self->location );
189 # If item was lost and has now been found,
190 # reverse any list item charges if necessary.
191 if ( exists $updated_columns{itemlost}
192 and $updated_columns{itemlost} <= 0
193 and $pre_mod_item->itemlost > 0 )
195 $self->_set_found_trigger($pre_mod_item);
200 my $result = $self->SUPER::store;
201 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
203 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
204 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
206 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
207 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
208 unless $params->{skip_record_index};
209 $self->get_from_storage->_after_item_action_hooks({ action => $action });
211 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
213 biblio_ids => [ $self->biblionumber ]
215 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
226 my $params = @_ ? shift : {};
228 # FIXME check the item has no current issues
229 # i.e. raise the appropriate exception
231 # Get the item group so we can delete it later if it has no items left
232 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
234 my $result = $self->SUPER::delete;
236 # Delete the item gorup if it has no items left
237 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
239 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
240 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
241 unless $params->{skip_record_index};
243 $self->_after_item_action_hooks({ action => 'delete' });
245 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
246 if C4::Context->preference("CataloguingLog");
248 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
250 biblio_ids => [ $self->biblionumber ]
252 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
263 my $params = @_ ? shift : {};
265 my $safe_to_delete = $self->safe_to_delete;
266 return $safe_to_delete unless $safe_to_delete;
268 $self->move_to_deleted;
270 return $self->delete($params);
273 =head3 safe_to_delete
275 returns 1 if the item is safe to delete,
277 "book_on_loan" if the item is checked out,
279 "not_same_branch" if the item is blocked by independent branches,
281 "book_reserved" if the there are holds aganst the item, or
283 "linked_analytics" if the item has linked analytic records.
285 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
294 $error = "book_on_loan" if $self->checkout;
296 $error //= "not_same_branch"
297 if defined C4::Context->userenv
298 and defined C4::Context->userenv->{number}
299 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_item( $self );
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 borrower to return an item.
604 Accepts and returns Koha::Patron objects
606 $item->last_returned_by( $borrowernumber );
608 $last_returned_by = $item->last_returned_by();
612 sub last_returned_by {
613 my ( $self, $borrower ) = @_;
615 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
618 return $items_last_returned_by_rs->update_or_create(
619 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
622 unless ( $self->{_last_returned_by} ) {
623 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
625 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
629 return $self->{_last_returned_by};
633 =head3 can_article_request
635 my $bool = $item->can_article_request( $borrower )
637 Returns true if item can be specifically requested
639 $borrower must be a Koha::Patron object
643 sub can_article_request {
644 my ( $self, $borrower ) = @_;
646 my $rule = $self->article_request_type($borrower);
648 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
652 =head3 hidden_in_opac
654 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
656 Returns true if item fields match the hidding criteria defined in $rules.
657 Returns false otherwise.
659 Takes HASHref that can have the following parameters:
661 $rules : { <field> => [ value_1, ... ], ... }
663 Note: $rules inherits its structure from the parsed YAML from reading
664 the I<OpacHiddenItems> system preference.
669 my ( $self, $params ) = @_;
671 my $rules = $params->{rules} // {};
674 if C4::Context->preference('hidelostitems') and
677 my $hidden_in_opac = 0;
679 foreach my $field ( keys %{$rules} ) {
681 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
687 return $hidden_in_opac;
690 =head3 can_be_transferred
692 $item->can_be_transferred({ to => $to_library, from => $from_library })
693 Checks if an item can be transferred to given library.
695 This feature is controlled by two system preferences:
696 UseBranchTransferLimits to enable / disable the feature
697 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
698 for setting the limitations
700 Takes HASHref that can have the following parameters:
701 MANDATORY PARAMETERS:
704 $from : Koha::Library # if not given, item holdingbranch
705 # will be used instead
707 Returns 1 if item can be transferred to $to_library, otherwise 0.
709 To find out whether at least one item of a Koha::Biblio can be transferred, please
710 see Koha::Biblio->can_be_transferred() instead of using this method for
711 multiple items of the same biblio.
715 sub can_be_transferred {
716 my ($self, $params) = @_;
718 my $to = $params->{to};
719 my $from = $params->{from};
721 $to = $to->branchcode;
722 $from = defined $from ? $from->branchcode : $self->holdingbranch;
724 return 1 if $from eq $to; # Transfer to current branch is allowed
725 return 1 unless C4::Context->preference('UseBranchTransferLimits');
727 my $limittype = C4::Context->preference('BranchTransferLimitsType');
728 return Koha::Item::Transfer::Limits->search({
731 $limittype => $limittype eq 'itemtype'
732 ? $self->effective_itemtype : $self->ccode
737 =head3 pickup_locations
739 $pickup_locations = $item->pickup_locations( {patron => $patron } )
741 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
742 and if item can be transferred to each pickup location.
746 sub pickup_locations {
747 my ($self, $params) = @_;
749 my $patron = $params->{patron};
751 my $circ_control_branch =
752 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
754 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
756 if(defined $patron) {
757 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
758 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
761 my $pickup_libraries = Koha::Libraries->search();
762 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
763 $pickup_libraries = $self->home_branch->get_hold_libraries;
764 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
765 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
766 $pickup_libraries = $plib->get_hold_libraries;
767 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
768 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
769 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
770 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
773 return $pickup_libraries->search(
778 order_by => ['branchname']
780 ) unless C4::Context->preference('UseBranchTransferLimits');
782 my $limittype = C4::Context->preference('BranchTransferLimitsType');
783 my ($ccode, $itype) = (undef, undef);
784 if( $limittype eq 'ccode' ){
785 $ccode = $self->ccode;
787 $itype = $self->itype;
789 my $limits = Koha::Item::Transfer::Limits->search(
791 fromBranch => $self->holdingbranch,
795 { columns => ['toBranch'] }
798 return $pickup_libraries->search(
800 pickup_location => 1,
802 '-not_in' => $limits->_resultset->as_query
806 order_by => ['branchname']
811 =head3 article_request_type
813 my $type = $item->article_request_type( $borrower )
815 returns 'yes', 'no', 'bib_only', or 'item_only'
817 $borrower must be a Koha::Patron object
821 sub article_request_type {
822 my ( $self, $borrower ) = @_;
824 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
826 $branch_control eq 'homebranch' ? $self->homebranch
827 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
829 my $borrowertype = $borrower->categorycode;
830 my $itemtype = $self->effective_itemtype();
831 my $rule = Koha::CirculationRules->get_effective_rule(
833 rule_name => 'article_requests',
834 categorycode => $borrowertype,
835 itemtype => $itemtype,
836 branchcode => $branchcode
840 return q{} unless $rule;
841 return $rule->rule_value || q{}
850 my $attributes = { order_by => 'priority' };
851 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
853 itemnumber => $self->itemnumber,
856 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
857 waitingdate => { '!=' => undef },
860 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
861 return Koha::Holds->_new_from_dbic($hold_rs);
864 =head3 stockrotationitem
866 my $sritem = Koha::Item->stockrotationitem;
868 Returns the stock rotation item associated with the current item.
872 sub stockrotationitem {
874 my $rs = $self->_result->stockrotationitem;
876 return Koha::StockRotationItem->_new_from_dbic( $rs );
881 my $item = $item->add_to_rota($rota_id);
883 Add this item to the rota identified by $ROTA_ID, which means associating it
884 with the first stage of that rota. Should this item already be associated
885 with a rota, then we will move it to the new rota.
890 my ( $self, $rota_id ) = @_;
891 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
895 =head3 has_pending_hold
897 my $is_pending_hold = $item->has_pending_hold();
899 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
903 sub has_pending_hold {
905 my $pending_hold = $self->_result->tmp_holdsqueues;
906 return $pending_hold->count ? 1: 0;
909 =head3 has_pending_recall {
911 my $has_pending_recall
913 Return if whether has pending recall of not.
917 sub has_pending_recall {
920 # FIXME Must be moved to $self->recalls
921 return Koha::Recalls->search(
923 item_id => $self->itemnumber,
931 my $field = $item->as_marc_field;
933 This method returns a MARC::Field object representing the Koha::Item object
934 with the current mappings configuration.
941 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
943 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
947 my $item_field = $tagslib->{$itemtag};
949 my $more_subfields = $self->additional_attributes->to_hashref;
950 foreach my $subfield (
952 $a->{display_order} <=> $b->{display_order}
953 || $a->{subfield} cmp $b->{subfield}
954 } grep { ref($_) && %$_ } values %$item_field
957 my $kohafield = $subfield->{kohafield};
958 my $tagsubfield = $subfield->{tagsubfield};
960 if ( defined $kohafield && $kohafield ne '' ) {
961 next if $kohafield !~ m{^items\.}; # That would be weird!
962 ( my $attribute = $kohafield ) =~ s|^items\.||;
963 $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
964 if defined $self->$attribute and $self->$attribute ne '';
966 $value = $more_subfields->{$tagsubfield}
969 next unless defined $value
972 if ( $subfield->{repeatable} ) {
973 my @values = split '\|', $value;
974 push @subfields, ( $tagsubfield => $_ ) for @values;
977 push @subfields, ( $tagsubfield => $value );
982 return unless @subfields;
984 return MARC::Field->new(
985 "$itemtag", ' ', ' ', @subfields
989 =head3 renewal_branchcode
991 Returns the branchcode to be recorded in statistics renewal of the item
995 sub renewal_branchcode {
997 my ($self, $params ) = @_;
999 my $interface = C4::Context->interface;
1001 if ( $interface eq 'opac' ){
1002 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1003 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1004 $branchcode = 'OPACRenew';
1006 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1007 $branchcode = $self->homebranch;
1009 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1010 $branchcode = $self->checkout->patron->branchcode;
1012 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1013 $branchcode = $self->checkout->branchcode;
1019 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1020 ? C4::Context->userenv->{branch} : $params->{branch};
1027 Return the cover images associated with this item.
1034 my $cover_image_rs = $self->_result->cover_images;
1035 return unless $cover_image_rs;
1036 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1039 =head3 columns_to_str
1041 my $values = $items->columns_to_str;
1043 Return a hashref with the string representation of the different attribute of the item.
1045 This is meant to be used for display purpose only.
1049 sub columns_to_str {
1052 my $frameworkcode = $self->biblio->frameworkcode;
1053 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1054 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1056 my $columns_info = $self->_result->result_source->columns_info;
1058 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1060 for my $column ( keys %$columns_info ) {
1062 next if $column eq 'more_subfields_xml';
1064 my $value = $self->$column;
1065 # 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
1067 if ( not defined $value or $value eq "" ) {
1068 $values->{$column} = $value;
1073 exists $mss->{"items.$column"}
1074 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1077 $values->{$column} =
1079 ? $subfield->{authorised_value}
1080 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1081 $subfield->{tagsubfield}, $value, '', $tagslib )
1087 $self->more_subfields_xml
1088 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1093 my ( $field ) = $marc_more->fields;
1094 for my $sf ( $field->subfields ) {
1095 my $subfield_code = $sf->[0];
1096 my $value = $sf->[1];
1097 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1098 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1100 $subfield->{authorised_value}
1101 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1102 $subfield->{tagsubfield}, $value, '', $tagslib )
1105 push @{$more_values->{$subfield_code}}, $value;
1108 while ( my ( $k, $v ) = each %$more_values ) {
1109 $values->{$k} = join ' | ', @$v;
1116 =head3 additional_attributes
1118 my $attributes = $item->additional_attributes;
1119 $attributes->{k} = 'new k';
1120 $item->update({ more_subfields => $attributes->to_marcxml });
1122 Returns a Koha::Item::Attributes object that represents the non-mapped
1123 attributes for this item.
1127 sub additional_attributes {
1130 return Koha::Item::Attributes->new_from_marcxml(
1131 $self->more_subfields_xml,
1135 =head3 _set_found_trigger
1137 $self->_set_found_trigger
1139 Finds the most recent lost item charge for this item and refunds the patron
1140 appropriately, taking into account any payments or writeoffs already applied
1143 Internal function, not exported, called only by Koha::Item->store.
1147 sub _set_found_trigger {
1148 my ( $self, $pre_mod_item ) = @_;
1150 # Reverse any lost item charges if necessary.
1151 my $no_refund_after_days =
1152 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1153 if ($no_refund_after_days) {
1154 my $today = dt_from_string();
1155 my $lost_age_in_days =
1156 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1159 return $self unless $lost_age_in_days < $no_refund_after_days;
1162 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1165 return_branch => C4::Context->userenv
1166 ? C4::Context->userenv->{'branch'}
1170 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1172 if ( $lostreturn_policy ) {
1174 # refund charge made for lost book
1175 my $lost_charge = Koha::Account::Lines->search(
1177 itemnumber => $self->itemnumber,
1178 debit_type_code => 'LOST',
1179 status => [ undef, { '<>' => 'FOUND' } ]
1182 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1187 if ( $lost_charge ) {
1189 my $patron = $lost_charge->patron;
1192 my $account = $patron->account;
1194 # Credit outstanding amount
1195 my $credit_total = $lost_charge->amountoutstanding;
1199 $lost_charge->amount > $lost_charge->amountoutstanding &&
1200 $lostreturn_policy ne "refund_unpaid"
1202 # some amount has been cancelled. collect the offsets that are not writeoffs
1203 # this works because the only way to subtract from this kind of a debt is
1204 # using the UI buttons 'Pay' and 'Write off'
1206 # We don't credit any payments if return policy is
1209 # In that case only unpaid/outstanding amount
1210 # will be credited which settles the debt without
1211 # creating extra credits
1213 my $credit_offsets = $lost_charge->debit_offsets(
1215 'credit_id' => { '!=' => undef },
1216 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1218 { join => 'credit' }
1221 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1222 # credits are negative on the DB
1223 $credit_offsets->total * -1 :
1225 # Credit the outstanding amount, then add what has been
1226 # paid to create a net credit for this amount
1227 $credit_total += $total_to_refund;
1231 if ( $credit_total > 0 ) {
1233 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1234 $credit = $account->add_credit(
1236 amount => $credit_total,
1237 description => 'Item found ' . $self->itemnumber,
1238 type => 'LOST_FOUND',
1239 interface => C4::Context->interface,
1240 library_id => $branchcode,
1241 item_id => $self->itemnumber,
1242 issue_id => $lost_charge->issue_id
1246 $credit->apply( { debits => [$lost_charge] } );
1250 message => 'lost_refunded',
1251 payload => { credit_id => $credit->id }
1256 # Update the account status
1257 $lost_charge->status('FOUND');
1258 $lost_charge->store();
1260 # Reconcile balances if required
1261 if ( C4::Context->preference('AccountAutoReconcile') ) {
1262 $account->reconcile_balance;
1267 # possibly restore fine for lost book
1268 my $lost_overdue = Koha::Account::Lines->search(
1270 itemnumber => $self->itemnumber,
1271 debit_type_code => 'OVERDUE',
1275 order_by => { '-desc' => 'date' },
1279 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1281 my $patron = $lost_overdue->patron;
1283 my $account = $patron->account;
1285 # Update status of fine
1286 $lost_overdue->status('FOUND')->store();
1288 # Find related forgive credit
1289 my $refund = $lost_overdue->credits(
1291 credit_type_code => 'FORGIVEN',
1292 itemnumber => $self->itemnumber,
1293 status => [ { '!=' => 'VOID' }, undef ]
1295 { order_by => { '-desc' => 'date' }, rows => 1 }
1299 # Revert the forgive credit
1300 $refund->void({ interface => 'trigger' });
1304 message => 'lost_restored',
1305 payload => { refund_id => $refund->id }
1310 # Reconcile balances if required
1311 if ( C4::Context->preference('AccountAutoReconcile') ) {
1312 $account->reconcile_balance;
1316 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1320 message => 'lost_charge',
1326 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1328 if ( $processingreturn_policy ) {
1330 # refund processing charge made for lost book
1331 my $processing_charge = Koha::Account::Lines->search(
1333 itemnumber => $self->itemnumber,
1334 debit_type_code => 'PROCESSING',
1335 status => [ undef, { '<>' => 'FOUND' } ]
1338 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1343 if ( $processing_charge ) {
1345 my $patron = $processing_charge->patron;
1348 my $account = $patron->account;
1350 # Credit outstanding amount
1351 my $credit_total = $processing_charge->amountoutstanding;
1355 $processing_charge->amount > $processing_charge->amountoutstanding &&
1356 $processingreturn_policy ne "refund_unpaid"
1358 # some amount has been cancelled. collect the offsets that are not writeoffs
1359 # this works because the only way to subtract from this kind of a debt is
1360 # using the UI buttons 'Pay' and 'Write off'
1362 # We don't credit any payments if return policy is
1365 # In that case only unpaid/outstanding amount
1366 # will be credited which settles the debt without
1367 # creating extra credits
1369 my $credit_offsets = $processing_charge->debit_offsets(
1371 'credit_id' => { '!=' => undef },
1372 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1374 { join => 'credit' }
1377 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1378 # credits are negative on the DB
1379 $credit_offsets->total * -1 :
1381 # Credit the outstanding amount, then add what has been
1382 # paid to create a net credit for this amount
1383 $credit_total += $total_to_refund;
1387 if ( $credit_total > 0 ) {
1389 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1390 $credit = $account->add_credit(
1392 amount => $credit_total,
1393 description => 'Item found ' . $self->itemnumber,
1394 type => 'PROCESSING_FOUND',
1395 interface => C4::Context->interface,
1396 library_id => $branchcode,
1397 item_id => $self->itemnumber,
1398 issue_id => $processing_charge->issue_id
1402 $credit->apply( { debits => [$processing_charge] } );
1406 message => 'processing_refunded',
1407 payload => { credit_id => $credit->id }
1412 # Update the account status
1413 $processing_charge->status('FOUND');
1414 $processing_charge->store();
1416 # Reconcile balances if required
1417 if ( C4::Context->preference('AccountAutoReconcile') ) {
1418 $account->reconcile_balance;
1427 =head3 public_read_list
1429 This method returns the list of publicly readable database fields for both API and UI output purposes
1433 sub public_read_list {
1435 'itemnumber', 'biblionumber', 'homebranch',
1436 'holdingbranch', 'location', 'collectioncode',
1437 'itemcallnumber', 'copynumber', 'enumchron',
1438 'barcode', 'dateaccessioned', 'itemnotes',
1439 'onloan', 'uri', 'itype',
1440 'notforloan', 'damaged', 'itemlost',
1441 'withdrawn', 'restricted'
1447 Overloaded to_api method to ensure item-level itypes is adhered to.
1452 my ($self, $params) = @_;
1454 my $response = $self->SUPER::to_api($params);
1457 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1458 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1460 return { %$response, %$overrides };
1463 =head3 to_api_mapping
1465 This method returns the mapping for representing a Koha::Item object
1470 sub to_api_mapping {
1472 itemnumber => 'item_id',
1473 biblionumber => 'biblio_id',
1474 biblioitemnumber => undef,
1475 barcode => 'external_id',
1476 dateaccessioned => 'acquisition_date',
1477 booksellerid => 'acquisition_source',
1478 homebranch => 'home_library_id',
1479 price => 'purchase_price',
1480 replacementprice => 'replacement_price',
1481 replacementpricedate => 'replacement_price_date',
1482 datelastborrowed => 'last_checkout_date',
1483 datelastseen => 'last_seen_date',
1485 notforloan => 'not_for_loan_status',
1486 damaged => 'damaged_status',
1487 damaged_on => 'damaged_date',
1488 itemlost => 'lost_status',
1489 itemlost_on => 'lost_date',
1490 withdrawn => 'withdrawn',
1491 withdrawn_on => 'withdrawn_date',
1492 itemcallnumber => 'callnumber',
1493 coded_location_qualifier => 'coded_location_qualifier',
1494 issues => 'checkouts_count',
1495 renewals => 'renewals_count',
1496 reserves => 'holds_count',
1497 restricted => 'restricted_status',
1498 itemnotes => 'public_notes',
1499 itemnotes_nonpublic => 'internal_notes',
1500 holdingbranch => 'holding_library_id',
1501 timestamp => 'timestamp',
1502 location => 'location',
1503 permanent_location => 'permanent_location',
1504 onloan => 'checked_out_date',
1505 cn_source => 'call_number_source',
1506 cn_sort => 'call_number_sort',
1507 ccode => 'collection_code',
1508 materials => 'materials_notes',
1510 itype => 'item_type_id',
1511 more_subfields_xml => 'extended_subfields',
1512 enumchron => 'serial_issue_number',
1513 copynumber => 'copy_number',
1514 stocknumber => 'inventory_number',
1515 new_status => 'new_status',
1516 deleted_on => undef,
1522 my $itemtype = $item->itemtype;
1524 Returns Koha object for effective itemtype
1531 return Koha::ItemTypes->find( $self->effective_itemtype );
1536 my $orders = $item->orders();
1538 Returns a Koha::Acquisition::Orders object
1545 my $orders = $self->_result->item_orders;
1546 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1549 =head3 tracked_links
1551 my $tracked_links = $item->tracked_links();
1553 Returns a Koha::TrackedLinks object
1560 my $tracked_links = $self->_result->linktrackers;
1561 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1564 =head3 move_to_biblio
1566 $item->move_to_biblio($to_biblio[, $params]);
1568 Move the item to another biblio and update any references in other tables.
1570 The final optional parameter, C<$params>, is expected to contain the
1571 'skip_record_index' key, which is relayed down to Koha::Item->store.
1572 There it prevents calling index_records, which takes most of the
1573 time in batch adds/deletes. The caller must take care of calling
1574 index_records separately.
1577 skip_record_index => 1|0
1579 Returns undef if the move failed or the biblionumber of the destination record otherwise
1583 sub move_to_biblio {
1584 my ( $self, $to_biblio, $params ) = @_;
1588 return if $self->biblionumber == $to_biblio->biblionumber;
1590 my $from_biblionumber = $self->biblionumber;
1591 my $to_biblionumber = $to_biblio->biblionumber;
1593 # Own biblionumber and biblioitemnumber
1595 biblionumber => $to_biblionumber,
1596 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1597 })->store({ skip_record_index => $params->{skip_record_index} });
1599 unless ($params->{skip_record_index}) {
1600 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1601 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1604 # Acquisition orders
1605 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1608 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1610 # hold_fill_target (there's no Koha object available yet)
1611 my $hold_fill_target = $self->_result->hold_fill_target;
1612 if ($hold_fill_target) {
1613 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1616 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1617 # and can't even fake one since the significant columns are nullable.
1618 my $storage = $self->_result->result_source->storage;
1621 my ($storage, $dbh, @cols) = @_;
1623 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1628 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1630 return $to_biblionumber;
1635 my $bundle_items = $item->bundle_items;
1637 Returns the items associated with this bundle
1644 if ( !$self->{_bundle_items_cached} ) {
1645 my $bundle_items = Koha::Items->search(
1646 { 'item_bundles_item.host' => $self->itemnumber },
1647 { join => 'item_bundles_item' } );
1648 $self->{_bundle_items} = $bundle_items;
1649 $self->{_bundle_items_cached} = 1;
1652 return $self->{_bundle_items};
1657 my $is_bundle = $item->is_bundle;
1659 Returns whether the item is a bundle or not
1665 return $self->bundle_items->count ? 1 : 0;
1670 my $bundle = $item->bundle_host;
1672 Returns the bundle item this item is attached to
1679 my $bundle_items_rs = $self->_result->item_bundles_item;
1680 return unless $bundle_items_rs;
1681 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1686 my $in_bundle = $item->in_bundle;
1688 Returns whether this item is currently in a bundle
1694 return $self->bundle_host ? 1 : 0;
1697 =head3 add_to_bundle
1699 my $link = $item->add_to_bundle($bundle_item);
1701 Adds the bundle_item passed to this item
1706 my ( $self, $bundle_item ) = @_;
1708 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1709 if ( $self->itemnumber eq $bundle_item->itemnumber
1710 || $bundle_item->is_bundle
1711 || $self->in_bundle );
1713 my $schema = Koha::Database->new->schema;
1715 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1720 $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 $rule = Koha::CirculationRules->get_effective_rules({
1954 branchcode => $branchcode,
1955 categorycode => undef,
1956 itemtype => $self->effective_itemtype,
1962 # check recalls allowed has been set and is not zero
1963 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1969 =head3 check_recalls
1971 my $recall = $item->check_recalls;
1973 Get the most relevant recall for this item.
1980 my @recalls = Koha::Recalls->search(
1981 { biblio_id => $self->biblionumber,
1982 item_id => [ $self->itemnumber, undef ]
1984 { order_by => { -asc => 'created_date' } }
1985 )->filter_by_current->as_list;
1988 # iterate through relevant recalls to find the best one.
1989 # if we come across a waiting recall, use this one.
1990 # 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.
1991 foreach my $r ( @recalls ) {
1992 if ( $r->waiting ) {
1997 unless ( defined $recall ) {
1998 $recall = $recalls[0];
2004 =head3 is_notforloan
2006 my $is_notforloan = $item->is_notforloan;
2008 Determine whether or not this item is "notforloan" based on
2009 the item's notforloan status or its item type
2015 my $is_notforloan = 0;
2017 if ( $self->notforloan ){
2021 my $itemtype = $self->itemtype;
2023 if ( $itemtype->notforloan ){
2029 return $is_notforloan;
2032 =head3 is_denied_renewal
2034 my $is_denied_renewal = $item->is_denied_renewal;
2036 Determine whether or not this item can be renewed based on the
2037 rules set in the ItemsDeniedRenewal system preference.
2041 sub is_denied_renewal {
2044 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
2045 return 0 unless $denyingrules;
2046 foreach my $field (keys %$denyingrules) {
2047 my $val = $self->$field;
2048 if( !defined $val) {
2049 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2052 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2053 # If the results matches the values in the syspref
2054 # We return true if match found
2061 =head3 api_strings_mapping
2063 Retrieves for each column name the unblessed authorised value.
2067 sub api_strings_mapping {
2068 my ( $self, $params ) = @_;
2070 my $columns_info = $self->_result->result_source->columns_info;
2071 my $frameworkcode = $self->biblio->frameworkcode;
2072 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
2073 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2075 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2077 my $public_read_list = $params->{public} ? $self->public_read_list : [];
2078 my $to_api_mapping = $self->to_api_mapping;
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 ( keys %{$columns_info} ) {
2092 # Skip columns not in public read list
2094 unless !$params->{public}
2095 || any { $col eq $_ } $public_read_list;
2097 # Skip columns that are not exposed on the API by to_api_mapping
2098 # i.e. mapping exists but points to undef
2100 if $col eq 'more_subfields_xml' # not dealt with as a regular field
2101 || ( exists $to_api_mapping->{$col} && !defined $to_api_mapping->{$col} );
2103 # By now, we are done with known columns, now check the framework for mappings
2104 my $field = $self->_result->result_source->name . '.' . $col;
2106 # Check there's an entry in the MARC subfield structure for the field
2107 if ( exists $mss->{$field}
2108 && scalar @{ $mss->{$field} } > 0
2109 && $mss->{$field}[0]->{authorised_value} )
2111 my $subfield = $mss->{$field}[0];
2112 my $code = $subfield->{authorised_value};
2114 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2115 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2117 # The _strings entry should match the API attribute name
2118 my $mapped_attr = exists $to_api_mapping->{$col} ? $to_api_mapping->{$col} : $col;
2120 $strings->{$mapped_attr} = {
2123 ( $type eq 'av' ? ( category => $code ) : () ),
2141 Kyle M Hall <kyle@bywatersolutions.com>