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 );
25 use Koha::DateUtils qw( dt_from_string output_pref );
28 use C4::Circulation qw( barcodedecode GetBranchItemRule );
30 use C4::ClassSource qw( GetClassSort );
31 use C4::Log qw( logaction );
33 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
34 use Koha::Biblio::ItemGroups;
36 use Koha::CirculationRules;
37 use Koha::CoverImages;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Attributes;
40 use Koha::Item::Transfer::Limits;
41 use Koha::Item::Transfers;
46 use Koha::Result::Boolean;
47 use Koha::SearchEngine::Indexer;
48 use Koha::StockRotationItem;
49 use Koha::StockRotationRotas;
50 use Koha::TrackedLinks;
52 use base qw(Koha::Object);
56 Koha::Item - Koha Item object class
68 $params can take an optional 'skip_record_index' parameter.
69 If set, the reindexation process will not happen (index_records not called)
71 NOTE: This is a temporary fix to answer a performance issue when lot of items
72 are added (or modified) at the same time.
73 The correct way to fix this is to make the ES reindexation process async.
74 You should not turn it on if you do not understand what it is doing exactly.
80 my $params = @_ ? shift : {};
82 my $log_action = $params->{log_action} // 1;
84 # We do not want to oblige callers to pass this value
85 # Dev conveniences vs performance?
86 unless ( $self->biblioitemnumber ) {
87 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
90 # See related changes from C4::Items::AddItem
91 unless ( $self->itype ) {
92 $self->itype($self->biblio->biblioitem->itemtype);
95 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
97 my $today = dt_from_string;
98 my $action = 'create';
100 unless ( $self->in_storage ) { #AddItem
102 unless ( $self->permanent_location ) {
103 $self->permanent_location($self->location);
106 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
107 unless ( $self->location || !$default_location ) {
108 $self->permanent_location( $self->location || $default_location )
109 unless $self->permanent_location;
110 $self->location($default_location);
113 unless ( $self->replacementpricedate ) {
114 $self->replacementpricedate($today);
116 unless ( $self->datelastseen ) {
117 $self->datelastseen($today);
120 unless ( $self->dateaccessioned ) {
121 $self->dateaccessioned($today);
124 if ( $self->itemcallnumber
125 or $self->cn_source )
127 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
128 $self->cn_sort($cn_sort);
135 my %updated_columns = $self->_result->get_dirty_columns;
136 return $self->SUPER::store unless %updated_columns;
138 # Retrieve the item for comparison if we need to
140 exists $updated_columns{itemlost}
141 or exists $updated_columns{withdrawn}
142 or exists $updated_columns{damaged}
143 ) ? $self->get_from_storage : undef;
145 # Update *_on fields if needed
146 # FIXME: Why not for AddItem as well?
147 my @fields = qw( itemlost withdrawn damaged );
148 for my $field (@fields) {
150 # If the field is defined but empty or 0, we are
151 # removing/unsetting and thus need to clear out
153 if ( exists $updated_columns{$field}
154 && defined( $self->$field )
157 my $field_on = "${field}_on";
158 $self->$field_on(undef);
160 # If the field has changed otherwise, we much update
162 elsif (exists $updated_columns{$field}
163 && $updated_columns{$field}
164 && !$pre_mod_item->$field )
166 my $field_on = "${field}_on";
168 DateTime::Format::MySQL->format_datetime(
175 if ( exists $updated_columns{itemcallnumber}
176 or exists $updated_columns{cn_source} )
178 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
179 $self->cn_sort($cn_sort);
183 if ( exists $updated_columns{location}
184 and $self->location ne 'CART'
185 and $self->location ne 'PROC'
186 and not exists $updated_columns{permanent_location} )
188 $self->permanent_location( $self->location );
191 # If item was lost and has now been found,
192 # reverse any list item charges if necessary.
193 if ( exists $updated_columns{itemlost}
194 and $updated_columns{itemlost} <= 0
195 and $pre_mod_item->itemlost > 0 )
197 $self->_set_found_trigger($pre_mod_item);
202 my $result = $self->SUPER::store;
203 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
205 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
206 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
208 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
209 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
210 unless $params->{skip_record_index};
211 $self->get_from_storage->_after_item_action_hooks({ action => $action });
213 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
215 biblio_ids => [ $self->biblionumber ]
217 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
228 my $params = @_ ? shift : {};
230 # FIXME check the item has no current issues
231 # i.e. raise the appropriate exception
233 # Get the item group so we can delete it later if it has no items left
234 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
236 my $result = $self->SUPER::delete;
238 # Delete the item gorup if it has no items left
239 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
241 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
242 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
243 unless $params->{skip_record_index};
245 $self->_after_item_action_hooks({ action => 'delete' });
247 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
248 if C4::Context->preference("CataloguingLog");
250 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
252 biblio_ids => [ $self->biblionumber ]
254 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
265 my $params = @_ ? shift : {};
267 my $safe_to_delete = $self->safe_to_delete;
268 return $safe_to_delete unless $safe_to_delete;
270 $self->move_to_deleted;
272 return $self->delete($params);
275 =head3 safe_to_delete
277 returns 1 if the item is safe to delete,
279 "book_on_loan" if the item is checked out,
281 "not_same_branch" if the item is blocked by independent branches,
283 "book_reserved" if the there are holds aganst the item, or
285 "linked_analytics" if the item has linked analytic records.
287 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
296 $error = "book_on_loan" if $self->checkout;
298 $error = "not_same_branch"
299 if defined C4::Context->userenv
300 and !C4::Context->IsSuperLibrarian()
301 and C4::Context->preference("IndependentBranches")
302 and ( C4::Context->userenv->{branch} ne $self->homebranch );
304 # check it doesn't have a waiting reserve
305 $error = "book_reserved"
306 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
308 $error = "linked_analytics"
309 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
311 $error = "last_item_for_hold"
312 if $self->biblio->items->count == 1
313 && $self->biblio->holds->search(
320 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
323 return Koha::Result::Boolean->new(1);
326 =head3 move_to_deleted
328 my $is_moved = $item->move_to_deleted;
330 Move an item to the deleteditems table.
331 This can be done before deleting an item, to make sure the data are not completely deleted.
335 sub move_to_deleted {
337 my $item_infos = $self->unblessed;
338 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
339 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
343 =head3 effective_itemtype
345 Returns the itemtype for the item based on whether item level itemtypes are set or not.
349 sub effective_itemtype {
352 return $self->_result()->effective_itemtype();
362 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
364 return $self->{_home_branch};
367 =head3 holding_branch
374 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
376 return $self->{_holding_branch};
381 my $biblio = $item->biblio;
383 Return the bibliographic record of this item
389 my $biblio_rs = $self->_result->biblio;
390 return Koha::Biblio->_new_from_dbic( $biblio_rs );
395 my $biblioitem = $item->biblioitem;
397 Return the biblioitem record of this item
403 my $biblioitem_rs = $self->_result->biblioitem;
404 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
409 my $checkout = $item->checkout;
411 Return the checkout for this item
417 my $checkout_rs = $self->_result->issue;
418 return unless $checkout_rs;
419 return Koha::Checkout->_new_from_dbic( $checkout_rs );
424 my $item_group = $item->item_group;
426 Return the item group for this item
433 my $item_group_item = $self->_result->item_group_item;
434 return unless $item_group_item;
436 my $item_group_rs = $item_group_item->item_group;
437 return unless $item_group_rs;
439 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
445 my $holds = $item->holds();
446 my $holds = $item->holds($params);
447 my $holds = $item->holds({ found => 'W'});
449 Return holds attached to an item, optionally accept a hashref of params to pass to search
454 my ( $self,$params ) = @_;
455 my $holds_rs = $self->_result->reserves->search($params);
456 return Koha::Holds->_new_from_dbic( $holds_rs );
459 =head3 request_transfer
461 my $transfer = $item->request_transfer(
465 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
469 Add a transfer request for this item to the given branch for the given reason.
471 An exception will be thrown if the BranchTransferLimits would prevent the requested
472 transfer, unless 'ignore_limits' is passed to override the limits.
474 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
475 The caller should catch such cases and retry the transfer request as appropriate passing
476 an appropriate override.
479 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
480 * replace - Used to replace the existing transfer request with your own.
484 sub request_transfer {
485 my ( $self, $params ) = @_;
487 # check for mandatory params
488 my @mandatory = ( 'to', 'reason' );
489 for my $param (@mandatory) {
490 unless ( defined( $params->{$param} ) ) {
491 Koha::Exceptions::MissingParameter->throw(
492 error => "The $param parameter is mandatory" );
496 Koha::Exceptions::Item::Transfer::Limit->throw()
497 unless ( $params->{ignore_limits}
498 || $self->can_be_transferred( { to => $params->{to} } ) );
500 my $request = $self->get_transfer;
501 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
502 if ( $request && !$params->{enqueue} && !$params->{replace} );
504 $request->cancel( { reason => $params->{reason}, force => 1 } )
505 if ( defined($request) && $params->{replace} );
507 my $transfer = Koha::Item::Transfer->new(
509 itemnumber => $self->itemnumber,
510 daterequested => dt_from_string,
511 frombranch => $self->holdingbranch,
512 tobranch => $params->{to}->branchcode,
513 reason => $params->{reason},
514 comments => $params->{comment}
523 my $transfer = $item->get_transfer;
525 Return the active transfer request or undef
527 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
528 whereby the most recently sent, but not received, transfer will be returned
529 if it exists, otherwise the oldest unsatisfied transfer will be returned.
531 This allows for transfers to queue, which is the case for stock rotation and
532 rotating collections where a manual transfer may need to take precedence but
533 we still expect the item to end up at a final location eventually.
539 my $transfer_rs = $self->_result->branchtransfers->search(
541 datearrived => undef,
542 datecancelled => undef
546 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
550 return unless $transfer_rs;
551 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
556 my $transfer = $item->get_transfers;
558 Return the list of outstanding transfers (i.e requested but not yet cancelled
561 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
562 whereby the most recently sent, but not received, transfer will be returned
563 first if it exists, otherwise requests are in oldest to newest request order.
565 This allows for transfers to queue, which is the case for stock rotation and
566 rotating collections where a manual transfer may need to take precedence but
567 we still expect the item to end up at a final location eventually.
573 my $transfer_rs = $self->_result->branchtransfers->search(
575 datearrived => undef,
576 datecancelled => undef
580 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
583 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
586 =head3 last_returned_by
588 Gets and sets the last borrower to return an item.
590 Accepts and returns Koha::Patron objects
592 $item->last_returned_by( $borrowernumber );
594 $last_returned_by = $item->last_returned_by();
598 sub last_returned_by {
599 my ( $self, $borrower ) = @_;
601 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
604 return $items_last_returned_by_rs->update_or_create(
605 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
608 unless ( $self->{_last_returned_by} ) {
609 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
611 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
615 return $self->{_last_returned_by};
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 $pickup_locations = $item->pickup_locations( {patron => $patron } )
727 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
728 and if item can be transferred to each pickup location.
732 sub pickup_locations {
733 my ($self, $params) = @_;
735 my $patron = $params->{patron};
737 my $circ_control_branch =
738 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
740 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
742 if(defined $patron) {
743 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
744 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
747 my $pickup_libraries = Koha::Libraries->search();
748 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
749 $pickup_libraries = $self->home_branch->get_hold_libraries;
750 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
751 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
752 $pickup_libraries = $plib->get_hold_libraries;
753 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
754 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
755 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
756 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
759 return $pickup_libraries->search(
764 order_by => ['branchname']
766 ) unless C4::Context->preference('UseBranchTransferLimits');
768 my $limittype = C4::Context->preference('BranchTransferLimitsType');
769 my ($ccode, $itype) = (undef, undef);
770 if( $limittype eq 'ccode' ){
771 $ccode = $self->ccode;
773 $itype = $self->itype;
775 my $limits = Koha::Item::Transfer::Limits->search(
777 fromBranch => $self->holdingbranch,
781 { columns => ['toBranch'] }
784 return $pickup_libraries->search(
786 pickup_location => 1,
788 '-not_in' => $limits->_resultset->as_query
792 order_by => ['branchname']
797 =head3 article_request_type
799 my $type = $item->article_request_type( $borrower )
801 returns 'yes', 'no', 'bib_only', or 'item_only'
803 $borrower must be a Koha::Patron object
807 sub article_request_type {
808 my ( $self, $borrower ) = @_;
810 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
812 $branch_control eq 'homebranch' ? $self->homebranch
813 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
815 my $borrowertype = $borrower->categorycode;
816 my $itemtype = $self->effective_itemtype();
817 my $rule = Koha::CirculationRules->get_effective_rule(
819 rule_name => 'article_requests',
820 categorycode => $borrowertype,
821 itemtype => $itemtype,
822 branchcode => $branchcode
826 return q{} unless $rule;
827 return $rule->rule_value || q{}
836 my $attributes = { order_by => 'priority' };
837 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
839 itemnumber => $self->itemnumber,
842 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
843 waitingdate => { '!=' => undef },
846 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
847 return Koha::Holds->_new_from_dbic($hold_rs);
850 =head3 stockrotationitem
852 my $sritem = Koha::Item->stockrotationitem;
854 Returns the stock rotation item associated with the current item.
858 sub stockrotationitem {
860 my $rs = $self->_result->stockrotationitem;
862 return Koha::StockRotationItem->_new_from_dbic( $rs );
867 my $item = $item->add_to_rota($rota_id);
869 Add this item to the rota identified by $ROTA_ID, which means associating it
870 with the first stage of that rota. Should this item already be associated
871 with a rota, then we will move it to the new rota.
876 my ( $self, $rota_id ) = @_;
877 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
881 =head3 has_pending_hold
883 my $is_pending_hold = $item->has_pending_hold();
885 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
889 sub has_pending_hold {
891 my $pending_hold = $self->_result->tmp_holdsqueues;
892 return $pending_hold->count ? 1: 0;
897 my $field = $item->as_marc_field;
899 This method returns a MARC::Field object representing the Koha::Item object
900 with the current mappings configuration.
907 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
909 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
913 my $item_field = $tagslib->{$itemtag};
915 my $more_subfields = $self->additional_attributes->to_hashref;
916 foreach my $subfield (
918 $a->{display_order} <=> $b->{display_order}
919 || $a->{subfield} cmp $b->{subfield}
920 } grep { ref($_) && %$_ } values %$item_field
923 my $kohafield = $subfield->{kohafield};
924 my $tagsubfield = $subfield->{tagsubfield};
926 if ( defined $kohafield ) {
927 next if $kohafield !~ m{^items\.}; # That would be weird!
928 ( my $attribute = $kohafield ) =~ s|^items\.||;
929 $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
930 if defined $self->$attribute and $self->$attribute ne '';
932 $value = $more_subfields->{$tagsubfield}
935 next unless defined $value
938 if ( $subfield->{repeatable} ) {
939 my @values = split '\|', $value;
940 push @subfields, ( $tagsubfield => $_ ) for @values;
943 push @subfields, ( $tagsubfield => $value );
948 return unless @subfields;
950 return MARC::Field->new(
951 "$itemtag", ' ', ' ', @subfields
955 =head3 renewal_branchcode
957 Returns the branchcode to be recorded in statistics renewal of the item
961 sub renewal_branchcode {
963 my ($self, $params ) = @_;
965 my $interface = C4::Context->interface;
967 if ( $interface eq 'opac' ){
968 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
969 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
970 $branchcode = 'OPACRenew';
972 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
973 $branchcode = $self->homebranch;
975 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
976 $branchcode = $self->checkout->patron->branchcode;
978 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
979 $branchcode = $self->checkout->branchcode;
985 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
986 ? C4::Context->userenv->{branch} : $params->{branch};
993 Return the cover images associated with this item.
1000 my $cover_image_rs = $self->_result->cover_images;
1001 return unless $cover_image_rs;
1002 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1005 =head3 columns_to_str
1007 my $values = $items->columns_to_str;
1009 Return a hashref with the string representation of the different attribute of the item.
1011 This is meant to be used for display purpose only.
1015 sub columns_to_str {
1018 my $frameworkcode = $self->biblio->frameworkcode;
1019 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1020 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1022 my $columns_info = $self->_result->result_source->columns_info;
1024 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1026 for my $column ( keys %$columns_info ) {
1028 next if $column eq 'more_subfields_xml';
1030 my $value = $self->$column;
1031 # 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
1033 if ( not defined $value or $value eq "" ) {
1034 $values->{$column} = $value;
1039 exists $mss->{"items.$column"}
1040 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1043 $values->{$column} =
1045 ? $subfield->{authorised_value}
1046 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1047 $subfield->{tagsubfield}, $value, '', $tagslib )
1053 $self->more_subfields_xml
1054 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1059 my ( $field ) = $marc_more->fields;
1060 for my $sf ( $field->subfields ) {
1061 my $subfield_code = $sf->[0];
1062 my $value = $sf->[1];
1063 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1064 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1066 $subfield->{authorised_value}
1067 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1068 $subfield->{tagsubfield}, $value, '', $tagslib )
1071 push @{$more_values->{$subfield_code}}, $value;
1074 while ( my ( $k, $v ) = each %$more_values ) {
1075 $values->{$k} = join ' | ', @$v;
1082 =head3 additional_attributes
1084 my $attributes = $item->additional_attributes;
1085 $attributes->{k} = 'new k';
1086 $item->update({ more_subfields => $attributes->to_marcxml });
1088 Returns a Koha::Item::Attributes object that represents the non-mapped
1089 attributes for this item.
1093 sub additional_attributes {
1096 return Koha::Item::Attributes->new_from_marcxml(
1097 $self->more_subfields_xml,
1101 =head3 _set_found_trigger
1103 $self->_set_found_trigger
1105 Finds the most recent lost item charge for this item and refunds the patron
1106 appropriately, taking into account any payments or writeoffs already applied
1109 Internal function, not exported, called only by Koha::Item->store.
1113 sub _set_found_trigger {
1114 my ( $self, $pre_mod_item ) = @_;
1116 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1117 my $no_refund_after_days =
1118 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1119 if ($no_refund_after_days) {
1120 my $today = dt_from_string();
1121 my $lost_age_in_days =
1122 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1125 return $self unless $lost_age_in_days < $no_refund_after_days;
1128 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1131 return_branch => C4::Context->userenv
1132 ? C4::Context->userenv->{'branch'}
1137 if ( $lostreturn_policy ) {
1139 # refund charge made for lost book
1140 my $lost_charge = Koha::Account::Lines->search(
1142 itemnumber => $self->itemnumber,
1143 debit_type_code => 'LOST',
1144 status => [ undef, { '<>' => 'FOUND' } ]
1147 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1152 if ( $lost_charge ) {
1154 my $patron = $lost_charge->patron;
1157 my $account = $patron->account;
1158 my $total_to_refund = 0;
1161 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1163 # some amount has been cancelled. collect the offsets that are not writeoffs
1164 # this works because the only way to subtract from this kind of a debt is
1165 # using the UI buttons 'Pay' and 'Write off'
1166 my $credit_offsets = $lost_charge->debit_offsets(
1168 'credit_id' => { '!=' => undef },
1169 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1171 { join => 'credit' }
1174 $total_to_refund = ( $credit_offsets->count > 0 )
1175 ? $credit_offsets->total * -1 # credits are negative on the DB
1179 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1182 if ( $credit_total > 0 ) {
1184 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1185 $credit = $account->add_credit(
1187 amount => $credit_total,
1188 description => 'Item found ' . $self->itemnumber,
1189 type => 'LOST_FOUND',
1190 interface => C4::Context->interface,
1191 library_id => $branchcode,
1192 item_id => $self->itemnumber,
1193 issue_id => $lost_charge->issue_id
1197 $credit->apply( { debits => [$lost_charge] } );
1201 message => 'lost_refunded',
1202 payload => { credit_id => $credit->id }
1207 # Update the account status
1208 $lost_charge->status('FOUND');
1209 $lost_charge->store();
1211 # Reconcile balances if required
1212 if ( C4::Context->preference('AccountAutoReconcile') ) {
1213 $account->reconcile_balance;
1218 # restore fine for lost book
1219 if ( $lostreturn_policy eq 'restore' ) {
1220 my $lost_overdue = Koha::Account::Lines->search(
1222 itemnumber => $self->itemnumber,
1223 debit_type_code => 'OVERDUE',
1227 order_by => { '-desc' => 'date' },
1232 if ( $lost_overdue ) {
1234 my $patron = $lost_overdue->patron;
1236 my $account = $patron->account;
1238 # Update status of fine
1239 $lost_overdue->status('FOUND')->store();
1241 # Find related forgive credit
1242 my $refund = $lost_overdue->credits(
1244 credit_type_code => 'FORGIVEN',
1245 itemnumber => $self->itemnumber,
1246 status => [ { '!=' => 'VOID' }, undef ]
1248 { order_by => { '-desc' => 'date' }, rows => 1 }
1252 # Revert the forgive credit
1253 $refund->void({ interface => 'trigger' });
1257 message => 'lost_restored',
1258 payload => { refund_id => $refund->id }
1263 # Reconcile balances if required
1264 if ( C4::Context->preference('AccountAutoReconcile') ) {
1265 $account->reconcile_balance;
1269 } elsif ( $lostreturn_policy eq 'charge' ) {
1273 message => 'lost_charge',
1282 =head3 public_read_list
1284 This method returns the list of publicly readable database fields for both API and UI output purposes
1288 sub public_read_list {
1290 'itemnumber', 'biblionumber', 'homebranch',
1291 'holdingbranch', 'location', 'collectioncode',
1292 'itemcallnumber', 'copynumber', 'enumchron',
1293 'barcode', 'dateaccessioned', 'itemnotes',
1294 'onloan', 'uri', 'itype',
1295 'notforloan', 'damaged', 'itemlost',
1296 'withdrawn', 'restricted'
1300 =head3 to_api_mapping
1302 This method returns the mapping for representing a Koha::Item object
1307 sub to_api_mapping {
1309 itemnumber => 'item_id',
1310 biblionumber => 'biblio_id',
1311 biblioitemnumber => undef,
1312 barcode => 'external_id',
1313 dateaccessioned => 'acquisition_date',
1314 booksellerid => 'acquisition_source',
1315 homebranch => 'home_library_id',
1316 price => 'purchase_price',
1317 replacementprice => 'replacement_price',
1318 replacementpricedate => 'replacement_price_date',
1319 datelastborrowed => 'last_checkout_date',
1320 datelastseen => 'last_seen_date',
1322 notforloan => 'not_for_loan_status',
1323 damaged => 'damaged_status',
1324 damaged_on => 'damaged_date',
1325 itemlost => 'lost_status',
1326 itemlost_on => 'lost_date',
1327 withdrawn => 'withdrawn',
1328 withdrawn_on => 'withdrawn_date',
1329 itemcallnumber => 'callnumber',
1330 coded_location_qualifier => 'coded_location_qualifier',
1331 issues => 'checkouts_count',
1332 renewals => 'renewals_count',
1333 reserves => 'holds_count',
1334 restricted => 'restricted_status',
1335 itemnotes => 'public_notes',
1336 itemnotes_nonpublic => 'internal_notes',
1337 holdingbranch => 'holding_library_id',
1338 timestamp => 'timestamp',
1339 location => 'location',
1340 permanent_location => 'permanent_location',
1341 onloan => 'checked_out_date',
1342 cn_source => 'call_number_source',
1343 cn_sort => 'call_number_sort',
1344 ccode => 'collection_code',
1345 materials => 'materials_notes',
1347 itype => 'item_type_id',
1348 more_subfields_xml => 'extended_subfields',
1349 enumchron => 'serial_issue_number',
1350 copynumber => 'copy_number',
1351 stocknumber => 'inventory_number',
1352 new_status => 'new_status'
1358 my $itemtype = $item->itemtype;
1360 Returns Koha object for effective itemtype
1366 return Koha::ItemTypes->find( $self->effective_itemtype );
1371 my $orders = $item->orders();
1373 Returns a Koha::Acquisition::Orders object
1380 my $orders = $self->_result->item_orders;
1381 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1384 =head3 tracked_links
1386 my $tracked_links = $item->tracked_links();
1388 Returns a Koha::TrackedLinks object
1395 my $tracked_links = $self->_result->linktrackers;
1396 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1399 =head3 move_to_biblio
1401 $item->move_to_biblio($to_biblio[, $params]);
1403 Move the item to another biblio and update any references in other tables.
1405 The final optional parameter, C<$params>, is expected to contain the
1406 'skip_record_index' key, which is relayed down to Koha::Item->store.
1407 There it prevents calling index_records, which takes most of the
1408 time in batch adds/deletes. The caller must take care of calling
1409 index_records separately.
1412 skip_record_index => 1|0
1414 Returns undef if the move failed or the biblionumber of the destination record otherwise
1418 sub move_to_biblio {
1419 my ( $self, $to_biblio, $params ) = @_;
1423 return if $self->biblionumber == $to_biblio->biblionumber;
1425 my $from_biblionumber = $self->biblionumber;
1426 my $to_biblionumber = $to_biblio->biblionumber;
1428 # Own biblionumber and biblioitemnumber
1430 biblionumber => $to_biblionumber,
1431 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1432 })->store({ skip_record_index => $params->{skip_record_index} });
1434 unless ($params->{skip_record_index}) {
1435 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1436 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1439 # Acquisition orders
1440 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1443 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1445 # hold_fill_target (there's no Koha object available yet)
1446 my $hold_fill_target = $self->_result->hold_fill_target;
1447 if ($hold_fill_target) {
1448 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1451 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1452 # and can't even fake one since the significant columns are nullable.
1453 my $storage = $self->_result->result_source->storage;
1456 my ($storage, $dbh, @cols) = @_;
1458 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1463 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1465 return $to_biblionumber;
1468 =head2 Internal methods
1470 =head3 _after_item_action_hooks
1472 Helper method that takes care of calling all plugin hooks
1476 sub _after_item_action_hooks {
1477 my ( $self, $params ) = @_;
1479 my $action = $params->{action};
1481 Koha::Plugins->call(
1482 'after_item_action',
1486 item_id => $self->itemnumber,
1493 my $recall = $item->recall;
1495 Return the relevant recall for this item
1501 my @recalls = Koha::Recalls->search(
1503 biblio_id => $self->biblionumber,
1506 { order_by => { -asc => 'created_date' } }
1508 foreach my $recall (@recalls) {
1509 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1513 # no item-level recall to return, so return earliest biblio-level
1514 # FIXME: eventually this will be based on priority
1518 =head3 can_be_recalled
1520 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1522 Does item-level checks and returns if items can be recalled by this borrower
1526 sub can_be_recalled {
1527 my ( $self, $params ) = @_;
1529 return 0 if !( C4::Context->preference('UseRecalls') );
1531 # check if this item is not for loan, withdrawn or lost
1532 return 0 if ( $self->notforloan != 0 );
1533 return 0 if ( $self->itemlost != 0 );
1534 return 0 if ( $self->withdrawn != 0 );
1536 # check if this item is not checked out - if not checked out, can't be recalled
1537 return 0 if ( !defined( $self->checkout ) );
1539 my $patron = $params->{patron};
1541 my $branchcode = C4::Context->userenv->{'branch'};
1543 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1546 # Check the circulation rule for each relevant itemtype for this item
1547 my $rule = Koha::CirculationRules->get_effective_rules({
1548 branchcode => $branchcode,
1549 categorycode => $patron ? $patron->categorycode : undef,
1550 itemtype => $self->effective_itemtype,
1553 'recalls_per_record',
1558 # check recalls allowed has been set and is not zero
1559 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1562 # check borrower has not reached open recalls allowed limit
1563 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1565 # check borrower has not reach open recalls allowed per record limit
1566 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1568 # check if this patron has already recalled this item
1569 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1571 # check if this patron has already checked out this item
1572 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1574 # check if this patron has already reserved this item
1575 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1578 # check item availability
1579 # items are unavailable for recall if they are lost, withdrawn or notforloan
1580 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1582 # if there are no available items at all, no recall can be placed
1583 return 0 if ( scalar @items == 0 );
1585 my $checked_out_count = 0;
1587 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1590 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1591 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1593 # can't recall if no items have been checked out
1594 return 0 if ( $checked_out_count == 0 );
1600 =head3 can_be_waiting_recall
1602 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1604 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1605 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1609 sub can_be_waiting_recall {
1612 return 0 if !( C4::Context->preference('UseRecalls') );
1614 # check if this item is not for loan, withdrawn or lost
1615 return 0 if ( $self->notforloan != 0 );
1616 return 0 if ( $self->itemlost != 0 );
1617 return 0 if ( $self->withdrawn != 0 );
1619 my $branchcode = $self->holdingbranch;
1620 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1621 $branchcode = C4::Context->userenv->{'branch'};
1623 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1626 # Check the circulation rule for each relevant itemtype for this item
1627 my $rule = Koha::CirculationRules->get_effective_rules({
1628 branchcode => $branchcode,
1629 categorycode => undef,
1630 itemtype => $self->effective_itemtype,
1636 # check recalls allowed has been set and is not zero
1637 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1643 =head3 check_recalls
1645 my $recall = $item->check_recalls;
1647 Get the most relevant recall for this item.
1654 my @recalls = Koha::Recalls->search(
1655 { biblio_id => $self->biblionumber,
1656 item_id => [ $self->itemnumber, undef ]
1658 { order_by => { -asc => 'created_date' } }
1659 )->filter_by_current->as_list;
1662 # iterate through relevant recalls to find the best one.
1663 # if we come across a waiting recall, use this one.
1664 # 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.
1665 foreach my $r ( @recalls ) {
1666 if ( $r->waiting ) {
1671 unless ( defined $recall ) {
1672 $recall = $recalls[0];
1678 =head3 is_notforloan
1680 my $is_notforloan = $item->is_notforloan;
1682 Determine whether or not this item is "notforloan" based on
1683 the item's notforloan status or its item type
1689 my $is_notforloan = 0;
1691 if ( $self->notforloan ){
1695 my $itemtype = $self->itemtype;
1697 if ( $itemtype->notforloan ){
1703 return $is_notforloan;
1716 Kyle M Hall <kyle@bywatersolutions.com>