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;
57 use Koha::Policy::Holds;
59 use base qw(Koha::Object);
63 Koha::Item - Koha Item object class
75 $params can take an optional 'skip_record_index' parameter.
76 If set, the reindexation process will not happen (index_records not called)
77 You should not turn it on if you do not understand what it is doing exactly.
83 my $params = @_ ? shift : {};
85 my $log_action = $params->{log_action} // 1;
87 # We do not want to oblige callers to pass this value
88 # Dev conveniences vs performance?
89 unless ( $self->biblioitemnumber ) {
90 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
93 # See related changes from C4::Items::AddItem
94 unless ( $self->itype ) {
95 $self->itype($self->biblio->biblioitem->itemtype);
98 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
100 my $today = dt_from_string;
101 my $action = 'create';
103 unless ( $self->in_storage ) { #AddItem
105 unless ( $self->permanent_location ) {
106 $self->permanent_location($self->location);
109 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
110 unless ( $self->location || !$default_location ) {
111 $self->permanent_location( $self->location || $default_location )
112 unless $self->permanent_location;
113 $self->location($default_location);
116 unless ( $self->replacementpricedate ) {
117 $self->replacementpricedate($today);
119 unless ( $self->datelastseen ) {
120 $self->datelastseen($today);
123 unless ( $self->dateaccessioned ) {
124 $self->dateaccessioned($today);
127 if ( $self->itemcallnumber
128 or $self->cn_source )
130 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
131 $self->cn_sort($cn_sort);
134 # should be quite rare when adding item
135 if ( $self->itemlost && $self->itemlost > 0 ) { # TODO BZ34308
136 $self->_add_statistic('item_lost');
143 my %updated_columns = $self->_result->get_dirty_columns;
144 return $self->SUPER::store unless %updated_columns;
146 # Retrieve the item for comparison if we need to
148 exists $updated_columns{itemlost}
149 or exists $updated_columns{withdrawn}
150 or exists $updated_columns{damaged}
151 ) ? $self->get_from_storage : undef;
153 # Update *_on fields if needed
154 # FIXME: Why not for AddItem as well?
155 my @fields = qw( itemlost withdrawn damaged );
156 for my $field (@fields) {
158 # If the field is defined but empty or 0, we are
159 # removing/unsetting and thus need to clear out
161 if ( exists $updated_columns{$field}
162 && defined( $self->$field )
165 my $field_on = "${field}_on";
166 $self->$field_on(undef);
168 # If the field has changed otherwise, we much update
170 elsif (exists $updated_columns{$field}
171 && $updated_columns{$field}
172 && !$pre_mod_item->$field )
174 my $field_on = "${field}_on";
175 $self->$field_on(dt_from_string);
179 if ( exists $updated_columns{itemcallnumber}
180 or exists $updated_columns{cn_source} )
182 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
183 $self->cn_sort($cn_sort);
187 if ( exists $updated_columns{location}
188 and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
189 and not exists $updated_columns{permanent_location} )
191 $self->permanent_location( $self->location );
194 # TODO BZ 34308 (gt zero checks)
195 if ( exists $updated_columns{itemlost}
196 && ( !$updated_columns{itemlost} || $updated_columns{itemlost} <= 0 )
197 && ( $pre_mod_item->itemlost && $pre_mod_item->itemlost > 0 ) )
200 # reverse any list item charges if necessary
201 $self->_set_found_trigger($pre_mod_item);
202 $self->_add_statistic('item_found');
203 } elsif ( exists $updated_columns{itemlost}
204 && ( $updated_columns{itemlost} && $updated_columns{itemlost} > 0 )
205 && ( !$pre_mod_item->itemlost || $pre_mod_item->itemlost <= 0 ) )
208 $self->_add_statistic('item_lost');
212 my $result = $self->SUPER::store;
213 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
215 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
216 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
218 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
219 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
220 unless $params->{skip_record_index};
221 $self->get_from_storage->_after_item_action_hooks({ action => $action });
223 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
225 biblio_ids => [ $self->biblionumber ]
227 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
233 my ( $self, $type ) = @_;
234 C4::Stats::UpdateStats(
236 borrowernumber => undef,
237 branch => C4::Context->userenv ? C4::Context->userenv->{branch} : undef,
238 categorycode => undef,
239 ccode => $self->ccode,
240 itemnumber => $self->itemnumber,
241 itemtype => $self->effective_itemtype,
242 location => $self->location,
254 my $params = @_ ? shift : {};
256 # FIXME check the item has no current issues
257 # i.e. raise the appropriate exception
259 # Get the item group so we can delete it later if it has no items left
260 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
262 my $result = $self->SUPER::delete;
264 # Delete the item group if it has no items left
265 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
267 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
268 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
269 unless $params->{skip_record_index};
271 $self->_after_item_action_hooks({ action => 'delete' });
273 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
274 if C4::Context->preference("CataloguingLog");
276 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
278 biblio_ids => [ $self->biblionumber ]
280 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
291 my $params = @_ ? shift : {};
293 my $safe_to_delete = $self->safe_to_delete;
294 return $safe_to_delete unless $safe_to_delete;
296 $self->move_to_deleted;
298 return $self->delete($params);
301 =head3 safe_to_delete
303 returns 1 if the item is safe to delete,
305 "book_on_loan" if the item is checked out,
307 "not_same_branch" if the item is blocked by independent branches,
309 "book_reserved" if the there are holds aganst the item, or
311 "linked_analytics" if the item has linked analytic records.
313 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
322 $error = "book_on_loan" if $self->checkout;
324 $error //= "not_same_branch"
325 if defined C4::Context->userenv
326 and defined C4::Context->userenv->{number}
327 and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
329 # check it doesn't have a waiting reserve
330 $error //= "book_reserved"
331 if $self->holds->filter_by_found->count;
333 $error //= "linked_analytics"
334 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
336 $error //= "last_item_for_hold"
337 if $self->biblio->items->count == 1
338 && $self->biblio->holds->search(
345 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
348 return Koha::Result::Boolean->new(1);
351 =head3 move_to_deleted
353 my $is_moved = $item->move_to_deleted;
355 Move an item to the deleteditems table.
356 This can be done before deleting an item, to make sure the data are not completely deleted.
360 sub move_to_deleted {
362 my $item_infos = $self->unblessed;
363 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
364 $item_infos->{deleted_on} = dt_from_string;
365 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
368 =head3 effective_itemtype
370 Returns the itemtype for the item based on whether item level itemtypes are set or not.
374 sub effective_itemtype {
377 return $self->_result()->effective_itemtype();
387 my $hb_rs = $self->_result->homebranch;
389 return Koha::Library->_new_from_dbic( $hb_rs );
392 =head3 holding_branch
399 my $hb_rs = $self->_result->holdingbranch;
401 return Koha::Library->_new_from_dbic( $hb_rs );
406 my $biblio = $item->biblio;
408 Return the bibliographic record of this item
414 my $biblio_rs = $self->_result->biblio;
415 return Koha::Biblio->_new_from_dbic( $biblio_rs );
420 my $biblioitem = $item->biblioitem;
422 Return the biblioitem record of this item
428 my $biblioitem_rs = $self->_result->biblioitem;
429 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
434 my $checkout = $item->checkout;
436 Return the checkout for this item
442 my $checkout_rs = $self->_result->issue;
443 return unless $checkout_rs;
444 return Koha::Checkout->_new_from_dbic( $checkout_rs );
449 my $item_group = $item->item_group;
451 Return the item group for this item
458 my $item_group_item = $self->_result->item_group_item;
459 return unless $item_group_item;
461 my $item_group_rs = $item_group_item->item_group;
462 return unless $item_group_rs;
464 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
470 my $return_claims = $item->return_claims;
472 Return any return_claims associated with this item
477 my ( $self, $params, $attrs ) = @_;
478 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
479 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
484 my $return_claim = $item->return_claim;
486 Returns the most recent unresolved return_claims associated with this item
493 $self->_result->return_claims->search( { resolution => undef },
494 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
495 return unless $claims_rs;
496 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
501 my $holds = $item->holds();
502 my $holds = $item->holds($params);
503 my $holds = $item->holds({ found => 'W'});
505 Return holds attached to an item, optionally accept a hashref of params to pass to search
510 my ( $self,$params ) = @_;
511 my $holds_rs = $self->_result->reserves->search($params);
512 return Koha::Holds->_new_from_dbic( $holds_rs );
517 my $bookings = $item->bookings();
519 Returns the bookings attached to this item.
524 my ( $self, $params ) = @_;
525 my $bookings_rs = $self->_result->bookings->search($params);
526 return Koha::Bookings->_new_from_dbic( $bookings_rs );
532 $item->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
534 Returns a boolean denoting whether the passed booking can be made without clashing.
536 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
541 my ($self, $params) = @_;
543 my $start_date = dt_from_string( $params->{start_date} );
544 my $end_date = dt_from_string( $params->{end_date} );
545 my $booking_id = $params->{booking_id};
547 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
548 my $existing_bookings = $self->bookings(
552 $dtf->format_datetime($start_date),
553 $dtf->format_datetime($end_date)
558 $dtf->format_datetime($start_date),
559 $dtf->format_datetime($end_date)
563 start_date => { '<' => $dtf->format_datetime($start_date) },
564 end_date => { '>' => $dtf->format_datetime($end_date) }
571 ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )
573 : $existing_bookings->count;
575 return $bookings_count ? 0 : 1;
580 my $booking = $item->place_booking(
583 start_date => $datetime,
584 end_date => $datetime
588 Add a booking for this item for the dates passed.
590 Returns the Koha::Booking object or throws an exception if the item cannot be booked for the given dates.
595 my ( $self, $params ) = @_;
597 # check for mandatory params
598 my @mandatory = ( 'start_date', 'end_date', 'patron' );
599 for my $param (@mandatory) {
600 unless ( defined( $params->{$param} ) ) {
601 Koha::Exceptions::MissingParameter->throw(
602 error => "The $param parameter is mandatory" );
605 my $patron = $params->{patron};
608 my $booking = Koha::Booking->new(
610 start_date => $params->{start_date},
611 end_date => $params->{end_date},
612 borrowernumber => $patron->borrowernumber,
613 biblionumber => $self->biblionumber,
614 itemnumber => $self->itemnumber,
620 =head3 request_transfer
622 my $transfer = $item->request_transfer(
626 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
630 Add a transfer request for this item to the given branch for the given reason.
632 An exception will be thrown if the BranchTransferLimits would prevent the requested
633 transfer, unless 'ignore_limits' is passed to override the limits.
635 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
636 The caller should catch such cases and retry the transfer request as appropriate passing
637 an appropriate override.
640 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
641 * replace - Used to replace the existing transfer request with your own.
645 sub request_transfer {
646 my ( $self, $params ) = @_;
648 # check for mandatory params
649 my @mandatory = ( 'to', 'reason' );
650 for my $param (@mandatory) {
651 unless ( defined( $params->{$param} ) ) {
652 Koha::Exceptions::MissingParameter->throw(
653 error => "The $param parameter is mandatory" );
657 Koha::Exceptions::Item::Transfer::Limit->throw()
658 unless ( $params->{ignore_limits}
659 || $self->can_be_transferred( { to => $params->{to} } ) );
661 my $request = $self->get_transfer;
662 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
663 if ( $request && !$params->{enqueue} && !$params->{replace} );
665 $request->cancel( { reason => $params->{reason}, force => 1 } )
666 if ( defined($request) && $params->{replace} );
668 my $transfer = Koha::Item::Transfer->new(
670 itemnumber => $self->itemnumber,
671 daterequested => dt_from_string,
672 frombranch => $self->holdingbranch,
673 tobranch => $params->{to}->branchcode,
674 reason => $params->{reason},
675 comments => $params->{comment}
684 my $transfer = $item->get_transfer;
686 Return the active transfer request or undef
688 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
689 whereby the most recently sent, but not received, transfer will be returned
690 if it exists, otherwise the oldest unsatisfied transfer will be returned.
692 This allows for transfers to queue, which is the case for stock rotation and
693 rotating collections where a manual transfer may need to take precedence but
694 we still expect the item to end up at a final location eventually.
701 my $transfer = $self->_result->current_branchtransfers->next;
702 return Koha::Item::Transfer->_new_from_dbic($transfer) if $transfer;
707 my $transfer = $item->get_transfers;
709 Return the list of outstanding transfers (i.e requested but not yet cancelled
712 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
713 whereby the most recently sent, but not received, transfer will be returned
714 first if it exists, otherwise requests are in oldest to newest request order.
716 This allows for transfers to queue, which is the case for stock rotation and
717 rotating collections where a manual transfer may need to take precedence but
718 we still expect the item to end up at a final location eventually.
725 my $transfer_rs = $self->_result->current_branchtransfers;
727 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
730 =head3 last_returned_by
732 Gets and sets the last patron to return an item.
734 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
736 $item->last_returned_by( $borrowernumber );
738 my $patron = $item->last_returned_by();
742 sub last_returned_by {
743 my ( $self, $borrowernumber ) = @_;
744 if ( $borrowernumber ) {
745 $self->_result->update_or_create_related('last_returned_by',
746 { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
748 my $rs = $self->_result->last_returned_by;
750 return Koha::Patron->_new_from_dbic($rs->borrowernumber);
753 =head3 can_article_request
755 my $bool = $item->can_article_request( $borrower )
757 Returns true if item can be specifically requested
759 $borrower must be a Koha::Patron object
763 sub can_article_request {
764 my ( $self, $borrower ) = @_;
766 my $rule = $self->article_request_type($borrower);
768 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
772 =head3 hidden_in_opac
774 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
776 Returns true if item fields match the hidding criteria defined in $rules.
777 Returns false otherwise.
779 Takes HASHref that can have the following parameters:
781 $rules : { <field> => [ value_1, ... ], ... }
783 Note: $rules inherits its structure from the parsed YAML from reading
784 the I<OpacHiddenItems> system preference.
789 my ( $self, $params ) = @_;
791 my $rules = $params->{rules} // {};
794 if C4::Context->preference('hidelostitems') and
797 my $hidden_in_opac = 0;
799 foreach my $field ( keys %{$rules} ) {
801 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
807 return $hidden_in_opac;
810 =head3 can_be_transferred
812 $item->can_be_transferred({ to => $to_library, from => $from_library })
813 Checks if an item can be transferred to given library.
815 This feature is controlled by two system preferences:
816 UseBranchTransferLimits to enable / disable the feature
817 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
818 for setting the limitations
820 Takes HASHref that can have the following parameters:
821 MANDATORY PARAMETERS:
824 $from : Koha::Library # if not given, item holdingbranch
825 # will be used instead
827 Returns 1 if item can be transferred to $to_library, otherwise 0.
829 To find out whether at least one item of a Koha::Biblio can be transferred, please
830 see Koha::Biblio->can_be_transferred() instead of using this method for
831 multiple items of the same biblio.
835 sub can_be_transferred {
836 my ($self, $params) = @_;
838 my $to = $params->{to};
839 my $from = $params->{from};
841 $to = $to->branchcode;
842 $from = defined $from ? $from->branchcode : $self->holdingbranch;
844 return 1 if $from eq $to; # Transfer to current branch is allowed
845 return 1 unless C4::Context->preference('UseBranchTransferLimits');
847 my $limittype = C4::Context->preference('BranchTransferLimitsType');
848 return Koha::Item::Transfer::Limits->search({
851 $limittype => $limittype eq 'itemtype'
852 ? $self->effective_itemtype : $self->ccode
857 =head3 pickup_locations
859 my $pickup_locations = $item->pickup_locations({ patron => $patron })
861 Returns possible pickup locations for this item, according to patron's home library
862 and if item can be transferred to each pickup location.
864 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
869 sub pickup_locations {
870 my ($self, $params) = @_;
872 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
873 unless exists $params->{patron};
875 my $patron = $params->{patron};
877 my $circ_control_branch = Koha::Policy::Holds->holds_control_library( $self, $patron );
879 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
881 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
882 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
884 my $pickup_libraries = Koha::Libraries->search();
885 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
886 $pickup_libraries = $self->home_branch->get_hold_libraries;
887 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
888 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
889 $pickup_libraries = $plib->get_hold_libraries;
890 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
891 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
892 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
893 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
896 return $pickup_libraries->search(
901 order_by => ['branchname']
903 ) unless C4::Context->preference('UseBranchTransferLimits');
905 my $limittype = C4::Context->preference('BranchTransferLimitsType');
906 my ($ccode, $itype) = (undef, undef);
907 if( $limittype eq 'ccode' ){
908 $ccode = $self->ccode;
910 $itype = $self->itype;
912 my $limits = Koha::Item::Transfer::Limits->search(
914 fromBranch => $self->holdingbranch,
918 { columns => ['toBranch'] }
921 return $pickup_libraries->search(
923 pickup_location => 1,
925 '-not_in' => $limits->_resultset->as_query
929 order_by => ['branchname']
934 =head3 article_request_type
936 my $type = $item->article_request_type( $borrower )
938 returns 'yes', 'no', 'bib_only', or 'item_only'
940 $borrower must be a Koha::Patron object
944 sub article_request_type {
945 my ( $self, $borrower ) = @_;
947 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
949 $branch_control eq 'homebranch' ? $self->homebranch
950 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
952 my $borrowertype = $borrower->categorycode;
953 my $itemtype = $self->effective_itemtype();
954 my $rule = Koha::CirculationRules->get_effective_rule(
956 rule_name => 'article_requests',
957 categorycode => $borrowertype,
958 itemtype => $itemtype,
959 branchcode => $branchcode
963 return q{} unless $rule;
964 return $rule->rule_value || q{}
973 my $attributes = { order_by => 'priority' };
974 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
976 itemnumber => $self->itemnumber,
979 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
980 waitingdate => { '!=' => undef },
983 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
984 return Koha::Holds->_new_from_dbic($hold_rs);
987 =head3 stockrotationitem
989 my $sritem = Koha::Item->stockrotationitem;
991 Returns the stock rotation item associated with the current item.
995 sub stockrotationitem {
997 my $rs = $self->_result->stockrotationitem;
999 return Koha::StockRotationItem->_new_from_dbic( $rs );
1004 my $item = $item->add_to_rota($rota_id);
1006 Add this item to the rota identified by $ROTA_ID, which means associating it
1007 with the first stage of that rota. Should this item already be associated
1008 with a rota, then we will move it to the new rota.
1013 my ( $self, $rota_id ) = @_;
1014 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
1018 =head3 has_pending_hold
1020 my $is_pending_hold = $item->has_pending_hold();
1022 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
1026 sub has_pending_hold {
1028 return $self->_result->tmp_holdsqueue ? 1 : 0;
1031 =head3 has_pending_recall {
1033 my $has_pending_recall
1035 Return if whether has pending recall of not.
1039 sub has_pending_recall {
1042 # FIXME Must be moved to $self->recalls
1043 return Koha::Recalls->search(
1045 item_id => $self->itemnumber,
1046 status => 'waiting',
1051 =head3 as_marc_field
1053 my $field = $item->as_marc_field;
1055 This method returns a MARC::Field object representing the Koha::Item object
1056 with the current mappings configuration.
1063 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1065 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
1069 my $item_field = $tagslib->{$itemtag};
1071 my $more_subfields = $self->additional_attributes->to_hashref;
1072 foreach my $subfield (
1074 $a->{display_order} <=> $b->{display_order}
1075 || $a->{subfield} cmp $b->{subfield}
1076 } grep { ref($_) && %$_ } values %$item_field
1079 my $kohafield = $subfield->{kohafield};
1080 my $tagsubfield = $subfield->{tagsubfield};
1082 if ( defined $kohafield && $kohafield ne '' ) {
1083 next if $kohafield !~ m{^items\.}; # That would be weird!
1084 ( my $attribute = $kohafield ) =~ s|^items\.||;
1085 $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
1086 if defined $self->$attribute and $self->$attribute ne '';
1088 $value = $more_subfields->{$tagsubfield}
1091 next unless defined $value
1094 if ( $subfield->{repeatable} ) {
1095 my @values = split '\|', $value;
1096 push @subfields, ( $tagsubfield => $_ ) for @values;
1099 push @subfields, ( $tagsubfield => $value );
1104 return unless @subfields;
1106 return MARC::Field->new(
1107 "$itemtag", ' ', ' ', @subfields
1111 =head3 renewal_branchcode
1113 Returns the branchcode to be recorded in statistics renewal of the item
1117 sub renewal_branchcode {
1119 my ($self, $params ) = @_;
1121 my $interface = C4::Context->interface;
1123 if ( $interface eq 'opac' ){
1124 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1125 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1126 $branchcode = 'OPACRenew';
1128 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1129 $branchcode = $self->homebranch;
1131 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1132 $branchcode = $self->checkout->patron->branchcode;
1134 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1135 $branchcode = $self->checkout->branchcode;
1141 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1142 ? C4::Context->userenv->{branch} : $params->{branch};
1149 Return the cover images associated with this item.
1156 my $cover_image_rs = $self->_result->cover_images;
1157 return unless $cover_image_rs;
1158 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1161 =head3 columns_to_str
1163 my $values = $items->columns_to_str;
1165 Return a hashref with the string representation of the different attribute of the item.
1167 This is meant to be used for display purpose only.
1171 sub columns_to_str {
1173 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1174 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1175 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1177 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1180 for my $column ( @{$self->_columns}) {
1182 next if $column eq 'more_subfields_xml';
1184 my $value = $self->$column;
1185 # 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
1187 if ( not defined $value or $value eq "" ) {
1188 $values->{$column} = $value;
1193 exists $mss->{"items.$column"}
1194 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1197 $values->{$column} =
1199 ? $subfield->{authorised_value}
1200 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1201 $subfield->{tagsubfield}, $value, '', $tagslib )
1207 $self->more_subfields_xml
1208 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1213 my ( $field ) = $marc_more->fields;
1214 for my $sf ( $field->subfields ) {
1215 my $subfield_code = $sf->[0];
1216 my $value = $sf->[1];
1217 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1218 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1220 $subfield->{authorised_value}
1221 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1222 $subfield->{tagsubfield}, $value, '', $tagslib )
1225 push @{$more_values->{$subfield_code}}, $value;
1228 while ( my ( $k, $v ) = each %$more_values ) {
1229 $values->{$k} = join ' | ', @$v;
1236 =head3 additional_attributes
1238 my $attributes = $item->additional_attributes;
1239 $attributes->{k} = 'new k';
1240 $item->update({ more_subfields => $attributes->to_marcxml });
1242 Returns a Koha::Item::Attributes object that represents the non-mapped
1243 attributes for this item.
1247 sub additional_attributes {
1250 return Koha::Item::Attributes->new_from_marcxml(
1251 $self->more_subfields_xml,
1255 =head3 _set_found_trigger
1257 $self->_set_found_trigger
1259 Finds the most recent lost item charge for this item and refunds the patron
1260 appropriately, taking into account any payments or writeoffs already applied
1263 Internal function, not exported, called only by Koha::Item->store.
1267 sub _set_found_trigger {
1268 my ( $self, $pre_mod_item ) = @_;
1270 # Reverse any lost item charges if necessary.
1271 my $no_refund_after_days =
1272 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1273 if ($no_refund_after_days) {
1274 my $today = dt_from_string();
1275 my $lost_age_in_days =
1276 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1279 return $self unless $lost_age_in_days < $no_refund_after_days;
1282 my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1285 return_branch => C4::Context->userenv
1286 ? C4::Context->userenv->{'branch'}
1290 my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1292 if ( $lostreturn_policy ) {
1294 # refund charge made for lost book
1295 my $lost_charge = Koha::Account::Lines->search(
1297 itemnumber => $self->itemnumber,
1298 debit_type_code => 'LOST',
1299 status => [ undef, { '<>' => 'FOUND' } ]
1302 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1307 if ( $lost_charge ) {
1309 my $patron = $lost_charge->patron;
1312 my $account = $patron->account;
1314 # Credit outstanding amount
1315 my $credit_total = $lost_charge->amountoutstanding;
1319 $lost_charge->amount > $lost_charge->amountoutstanding &&
1320 $lostreturn_policy ne "refund_unpaid"
1322 # some amount has been cancelled. collect the offsets that are not writeoffs
1323 # this works because the only way to subtract from this kind of a debt is
1324 # using the UI buttons 'Pay' and 'Write off'
1326 # We don't credit any payments if return policy is
1329 # In that case only unpaid/outstanding amount
1330 # will be credited which settles the debt without
1331 # creating extra credits
1333 my $credit_offsets = $lost_charge->debit_offsets(
1335 'credit_id' => { '!=' => undef },
1336 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1338 { join => 'credit' }
1341 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1342 # credits are negative on the DB
1343 $credit_offsets->total * -1 :
1345 # Credit the outstanding amount, then add what has been
1346 # paid to create a net credit for this amount
1347 $credit_total += $total_to_refund;
1351 if ( $credit_total > 0 ) {
1353 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1354 $credit = $account->add_credit(
1356 amount => $credit_total,
1357 description => 'Item found ' . $self->itemnumber,
1358 type => 'LOST_FOUND',
1359 interface => C4::Context->interface,
1360 library_id => $branchcode,
1361 item_id => $self->itemnumber,
1362 issue_id => $lost_charge->issue_id
1366 $credit->apply( { debits => [$lost_charge] } );
1370 message => 'lost_refunded',
1371 payload => { credit_id => $credit->id }
1376 # Update the account status
1377 $lost_charge->status('FOUND');
1378 $lost_charge->store();
1380 # Reconcile balances if required
1381 if ( C4::Context->preference('AccountAutoReconcile') ) {
1382 $account->reconcile_balance;
1387 # possibly restore fine for lost book
1388 my $lost_overdue = Koha::Account::Lines->search(
1390 itemnumber => $self->itemnumber,
1391 debit_type_code => 'OVERDUE',
1395 order_by => { '-desc' => 'date' },
1399 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1401 my $patron = $lost_overdue->patron;
1403 my $account = $patron->account;
1405 # Update status of fine
1406 $lost_overdue->status('FOUND')->store();
1408 # Find related forgive credit
1409 my $refund = $lost_overdue->credits(
1411 credit_type_code => 'FORGIVEN',
1412 itemnumber => $self->itemnumber,
1413 status => [ { '!=' => 'VOID' }, undef ]
1415 { order_by => { '-desc' => 'date' }, rows => 1 }
1419 # Revert the forgive credit
1420 $refund->void({ interface => 'trigger' });
1424 message => 'lost_restored',
1425 payload => { refund_id => $refund->id }
1430 # Reconcile balances if required
1431 if ( C4::Context->preference('AccountAutoReconcile') ) {
1432 $account->reconcile_balance;
1436 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1440 message => 'lost_charge',
1446 my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1448 if ( $processingreturn_policy ) {
1450 # refund processing charge made for lost book
1451 my $processing_charge = Koha::Account::Lines->search(
1453 itemnumber => $self->itemnumber,
1454 debit_type_code => 'PROCESSING',
1455 status => [ undef, { '<>' => 'FOUND' } ]
1458 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1463 if ( $processing_charge ) {
1465 my $patron = $processing_charge->patron;
1468 my $account = $patron->account;
1470 # Credit outstanding amount
1471 my $credit_total = $processing_charge->amountoutstanding;
1475 $processing_charge->amount > $processing_charge->amountoutstanding &&
1476 $processingreturn_policy ne "refund_unpaid"
1478 # some amount has been cancelled. collect the offsets that are not writeoffs
1479 # this works because the only way to subtract from this kind of a debt is
1480 # using the UI buttons 'Pay' and 'Write off'
1482 # We don't credit any payments if return policy is
1485 # In that case only unpaid/outstanding amount
1486 # will be credited which settles the debt without
1487 # creating extra credits
1489 my $credit_offsets = $processing_charge->debit_offsets(
1491 'credit_id' => { '!=' => undef },
1492 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1494 { join => 'credit' }
1497 my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1498 # credits are negative on the DB
1499 $credit_offsets->total * -1 :
1501 # Credit the outstanding amount, then add what has been
1502 # paid to create a net credit for this amount
1503 $credit_total += $total_to_refund;
1507 if ( $credit_total > 0 ) {
1509 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1510 $credit = $account->add_credit(
1512 amount => $credit_total,
1513 description => 'Item found ' . $self->itemnumber,
1514 type => 'PROCESSING_FOUND',
1515 interface => C4::Context->interface,
1516 library_id => $branchcode,
1517 item_id => $self->itemnumber,
1518 issue_id => $processing_charge->issue_id
1522 $credit->apply( { debits => [$processing_charge] } );
1526 message => 'processing_refunded',
1527 payload => { credit_id => $credit->id }
1532 # Update the account status
1533 $processing_charge->status('FOUND');
1534 $processing_charge->store();
1536 # Reconcile balances if required
1537 if ( C4::Context->preference('AccountAutoReconcile') ) {
1538 $account->reconcile_balance;
1547 =head3 public_read_list
1549 This method returns the list of publicly readable database fields for both API and UI output purposes
1553 sub public_read_list {
1555 'itemnumber', 'biblionumber', 'homebranch',
1556 'holdingbranch', 'location', 'collectioncode',
1557 'itemcallnumber', 'copynumber', 'enumchron',
1558 'barcode', 'dateaccessioned', 'itemnotes',
1559 'onloan', 'uri', 'itype',
1560 'notforloan', 'damaged', 'itemlost',
1561 'withdrawn', 'restricted'
1567 Overloaded to_api method to ensure item-level itypes is adhered to.
1572 my ($self, $params) = @_;
1574 my $response = $self->SUPER::to_api($params);
1577 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1579 my $itype_notforloan = $self->itemtype->notforloan;
1580 $overrides->{effective_not_for_loan_status} =
1581 ( defined $itype_notforloan && !$self->notforloan ) ? $itype_notforloan : $self->notforloan;
1583 return { %$response, %$overrides };
1586 =head3 to_api_mapping
1588 This method returns the mapping for representing a Koha::Item object
1593 sub to_api_mapping {
1595 itemnumber => 'item_id',
1596 biblionumber => 'biblio_id',
1597 biblioitemnumber => undef,
1598 barcode => 'external_id',
1599 dateaccessioned => 'acquisition_date',
1600 booksellerid => 'acquisition_source',
1601 homebranch => 'home_library_id',
1602 price => 'purchase_price',
1603 replacementprice => 'replacement_price',
1604 replacementpricedate => 'replacement_price_date',
1605 datelastborrowed => 'last_checkout_date',
1606 datelastseen => 'last_seen_date',
1608 notforloan => 'not_for_loan_status',
1609 damaged => 'damaged_status',
1610 damaged_on => 'damaged_date',
1611 itemlost => 'lost_status',
1612 itemlost_on => 'lost_date',
1613 withdrawn => 'withdrawn',
1614 withdrawn_on => 'withdrawn_date',
1615 itemcallnumber => 'callnumber',
1616 coded_location_qualifier => 'coded_location_qualifier',
1617 issues => 'checkouts_count',
1618 renewals => 'renewals_count',
1619 reserves => 'holds_count',
1620 restricted => 'restricted_status',
1621 itemnotes => 'public_notes',
1622 itemnotes_nonpublic => 'internal_notes',
1623 holdingbranch => 'holding_library_id',
1624 timestamp => 'timestamp',
1625 location => 'location',
1626 permanent_location => 'permanent_location',
1627 onloan => 'checked_out_date',
1628 cn_source => 'call_number_source',
1629 cn_sort => 'call_number_sort',
1630 ccode => 'collection_code',
1631 materials => 'materials_notes',
1633 itype => 'item_type_id',
1634 more_subfields_xml => 'extended_subfields',
1635 enumchron => 'serial_issue_number',
1636 copynumber => 'copy_number',
1637 stocknumber => 'inventory_number',
1638 new_status => 'new_status',
1639 deleted_on => undef,
1645 my $itemtype = $item->itemtype;
1647 Returns Koha object for effective itemtype
1654 return Koha::ItemTypes->find( $self->effective_itemtype );
1659 my $orders = $item->orders();
1661 Returns a Koha::Acquisition::Orders object
1668 my $orders = $self->_result->item_orders;
1669 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1672 =head3 tracked_links
1674 my $tracked_links = $item->tracked_links();
1676 Returns a Koha::TrackedLinks object
1683 my $tracked_links = $self->_result->linktrackers;
1684 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1687 =head3 move_to_biblio
1689 $item->move_to_biblio($to_biblio[, $params]);
1691 Move the item to another biblio and update any references in other tables.
1693 The final optional parameter, C<$params>, is expected to contain the
1694 'skip_record_index' key, which is relayed down to Koha::Item->store.
1695 There it prevents calling index_records, which takes most of the
1696 time in batch adds/deletes. The caller must take care of calling
1697 index_records separately.
1700 skip_record_index => 1|0
1702 Returns undef if the move failed or the biblionumber of the destination record otherwise
1706 sub move_to_biblio {
1707 my ( $self, $to_biblio, $params ) = @_;
1711 return if $self->biblionumber == $to_biblio->biblionumber;
1713 my $from_biblionumber = $self->biblionumber;
1714 my $to_biblionumber = $to_biblio->biblionumber;
1716 # Own biblionumber and biblioitemnumber
1718 biblionumber => $to_biblionumber,
1719 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1720 })->store({ skip_record_index => $params->{skip_record_index} });
1722 unless ($params->{skip_record_index}) {
1723 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1724 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1727 # Acquisition orders
1728 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1731 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1733 # hold_fill_target (there's no Koha object available yet)
1734 my $hold_fill_target = $self->_result->hold_fill_target;
1735 if ($hold_fill_target) {
1736 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1739 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1740 # and can't even fake one since the significant columns are nullable.
1741 my $storage = $self->_result->result_source->storage;
1744 my ($storage, $dbh, @cols) = @_;
1746 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1751 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1753 return $to_biblionumber;
1758 my $bundle_items = $item->bundle_items;
1760 Returns the items associated with this bundle
1767 my $rs = $self->_result->bundle_items;
1768 return Koha::Items->_new_from_dbic($rs);
1773 my $is_bundle = $item->is_bundle;
1775 Returns whether the item is a bundle or not
1781 return $self->bundle_items->count ? 1 : 0;
1786 my $bundle = $item->bundle_host;
1788 Returns the bundle item this item is attached to
1795 my $bundle_items_rs = $self->_result->item_bundles_item;
1796 return unless $bundle_items_rs;
1797 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1802 my $in_bundle = $item->in_bundle;
1804 Returns whether this item is currently in a bundle
1810 return $self->bundle_host ? 1 : 0;
1813 =head3 add_to_bundle
1815 my $link = $item->add_to_bundle($bundle_item);
1817 Adds the bundle_item passed to this item
1822 my ( $self, $bundle_item, $options ) = @_;
1826 Koha::Exceptions::Item::Bundle::IsBundle->throw()
1827 if ( $self->itemnumber eq $bundle_item->itemnumber
1828 || $bundle_item->is_bundle
1829 || $self->in_bundle );
1831 my $schema = Koha::Database->new->schema;
1833 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1839 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
1841 my $checkout = $bundle_item->checkout;
1843 unless ($options->{force_checkin}) {
1844 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1847 my $branchcode = C4::Context->userenv->{'branch'};
1848 my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1850 Koha::Exceptions::Checkin::FailedCheckin->throw();
1854 my $holds = $bundle_item->current_holds;
1855 if ($holds->count) {
1856 unless ($options->{ignore_holds}) {
1857 Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
1861 $self->_result->add_to_item_bundles_hosts(
1862 { item => $bundle_item->itemnumber } );
1864 $bundle_item->notforloan($BundleNotLoanValue)->store();
1870 # 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
1871 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1872 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1874 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1875 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1876 Koha::Exceptions::Object::FKConstraint->throw(
1877 error => 'Broken FK constraint',
1878 broken_fk => $+{column}
1883 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1885 Koha::Exceptions::Object::DuplicateID->throw(
1886 error => 'Duplicate ID',
1887 duplicate_id => $+{key}
1890 elsif ( $_->{msg} =~
1891 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1893 { # The optional \W in the regex might be a quote or backtick
1894 my $type = $+{type};
1895 my $value = $+{value};
1896 my $property = $+{property};
1897 $property =~ s/['`]//g;
1898 Koha::Exceptions::Object::BadValue->throw(
1901 property => $property =~ /(\w+\.\w+)$/
1904 , # results in table.column without quotes or backtics
1908 # Catch-all for foreign key breakages. It will help find other use cases
1917 =head3 remove_from_bundle
1919 Remove this item from any bundle it may have been attached to.
1923 sub remove_from_bundle {
1926 my $bundle_host = $self->bundle_host;
1928 return 0 unless $bundle_host; # Should not we raise an exception here?
1930 Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $bundle_host->checkout;
1932 my $bundle_item_rs = $self->_result->item_bundles_item;
1933 if ( $bundle_item_rs ) {
1934 $bundle_item_rs->delete;
1935 $self->notforloan(0)->store();
1941 =head2 Internal methods
1943 =head3 _after_item_action_hooks
1945 Helper method that takes care of calling all plugin hooks
1949 sub _after_item_action_hooks {
1950 my ( $self, $params ) = @_;
1952 my $action = $params->{action};
1954 Koha::Plugins->call(
1955 'after_item_action',
1959 item_id => $self->itemnumber,
1966 my $recall = $item->recall;
1968 Return the relevant recall for this item
1974 my @recalls = Koha::Recalls->search(
1976 biblio_id => $self->biblionumber,
1979 { order_by => { -asc => 'created_date' } }
1982 my $item_level_recall;
1983 foreach my $recall (@recalls) {
1984 if ( $recall->item_level ) {
1985 $item_level_recall = 1;
1986 if ( $recall->item_id == $self->itemnumber ) {
1991 if ($item_level_recall) {
1993 # recall needs to be filled be a specific item only
1994 # no other item is relevant to return
1998 # no item-level recall to return, so return earliest biblio-level
1999 # FIXME: eventually this will be based on priority
2003 =head3 can_be_recalled
2005 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
2007 Does item-level checks and returns if items can be recalled by this borrower
2011 sub can_be_recalled {
2012 my ( $self, $params ) = @_;
2014 return 0 if !( C4::Context->preference('UseRecalls') );
2016 # check if this item is not for loan, withdrawn or lost
2017 return 0 if ( $self->notforloan != 0 );
2018 return 0 if ( $self->itemlost != 0 );
2019 return 0 if ( $self->withdrawn != 0 );
2021 # check if this item is not checked out - if not checked out, can't be recalled
2022 return 0 if ( !defined( $self->checkout ) );
2024 my $patron = $params->{patron};
2026 my $branchcode = C4::Context->userenv->{'branch'};
2028 $branchcode = C4::Circulation::_GetCircControlBranch( $self, $patron );
2031 # Check the circulation rule for each relevant itemtype for this item
2032 my $rule = Koha::CirculationRules->get_effective_rules({
2033 branchcode => $branchcode,
2034 categorycode => $patron ? $patron->categorycode : undef,
2035 itemtype => $self->effective_itemtype,
2038 'recalls_per_record',
2043 # check recalls allowed has been set and is not zero
2044 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
2047 # check borrower has not reached open recalls allowed limit
2048 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
2050 # check borrower has not reach open recalls allowed per record limit
2051 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
2053 # check if this patron has already recalled this item
2054 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
2056 # check if this patron has already checked out this item
2057 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
2059 # check if this patron has already reserved this item
2060 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
2063 # check item availability
2064 # items are unavailable for recall if they are lost, withdrawn or notforloan
2065 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
2067 # if there are no available items at all, no recall can be placed
2068 return 0 if ( scalar @items == 0 );
2070 my $checked_out_count = 0;
2072 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
2075 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
2076 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
2078 # can't recall if no items have been checked out
2079 return 0 if ( $checked_out_count == 0 );
2085 =head3 can_be_waiting_recall
2087 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
2089 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
2090 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
2094 sub can_be_waiting_recall {
2097 return 0 if !( C4::Context->preference('UseRecalls') );
2099 # check if this item is not for loan, withdrawn or lost
2100 return 0 if ( $self->notforloan != 0 );
2101 return 0 if ( $self->itemlost != 0 );
2102 return 0 if ( $self->withdrawn != 0 );
2104 my $branchcode = $self->holdingbranch;
2105 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
2106 $branchcode = C4::Context->userenv->{'branch'};
2108 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
2111 # Check the circulation rule for each relevant itemtype for this item
2112 my $most_relevant_recall = $self->check_recalls;
2113 my $rule = Koha::CirculationRules->get_effective_rules(
2115 branchcode => $branchcode,
2116 categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
2117 itemtype => $self->effective_itemtype,
2118 rules => [ 'recalls_allowed', ],
2122 # check recalls allowed has been set and is not zero
2123 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
2129 =head3 check_recalls
2131 my $recall = $item->check_recalls;
2133 Get the most relevant recall for this item.
2140 my @recalls = Koha::Recalls->search(
2141 { biblio_id => $self->biblionumber,
2142 item_id => [ $self->itemnumber, undef ]
2144 { order_by => { -asc => 'created_date' } }
2145 )->filter_by_current->as_list;
2148 # iterate through relevant recalls to find the best one.
2149 # if we come across a waiting recall, use this one.
2150 # 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.
2151 foreach my $r ( @recalls ) {
2152 if ( $r->waiting ) {
2157 unless ( defined $recall ) {
2158 $recall = $recalls[0];
2164 =head3 is_notforloan
2166 my $is_notforloan = $item->is_notforloan;
2168 Determine whether or not this item is "notforloan" based on
2169 the item's notforloan status or its item type
2175 my $is_notforloan = 0;
2177 if ( $self->notforloan ){
2181 my $itemtype = $self->itemtype;
2183 if ( $itemtype->notforloan ){
2189 return $is_notforloan;
2192 =head3 is_denied_renewal
2194 my $is_denied_renewal = $item->is_denied_renewal;
2196 Determine whether or not this item can be renewed based on the
2197 rules set in the ItemsDeniedRenewal system preference.
2201 sub is_denied_renewal {
2203 my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2204 return 0 unless $denyingrules;
2205 foreach my $field (keys %$denyingrules) {
2206 # Silently ignore bad column names; TODO we should validate elsewhere
2207 next if !$self->_result->result_source->has_column($field);
2208 my $val = $self->$field;
2209 if( !defined $val) {
2210 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
2213 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2214 # If the results matches the values in the syspref
2215 # We return true if match found
2224 Returns a map of column name to string representations including the string,
2225 the mapping type and the mapping category where appropriate.
2227 Currently handles authorised value mappings, library, callnumber and itemtype
2230 Accepts a param hashref where the 'public' key denotes whether we want the public
2231 or staff client strings.
2236 my ( $self, $params ) = @_;
2237 my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2238 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2239 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2241 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2243 # Hardcoded known 'authorised_value' values mapped to API codes
2244 my $code_to_type = {
2245 branches => 'library',
2246 cn_source => 'call_number_source',
2247 itemtypes => 'item_type',
2250 # Handle not null and default values for integers and dates
2253 foreach my $col ( @{$self->_columns} ) {
2255 # By now, we are done with known columns, now check the framework for mappings
2256 my $field = $self->_result->result_source->name . '.' . $col;
2258 # Check there's an entry in the MARC subfield structure for the field
2259 if ( exists $mss->{$field}
2260 && scalar @{ $mss->{$field} } > 0
2261 && $mss->{$field}[0]->{authorised_value} )
2263 my $subfield = $mss->{$field}[0];
2264 my $code = $subfield->{authorised_value};
2266 my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2267 my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2268 $strings->{$col} = {
2271 ( $type eq 'av' ? ( category => $code ) : () ),
2289 Kyle M Hall <kyle@bywatersolutions.com>