3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
58 use base qw(Koha::Object);
62 Koha::Item - Koha Item object class
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
82 my $params = @_ ? shift : {};
84 my $log_action = $params->{log_action} // 1;
86 # We do not want to oblige callers to pass this value
87 # Dev conveniences vs performance?
88 unless ( $self->biblioitemnumber ) {
89 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
92 # See related changes from C4::Items::AddItem
93 unless ( $self->itype ) {
94 $self->itype($self->biblio->biblioitem->itemtype);
97 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
99 my $today = dt_from_string;
100 my $action = 'create';
102 unless ( $self->in_storage ) { #AddItem
104 unless ( $self->permanent_location ) {
105 $self->permanent_location($self->location);
108 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109 unless ( $self->location || !$default_location ) {
110 $self->permanent_location( $self->location || $default_location )
111 unless $self->permanent_location;
112 $self->location($default_location);
115 unless ( $self->replacementpricedate ) {
116 $self->replacementpricedate($today);
118 unless ( $self->datelastseen ) {
119 $self->datelastseen($today);
122 unless ( $self->dateaccessioned ) {
123 $self->dateaccessioned($today);
126 if ( $self->itemcallnumber
127 or $self->cn_source )
129 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130 $self->cn_sort($cn_sort);
137 my %updated_columns = $self->_result->get_dirty_columns;
138 return $self->SUPER::store unless %updated_columns;
140 # Retrieve the item for comparison if we need to
142 exists $updated_columns{itemlost}
143 or exists $updated_columns{withdrawn}
144 or exists $updated_columns{damaged}
145 ) ? $self->get_from_storage : undef;
147 # Update *_on fields if needed
148 # FIXME: Why not for AddItem as well?
149 my @fields = qw( itemlost withdrawn damaged );
150 for my $field (@fields) {
152 # If the field is defined but empty or 0, we are
153 # removing/unsetting and thus need to clear out
155 if ( exists $updated_columns{$field}
156 && defined( $self->$field )
159 my $field_on = "${field}_on";
160 $self->$field_on(undef);
162 # If the field has changed otherwise, we much update
164 elsif (exists $updated_columns{$field}
165 && $updated_columns{$field}
166 && !$pre_mod_item->$field )
168 my $field_on = "${field}_on";
169 $self->$field_on(dt_from_string);
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
210 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
212 biblio_ids => [ $self->biblionumber ]
214 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
225 my $params = @_ ? shift : {};
227 # FIXME check the item has no current issues
228 # i.e. raise the appropriate exception
230 # Get the item group so we can delete it later if it has no items left
231 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
233 my $result = $self->SUPER::delete;
235 # Delete the item gorup if it has no items left
236 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
251 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error //= "not_same_branch"
296 if defined C4::Context->userenv
297 and defined C4::Context->userenv->{number}
298 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
300 # check it doesn't have a waiting reserve
301 $error //= "book_reserved"
302 if $self->holds->filter_by_found->count;
304 $error //= "linked_analytics"
305 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
307 $error //= "last_item_for_hold"
308 if $self->biblio->items->count == 1
309 && $self->biblio->holds->search(
316 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
319 return Koha::Result::Boolean->new(1);
322 =head3 move_to_deleted
324 my $is_moved = $item->move_to_deleted;
326 Move an item to the deleteditems table.
327 This can be done before deleting an item, to make sure the data are not completely deleted.
331 sub move_to_deleted {
333 my $item_infos = $self->unblessed;
334 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
335 $item_infos->{deleted_on} = dt_from_string;
336 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
339 =head3 effective_itemtype
341 Returns the itemtype for the item based on whether item level itemtypes are set or not.
345 sub effective_itemtype {
348 return $self->_result()->effective_itemtype();
358 my $hb_rs = $self->_result->homebranch;
360 return Koha::Library->_new_from_dbic( $hb_rs );
363 =head3 holding_branch
370 my $hb_rs = $self->_result->holdingbranch;
372 return Koha::Library->_new_from_dbic( $hb_rs );
377 my $biblio = $item->biblio;
379 Return the bibliographic record of this item
385 my $biblio_rs = $self->_result->biblio;
386 return Koha::Biblio->_new_from_dbic( $biblio_rs );
391 my $biblioitem = $item->biblioitem;
393 Return the biblioitem record of this item
399 my $biblioitem_rs = $self->_result->biblioitem;
400 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
405 my $checkout = $item->checkout;
407 Return the checkout for this item
413 my $checkout_rs = $self->_result->issue;
414 return unless $checkout_rs;
415 return Koha::Checkout->_new_from_dbic( $checkout_rs );
420 my $item_group = $item->item_group;
422 Return the item group for this item
429 my $item_group_item = $self->_result->item_group_item;
430 return unless $item_group_item;
432 my $item_group_rs = $item_group_item->item_group;
433 return unless $item_group_rs;
435 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
441 my $return_claims = $item->return_claims;
443 Return any return_claims associated with this item
448 my ( $self, $params, $attrs ) = @_;
449 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
450 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
455 my $return_claim = $item->return_claim;
457 Returns the most recent unresolved return_claims associated with this item
464 $self->_result->return_claims->search( { resolution => undef },
465 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
466 return unless $claims_rs;
467 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
472 my $holds = $item->holds();
473 my $holds = $item->holds($params);
474 my $holds = $item->holds({ found => 'W'});
476 Return holds attached to an item, optionally accept a hashref of params to pass to search
481 my ( $self,$params ) = @_;
482 my $holds_rs = $self->_result->reserves->search($params);
483 return Koha::Holds->_new_from_dbic( $holds_rs );
486 =head3 request_transfer
488 my $transfer = $item->request_transfer(
492 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
496 Add a transfer request for this item to the given branch for the given reason.
498 An exception will be thrown if the BranchTransferLimits would prevent the requested
499 transfer, unless 'ignore_limits' is passed to override the limits.
501 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
502 The caller should catch such cases and retry the transfer request as appropriate passing
503 an appropriate override.
506 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
507 * replace - Used to replace the existing transfer request with your own.
511 sub request_transfer {
512 my ( $self, $params ) = @_;
514 # check for mandatory params
515 my @mandatory = ( 'to', 'reason' );
516 for my $param (@mandatory) {
517 unless ( defined( $params->{$param} ) ) {
518 Koha::Exceptions::MissingParameter->throw(
519 error => "The $param parameter is mandatory" );
523 Koha::Exceptions::Item::Transfer::Limit->throw()
524 unless ( $params->{ignore_limits}
525 || $self->can_be_transferred( { to => $params->{to} } ) );
527 my $request = $self->get_transfer;
528 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
529 if ( $request && !$params->{enqueue} && !$params->{replace} );
531 $request->cancel( { reason => $params->{reason}, force => 1 } )
532 if ( defined($request) && $params->{replace} );
534 my $transfer = Koha::Item::Transfer->new(
536 itemnumber => $self->itemnumber,
537 daterequested => dt_from_string,
538 frombranch => $self->holdingbranch,
539 tobranch => $params->{to}->branchcode,
540 reason => $params->{reason},
541 comments => $params->{comment}
550 my $transfer = $item->get_transfer;
552 Return the active transfer request or undef
554 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
555 whereby the most recently sent, but not received, transfer will be returned
556 if it exists, otherwise the oldest unsatisfied transfer will be returned.
558 This allows for transfers to queue, which is the case for stock rotation and
559 rotating collections where a manual transfer may need to take precedence but
560 we still expect the item to end up at a final location eventually.
567 my $transfer = $self->_result->current_branchtransfers->next;
568 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
573 my $transfer = $item->get_transfers;
575 Return the list of outstanding transfers (i.e requested but not yet cancelled
578 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
579 whereby the most recently sent, but not received, transfer will be returned
580 first if it exists, otherwise requests are in oldest to newest request order.
582 This allows for transfers to queue, which is the case for stock rotation and
583 rotating collections where a manual transfer may need to take precedence but
584 we still expect the item to end up at a final location eventually.
591 my $transfer_rs = $self->_result->current_branchtransfers;
593 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
596 =head3 last_returned_by
598 Gets and sets the last patron to return an item.
600 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
602 $item->last_returned_by( $borrowernumber );
604 my $patron = $item->last_returned_by();
608 sub last_returned_by {
609 my ( $self, $borrowernumber ) = @_;
610 if ( $borrowernumber ) {
611 $self->_result->update_or_create_related('last_returned_by',
612 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
614 my $rs = $self->_result->last_returned_by;
616 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
619 =head3 can_article_request
621 my $bool = $item->can_article_request( $borrower )
623 Returns true if item can be specifically requested
625 $borrower must be a Koha::Patron object
629 sub can_article_request {
630 my ( $self, $borrower ) = @_;
632 my $rule = $self->article_request_type($borrower);
634 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
638 =head3 hidden_in_opac
640 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
642 Returns true if item fields match the hidding criteria defined in $rules.
643 Returns false otherwise.
645 Takes HASHref that can have the following parameters:
647 $rules : { <field> => [ value_1, ... ], ... }
649 Note: $rules inherits its structure from the parsed YAML from reading
650 the I<OpacHiddenItems> system preference.
655 my ( $self, $params ) = @_;
657 my $rules = $params->{rules} // {};
660 if C4::Context->preference('hidelostitems') and
663 my $hidden_in_opac = 0;
665 foreach my $field ( keys %{$rules} ) {
667 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
673 return $hidden_in_opac;
676 =head3 can_be_transferred
678 $item->can_be_transferred({ to => $to_library, from => $from_library })
679 Checks if an item can be transferred to given library.
681 This feature is controlled by two system preferences:
682 UseBranchTransferLimits to enable / disable the feature
683 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
684 for setting the limitations
686 Takes HASHref that can have the following parameters:
687 MANDATORY PARAMETERS:
690 $from : Koha::Library # if not given, item holdingbranch
691 # will be used instead
693 Returns 1 if item can be transferred to $to_library, otherwise 0.
695 To find out whether at least one item of a Koha::Biblio can be transferred, please
696 see Koha::Biblio->can_be_transferred() instead of using this method for
697 multiple items of the same biblio.
701 sub can_be_transferred {
702 my ($self, $params) = @_;
704 my $to = $params->{to};
705 my $from = $params->{from};
707 $to = $to->branchcode;
708 $from = defined $from ? $from->branchcode : $self->holdingbranch;
710 return 1 if $from eq $to; # Transfer to current branch is allowed
711 return 1 unless C4::Context->preference('UseBranchTransferLimits');
713 my $limittype = C4::Context->preference('BranchTransferLimitsType');
714 return Koha::Item::Transfer::Limits->search({
717 $limittype => $limittype eq 'itemtype'
718 ? $self->effective_itemtype : $self->ccode
723 =head3 pickup_locations
725 my $pickup_locations = $item->pickup_locations({ patron => $patron })
727 Returns possible pickup locations for this item, according to patron's home library
728 and if item can be transferred to each pickup location.
730 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
735 sub pickup_locations {
736 my ($self, $params) = @_;
738 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
739 unless exists $params->{patron};
741 my $patron = $params->{patron};
743 my $circ_control_branch =
744 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
746 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
748 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
749 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
751 my $pickup_libraries = Koha::Libraries->search();
752 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
753 $pickup_libraries = $self->home_branch->get_hold_libraries;
754 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
755 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
756 $pickup_libraries = $plib->get_hold_libraries;
757 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
758 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
759 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
760 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
763 return $pickup_libraries->search(
768 order_by => ['branchname']
770 ) unless C4::Context->preference('UseBranchTransferLimits');
772 my $limittype = C4::Context->preference('BranchTransferLimitsType');
773 my ($ccode, $itype) = (undef, undef);
774 if( $limittype eq 'ccode' ){
775 $ccode = $self->ccode;
777 $itype = $self->itype;
779 my $limits = Koha::Item::Transfer::Limits->search(
781 fromBranch => $self->holdingbranch,
785 { columns => ['toBranch'] }
788 return $pickup_libraries->search(
790 pickup_location => 1,
792 '-not_in' => $limits->_resultset->as_query
796 order_by => ['branchname']
801 =head3 article_request_type
803 my $type = $item->article_request_type( $borrower )
805 returns 'yes', 'no', 'bib_only', or 'item_only'
807 $borrower must be a Koha::Patron object
811 sub article_request_type {
812 my ( $self, $borrower ) = @_;
814 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
816 $branch_control eq 'homebranch' ? $self->homebranch
817 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
819 my $borrowertype = $borrower->categorycode;
820 my $itemtype = $self->effective_itemtype();
821 my $rule = Koha::CirculationRules->get_effective_rule(
823 rule_name => 'article_requests',
824 categorycode => $borrowertype,
825 itemtype => $itemtype,
826 branchcode => $branchcode
830 return q{} unless $rule;
831 return $rule->rule_value || q{}
840 my $attributes = { order_by => 'priority' };
841 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
843 itemnumber => $self->itemnumber,
846 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
847 waitingdate => { '!=' => undef },
850 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
851 return Koha::Holds->_new_from_dbic($hold_rs);
854 =head3 stockrotationitem
856 my $sritem = Koha::Item->stockrotationitem;
858 Returns the stock rotation item associated with the current item.
862 sub stockrotationitem {
864 my $rs = $self->_result->stockrotationitem;
866 return Koha::StockRotationItem->_new_from_dbic( $rs );
871 my $item = $item->add_to_rota($rota_id);
873 Add this item to the rota identified by $ROTA_ID, which means associating it
874 with the first stage of that rota. Should this item already be associated
875 with a rota, then we will move it to the new rota.
880 my ( $self, $rota_id ) = @_;
881 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
885 =head3 has_pending_hold
887 my $is_pending_hold = $item->has_pending_hold();
889 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
893 sub has_pending_hold {
895 my $pending_hold = $self->_result->tmp_holdsqueues;
896 return $pending_hold->count ? 1: 0;
899 =head3 has_pending_recall {
901 my $has_pending_recall
903 Return if whether has pending recall of not.
907 sub has_pending_recall {
910 # FIXME Must be moved to $self->recalls
911 return Koha::Recalls->search(
913 item_id => $self->itemnumber,
921 my $field = $item->as_marc_field;
923 This method returns a MARC::Field object representing the Koha::Item object
924 with the current mappings configuration.
931 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
933 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
937 my $item_field = $tagslib->{$itemtag};
939 my $more_subfields = $self->additional_attributes->to_hashref;
940 foreach my $subfield (
942 $a->{display_order} <=> $b->{display_order}
943 || $a->{subfield} cmp $b->{subfield}
944 } grep { ref($_) && %$_ } values %$item_field
947 my $kohafield = $subfield->{kohafield};
948 my $tagsubfield = $subfield->{tagsubfield};
950 if ( defined $kohafield && $kohafield ne '' ) {
951 next if $kohafield !~ m{^items\.}; # That would be weird!
952 ( my $attribute = $kohafield ) =~ s|^items\.||;
953 $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
954 if defined $self->$attribute and $self->$attribute ne '';
956 $value = $more_subfields->{$tagsubfield}
959 next unless defined $value
962 if ( $subfield->{repeatable} ) {
963 my @values = split '\|', $value;
964 push @subfields, ( $tagsubfield => $_ ) for @values;
967 push @subfields, ( $tagsubfield => $value );
972 return unless @subfields;
974 return MARC::Field->new(
975 "$itemtag", ' ', ' ', @subfields
979 =head3 renewal_branchcode
981 Returns the branchcode to be recorded in statistics renewal of the item
985 sub renewal_branchcode {
987 my ($self, $params ) = @_;
989 my $interface = C4::Context->interface;
991 if ( $interface eq 'opac' ){
992 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
993 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
994 $branchcode = 'OPACRenew';
996 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
997 $branchcode = $self->homebranch;
999 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1000 $branchcode = $self->checkout->patron->branchcode;
1002 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1003 $branchcode = $self->checkout->branchcode;
1009 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1010 ? C4::Context->userenv->{branch} : $params->{branch};
1017 Return the cover images associated with this item.
1024 my $cover_image_rs = $self->_result->cover_images;
1025 return unless $cover_image_rs;
1026 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1029 =head3 columns_to_str
1031 my $values = $items->columns_to_str;
1033 Return a hashref with the string representation of the different attribute of the item.
1035 This is meant to be used for display purpose only.
1039 sub columns_to_str {
1041 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1042 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1043 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1045 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1048 for my $column ( @{$self->_columns}) {
1050 next if $column eq 'more_subfields_xml';
1052 my $value = $self->$column;
1053 # 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
1055 if ( not defined $value or $value eq "" ) {
1056 $values->{$column} = $value;
1061 exists $mss->{"items.$column"}
1062 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1065 $values->{$column} =
1067 ? $subfield->{authorised_value}
1068 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1069 $subfield->{tagsubfield}, $value, '', $tagslib )
1075 $self->more_subfields_xml
1076 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1081 my ( $field ) = $marc_more->fields;
1082 for my $sf ( $field->subfields ) {
1083 my $subfield_code = $sf->[0];
1084 my $value = $sf->[1];
1085 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1086 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1088 $subfield->{authorised_value}
1089 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1090 $subfield->{tagsubfield}, $value, '', $tagslib )
1093 push @{$more_values->{$subfield_code}}, $value;
1096 while ( my ( $k, $v ) = each %$more_values ) {
1097 $values->{$k} = join ' | ', @$v;
1104 =head3 additional_attributes
1106 my $attributes = $item->additional_attributes;
1107 $attributes->{k} = 'new k';
1108 $item->update({ more_subfields => $attributes->to_marcxml });
1110 Returns a Koha::Item::Attributes object that represents the non-mapped
1111 attributes for this item.
1115 sub additional_attributes {
1118 return Koha::Item::Attributes->new_from_marcxml(
1119 $self->more_subfields_xml,
1123 =head3 _set_found_trigger
1125 $self->_set_found_trigger
1127 Finds the most recent lost item charge for this item and refunds the patron
1128 appropriately, taking into account any payments or writeoffs already applied
1131 Internal function, not exported, called only by Koha::Item->store.
1135 sub _set_found_trigger {
1136 my ( $self, $pre_mod_item ) = @_;
1138 # Reverse any lost item charges if necessary.
1139 my $no_refund_after_days =
1140 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1141 if ($no_refund_after_days) {
1142 my $today = dt_from_string();
1143 my $lost_age_in_days =
1144 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1147 return $self unless $lost_age_in_days < $no_refund_after_days;
1150 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1153 return_branch => C4::Context->userenv
1154 ? C4::Context->userenv->{'branch'}
1158 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1160 if ( $lostreturn_policy ) {
1162 # refund charge made for lost book
1163 my $lost_charge = Koha::Account::Lines->search(
1165 itemnumber => $self->itemnumber,
1166 debit_type_code => 'LOST',
1167 status => [ undef, { '<>' => 'FOUND' } ]
1170 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1175 if ( $lost_charge ) {
1177 my $patron = $lost_charge->patron;
1180 my $account = $patron->account;
1182 # Credit outstanding amount
1183 my $credit_total = $lost_charge->amountoutstanding;
1187 $lost_charge->amount > $lost_charge->amountoutstanding &&
1188 $lostreturn_policy ne "refund_unpaid"
1190 # some amount has been cancelled. collect the offsets that are not writeoffs
1191 # this works because the only way to subtract from this kind of a debt is
1192 # using the UI buttons 'Pay' and 'Write off'
1194 # We don't credit any payments if return policy is
1197 # In that case only unpaid/outstanding amount
1198 # will be credited which settles the debt without
1199 # creating extra credits
1201 my $credit_offsets = $lost_charge->debit_offsets(
1203 'credit_id' => { '!=' => undef },
1204 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1206 { join => 'credit' }
1209 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1210 # credits are negative on the DB
1211 $credit_offsets->total * -1 :
1213 # Credit the outstanding amount, then add what has been
1214 # paid to create a net credit for this amount
1215 $credit_total += $total_to_refund;
1219 if ( $credit_total > 0 ) {
1221 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1222 $credit = $account->add_credit(
1224 amount => $credit_total,
1225 description => 'Item found ' . $self->itemnumber,
1226 type => 'LOST_FOUND',
1227 interface => C4::Context->interface,
1228 library_id => $branchcode,
1229 item_id => $self->itemnumber,
1230 issue_id => $lost_charge->issue_id
1234 $credit->apply( { debits => [$lost_charge] } );
1238 message => 'lost_refunded',
1239 payload => { credit_id => $credit->id }
1244 # Update the account status
1245 $lost_charge->status('FOUND');
1246 $lost_charge->store();
1248 # Reconcile balances if required
1249 if ( C4::Context->preference('AccountAutoReconcile') ) {
1250 $account->reconcile_balance;
1255 # possibly restore fine for lost book
1256 my $lost_overdue = Koha::Account::Lines->search(
1258 itemnumber => $self->itemnumber,
1259 debit_type_code => 'OVERDUE',
1263 order_by => { '-desc' => 'date' },
1267 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1269 my $patron = $lost_overdue->patron;
1271 my $account = $patron->account;
1273 # Update status of fine
1274 $lost_overdue->status('FOUND')->store();
1276 # Find related forgive credit
1277 my $refund = $lost_overdue->credits(
1279 credit_type_code => 'FORGIVEN',
1280 itemnumber => $self->itemnumber,
1281 status => [ { '!=' => 'VOID' }, undef ]
1283 { order_by => { '-desc' => 'date' }, rows => 1 }
1287 # Revert the forgive credit
1288 $refund->void({ interface => 'trigger' });
1292 message => 'lost_restored',
1293 payload => { refund_id => $refund->id }
1298 # Reconcile balances if required
1299 if ( C4::Context->preference('AccountAutoReconcile') ) {
1300 $account->reconcile_balance;
1304 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1308 message => 'lost_charge',
1314 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1316 if ( $processingreturn_policy ) {
1318 # refund processing charge made for lost book
1319 my $processing_charge = Koha::Account::Lines->search(
1321 itemnumber => $self->itemnumber,
1322 debit_type_code => 'PROCESSING',
1323 status => [ undef, { '<>' => 'FOUND' } ]
1326 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1331 if ( $processing_charge ) {
1333 my $patron = $processing_charge->patron;
1336 my $account = $patron->account;
1338 # Credit outstanding amount
1339 my $credit_total = $processing_charge->amountoutstanding;
1343 $processing_charge->amount > $processing_charge->amountoutstanding &&
1344 $processingreturn_policy ne "refund_unpaid"
1346 # some amount has been cancelled. collect the offsets that are not writeoffs
1347 # this works because the only way to subtract from this kind of a debt is
1348 # using the UI buttons 'Pay' and 'Write off'
1350 # We don't credit any payments if return policy is
1353 # In that case only unpaid/outstanding amount
1354 # will be credited which settles the debt without
1355 # creating extra credits
1357 my $credit_offsets = $processing_charge->debit_offsets(
1359 'credit_id' => { '!=' => undef },
1360 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1362 { join => 'credit' }
1365 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1366 # credits are negative on the DB
1367 $credit_offsets->total * -1 :
1369 # Credit the outstanding amount, then add what has been
1370 # paid to create a net credit for this amount
1371 $credit_total += $total_to_refund;
1375 if ( $credit_total > 0 ) {
1377 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1378 $credit = $account->add_credit(
1380 amount => $credit_total,
1381 description => 'Item found ' . $self->itemnumber,
1382 type => 'PROCESSING_FOUND',
1383 interface => C4::Context->interface,
1384 library_id => $branchcode,
1385 item_id => $self->itemnumber,
1386 issue_id => $processing_charge->issue_id
1390 $credit->apply( { debits => [$processing_charge] } );
1394 message => 'processing_refunded',
1395 payload => { credit_id => $credit->id }
1400 # Update the account status
1401 $processing_charge->status('FOUND');
1402 $processing_charge->store();
1404 # Reconcile balances if required
1405 if ( C4::Context->preference('AccountAutoReconcile') ) {
1406 $account->reconcile_balance;
1415 =head3 public_read_list
1417 This method returns the list of publicly readable database fields for both API and UI output purposes
1421 sub public_read_list {
1423 'itemnumber', 'biblionumber', 'homebranch',
1424 'holdingbranch', 'location', 'collectioncode',
1425 'itemcallnumber', 'copynumber', 'enumchron',
1426 'barcode', 'dateaccessioned', 'itemnotes',
1427 'onloan', 'uri', 'itype',
1428 'notforloan', 'damaged', 'itemlost',
1429 'withdrawn', 'restricted'
1435 Overloaded to_api method to ensure item-level itypes is adhered to.
1440 my ($self, $params) = @_;
1442 my $response = $self->SUPER::to_api($params);
1445 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1446 $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
1448 return { %$response, %$overrides };
1451 =head3 to_api_mapping
1453 This method returns the mapping for representing a Koha::Item object
1458 sub to_api_mapping {
1460 itemnumber => 'item_id',
1461 biblionumber => 'biblio_id',
1462 biblioitemnumber => undef,
1463 barcode => 'external_id',
1464 dateaccessioned => 'acquisition_date',
1465 booksellerid => 'acquisition_source',
1466 homebranch => 'home_library_id',
1467 price => 'purchase_price',
1468 replacementprice => 'replacement_price',
1469 replacementpricedate => 'replacement_price_date',
1470 datelastborrowed => 'last_checkout_date',
1471 datelastseen => 'last_seen_date',
1473 notforloan => 'not_for_loan_status',
1474 damaged => 'damaged_status',
1475 damaged_on => 'damaged_date',
1476 itemlost => 'lost_status',
1477 itemlost_on => 'lost_date',
1478 withdrawn => 'withdrawn',
1479 withdrawn_on => 'withdrawn_date',
1480 itemcallnumber => 'callnumber',
1481 coded_location_qualifier => 'coded_location_qualifier',
1482 issues => 'checkouts_count',
1483 renewals => 'renewals_count',
1484 reserves => 'holds_count',
1485 restricted => 'restricted_status',
1486 itemnotes => 'public_notes',
1487 itemnotes_nonpublic => 'internal_notes',
1488 holdingbranch => 'holding_library_id',
1489 timestamp => 'timestamp',
1490 location => 'location',
1491 permanent_location => 'permanent_location',
1492 onloan => 'checked_out_date',
1493 cn_source => 'call_number_source',
1494 cn_sort => 'call_number_sort',
1495 ccode => 'collection_code',
1496 materials => 'materials_notes',
1498 itype => 'item_type_id',
1499 more_subfields_xml => 'extended_subfields',
1500 enumchron => 'serial_issue_number',
1501 copynumber => 'copy_number',
1502 stocknumber => 'inventory_number',
1503 new_status => 'new_status',
1504 deleted_on => undef,
1510 my $itemtype = $item->itemtype;
1512 Returns Koha object for effective itemtype
1519 return Koha::ItemTypes->find( $self->effective_itemtype );
1524 my $orders = $item->orders();
1526 Returns a Koha::Acquisition::Orders object
1533 my $orders = $self->_result->item_orders;
1534 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1537 =head3 tracked_links
1539 my $tracked_links = $item->tracked_links();
1541 Returns a Koha::TrackedLinks object
1548 my $tracked_links = $self->_result->linktrackers;
1549 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1552 =head3 move_to_biblio
1554 $item->move_to_biblio($to_biblio[, $params]);
1556 Move the item to another biblio and update any references in other tables.
1558 The final optional parameter, C<$params>, is expected to contain the
1559 'skip_record_index' key, which is relayed down to Koha::Item->store.
1560 There it prevents calling index_records, which takes most of the
1561 time in batch adds/deletes. The caller must take care of calling
1562 index_records separately.
1565 skip_record_index => 1|0
1567 Returns undef if the move failed or the biblionumber of the destination record otherwise
1571 sub move_to_biblio {
1572 my ( $self, $to_biblio, $params ) = @_;
1576 return if $self->biblionumber == $to_biblio->biblionumber;
1578 my $from_biblionumber = $self->biblionumber;
1579 my $to_biblionumber = $to_biblio->biblionumber;
1581 # Own biblionumber and biblioitemnumber
1583 biblionumber => $to_biblionumber,
1584 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1585 })->store({ skip_record_index => $params->{skip_record_index} });
1587 unless ($params->{skip_record_index}) {
1588 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1589 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1592 # Acquisition orders
1593 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1596 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1598 # hold_fill_target (there's no Koha object available yet)
1599 my $hold_fill_target = $self->_result->hold_fill_target;
1600 if ($hold_fill_target) {
1601 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1604 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1605 # and can't even fake one since the significant columns are nullable.
1606 my $storage = $self->_result->result_source->storage;
1609 my ($storage, $dbh, @cols) = @_;
1611 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1616 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1618 return $to_biblionumber;
1623 my $bundle_items = $item->bundle_items;
1625 Returns the items associated with this bundle
1632 my $rs = $self->_result->bundle_items;
1633 return Koha::Items->_new_from_dbic($rs);
1638 my $is_bundle = $item->is_bundle;
1640 Returns whether the item is a bundle or not
1646 return $self->bundle_items->count ? 1 : 0;
1651 my $bundle = $item->bundle_host;
1653 Returns the bundle item this item is attached to
1660 my $bundle_items_rs = $self->_result->item_bundles_item;
1661 return unless $bundle_items_rs;
1662 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1667 my $in_bundle = $item->in_bundle;
1669 Returns whether this item is currently in a bundle
1675 return $self->bundle_host ? 1 : 0;
1678 =head3 add_to_bundle
1680 my $link = $item->add_to_bundle($bundle_item);
1682 Adds the bundle_item passed to this item
1687 my ( $self, $bundle_item, $options ) = @_;
1691 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1692 if ( $self->itemnumber eq $bundle_item->itemnumber
1693 || $bundle_item->is_bundle
1694 || $self->in_bundle );
1696 my $schema = Koha::Database->new->schema;
1698 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1703 my $checkout = $bundle_item->checkout;
1705 unless ($options->{force_checkin}) {
1706 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1709 my $branchcode = C4::Context->userenv->{'branch'};
1710 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1712 Koha::Exceptions::Checkin::FailedCheckin->throw();
1716 my $holds = $bundle_item->current_holds;
1717 if ($holds->count) {
1718 unless ($options->{ignore_holds}) {
1719 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1723 $self->_result->add_to_item_bundles_hosts(
1724 { item => $bundle_item->itemnumber } );
1726 $bundle_item->notforloan($BundleNotLoanValue)->store();
1732 # 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
1733 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1734 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1736 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1737 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1738 Koha::Exceptions::Object::FKConstraint->throw(
1739 error => 'Broken FK constraint',
1740 broken_fk => $+{column}
1745 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1747 Koha::Exceptions::Object::DuplicateID->throw(
1748 error => 'Duplicate ID',
1749 duplicate_id => $+{key}
1752 elsif ( $_->{msg} =~
1753 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1755 { # The optional \W in the regex might be a quote or backtick
1756 my $type = $+{type};
1757 my $value = $+{value};
1758 my $property = $+{property};
1759 $property =~ s/['`]//g;
1760 Koha::Exceptions::Object::BadValue->throw(
1763 property => $property =~ /(\w+\.\w+)$/
1766 , # results in table.column without quotes or backtics
1770 # Catch-all for foreign key breakages. It will help find other use cases
1779 =head3 remove_from_bundle
1781 Remove this item from any bundle it may have been attached to.
1785 sub remove_from_bundle {
1788 my $bundle_item_rs = $self->_result->item_bundles_item;
1789 if ( $bundle_item_rs ) {
1790 $bundle_item_rs->delete;
1791 $self->notforloan(0)->store();
1797 =head2 Internal methods
1799 =head3 _after_item_action_hooks
1801 Helper method that takes care of calling all plugin hooks
1805 sub _after_item_action_hooks {
1806 my ( $self, $params ) = @_;
1808 my $action = $params->{action};
1810 Koha::Plugins->call(
1811 'after_item_action',
1815 item_id => $self->itemnumber,
1822 my $recall = $item->recall;
1824 Return the relevant recall for this item
1830 my @recalls = Koha::Recalls->search(
1832 biblio_id => $self->biblionumber,
1835 { order_by => { -asc => 'created_date' } }
1837 foreach my $recall (@recalls) {
1838 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1842 # no item-level recall to return, so return earliest biblio-level
1843 # FIXME: eventually this will be based on priority
1847 =head3 can_be_recalled
1849 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1851 Does item-level checks and returns if items can be recalled by this borrower
1855 sub can_be_recalled {
1856 my ( $self, $params ) = @_;
1858 return 0 if !( C4::Context->preference('UseRecalls') );
1860 # check if this item is not for loan, withdrawn or lost
1861 return 0 if ( $self->notforloan != 0 );
1862 return 0 if ( $self->itemlost != 0 );
1863 return 0 if ( $self->withdrawn != 0 );
1865 # check if this item is not checked out - if not checked out, can't be recalled
1866 return 0 if ( !defined( $self->checkout ) );
1868 my $patron = $params->{patron};
1870 my $branchcode = C4::Context->userenv->{'branch'};
1872 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1875 # Check the circulation rule for each relevant itemtype for this item
1876 my $rule = Koha::CirculationRules->get_effective_rules({
1877 branchcode => $branchcode,
1878 categorycode => $patron ? $patron->categorycode : undef,
1879 itemtype => $self->effective_itemtype,
1882 'recalls_per_record',
1887 # check recalls allowed has been set and is not zero
1888 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1891 # check borrower has not reached open recalls allowed limit
1892 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1894 # check borrower has not reach open recalls allowed per record limit
1895 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1897 # check if this patron has already recalled this item
1898 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1900 # check if this patron has already checked out this item
1901 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1903 # check if this patron has already reserved this item
1904 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1907 # check item availability
1908 # items are unavailable for recall if they are lost, withdrawn or notforloan
1909 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1911 # if there are no available items at all, no recall can be placed
1912 return 0 if ( scalar @items == 0 );
1914 my $checked_out_count = 0;
1916 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1919 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1920 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1922 # can't recall if no items have been checked out
1923 return 0 if ( $checked_out_count == 0 );
1929 =head3 can_be_waiting_recall
1931 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1933 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1934 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1938 sub can_be_waiting_recall {
1941 return 0 if !( C4::Context->preference('UseRecalls') );
1943 # check if this item is not for loan, withdrawn or lost
1944 return 0 if ( $self->notforloan != 0 );
1945 return 0 if ( $self->itemlost != 0 );
1946 return 0 if ( $self->withdrawn != 0 );
1948 my $branchcode = $self->holdingbranch;
1949 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1950 $branchcode = C4::Context->userenv->{'branch'};
1952 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1955 # Check the circulation rule for each relevant itemtype for this item
1956 my $most_relevant_recall = $self->check_recalls;
1957 my $rule = Koha::CirculationRules->get_effective_rules(
1959 branchcode => $branchcode,
1960 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1961 itemtype => $self->effective_itemtype,
1962 rules => [ 'recalls_allowed', ],
1966 # check recalls allowed has been set and is not zero
1967 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1973 =head3 check_recalls
1975 my $recall = $item->check_recalls;
1977 Get the most relevant recall for this item.
1984 my @recalls = Koha::Recalls->search(
1985 { biblio_id => $self->biblionumber,
1986 item_id => [ $self->itemnumber, undef ]
1988 { order_by => { -asc => 'created_date' } }
1989 )->filter_by_current->as_list;
1992 # iterate through relevant recalls to find the best one.
1993 # if we come across a waiting recall, use this one.
1994 # 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.
1995 foreach my $r ( @recalls ) {
1996 if ( $r->waiting ) {
2001 unless ( defined $recall ) {
2002 $recall = $recalls[0];
2008 =head3 is_notforloan
2010 my $is_notforloan = $item->is_notforloan;
2012 Determine whether or not this item is "notforloan" based on
2013 the item's notforloan status or its item type
2019 my $is_notforloan = 0;
2021 if ( $self->notforloan ){
2025 my $itemtype = $self->itemtype;
2027 if ( $itemtype->notforloan ){
2033 return $is_notforloan;
2036 =head3 is_denied_renewal
2038 my $is_denied_renewal = $item->is_denied_renewal;
2040 Determine whether or not this item can be renewed based on the
2041 rules set in the ItemsDeniedRenewal system preference.
2045 sub is_denied_renewal {
2047 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2048 return 0 unless $denyingrules;
2049 foreach my $field (keys %$denyingrules) {
2050 # Silently ignore bad column names; TODO we should validate elsewhere
2051 next if !$self->_result->result_source->has_column($field);
2052 my $val = $self->$field;
2053 if( !defined $val) {
2054 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2057 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2058 # If the results matches the values in the syspref
2059 # We return true if match found
2068 Returns a map of column name to string representations including the string,
2069 the mapping type and the mapping category where appropriate.
2071 Currently handles authorised value mappings, library, callnumber and itemtype
2074 Accepts a param hashref where the 'public' key denotes whether we want the public
2075 or staff client strings.
2080 my ( $self, $params ) = @_;
2081 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2082 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2083 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2085 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2087 # Hardcoded known 'authorised_value' values mapped to API codes
2088 my $code_to_type = {
2089 branches => 'library',
2090 cn_source => 'call_number_source',
2091 itemtypes => 'item_type',
2094 # Handle not null and default values for integers and dates
2097 foreach my $col ( @{$self->_columns} ) {
2099 # By now, we are done with known columns, now check the framework for mappings
2100 my $field = $self->_result->result_source->name . '.' . $col;
2102 # Check there's an entry in the MARC subfield structure for the field
2103 if ( exists $mss->{$field}
2104 && scalar @{ $mss->{$field} } > 0
2105 && $mss->{$field}[0]->{authorised_value} )
2107 my $subfield = $mss->{$field}[0];
2108 my $code = $subfield->{authorised_value};
2110 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2111 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2112 $strings->{$col} = {
2115 ( $type eq 'av' ? ( category => $code ) : () ),
2133 Kyle M Hall <kyle@bywatersolutions.com>