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;
35 use Koha::CirculationRules;
36 use Koha::CoverImages;
37 use Koha::SearchEngine::Indexer;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Item::Attributes;
46 use Koha::StockRotationItem;
47 use Koha::StockRotationRotas;
48 use Koha::TrackedLinks;
49 use Koha::Result::Boolean;
51 use base qw(Koha::Object);
55 Koha::Item - Koha Item object class
67 $params can take an optional 'skip_record_index' parameter.
68 If set, the reindexation process will not happen (index_records not called)
70 NOTE: This is a temporary fix to answer a performance issue when lot of items
71 are added (or modified) at the same time.
72 The correct way to fix this is to make the ES reindexation process async.
73 You should not turn it on if you do not understand what it is doing exactly.
79 my $params = @_ ? shift : {};
81 my $log_action = $params->{log_action} // 1;
83 # We do not want to oblige callers to pass this value
84 # Dev conveniences vs performance?
85 unless ( $self->biblioitemnumber ) {
86 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
89 # See related changes from C4::Items::AddItem
90 unless ( $self->itype ) {
91 $self->itype($self->biblio->biblioitem->itemtype);
94 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
96 my $today = dt_from_string;
97 my $action = 'create';
99 unless ( $self->in_storage ) { #AddItem
101 unless ( $self->permanent_location ) {
102 $self->permanent_location($self->location);
105 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
106 unless ( $self->location || !$default_location ) {
107 $self->permanent_location( $self->location || $default_location )
108 unless $self->permanent_location;
109 $self->location($default_location);
112 unless ( $self->replacementpricedate ) {
113 $self->replacementpricedate($today);
115 unless ( $self->datelastseen ) {
116 $self->datelastseen($today);
119 unless ( $self->dateaccessioned ) {
120 $self->dateaccessioned($today);
123 if ( $self->itemcallnumber
124 or $self->cn_source )
126 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
127 $self->cn_sort($cn_sort);
134 my %updated_columns = $self->_result->get_dirty_columns;
135 return $self->SUPER::store unless %updated_columns;
137 # Retrieve the item for comparison if we need to
139 exists $updated_columns{itemlost}
140 or exists $updated_columns{withdrawn}
141 or exists $updated_columns{damaged}
142 ) ? $self->get_from_storage : undef;
144 # Update *_on fields if needed
145 # FIXME: Why not for AddItem as well?
146 my @fields = qw( itemlost withdrawn damaged );
147 for my $field (@fields) {
149 # If the field is defined but empty or 0, we are
150 # removing/unsetting and thus need to clear out
152 if ( exists $updated_columns{$field}
153 && defined( $self->$field )
156 my $field_on = "${field}_on";
157 $self->$field_on(undef);
159 # If the field has changed otherwise, we much update
161 elsif (exists $updated_columns{$field}
162 && $updated_columns{$field}
163 && !$pre_mod_item->$field )
165 my $field_on = "${field}_on";
167 DateTime::Format::MySQL->format_datetime(
174 if ( exists $updated_columns{itemcallnumber}
175 or exists $updated_columns{cn_source} )
177 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
178 $self->cn_sort($cn_sort);
182 if ( exists $updated_columns{location}
183 and $self->location ne 'CART'
184 and $self->location ne 'PROC'
185 and not exists $updated_columns{permanent_location} )
187 $self->permanent_location( $self->location );
190 # If item was lost and has now been found,
191 # reverse any list item charges if necessary.
192 if ( exists $updated_columns{itemlost}
193 and $updated_columns{itemlost} <= 0
194 and $pre_mod_item->itemlost > 0 )
196 $self->_set_found_trigger($pre_mod_item);
201 my $result = $self->SUPER::store;
202 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
204 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
205 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
207 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
208 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
209 unless $params->{skip_record_index};
210 $self->get_from_storage->_after_item_action_hooks({ action => $action });
212 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
214 biblio_ids => [ $self->biblionumber ]
216 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
227 my $params = @_ ? shift : {};
229 # FIXME check the item has no current issues
230 # i.e. raise the appropriate exception
232 my $result = $self->SUPER::delete;
234 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
235 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
236 unless $params->{skip_record_index};
238 $self->_after_item_action_hooks({ action => 'delete' });
240 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
241 if C4::Context->preference("CataloguingLog");
243 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
245 biblio_ids => [ $self->biblionumber ]
247 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
258 my $params = @_ ? shift : {};
260 my $safe_to_delete = $self->safe_to_delete;
261 return $safe_to_delete unless $safe_to_delete;
263 $self->move_to_deleted;
265 return $self->delete($params);
268 =head3 safe_to_delete
270 returns 1 if the item is safe to delete,
272 "book_on_loan" if the item is checked out,
274 "not_same_branch" if the item is blocked by independent branches,
276 "book_reserved" if the there are holds aganst the item, or
278 "linked_analytics" if the item has linked analytic records.
280 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
289 $error = "book_on_loan" if $self->checkout;
291 $error = "not_same_branch"
292 if defined C4::Context->userenv
293 and !C4::Context->IsSuperLibrarian()
294 and C4::Context->preference("IndependentBranches")
295 and ( C4::Context->userenv->{branch} ne $self->homebranch );
297 # check it doesn't have a waiting reserve
298 $error = "book_reserved"
299 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
301 $error = "linked_analytics"
302 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
304 $error = "last_item_for_hold"
305 if $self->biblio->items->count == 1
306 && $self->biblio->holds->search(
313 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
316 return Koha::Result::Boolean->new(1);
319 =head3 move_to_deleted
321 my $is_moved = $item->move_to_deleted;
323 Move an item to the deleteditems table.
324 This can be done before deleting an item, to make sure the data are not completely deleted.
328 sub move_to_deleted {
330 my $item_infos = $self->unblessed;
331 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
332 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
336 =head3 effective_itemtype
338 Returns the itemtype for the item based on whether item level itemtypes are set or not.
342 sub effective_itemtype {
345 return $self->_result()->effective_itemtype();
355 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
357 return $self->{_home_branch};
360 =head3 holding_branch
367 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
369 return $self->{_holding_branch};
374 my $biblio = $item->biblio;
376 Return the bibliographic record of this item
382 my $biblio_rs = $self->_result->biblio;
383 return Koha::Biblio->_new_from_dbic( $biblio_rs );
388 my $biblioitem = $item->biblioitem;
390 Return the biblioitem record of this item
396 my $biblioitem_rs = $self->_result->biblioitem;
397 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
402 my $checkout = $item->checkout;
404 Return the checkout for this item
410 my $checkout_rs = $self->_result->issue;
411 return unless $checkout_rs;
412 return Koha::Checkout->_new_from_dbic( $checkout_rs );
417 my $holds = $item->holds();
418 my $holds = $item->holds($params);
419 my $holds = $item->holds({ found => 'W'});
421 Return holds attached to an item, optionally accept a hashref of params to pass to search
426 my ( $self,$params ) = @_;
427 my $holds_rs = $self->_result->reserves->search($params);
428 return Koha::Holds->_new_from_dbic( $holds_rs );
431 =head3 request_transfer
433 my $transfer = $item->request_transfer(
437 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
441 Add a transfer request for this item to the given branch for the given reason.
443 An exception will be thrown if the BranchTransferLimits would prevent the requested
444 transfer, unless 'ignore_limits' is passed to override the limits.
446 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
447 The caller should catch such cases and retry the transfer request as appropriate passing
448 an appropriate override.
451 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
452 * replace - Used to replace the existing transfer request with your own.
456 sub request_transfer {
457 my ( $self, $params ) = @_;
459 # check for mandatory params
460 my @mandatory = ( 'to', 'reason' );
461 for my $param (@mandatory) {
462 unless ( defined( $params->{$param} ) ) {
463 Koha::Exceptions::MissingParameter->throw(
464 error => "The $param parameter is mandatory" );
468 Koha::Exceptions::Item::Transfer::Limit->throw()
469 unless ( $params->{ignore_limits}
470 || $self->can_be_transferred( { to => $params->{to} } ) );
472 my $request = $self->get_transfer;
473 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
474 if ( $request && !$params->{enqueue} && !$params->{replace} );
476 $request->cancel( { reason => $params->{reason}, force => 1 } )
477 if ( defined($request) && $params->{replace} );
479 my $transfer = Koha::Item::Transfer->new(
481 itemnumber => $self->itemnumber,
482 daterequested => dt_from_string,
483 frombranch => $self->holdingbranch,
484 tobranch => $params->{to}->branchcode,
485 reason => $params->{reason},
486 comments => $params->{comment}
495 my $transfer = $item->get_transfer;
497 Return the active transfer request or undef
499 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
500 whereby the most recently sent, but not received, transfer will be returned
501 if it exists, otherwise the oldest unsatisfied transfer will be returned.
503 This allows for transfers to queue, which is the case for stock rotation and
504 rotating collections where a manual transfer may need to take precedence but
505 we still expect the item to end up at a final location eventually.
511 my $transfer_rs = $self->_result->branchtransfers->search(
513 datearrived => undef,
514 datecancelled => undef
518 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
522 return unless $transfer_rs;
523 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
528 my $transfer = $item->get_transfers;
530 Return the list of outstanding transfers (i.e requested but not yet cancelled
533 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
534 whereby the most recently sent, but not received, transfer will be returned
535 first if it exists, otherwise requests are in oldest to newest request order.
537 This allows for transfers to queue, which is the case for stock rotation and
538 rotating collections where a manual transfer may need to take precedence but
539 we still expect the item to end up at a final location eventually.
545 my $transfer_rs = $self->_result->branchtransfers->search(
547 datearrived => undef,
548 datecancelled => undef
552 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
555 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
558 =head3 last_returned_by
560 Gets and sets the last borrower to return an item.
562 Accepts and returns Koha::Patron objects
564 $item->last_returned_by( $borrowernumber );
566 $last_returned_by = $item->last_returned_by();
570 sub last_returned_by {
571 my ( $self, $borrower ) = @_;
573 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
576 return $items_last_returned_by_rs->update_or_create(
577 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
580 unless ( $self->{_last_returned_by} ) {
581 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
583 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
587 return $self->{_last_returned_by};
591 =head3 can_article_request
593 my $bool = $item->can_article_request( $borrower )
595 Returns true if item can be specifically requested
597 $borrower must be a Koha::Patron object
601 sub can_article_request {
602 my ( $self, $borrower ) = @_;
604 my $rule = $self->article_request_type($borrower);
606 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
610 =head3 hidden_in_opac
612 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
614 Returns true if item fields match the hidding criteria defined in $rules.
615 Returns false otherwise.
617 Takes HASHref that can have the following parameters:
619 $rules : { <field> => [ value_1, ... ], ... }
621 Note: $rules inherits its structure from the parsed YAML from reading
622 the I<OpacHiddenItems> system preference.
627 my ( $self, $params ) = @_;
629 my $rules = $params->{rules} // {};
632 if C4::Context->preference('hidelostitems') and
635 my $hidden_in_opac = 0;
637 foreach my $field ( keys %{$rules} ) {
639 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
645 return $hidden_in_opac;
648 =head3 can_be_transferred
650 $item->can_be_transferred({ to => $to_library, from => $from_library })
651 Checks if an item can be transferred to given library.
653 This feature is controlled by two system preferences:
654 UseBranchTransferLimits to enable / disable the feature
655 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
656 for setting the limitations
658 Takes HASHref that can have the following parameters:
659 MANDATORY PARAMETERS:
662 $from : Koha::Library # if not given, item holdingbranch
663 # will be used instead
665 Returns 1 if item can be transferred to $to_library, otherwise 0.
667 To find out whether at least one item of a Koha::Biblio can be transferred, please
668 see Koha::Biblio->can_be_transferred() instead of using this method for
669 multiple items of the same biblio.
673 sub can_be_transferred {
674 my ($self, $params) = @_;
676 my $to = $params->{to};
677 my $from = $params->{from};
679 $to = $to->branchcode;
680 $from = defined $from ? $from->branchcode : $self->holdingbranch;
682 return 1 if $from eq $to; # Transfer to current branch is allowed
683 return 1 unless C4::Context->preference('UseBranchTransferLimits');
685 my $limittype = C4::Context->preference('BranchTransferLimitsType');
686 return Koha::Item::Transfer::Limits->search({
689 $limittype => $limittype eq 'itemtype'
690 ? $self->effective_itemtype : $self->ccode
695 =head3 pickup_locations
697 $pickup_locations = $item->pickup_locations( {patron => $patron } )
699 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)
700 and if item can be transferred to each pickup location.
704 sub pickup_locations {
705 my ($self, $params) = @_;
707 my $patron = $params->{patron};
709 my $circ_control_branch =
710 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
712 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
714 if(defined $patron) {
715 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
716 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
719 my $pickup_libraries = Koha::Libraries->search();
720 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
721 $pickup_libraries = $self->home_branch->get_hold_libraries;
722 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
723 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
724 $pickup_libraries = $plib->get_hold_libraries;
725 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
726 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
727 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
728 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
731 return $pickup_libraries->search(
736 order_by => ['branchname']
738 ) unless C4::Context->preference('UseBranchTransferLimits');
740 my $limittype = C4::Context->preference('BranchTransferLimitsType');
741 my ($ccode, $itype) = (undef, undef);
742 if( $limittype eq 'ccode' ){
743 $ccode = $self->ccode;
745 $itype = $self->itype;
747 my $limits = Koha::Item::Transfer::Limits->search(
749 fromBranch => $self->holdingbranch,
753 { columns => ['toBranch'] }
756 return $pickup_libraries->search(
758 pickup_location => 1,
760 '-not_in' => $limits->_resultset->as_query
764 order_by => ['branchname']
769 =head3 article_request_type
771 my $type = $item->article_request_type( $borrower )
773 returns 'yes', 'no', 'bib_only', or 'item_only'
775 $borrower must be a Koha::Patron object
779 sub article_request_type {
780 my ( $self, $borrower ) = @_;
782 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
784 $branch_control eq 'homebranch' ? $self->homebranch
785 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
787 my $borrowertype = $borrower->categorycode;
788 my $itemtype = $self->effective_itemtype();
789 my $rule = Koha::CirculationRules->get_effective_rule(
791 rule_name => 'article_requests',
792 categorycode => $borrowertype,
793 itemtype => $itemtype,
794 branchcode => $branchcode
798 return q{} unless $rule;
799 return $rule->rule_value || q{}
808 my $attributes = { order_by => 'priority' };
809 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
811 itemnumber => $self->itemnumber,
814 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
815 waitingdate => { '!=' => undef },
818 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
819 return Koha::Holds->_new_from_dbic($hold_rs);
822 =head3 stockrotationitem
824 my $sritem = Koha::Item->stockrotationitem;
826 Returns the stock rotation item associated with the current item.
830 sub stockrotationitem {
832 my $rs = $self->_result->stockrotationitem;
834 return Koha::StockRotationItem->_new_from_dbic( $rs );
839 my $item = $item->add_to_rota($rota_id);
841 Add this item to the rota identified by $ROTA_ID, which means associating it
842 with the first stage of that rota. Should this item already be associated
843 with a rota, then we will move it to the new rota.
848 my ( $self, $rota_id ) = @_;
849 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
853 =head3 has_pending_hold
855 my $is_pending_hold = $item->has_pending_hold();
857 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
861 sub has_pending_hold {
863 my $pending_hold = $self->_result->tmp_holdsqueues;
864 return $pending_hold->count ? 1: 0;
869 my $field = $item->as_marc_field;
871 This method returns a MARC::Field object representing the Koha::Item object
872 with the current mappings configuration.
879 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
881 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
885 my $item_field = $tagslib->{$itemtag};
887 my $more_subfields = $self->additional_attributes->to_hashref;
888 foreach my $subfield (
890 $a->{display_order} <=> $b->{display_order}
891 || $a->{subfield} cmp $b->{subfield}
892 } grep { ref($_) && %$_ } values %$item_field
895 my $kohafield = $subfield->{kohafield};
896 my $tagsubfield = $subfield->{tagsubfield};
898 if ( defined $kohafield ) {
899 next if $kohafield !~ m{^items\.}; # That would be weird!
900 ( my $attribute = $kohafield ) =~ s|^items\.||;
901 $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
902 if defined $self->$attribute and $self->$attribute ne '';
904 $value = $more_subfields->{$tagsubfield}
907 next unless defined $value
910 if ( $subfield->{repeatable} ) {
911 my @values = split '\|', $value;
912 push @subfields, ( $tagsubfield => $_ ) for @values;
915 push @subfields, ( $tagsubfield => $value );
920 return unless @subfields;
922 return MARC::Field->new(
923 "$itemtag", ' ', ' ', @subfields
927 =head3 renewal_branchcode
929 Returns the branchcode to be recorded in statistics renewal of the item
933 sub renewal_branchcode {
935 my ($self, $params ) = @_;
937 my $interface = C4::Context->interface;
939 if ( $interface eq 'opac' ){
940 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
941 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
942 $branchcode = 'OPACRenew';
944 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
945 $branchcode = $self->homebranch;
947 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
948 $branchcode = $self->checkout->patron->branchcode;
950 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
951 $branchcode = $self->checkout->branchcode;
957 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
958 ? C4::Context->userenv->{branch} : $params->{branch};
965 Return the cover images associated with this item.
972 my $cover_image_rs = $self->_result->cover_images;
973 return unless $cover_image_rs;
974 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
977 =head3 columns_to_str
979 my $values = $items->columns_to_str;
981 Return a hashref with the string representation of the different attribute of the item.
983 This is meant to be used for display purpose only.
990 my $frameworkcode = $self->biblio->frameworkcode;
991 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
992 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
994 my $columns_info = $self->_result->result_source->columns_info;
996 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
998 for my $column ( keys %$columns_info ) {
1000 next if $column eq 'more_subfields_xml';
1002 my $value = $self->$column;
1003 # 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
1005 if ( not defined $value or $value eq "" ) {
1006 $values->{$column} = $value;
1011 exists $mss->{"items.$column"}
1012 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1015 $values->{$column} =
1017 ? $subfield->{authorised_value}
1018 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1019 $subfield->{tagsubfield}, $value, '', $tagslib )
1025 $self->more_subfields_xml
1026 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1031 my ( $field ) = $marc_more->fields;
1032 for my $sf ( $field->subfields ) {
1033 my $subfield_code = $sf->[0];
1034 my $value = $sf->[1];
1035 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1036 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1038 $subfield->{authorised_value}
1039 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1040 $subfield->{tagsubfield}, $value, '', $tagslib )
1043 push @{$more_values->{$subfield_code}}, $value;
1046 while ( my ( $k, $v ) = each %$more_values ) {
1047 $values->{$k} = join ' | ', @$v;
1054 =head3 additional_attributes
1056 my $attributes = $item->additional_attributes;
1057 $attributes->{k} = 'new k';
1058 $item->update({ more_subfields => $attributes->to_marcxml });
1060 Returns a Koha::Item::Attributes object that represents the non-mapped
1061 attributes for this item.
1065 sub additional_attributes {
1068 return Koha::Item::Attributes->new_from_marcxml(
1069 $self->more_subfields_xml,
1073 =head3 _set_found_trigger
1075 $self->_set_found_trigger
1077 Finds the most recent lost item charge for this item and refunds the patron
1078 appropriately, taking into account any payments or writeoffs already applied
1081 Internal function, not exported, called only by Koha::Item->store.
1085 sub _set_found_trigger {
1086 my ( $self, $pre_mod_item ) = @_;
1088 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1089 my $no_refund_after_days =
1090 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1091 if ($no_refund_after_days) {
1092 my $today = dt_from_string();
1093 my $lost_age_in_days =
1094 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1097 return $self unless $lost_age_in_days < $no_refund_after_days;
1100 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1103 return_branch => C4::Context->userenv
1104 ? C4::Context->userenv->{'branch'}
1109 if ( $lostreturn_policy ) {
1111 # refund charge made for lost book
1112 my $lost_charge = Koha::Account::Lines->search(
1114 itemnumber => $self->itemnumber,
1115 debit_type_code => 'LOST',
1116 status => [ undef, { '<>' => 'FOUND' } ]
1119 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1124 if ( $lost_charge ) {
1126 my $patron = $lost_charge->patron;
1129 my $account = $patron->account;
1130 my $total_to_refund = 0;
1133 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1135 # some amount has been cancelled. collect the offsets that are not writeoffs
1136 # this works because the only way to subtract from this kind of a debt is
1137 # using the UI buttons 'Pay' and 'Write off'
1138 my $credit_offsets = $lost_charge->debit_offsets(
1140 'credit_id' => { '!=' => undef },
1141 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1143 { join => 'credit' }
1146 $total_to_refund = ( $credit_offsets->count > 0 )
1147 ? $credit_offsets->total * -1 # credits are negative on the DB
1151 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1154 if ( $credit_total > 0 ) {
1156 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1157 $credit = $account->add_credit(
1159 amount => $credit_total,
1160 description => 'Item found ' . $self->itemnumber,
1161 type => 'LOST_FOUND',
1162 interface => C4::Context->interface,
1163 library_id => $branchcode,
1164 item_id => $self->itemnumber,
1165 issue_id => $lost_charge->issue_id
1169 $credit->apply( { debits => [$lost_charge] } );
1173 message => 'lost_refunded',
1174 payload => { credit_id => $credit->id }
1179 # Update the account status
1180 $lost_charge->status('FOUND');
1181 $lost_charge->store();
1183 # Reconcile balances if required
1184 if ( C4::Context->preference('AccountAutoReconcile') ) {
1185 $account->reconcile_balance;
1190 # restore fine for lost book
1191 if ( $lostreturn_policy eq 'restore' ) {
1192 my $lost_overdue = Koha::Account::Lines->search(
1194 itemnumber => $self->itemnumber,
1195 debit_type_code => 'OVERDUE',
1199 order_by => { '-desc' => 'date' },
1204 if ( $lost_overdue ) {
1206 my $patron = $lost_overdue->patron;
1208 my $account = $patron->account;
1210 # Update status of fine
1211 $lost_overdue->status('FOUND')->store();
1213 # Find related forgive credit
1214 my $refund = $lost_overdue->credits(
1216 credit_type_code => 'FORGIVEN',
1217 itemnumber => $self->itemnumber,
1218 status => [ { '!=' => 'VOID' }, undef ]
1220 { order_by => { '-desc' => 'date' }, rows => 1 }
1224 # Revert the forgive credit
1225 $refund->void({ interface => 'trigger' });
1229 message => 'lost_restored',
1230 payload => { refund_id => $refund->id }
1235 # Reconcile balances if required
1236 if ( C4::Context->preference('AccountAutoReconcile') ) {
1237 $account->reconcile_balance;
1241 } elsif ( $lostreturn_policy eq 'charge' ) {
1245 message => 'lost_charge',
1254 =head3 public_read_list
1256 This method returns the list of publicly readable database fields for both API and UI output purposes
1260 sub public_read_list {
1262 'itemnumber', 'biblionumber', 'homebranch',
1263 'holdingbranch', 'location', 'collectioncode',
1264 'itemcallnumber', 'copynumber', 'enumchron',
1265 'barcode', 'dateaccessioned', 'itemnotes',
1266 'onloan', 'uri', 'itype',
1267 'notforloan', 'damaged', 'itemlost',
1268 'withdrawn', 'restricted'
1272 =head3 to_api_mapping
1274 This method returns the mapping for representing a Koha::Item object
1279 sub to_api_mapping {
1281 itemnumber => 'item_id',
1282 biblionumber => 'biblio_id',
1283 biblioitemnumber => undef,
1284 barcode => 'external_id',
1285 dateaccessioned => 'acquisition_date',
1286 booksellerid => 'acquisition_source',
1287 homebranch => 'home_library_id',
1288 price => 'purchase_price',
1289 replacementprice => 'replacement_price',
1290 replacementpricedate => 'replacement_price_date',
1291 datelastborrowed => 'last_checkout_date',
1292 datelastseen => 'last_seen_date',
1294 notforloan => 'not_for_loan_status',
1295 damaged => 'damaged_status',
1296 damaged_on => 'damaged_date',
1297 itemlost => 'lost_status',
1298 itemlost_on => 'lost_date',
1299 withdrawn => 'withdrawn',
1300 withdrawn_on => 'withdrawn_date',
1301 itemcallnumber => 'callnumber',
1302 coded_location_qualifier => 'coded_location_qualifier',
1303 issues => 'checkouts_count',
1304 renewals => 'renewals_count',
1305 reserves => 'holds_count',
1306 restricted => 'restricted_status',
1307 itemnotes => 'public_notes',
1308 itemnotes_nonpublic => 'internal_notes',
1309 holdingbranch => 'holding_library_id',
1310 timestamp => 'timestamp',
1311 location => 'location',
1312 permanent_location => 'permanent_location',
1313 onloan => 'checked_out_date',
1314 cn_source => 'call_number_source',
1315 cn_sort => 'call_number_sort',
1316 ccode => 'collection_code',
1317 materials => 'materials_notes',
1319 itype => 'item_type_id',
1320 more_subfields_xml => 'extended_subfields',
1321 enumchron => 'serial_issue_number',
1322 copynumber => 'copy_number',
1323 stocknumber => 'inventory_number',
1324 new_status => 'new_status'
1330 my $itemtype = $item->itemtype;
1332 Returns Koha object for effective itemtype
1338 return Koha::ItemTypes->find( $self->effective_itemtype );
1343 my $orders = $item->orders();
1345 Returns a Koha::Acquisition::Orders object
1352 my $orders = $self->_result->item_orders;
1353 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1356 =head3 tracked_links
1358 my $tracked_links = $item->tracked_links();
1360 Returns a Koha::TrackedLinks object
1367 my $tracked_links = $self->_result->linktrackers;
1368 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1371 =head3 move_to_biblio
1373 $item->move_to_biblio($to_biblio[, $params]);
1375 Move the item to another biblio and update any references in other tables.
1377 The final optional parameter, C<$params>, is expected to contain the
1378 'skip_record_index' key, which is relayed down to Koha::Item->store.
1379 There it prevents calling index_records, which takes most of the
1380 time in batch adds/deletes. The caller must take care of calling
1381 index_records separately.
1384 skip_record_index => 1|0
1386 Returns undef if the move failed or the biblionumber of the destination record otherwise
1390 sub move_to_biblio {
1391 my ( $self, $to_biblio, $params ) = @_;
1395 return if $self->biblionumber == $to_biblio->biblionumber;
1397 my $from_biblionumber = $self->biblionumber;
1398 my $to_biblionumber = $to_biblio->biblionumber;
1400 # Own biblionumber and biblioitemnumber
1402 biblionumber => $to_biblionumber,
1403 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1404 })->store({ skip_record_index => $params->{skip_record_index} });
1406 unless ($params->{skip_record_index}) {
1407 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1408 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1411 # Acquisition orders
1412 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1415 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1417 # hold_fill_target (there's no Koha object available yet)
1418 my $hold_fill_target = $self->_result->hold_fill_target;
1419 if ($hold_fill_target) {
1420 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1423 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1424 # and can't even fake one since the significant columns are nullable.
1425 my $storage = $self->_result->result_source->storage;
1428 my ($storage, $dbh, @cols) = @_;
1430 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1435 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1437 return $to_biblionumber;
1440 =head2 Internal methods
1442 =head3 _after_item_action_hooks
1444 Helper method that takes care of calling all plugin hooks
1448 sub _after_item_action_hooks {
1449 my ( $self, $params ) = @_;
1451 my $action = $params->{action};
1453 Koha::Plugins->call(
1454 'after_item_action',
1458 item_id => $self->itemnumber,
1465 my $recall = $item->recall;
1467 Return the relevant recall for this item
1473 my @recalls = Koha::Recalls->search(
1475 biblio_id => $self->biblionumber,
1478 { order_by => { -asc => 'created_date' } }
1480 foreach my $recall (@recalls) {
1481 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1485 # no item-level recall to return, so return earliest biblio-level
1486 # FIXME: eventually this will be based on priority
1490 =head3 can_be_recalled
1492 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1494 Does item-level checks and returns if items can be recalled by this borrower
1498 sub can_be_recalled {
1499 my ( $self, $params ) = @_;
1501 return 0 if !( C4::Context->preference('UseRecalls') );
1503 # check if this item is not for loan, withdrawn or lost
1504 return 0 if ( $self->notforloan != 0 );
1505 return 0 if ( $self->itemlost != 0 );
1506 return 0 if ( $self->withdrawn != 0 );
1508 # check if this item is not checked out - if not checked out, can't be recalled
1509 return 0 if ( !defined( $self->checkout ) );
1511 my $patron = $params->{patron};
1513 my $branchcode = C4::Context->userenv->{'branch'};
1515 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1518 # Check the circulation rule for each relevant itemtype for this item
1519 my $rule = Koha::CirculationRules->get_effective_rules({
1520 branchcode => $branchcode,
1521 categorycode => $patron ? $patron->categorycode : undef,
1522 itemtype => $self->effective_itemtype,
1525 'recalls_per_record',
1530 # check recalls allowed has been set and is not zero
1531 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1534 # check borrower has not reached open recalls allowed limit
1535 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1537 # check borrower has not reach open recalls allowed per record limit
1538 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1540 # check if this patron has already recalled this item
1541 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1543 # check if this patron has already checked out this item
1544 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1546 # check if this patron has already reserved this item
1547 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1550 # check item availability
1551 # items are unavailable for recall if they are lost, withdrawn or notforloan
1552 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1554 # if there are no available items at all, no recall can be placed
1555 return 0 if ( scalar @items == 0 );
1557 my $checked_out_count = 0;
1559 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1562 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1563 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1565 # can't recall if no items have been checked out
1566 return 0 if ( $checked_out_count == 0 );
1572 =head3 can_be_waiting_recall
1574 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1576 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1577 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1581 sub can_be_waiting_recall {
1584 return 0 if !( C4::Context->preference('UseRecalls') );
1586 # check if this item is not for loan, withdrawn or lost
1587 return 0 if ( $self->notforloan != 0 );
1588 return 0 if ( $self->itemlost != 0 );
1589 return 0 if ( $self->withdrawn != 0 );
1591 my $branchcode = $self->holdingbranch;
1592 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1593 $branchcode = C4::Context->userenv->{'branch'};
1595 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1598 # Check the circulation rule for each relevant itemtype for this item
1599 my $rule = Koha::CirculationRules->get_effective_rules({
1600 branchcode => $branchcode,
1601 categorycode => undef,
1602 itemtype => $self->effective_itemtype,
1608 # check recalls allowed has been set and is not zero
1609 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1615 =head3 check_recalls
1617 my $recall = $item->check_recalls;
1619 Get the most relevant recall for this item.
1626 my @recalls = Koha::Recalls->search(
1627 { biblio_id => $self->biblionumber,
1628 item_id => [ $self->itemnumber, undef ]
1630 { order_by => { -asc => 'created_date' } }
1631 )->filter_by_current->as_list;
1634 # iterate through relevant recalls to find the best one.
1635 # if we come across a waiting recall, use this one.
1636 # 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.
1637 foreach my $r ( @recalls ) {
1638 if ( $r->waiting ) {
1643 unless ( defined $recall ) {
1644 $recall = $recalls[0];
1650 =head3 is_notforloan
1652 my $is_notforloan = $item->is_notforloan;
1654 Determine whether or not this item is "notforloan" based on
1655 the item's notforloan status or its item type
1661 my $is_notforloan = 0;
1663 if ( $self->notforloan ){
1667 my $itemtype = $self->itemtype;
1669 if ( $itemtype->notforloan ){
1675 return $is_notforloan;
1688 Kyle M Hall <kyle@bywatersolutions.com>