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)
73 You should not turn it on if you do not understand what it is doing exactly.
79 my $params = @_ ? shift : {};
81 my $log_action = $params->{log_action} // 1;
83 # We do not want to oblige callers to pass this value
84 # Dev conveniences vs performance?
85 unless ( $self->biblioitemnumber ) {
86 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
89 # See related changes from C4::Items::AddItem
90 unless ( $self->itype ) {
91 $self->itype($self->biblio->biblioitem->itemtype);
94 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
96 my $today = dt_from_string;
97 my $action = 'create';
99 unless ( $self->in_storage ) { #AddItem
101 unless ( $self->permanent_location ) {
102 $self->permanent_location($self->location);
105 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
106 unless ( $self->location || !$default_location ) {
107 $self->permanent_location( $self->location || $default_location )
108 unless $self->permanent_location;
109 $self->location($default_location);
112 unless ( $self->replacementpricedate ) {
113 $self->replacementpricedate($today);
115 unless ( $self->datelastseen ) {
116 $self->datelastseen($today);
119 unless ( $self->dateaccessioned ) {
120 $self->dateaccessioned($today);
123 if ( $self->itemcallnumber
124 or $self->cn_source )
126 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
127 $self->cn_sort($cn_sort);
134 my %updated_columns = $self->_result->get_dirty_columns;
135 return $self->SUPER::store unless %updated_columns;
137 # Retrieve the item for comparison if we need to
139 exists $updated_columns{itemlost}
140 or exists $updated_columns{withdrawn}
141 or exists $updated_columns{damaged}
142 ) ? $self->get_from_storage : undef;
144 # Update *_on fields if needed
145 # FIXME: Why not for AddItem as well?
146 my @fields = qw( itemlost withdrawn damaged );
147 for my $field (@fields) {
149 # If the field is defined but empty or 0, we are
150 # removing/unsetting and thus need to clear out
152 if ( exists $updated_columns{$field}
153 && defined( $self->$field )
156 my $field_on = "${field}_on";
157 $self->$field_on(undef);
159 # If the field has changed otherwise, we much update
161 elsif (exists $updated_columns{$field}
162 && $updated_columns{$field}
163 && !$pre_mod_item->$field )
165 my $field_on = "${field}_on";
166 $self->$field_on(dt_from_string);
170 if ( exists $updated_columns{itemcallnumber}
171 or exists $updated_columns{cn_source} )
173 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
174 $self->cn_sort($cn_sort);
178 if ( exists $updated_columns{location}
179 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
180 and not exists $updated_columns{permanent_location} )
182 $self->permanent_location( $self->location );
185 # If item was lost and has now been found,
186 # reverse any list item charges if necessary.
187 if ( exists $updated_columns{itemlost}
188 and $updated_columns{itemlost} <= 0
189 and $pre_mod_item->itemlost > 0 )
191 $self->_set_found_trigger($pre_mod_item);
196 my $result = $self->SUPER::store;
197 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
199 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
200 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
202 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
203 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
204 unless $params->{skip_record_index};
205 $self->get_from_storage->_after_item_action_hooks({ action => $action });
207 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
209 biblio_ids => [ $self->biblionumber ]
211 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
222 my $params = @_ ? shift : {};
224 # FIXME check the item has no current issues
225 # i.e. raise the appropriate exception
227 # Get the item group so we can delete it later if it has no items left
228 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
230 my $result = $self->SUPER::delete;
232 # Delete the item gorup if it has no items left
233 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
235 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
236 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
237 unless $params->{skip_record_index};
239 $self->_after_item_action_hooks({ action => 'delete' });
241 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
242 if C4::Context->preference("CataloguingLog");
244 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
246 biblio_ids => [ $self->biblionumber ]
248 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
259 my $params = @_ ? shift : {};
261 my $safe_to_delete = $self->safe_to_delete;
262 return $safe_to_delete unless $safe_to_delete;
264 $self->move_to_deleted;
266 return $self->delete($params);
269 =head3 safe_to_delete
271 returns 1 if the item is safe to delete,
273 "book_on_loan" if the item is checked out,
275 "not_same_branch" if the item is blocked by independent branches,
277 "book_reserved" if the there are holds aganst the item, or
279 "linked_analytics" if the item has linked analytic records.
281 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
290 $error = "book_on_loan" if $self->checkout;
292 $error //= "not_same_branch"
293 if defined C4::Context->userenv
294 and defined C4::Context->userenv->{number}
295 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
297 # check it doesn't have a waiting reserve
298 $error //= "book_reserved"
299 if $self->holds->filter_by_found->count;
301 $error //= "linked_analytics"
302 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
304 $error //= "last_item_for_hold"
305 if $self->biblio->items->count == 1
306 && $self->biblio->holds->search(
313 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
316 return Koha::Result::Boolean->new(1);
319 =head3 move_to_deleted
321 my $is_moved = $item->move_to_deleted;
323 Move an item to the deleteditems table.
324 This can be done before deleting an item, to make sure the data are not completely deleted.
328 sub move_to_deleted {
330 my $item_infos = $self->unblessed;
331 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
332 $item_infos->{deleted_on} = dt_from_string;
333 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
337 =head3 effective_itemtype
339 Returns the itemtype for the item based on whether item level itemtypes are set or not.
343 sub effective_itemtype {
346 return $self->_result()->effective_itemtype();
356 my $hb_rs = $self->_result->homebranch;
358 return Koha::Library->_new_from_dbic( $hb_rs );
361 =head3 holding_branch
368 my $hb_rs = $self->_result->holdingbranch;
370 return Koha::Library->_new_from_dbic( $hb_rs );
375 my $biblio = $item->biblio;
377 Return the bibliographic record of this item
383 my $biblio_rs = $self->_result->biblio;
384 return Koha::Biblio->_new_from_dbic( $biblio_rs );
389 my $biblioitem = $item->biblioitem;
391 Return the biblioitem record of this item
397 my $biblioitem_rs = $self->_result->biblioitem;
398 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
403 my $checkout = $item->checkout;
405 Return the checkout for this item
411 my $checkout_rs = $self->_result->issue;
412 return unless $checkout_rs;
413 return Koha::Checkout->_new_from_dbic( $checkout_rs );
418 my $item_group = $item->item_group;
420 Return the item group for this item
427 my $item_group_item = $self->_result->item_group_item;
428 return unless $item_group_item;
430 my $item_group_rs = $item_group_item->item_group;
431 return unless $item_group_rs;
433 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
439 my $return_claims = $item->return_claims;
441 Return any return_claims associated with this item
446 my ( $self, $params, $attrs ) = @_;
447 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
448 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
453 my $return_claim = $item->return_claim;
455 Returns the most recent unresolved return_claims associated with this item
462 $self->_result->return_claims->search( { resolution => undef },
463 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
464 return unless $claims_rs;
465 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
470 my $holds = $item->holds();
471 my $holds = $item->holds($params);
472 my $holds = $item->holds({ found => 'W'});
474 Return holds attached to an item, optionally accept a hashref of params to pass to search
479 my ( $self,$params ) = @_;
480 my $holds_rs = $self->_result->reserves->search($params);
481 return Koha::Holds->_new_from_dbic( $holds_rs );
484 =head3 request_transfer
486 my $transfer = $item->request_transfer(
490 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
494 Add a transfer request for this item to the given branch for the given reason.
496 An exception will be thrown if the BranchTransferLimits would prevent the requested
497 transfer, unless 'ignore_limits' is passed to override the limits.
499 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
500 The caller should catch such cases and retry the transfer request as appropriate passing
501 an appropriate override.
504 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
505 * replace - Used to replace the existing transfer request with your own.
509 sub request_transfer {
510 my ( $self, $params ) = @_;
512 # check for mandatory params
513 my @mandatory = ( 'to', 'reason' );
514 for my $param (@mandatory) {
515 unless ( defined( $params->{$param} ) ) {
516 Koha::Exceptions::MissingParameter->throw(
517 error => "The $param parameter is mandatory" );
521 Koha::Exceptions::Item::Transfer::Limit->throw()
522 unless ( $params->{ignore_limits}
523 || $self->can_be_transferred( { to => $params->{to} } ) );
525 my $request = $self->get_transfer;
526 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
527 if ( $request && !$params->{enqueue} && !$params->{replace} );
529 $request->cancel( { reason => $params->{reason}, force => 1 } )
530 if ( defined($request) && $params->{replace} );
532 my $transfer = Koha::Item::Transfer->new(
534 itemnumber => $self->itemnumber,
535 daterequested => dt_from_string,
536 frombranch => $self->holdingbranch,
537 tobranch => $params->{to}->branchcode,
538 reason => $params->{reason},
539 comments => $params->{comment}
548 my $transfer = $item->get_transfer;
550 Return the active transfer request or undef
552 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
553 whereby the most recently sent, but not received, transfer will be returned
554 if it exists, otherwise the oldest unsatisfied transfer will be returned.
556 This allows for transfers to queue, which is the case for stock rotation and
557 rotating collections where a manual transfer may need to take precedence but
558 we still expect the item to end up at a final location eventually.
565 return $self->get_transfers->search( {}, { rows => 1 } )->next;
570 my $transfer = $item->get_transfers;
572 Return the list of outstanding transfers (i.e requested but not yet cancelled
575 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
576 whereby the most recently sent, but not received, transfer will be returned
577 first if it exists, otherwise requests are in oldest to newest request order.
579 This allows for transfers to queue, which is the case for stock rotation and
580 rotating collections where a manual transfer may need to take precedence but
581 we still expect the item to end up at a final location eventually.
588 my $transfer_rs = $self->_result->branchtransfers;
590 return Koha::Item::Transfers
591 ->_new_from_dbic($transfer_rs)
593 ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
596 =head3 last_returned_by
598 Gets and sets the last borrower to return an item.
600 Accepts and returns Koha::Patron objects
602 $item->last_returned_by( $borrowernumber );
604 $last_returned_by = $item->last_returned_by();
608 sub last_returned_by {
609 my ( $self, $borrower ) = @_;
611 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
614 return $items_last_returned_by_rs->update_or_create(
615 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
618 unless ( $self->{_last_returned_by} ) {
619 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
621 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
625 return $self->{_last_returned_by};
629 =head3 can_article_request
631 my $bool = $item->can_article_request( $borrower )
633 Returns true if item can be specifically requested
635 $borrower must be a Koha::Patron object
639 sub can_article_request {
640 my ( $self, $borrower ) = @_;
642 my $rule = $self->article_request_type($borrower);
644 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
648 =head3 hidden_in_opac
650 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
652 Returns true if item fields match the hidding criteria defined in $rules.
653 Returns false otherwise.
655 Takes HASHref that can have the following parameters:
657 $rules : { <field> => [ value_1, ... ], ... }
659 Note: $rules inherits its structure from the parsed YAML from reading
660 the I<OpacHiddenItems> system preference.
665 my ( $self, $params ) = @_;
667 my $rules = $params->{rules} // {};
670 if C4::Context->preference('hidelostitems') and
673 my $hidden_in_opac = 0;
675 foreach my $field ( keys %{$rules} ) {
677 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
683 return $hidden_in_opac;
686 =head3 can_be_transferred
688 $item->can_be_transferred({ to => $to_library, from => $from_library })
689 Checks if an item can be transferred to given library.
691 This feature is controlled by two system preferences:
692 UseBranchTransferLimits to enable / disable the feature
693 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
694 for setting the limitations
696 Takes HASHref that can have the following parameters:
697 MANDATORY PARAMETERS:
700 $from : Koha::Library # if not given, item holdingbranch
701 # will be used instead
703 Returns 1 if item can be transferred to $to_library, otherwise 0.
705 To find out whether at least one item of a Koha::Biblio can be transferred, please
706 see Koha::Biblio->can_be_transferred() instead of using this method for
707 multiple items of the same biblio.
711 sub can_be_transferred {
712 my ($self, $params) = @_;
714 my $to = $params->{to};
715 my $from = $params->{from};
717 $to = $to->branchcode;
718 $from = defined $from ? $from->branchcode : $self->holdingbranch;
720 return 1 if $from eq $to; # Transfer to current branch is allowed
721 return 1 unless C4::Context->preference('UseBranchTransferLimits');
723 my $limittype = C4::Context->preference('BranchTransferLimitsType');
724 return Koha::Item::Transfer::Limits->search({
727 $limittype => $limittype eq 'itemtype'
728 ? $self->effective_itemtype : $self->ccode
733 =head3 pickup_locations
735 $pickup_locations = $item->pickup_locations( {patron => $patron } )
737 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)
738 and if item can be transferred to each pickup location.
742 sub pickup_locations {
743 my ($self, $params) = @_;
745 my $patron = $params->{patron};
747 my $circ_control_branch =
748 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
750 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
752 if(defined $patron) {
753 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
754 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
757 my $pickup_libraries = Koha::Libraries->search();
758 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
759 $pickup_libraries = $self->home_branch->get_hold_libraries;
760 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
761 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
762 $pickup_libraries = $plib->get_hold_libraries;
763 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
764 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
765 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
766 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
769 return $pickup_libraries->search(
774 order_by => ['branchname']
776 ) unless C4::Context->preference('UseBranchTransferLimits');
778 my $limittype = C4::Context->preference('BranchTransferLimitsType');
779 my ($ccode, $itype) = (undef, undef);
780 if( $limittype eq 'ccode' ){
781 $ccode = $self->ccode;
783 $itype = $self->itype;
785 my $limits = Koha::Item::Transfer::Limits->search(
787 fromBranch => $self->holdingbranch,
791 { columns => ['toBranch'] }
794 return $pickup_libraries->search(
796 pickup_location => 1,
798 '-not_in' => $limits->_resultset->as_query
802 order_by => ['branchname']
807 =head3 article_request_type
809 my $type = $item->article_request_type( $borrower )
811 returns 'yes', 'no', 'bib_only', or 'item_only'
813 $borrower must be a Koha::Patron object
817 sub article_request_type {
818 my ( $self, $borrower ) = @_;
820 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
822 $branch_control eq 'homebranch' ? $self->homebranch
823 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
825 my $borrowertype = $borrower->categorycode;
826 my $itemtype = $self->effective_itemtype();
827 my $rule = Koha::CirculationRules->get_effective_rule(
829 rule_name => 'article_requests',
830 categorycode => $borrowertype,
831 itemtype => $itemtype,
832 branchcode => $branchcode
836 return q{} unless $rule;
837 return $rule->rule_value || q{}
846 my $attributes = { order_by => 'priority' };
847 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
849 itemnumber => $self->itemnumber,
852 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
853 waitingdate => { '!=' => undef },
856 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
857 return Koha::Holds->_new_from_dbic($hold_rs);
860 =head3 stockrotationitem
862 my $sritem = Koha::Item->stockrotationitem;
864 Returns the stock rotation item associated with the current item.
868 sub stockrotationitem {
870 my $rs = $self->_result->stockrotationitem;
872 return Koha::StockRotationItem->_new_from_dbic( $rs );
877 my $item = $item->add_to_rota($rota_id);
879 Add this item to the rota identified by $ROTA_ID, which means associating it
880 with the first stage of that rota. Should this item already be associated
881 with a rota, then we will move it to the new rota.
886 my ( $self, $rota_id ) = @_;
887 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
891 =head3 has_pending_hold
893 my $is_pending_hold = $item->has_pending_hold();
895 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
899 sub has_pending_hold {
901 my $pending_hold = $self->_result->tmp_holdsqueues;
902 return $pending_hold->count ? 1: 0;
905 =head3 has_pending_recall {
907 my $has_pending_recall
909 Return if whether has pending recall of not.
913 sub has_pending_recall {
916 # FIXME Must be moved to $self->recalls
917 return Koha::Recalls->search(
919 item_id => $self->itemnumber,
927 my $field = $item->as_marc_field;
929 This method returns a MARC::Field object representing the Koha::Item object
930 with the current mappings configuration.
937 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
939 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
943 my $item_field = $tagslib->{$itemtag};
945 my $more_subfields = $self->additional_attributes->to_hashref;
946 foreach my $subfield (
948 $a->{display_order} <=> $b->{display_order}
949 || $a->{subfield} cmp $b->{subfield}
950 } grep { ref($_) && %$_ } values %$item_field
953 my $kohafield = $subfield->{kohafield};
954 my $tagsubfield = $subfield->{tagsubfield};
956 if ( defined $kohafield && $kohafield ne '' ) {
957 next if $kohafield !~ m{^items\.}; # That would be weird!
958 ( my $attribute = $kohafield ) =~ s|^items\.||;
959 $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
960 if defined $self->$attribute and $self->$attribute ne '';
962 $value = $more_subfields->{$tagsubfield}
965 next unless defined $value
968 if ( $subfield->{repeatable} ) {
969 my @values = split '\|', $value;
970 push @subfields, ( $tagsubfield => $_ ) for @values;
973 push @subfields, ( $tagsubfield => $value );
978 return unless @subfields;
980 return MARC::Field->new(
981 "$itemtag", ' ', ' ', @subfields
985 =head3 renewal_branchcode
987 Returns the branchcode to be recorded in statistics renewal of the item
991 sub renewal_branchcode {
993 my ($self, $params ) = @_;
995 my $interface = C4::Context->interface;
997 if ( $interface eq 'opac' ){
998 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
999 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1000 $branchcode = 'OPACRenew';
1002 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1003 $branchcode = $self->homebranch;
1005 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1006 $branchcode = $self->checkout->patron->branchcode;
1008 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1009 $branchcode = $self->checkout->branchcode;
1015 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1016 ? C4::Context->userenv->{branch} : $params->{branch};
1023 Return the cover images associated with this item.
1030 my $cover_image_rs = $self->_result->cover_images;
1031 return unless $cover_image_rs;
1032 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1035 =head3 columns_to_str
1037 my $values = $items->columns_to_str;
1039 Return a hashref with the string representation of the different attribute of the item.
1041 This is meant to be used for display purpose only.
1045 sub columns_to_str {
1048 my $frameworkcode = $self->biblio->frameworkcode;
1049 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1050 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1052 my $columns_info = $self->_result->result_source->columns_info;
1054 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1056 for my $column ( keys %$columns_info ) {
1058 next if $column eq 'more_subfields_xml';
1060 my $value = $self->$column;
1061 # 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
1063 if ( not defined $value or $value eq "" ) {
1064 $values->{$column} = $value;
1069 exists $mss->{"items.$column"}
1070 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1073 $values->{$column} =
1075 ? $subfield->{authorised_value}
1076 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1077 $subfield->{tagsubfield}, $value, '', $tagslib )
1083 $self->more_subfields_xml
1084 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1089 my ( $field ) = $marc_more->fields;
1090 for my $sf ( $field->subfields ) {
1091 my $subfield_code = $sf->[0];
1092 my $value = $sf->[1];
1093 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1094 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1096 $subfield->{authorised_value}
1097 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1098 $subfield->{tagsubfield}, $value, '', $tagslib )
1101 push @{$more_values->{$subfield_code}}, $value;
1104 while ( my ( $k, $v ) = each %$more_values ) {
1105 $values->{$k} = join ' | ', @$v;
1112 =head3 additional_attributes
1114 my $attributes = $item->additional_attributes;
1115 $attributes->{k} = 'new k';
1116 $item->update({ more_subfields => $attributes->to_marcxml });
1118 Returns a Koha::Item::Attributes object that represents the non-mapped
1119 attributes for this item.
1123 sub additional_attributes {
1126 return Koha::Item::Attributes->new_from_marcxml(
1127 $self->more_subfields_xml,
1131 =head3 _set_found_trigger
1133 $self->_set_found_trigger
1135 Finds the most recent lost item charge for this item and refunds the patron
1136 appropriately, taking into account any payments or writeoffs already applied
1139 Internal function, not exported, called only by Koha::Item->store.
1143 sub _set_found_trigger {
1144 my ( $self, $pre_mod_item ) = @_;
1146 # Reverse any lost item charges if necessary.
1147 my $no_refund_after_days =
1148 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1149 if ($no_refund_after_days) {
1150 my $today = dt_from_string();
1151 my $lost_age_in_days =
1152 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1155 return $self unless $lost_age_in_days < $no_refund_after_days;
1158 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1161 return_branch => C4::Context->userenv
1162 ? C4::Context->userenv->{'branch'}
1166 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1168 if ( $lostreturn_policy ) {
1170 # refund charge made for lost book
1171 my $lost_charge = Koha::Account::Lines->search(
1173 itemnumber => $self->itemnumber,
1174 debit_type_code => 'LOST',
1175 status => [ undef, { '<>' => 'FOUND' } ]
1178 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1183 if ( $lost_charge ) {
1185 my $patron = $lost_charge->patron;
1188 my $account = $patron->account;
1190 # Credit outstanding amount
1191 my $credit_total = $lost_charge->amountoutstanding;
1195 $lost_charge->amount > $lost_charge->amountoutstanding &&
1196 $lostreturn_policy ne "refund_unpaid"
1198 # some amount has been cancelled. collect the offsets that are not writeoffs
1199 # this works because the only way to subtract from this kind of a debt is
1200 # using the UI buttons 'Pay' and 'Write off'
1202 # We don't credit any payments if return policy is
1205 # In that case only unpaid/outstanding amount
1206 # will be credited which settles the debt without
1207 # creating extra credits
1209 my $credit_offsets = $lost_charge->debit_offsets(
1211 'credit_id' => { '!=' => undef },
1212 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1214 { join => 'credit' }
1217 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1218 # credits are negative on the DB
1219 $credit_offsets->total * -1 :
1221 # Credit the outstanding amount, then add what has been
1222 # paid to create a net credit for this amount
1223 $credit_total += $total_to_refund;
1227 if ( $credit_total > 0 ) {
1229 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1230 $credit = $account->add_credit(
1232 amount => $credit_total,
1233 description => 'Item found ' . $self->itemnumber,
1234 type => 'LOST_FOUND',
1235 interface => C4::Context->interface,
1236 library_id => $branchcode,
1237 item_id => $self->itemnumber,
1238 issue_id => $lost_charge->issue_id
1242 $credit->apply( { debits => [$lost_charge] } );
1246 message => 'lost_refunded',
1247 payload => { credit_id => $credit->id }
1252 # Update the account status
1253 $lost_charge->status('FOUND');
1254 $lost_charge->store();
1256 # Reconcile balances if required
1257 if ( C4::Context->preference('AccountAutoReconcile') ) {
1258 $account->reconcile_balance;
1263 # possibly restore fine for lost book
1264 my $lost_overdue = Koha::Account::Lines->search(
1266 itemnumber => $self->itemnumber,
1267 debit_type_code => 'OVERDUE',
1271 order_by => { '-desc' => 'date' },
1275 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1277 my $patron = $lost_overdue->patron;
1279 my $account = $patron->account;
1281 # Update status of fine
1282 $lost_overdue->status('FOUND')->store();
1284 # Find related forgive credit
1285 my $refund = $lost_overdue->credits(
1287 credit_type_code => 'FORGIVEN',
1288 itemnumber => $self->itemnumber,
1289 status => [ { '!=' => 'VOID' }, undef ]
1291 { order_by => { '-desc' => 'date' }, rows => 1 }
1295 # Revert the forgive credit
1296 $refund->void({ interface => 'trigger' });
1300 message => 'lost_restored',
1301 payload => { refund_id => $refund->id }
1306 # Reconcile balances if required
1307 if ( C4::Context->preference('AccountAutoReconcile') ) {
1308 $account->reconcile_balance;
1312 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1316 message => 'lost_charge',
1322 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1324 if ( $processingreturn_policy ) {
1326 # refund processing charge made for lost book
1327 my $processing_charge = Koha::Account::Lines->search(
1329 itemnumber => $self->itemnumber,
1330 debit_type_code => 'PROCESSING',
1331 status => [ undef, { '<>' => 'FOUND' } ]
1334 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1339 if ( $processing_charge ) {
1341 my $patron = $processing_charge->patron;
1344 my $account = $patron->account;
1346 # Credit outstanding amount
1347 my $credit_total = $processing_charge->amountoutstanding;
1351 $processing_charge->amount > $processing_charge->amountoutstanding &&
1352 $processingreturn_policy ne "refund_unpaid"
1354 # some amount has been cancelled. collect the offsets that are not writeoffs
1355 # this works because the only way to subtract from this kind of a debt is
1356 # using the UI buttons 'Pay' and 'Write off'
1358 # We don't credit any payments if return policy is
1361 # In that case only unpaid/outstanding amount
1362 # will be credited which settles the debt without
1363 # creating extra credits
1365 my $credit_offsets = $processing_charge->debit_offsets(
1367 'credit_id' => { '!=' => undef },
1368 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1370 { join => 'credit' }
1373 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1374 # credits are negative on the DB
1375 $credit_offsets->total * -1 :
1377 # Credit the outstanding amount, then add what has been
1378 # paid to create a net credit for this amount
1379 $credit_total += $total_to_refund;
1383 if ( $credit_total > 0 ) {
1385 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1386 $credit = $account->add_credit(
1388 amount => $credit_total,
1389 description => 'Item found ' . $self->itemnumber,
1390 type => 'PROCESSING_FOUND',
1391 interface => C4::Context->interface,
1392 library_id => $branchcode,
1393 item_id => $self->itemnumber,
1394 issue_id => $processing_charge->issue_id
1398 $credit->apply( { debits => [$processing_charge] } );
1402 message => 'processing_refunded',
1403 payload => { credit_id => $credit->id }
1408 # Update the account status
1409 $processing_charge->status('FOUND');
1410 $processing_charge->store();
1412 # Reconcile balances if required
1413 if ( C4::Context->preference('AccountAutoReconcile') ) {
1414 $account->reconcile_balance;
1423 =head3 public_read_list
1425 This method returns the list of publicly readable database fields for both API and UI output purposes
1429 sub public_read_list {
1431 'itemnumber', 'biblionumber', 'homebranch',
1432 'holdingbranch', 'location', 'collectioncode',
1433 'itemcallnumber', 'copynumber', 'enumchron',
1434 'barcode', 'dateaccessioned', 'itemnotes',
1435 'onloan', 'uri', 'itype',
1436 'notforloan', 'damaged', 'itemlost',
1437 'withdrawn', 'restricted'
1443 Overloaded to_api method to ensure item-level itypes is adhered to.
1448 my ($self, $params) = @_;
1450 my $response = $self->SUPER::to_api($params);
1453 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1454 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1456 return { %$response, %$overrides };
1459 =head3 to_api_mapping
1461 This method returns the mapping for representing a Koha::Item object
1466 sub to_api_mapping {
1468 itemnumber => 'item_id',
1469 biblionumber => 'biblio_id',
1470 biblioitemnumber => undef,
1471 barcode => 'external_id',
1472 dateaccessioned => 'acquisition_date',
1473 booksellerid => 'acquisition_source',
1474 homebranch => 'home_library_id',
1475 price => 'purchase_price',
1476 replacementprice => 'replacement_price',
1477 replacementpricedate => 'replacement_price_date',
1478 datelastborrowed => 'last_checkout_date',
1479 datelastseen => 'last_seen_date',
1481 notforloan => 'not_for_loan_status',
1482 damaged => 'damaged_status',
1483 damaged_on => 'damaged_date',
1484 itemlost => 'lost_status',
1485 itemlost_on => 'lost_date',
1486 withdrawn => 'withdrawn',
1487 withdrawn_on => 'withdrawn_date',
1488 itemcallnumber => 'callnumber',
1489 coded_location_qualifier => 'coded_location_qualifier',
1490 issues => 'checkouts_count',
1491 renewals => 'renewals_count',
1492 reserves => 'holds_count',
1493 restricted => 'restricted_status',
1494 itemnotes => 'public_notes',
1495 itemnotes_nonpublic => 'internal_notes',
1496 holdingbranch => 'holding_library_id',
1497 timestamp => 'timestamp',
1498 location => 'location',
1499 permanent_location => 'permanent_location',
1500 onloan => 'checked_out_date',
1501 cn_source => 'call_number_source',
1502 cn_sort => 'call_number_sort',
1503 ccode => 'collection_code',
1504 materials => 'materials_notes',
1506 itype => 'item_type_id',
1507 more_subfields_xml => 'extended_subfields',
1508 enumchron => 'serial_issue_number',
1509 copynumber => 'copy_number',
1510 stocknumber => 'inventory_number',
1511 new_status => 'new_status',
1512 deleted_on => undef,
1518 my $itemtype = $item->itemtype;
1520 Returns Koha object for effective itemtype
1527 return Koha::ItemTypes->find( $self->effective_itemtype );
1532 my $orders = $item->orders();
1534 Returns a Koha::Acquisition::Orders object
1541 my $orders = $self->_result->item_orders;
1542 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1545 =head3 tracked_links
1547 my $tracked_links = $item->tracked_links();
1549 Returns a Koha::TrackedLinks object
1556 my $tracked_links = $self->_result->linktrackers;
1557 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1560 =head3 move_to_biblio
1562 $item->move_to_biblio($to_biblio[, $params]);
1564 Move the item to another biblio and update any references in other tables.
1566 The final optional parameter, C<$params>, is expected to contain the
1567 'skip_record_index' key, which is relayed down to Koha::Item->store.
1568 There it prevents calling index_records, which takes most of the
1569 time in batch adds/deletes. The caller must take care of calling
1570 index_records separately.
1573 skip_record_index => 1|0
1575 Returns undef if the move failed or the biblionumber of the destination record otherwise
1579 sub move_to_biblio {
1580 my ( $self, $to_biblio, $params ) = @_;
1584 return if $self->biblionumber == $to_biblio->biblionumber;
1586 my $from_biblionumber = $self->biblionumber;
1587 my $to_biblionumber = $to_biblio->biblionumber;
1589 # Own biblionumber and biblioitemnumber
1591 biblionumber => $to_biblionumber,
1592 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1593 })->store({ skip_record_index => $params->{skip_record_index} });
1595 unless ($params->{skip_record_index}) {
1596 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1597 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1600 # Acquisition orders
1601 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1604 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1606 # hold_fill_target (there's no Koha object available yet)
1607 my $hold_fill_target = $self->_result->hold_fill_target;
1608 if ($hold_fill_target) {
1609 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1612 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1613 # and can't even fake one since the significant columns are nullable.
1614 my $storage = $self->_result->result_source->storage;
1617 my ($storage, $dbh, @cols) = @_;
1619 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1624 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1626 return $to_biblionumber;
1631 my $bundle_items = $item->bundle_items;
1633 Returns the items associated with this bundle
1640 if ( !$self->{_bundle_items_cached} ) {
1641 my $bundle_items = Koha::Items->search(
1642 { 'item_bundles_item.host' => $self->itemnumber },
1643 { join => 'item_bundles_item' } );
1644 $self->{_bundle_items} = $bundle_items;
1645 $self->{_bundle_items_cached} = 1;
1648 return $self->{_bundle_items};
1653 my $is_bundle = $item->is_bundle;
1655 Returns whether the item is a bundle or not
1661 return $self->bundle_items->count ? 1 : 0;
1666 my $bundle = $item->bundle_host;
1668 Returns the bundle item this item is attached to
1675 my $bundle_items_rs = $self->_result->item_bundles_item;
1676 return unless $bundle_items_rs;
1677 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1682 my $in_bundle = $item->in_bundle;
1684 Returns whether this item is currently in a bundle
1690 return $self->bundle_host ? 1 : 0;
1693 =head3 add_to_bundle
1695 my $link = $item->add_to_bundle($bundle_item);
1697 Adds the bundle_item passed to this item
1702 my ( $self, $bundle_item ) = @_;
1704 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1705 if ( $self->itemnumber eq $bundle_item->itemnumber
1706 || $bundle_item->is_bundle
1707 || $self->in_bundle );
1709 my $schema = Koha::Database->new->schema;
1711 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
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 $rule = Koha::CirculationRules->get_effective_rules({
1950 branchcode => $branchcode,
1951 categorycode => undef,
1952 itemtype => $self->effective_itemtype,
1958 # check recalls allowed has been set and is not zero
1959 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1965 =head3 check_recalls
1967 my $recall = $item->check_recalls;
1969 Get the most relevant recall for this item.
1976 my @recalls = Koha::Recalls->search(
1977 { biblio_id => $self->biblionumber,
1978 item_id => [ $self->itemnumber, undef ]
1980 { order_by => { -asc => 'created_date' } }
1981 )->filter_by_current->as_list;
1984 # iterate through relevant recalls to find the best one.
1985 # if we come across a waiting recall, use this one.
1986 # 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.
1987 foreach my $r ( @recalls ) {
1988 if ( $r->waiting ) {
1993 unless ( defined $recall ) {
1994 $recall = $recalls[0];
2000 =head3 is_notforloan
2002 my $is_notforloan = $item->is_notforloan;
2004 Determine whether or not this item is "notforloan" based on
2005 the item's notforloan status or its item type
2011 my $is_notforloan = 0;
2013 if ( $self->notforloan ){
2017 my $itemtype = $self->itemtype;
2019 if ( $itemtype->notforloan ){
2025 return $is_notforloan;
2028 =head3 is_denied_renewal
2030 my $is_denied_renewal = $item->is_denied_renewal;
2032 Determine whether or not this item can be renewed based on the
2033 rules set in the ItemsDeniedRenewal system preference.
2037 sub is_denied_renewal {
2040 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
2041 return 0 unless $denyingrules;
2042 foreach my $field (keys %$denyingrules) {
2043 my $val = $self->$field;
2044 if( !defined $val) {
2045 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2048 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2049 # If the results matches the values in the syspref
2050 # We return true if match found
2059 Returns a map of column name to string representations including the string,
2060 the mapping type and the mapping category where appropriate.
2062 Currently handles authorised value mappings, library, callnumber and itemtype
2065 Accepts a param hashref where the 'public' key denotes whether we want the public
2066 or staff client strings.
2071 my ( $self, $params ) = @_;
2073 my $columns_info = $self->_result->result_source->columns_info;
2074 my $frameworkcode = $self->biblio->frameworkcode;
2075 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
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 ( keys %{$columns_info} ) {
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>