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)
69 You should not turn it on if you do not understand what it is doing exactly.
75 my $params = @_ ? shift : {};
77 my $log_action = $params->{log_action} // 1;
79 # We do not want to oblige callers to pass this value
80 # Dev conveniences vs performance?
81 unless ( $self->biblioitemnumber ) {
82 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
85 # See related changes from C4::Items::AddItem
86 unless ( $self->itype ) {
87 $self->itype($self->biblio->biblioitem->itemtype);
90 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
92 my $today = dt_from_string;
93 my $action = 'create';
95 unless ( $self->in_storage ) { #AddItem
97 unless ( $self->permanent_location ) {
98 $self->permanent_location($self->location);
101 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
102 unless ( $self->location || !$default_location ) {
103 $self->permanent_location( $self->location || $default_location )
104 unless $self->permanent_location;
105 $self->location($default_location);
108 unless ( $self->replacementpricedate ) {
109 $self->replacementpricedate($today);
111 unless ( $self->datelastseen ) {
112 $self->datelastseen($today);
115 unless ( $self->dateaccessioned ) {
116 $self->dateaccessioned($today);
119 if ( $self->itemcallnumber
120 or $self->cn_source )
122 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
123 $self->cn_sort($cn_sort);
130 my %updated_columns = $self->_result->get_dirty_columns;
131 return $self->SUPER::store unless %updated_columns;
133 # Retrieve the item for comparison if we need to
135 exists $updated_columns{itemlost}
136 or exists $updated_columns{withdrawn}
137 or exists $updated_columns{damaged}
138 ) ? $self->get_from_storage : undef;
140 # Update *_on fields if needed
141 # FIXME: Why not for AddItem as well?
142 my @fields = qw( itemlost withdrawn damaged );
143 for my $field (@fields) {
145 # If the field is defined but empty or 0, we are
146 # removing/unsetting and thus need to clear out
148 if ( exists $updated_columns{$field}
149 && defined( $self->$field )
152 my $field_on = "${field}_on";
153 $self->$field_on(undef);
155 # If the field has changed otherwise, we much update
157 elsif (exists $updated_columns{$field}
158 && $updated_columns{$field}
159 && !$pre_mod_item->$field )
161 my $field_on = "${field}_on";
163 DateTime::Format::MySQL->format_datetime(
170 if ( exists $updated_columns{itemcallnumber}
171 or exists $updated_columns{cn_source} )
173 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
174 $self->cn_sort($cn_sort);
178 if ( exists $updated_columns{location}
179 and $self->location ne 'CART'
180 and $self->location ne 'PROC'
181 and not exists $updated_columns{permanent_location} )
183 $self->permanent_location( $self->location );
186 # If item was lost and has now been found,
187 # reverse any list item charges if necessary.
188 if ( exists $updated_columns{itemlost}
189 and $updated_columns{itemlost} <= 0
190 and $pre_mod_item->itemlost > 0 )
192 $self->_set_found_trigger($pre_mod_item);
197 my $result = $self->SUPER::store;
198 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
200 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
201 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
203 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
204 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
205 unless $params->{skip_record_index};
206 $self->get_from_storage->_after_item_action_hooks({ action => $action });
208 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
210 biblio_ids => [ $self->biblionumber ]
212 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
223 my $params = @_ ? shift : {};
225 # FIXME check the item has no current issues
226 # i.e. raise the appropriate exception
228 my $result = $self->SUPER::delete;
230 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
231 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
232 unless $params->{skip_record_index};
234 $self->_after_item_action_hooks({ action => 'delete' });
236 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
237 if C4::Context->preference("CataloguingLog");
239 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
241 biblio_ids => [ $self->biblionumber ]
243 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
254 my $params = @_ ? shift : {};
256 my $safe_to_delete = $self->safe_to_delete;
257 return $safe_to_delete unless $safe_to_delete;
259 $self->move_to_deleted;
261 return $self->delete($params);
264 =head3 safe_to_delete
266 returns 1 if the item is safe to delete,
268 "book_on_loan" if the item is checked out,
270 "not_same_branch" if the item is blocked by independent branches,
272 "book_reserved" if the there are holds aganst the item, or
274 "linked_analytics" if the item has linked analytic records.
276 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
285 $error = "book_on_loan" if $self->checkout;
287 $error = "not_same_branch"
288 if defined C4::Context->userenv
289 and !C4::Context->IsSuperLibrarian()
290 and C4::Context->preference("IndependentBranches")
291 and ( C4::Context->userenv->{branch} ne $self->homebranch );
293 # check it doesn't have a waiting reserve
294 $error = "book_reserved"
295 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
297 $error = "linked_analytics"
298 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
300 $error = "last_item_for_hold"
301 if $self->biblio->items->count == 1
302 && $self->biblio->holds->search(
309 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
312 return Koha::Result::Boolean->new(1);
315 =head3 move_to_deleted
317 my $is_moved = $item->move_to_deleted;
319 Move an item to the deleteditems table.
320 This can be done before deleting an item, to make sure the data are not completely deleted.
324 sub move_to_deleted {
326 my $item_infos = $self->unblessed;
327 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
328 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
332 =head3 effective_itemtype
334 Returns the itemtype for the item based on whether item level itemtypes are set or not.
338 sub effective_itemtype {
341 return $self->_result()->effective_itemtype();
351 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
353 return $self->{_home_branch};
356 =head3 holding_branch
363 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
365 return $self->{_holding_branch};
370 my $biblio = $item->biblio;
372 Return the bibliographic record of this item
378 my $biblio_rs = $self->_result->biblio;
379 return Koha::Biblio->_new_from_dbic( $biblio_rs );
384 my $biblioitem = $item->biblioitem;
386 Return the biblioitem record of this item
392 my $biblioitem_rs = $self->_result->biblioitem;
393 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
398 my $checkout = $item->checkout;
400 Return the checkout for this item
406 my $checkout_rs = $self->_result->issue;
407 return unless $checkout_rs;
408 return Koha::Checkout->_new_from_dbic( $checkout_rs );
413 my $holds = $item->holds();
414 my $holds = $item->holds($params);
415 my $holds = $item->holds({ found => 'W'});
417 Return holds attached to an item, optionally accept a hashref of params to pass to search
422 my ( $self,$params ) = @_;
423 my $holds_rs = $self->_result->reserves->search($params);
424 return Koha::Holds->_new_from_dbic( $holds_rs );
427 =head3 request_transfer
429 my $transfer = $item->request_transfer(
433 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
437 Add a transfer request for this item to the given branch for the given reason.
439 An exception will be thrown if the BranchTransferLimits would prevent the requested
440 transfer, unless 'ignore_limits' is passed to override the limits.
442 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
443 The caller should catch such cases and retry the transfer request as appropriate passing
444 an appropriate override.
447 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
448 * replace - Used to replace the existing transfer request with your own.
452 sub request_transfer {
453 my ( $self, $params ) = @_;
455 # check for mandatory params
456 my @mandatory = ( 'to', 'reason' );
457 for my $param (@mandatory) {
458 unless ( defined( $params->{$param} ) ) {
459 Koha::Exceptions::MissingParameter->throw(
460 error => "The $param parameter is mandatory" );
464 Koha::Exceptions::Item::Transfer::Limit->throw()
465 unless ( $params->{ignore_limits}
466 || $self->can_be_transferred( { to => $params->{to} } ) );
468 my $request = $self->get_transfer;
469 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
470 if ( $request && !$params->{enqueue} && !$params->{replace} );
472 $request->cancel( { reason => $params->{reason}, force => 1 } )
473 if ( defined($request) && $params->{replace} );
475 my $transfer = Koha::Item::Transfer->new(
477 itemnumber => $self->itemnumber,
478 daterequested => dt_from_string,
479 frombranch => $self->holdingbranch,
480 tobranch => $params->{to}->branchcode,
481 reason => $params->{reason},
482 comments => $params->{comment}
491 my $transfer = $item->get_transfer;
493 Return the active transfer request or undef
495 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
496 whereby the most recently sent, but not received, transfer will be returned
497 if it exists, otherwise the oldest unsatisfied transfer will be returned.
499 This allows for transfers to queue, which is the case for stock rotation and
500 rotating collections where a manual transfer may need to take precedence but
501 we still expect the item to end up at a final location eventually.
507 my $transfer_rs = $self->_result->branchtransfers->search(
509 datearrived => undef,
510 datecancelled => undef
514 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
518 return unless $transfer_rs;
519 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
524 my $transfer = $item->get_transfers;
526 Return the list of outstanding transfers (i.e requested but not yet cancelled
529 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
530 whereby the most recently sent, but not received, transfer will be returned
531 first if it exists, otherwise requests are in oldest to newest request order.
533 This allows for transfers to queue, which is the case for stock rotation and
534 rotating collections where a manual transfer may need to take precedence but
535 we still expect the item to end up at a final location eventually.
541 my $transfer_rs = $self->_result->branchtransfers->search(
543 datearrived => undef,
544 datecancelled => undef
548 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
551 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
554 =head3 last_returned_by
556 Gets and sets the last borrower to return an item.
558 Accepts and returns Koha::Patron objects
560 $item->last_returned_by( $borrowernumber );
562 $last_returned_by = $item->last_returned_by();
566 sub last_returned_by {
567 my ( $self, $borrower ) = @_;
569 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
572 return $items_last_returned_by_rs->update_or_create(
573 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
576 unless ( $self->{_last_returned_by} ) {
577 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
579 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
583 return $self->{_last_returned_by};
587 =head3 can_article_request
589 my $bool = $item->can_article_request( $borrower )
591 Returns true if item can be specifically requested
593 $borrower must be a Koha::Patron object
597 sub can_article_request {
598 my ( $self, $borrower ) = @_;
600 my $rule = $self->article_request_type($borrower);
602 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
606 =head3 hidden_in_opac
608 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
610 Returns true if item fields match the hidding criteria defined in $rules.
611 Returns false otherwise.
613 Takes HASHref that can have the following parameters:
615 $rules : { <field> => [ value_1, ... ], ... }
617 Note: $rules inherits its structure from the parsed YAML from reading
618 the I<OpacHiddenItems> system preference.
623 my ( $self, $params ) = @_;
625 my $rules = $params->{rules} // {};
628 if C4::Context->preference('hidelostitems') and
631 my $hidden_in_opac = 0;
633 foreach my $field ( keys %{$rules} ) {
635 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
641 return $hidden_in_opac;
644 =head3 can_be_transferred
646 $item->can_be_transferred({ to => $to_library, from => $from_library })
647 Checks if an item can be transferred to given library.
649 This feature is controlled by two system preferences:
650 UseBranchTransferLimits to enable / disable the feature
651 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
652 for setting the limitations
654 Takes HASHref that can have the following parameters:
655 MANDATORY PARAMETERS:
658 $from : Koha::Library # if not given, item holdingbranch
659 # will be used instead
661 Returns 1 if item can be transferred to $to_library, otherwise 0.
663 To find out whether at least one item of a Koha::Biblio can be transferred, please
664 see Koha::Biblio->can_be_transferred() instead of using this method for
665 multiple items of the same biblio.
669 sub can_be_transferred {
670 my ($self, $params) = @_;
672 my $to = $params->{to};
673 my $from = $params->{from};
675 $to = $to->branchcode;
676 $from = defined $from ? $from->branchcode : $self->holdingbranch;
678 return 1 if $from eq $to; # Transfer to current branch is allowed
679 return 1 unless C4::Context->preference('UseBranchTransferLimits');
681 my $limittype = C4::Context->preference('BranchTransferLimitsType');
682 return Koha::Item::Transfer::Limits->search({
685 $limittype => $limittype eq 'itemtype'
686 ? $self->effective_itemtype : $self->ccode
691 =head3 pickup_locations
693 $pickup_locations = $item->pickup_locations( {patron => $patron } )
695 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)
696 and if item can be transferred to each pickup location.
700 sub pickup_locations {
701 my ($self, $params) = @_;
703 my $patron = $params->{patron};
705 my $circ_control_branch =
706 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
708 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
710 if(defined $patron) {
711 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
712 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
715 my $pickup_libraries = Koha::Libraries->search();
716 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
717 $pickup_libraries = $self->home_branch->get_hold_libraries;
718 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
719 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
720 $pickup_libraries = $plib->get_hold_libraries;
721 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
722 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
723 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
724 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
727 return $pickup_libraries->search(
732 order_by => ['branchname']
734 ) unless C4::Context->preference('UseBranchTransferLimits');
736 my $limittype = C4::Context->preference('BranchTransferLimitsType');
737 my ($ccode, $itype) = (undef, undef);
738 if( $limittype eq 'ccode' ){
739 $ccode = $self->ccode;
741 $itype = $self->itype;
743 my $limits = Koha::Item::Transfer::Limits->search(
745 fromBranch => $self->holdingbranch,
749 { columns => ['toBranch'] }
752 return $pickup_libraries->search(
754 pickup_location => 1,
756 '-not_in' => $limits->_resultset->as_query
760 order_by => ['branchname']
765 =head3 article_request_type
767 my $type = $item->article_request_type( $borrower )
769 returns 'yes', 'no', 'bib_only', or 'item_only'
771 $borrower must be a Koha::Patron object
775 sub article_request_type {
776 my ( $self, $borrower ) = @_;
778 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
780 $branch_control eq 'homebranch' ? $self->homebranch
781 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
783 my $borrowertype = $borrower->categorycode;
784 my $itemtype = $self->effective_itemtype();
785 my $rule = Koha::CirculationRules->get_effective_rule(
787 rule_name => 'article_requests',
788 categorycode => $borrowertype,
789 itemtype => $itemtype,
790 branchcode => $branchcode
794 return q{} unless $rule;
795 return $rule->rule_value || q{}
804 my $attributes = { order_by => 'priority' };
805 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
807 itemnumber => $self->itemnumber,
810 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
811 waitingdate => { '!=' => undef },
814 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
815 return Koha::Holds->_new_from_dbic($hold_rs);
818 =head3 stockrotationitem
820 my $sritem = Koha::Item->stockrotationitem;
822 Returns the stock rotation item associated with the current item.
826 sub stockrotationitem {
828 my $rs = $self->_result->stockrotationitem;
830 return Koha::StockRotationItem->_new_from_dbic( $rs );
835 my $item = $item->add_to_rota($rota_id);
837 Add this item to the rota identified by $ROTA_ID, which means associating it
838 with the first stage of that rota. Should this item already be associated
839 with a rota, then we will move it to the new rota.
844 my ( $self, $rota_id ) = @_;
845 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
849 =head3 has_pending_hold
851 my $is_pending_hold = $item->has_pending_hold();
853 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
857 sub has_pending_hold {
859 my $pending_hold = $self->_result->tmp_holdsqueues;
860 return $pending_hold->count ? 1: 0;
865 my $field = $item->as_marc_field;
867 This method returns a MARC::Field object representing the Koha::Item object
868 with the current mappings configuration.
875 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
877 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
881 my $item_field = $tagslib->{$itemtag};
883 my $more_subfields = $self->additional_attributes->to_hashref;
884 foreach my $subfield (
886 $a->{display_order} <=> $b->{display_order}
887 || $a->{subfield} cmp $b->{subfield}
888 } grep { ref($_) && %$_ } values %$item_field
891 my $kohafield = $subfield->{kohafield};
892 my $tagsubfield = $subfield->{tagsubfield};
894 if ( defined $kohafield && $kohafield ne '' ) {
895 next if $kohafield !~ m{^items\.}; # That would be weird!
896 ( my $attribute = $kohafield ) =~ s|^items\.||;
897 $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
898 if defined $self->$attribute and $self->$attribute ne '';
900 $value = $more_subfields->{$tagsubfield}
903 next unless defined $value
906 if ( $subfield->{repeatable} ) {
907 my @values = split '\|', $value;
908 push @subfields, ( $tagsubfield => $_ ) for @values;
911 push @subfields, ( $tagsubfield => $value );
916 return unless @subfields;
918 return MARC::Field->new(
919 "$itemtag", ' ', ' ', @subfields
923 =head3 renewal_branchcode
925 Returns the branchcode to be recorded in statistics renewal of the item
929 sub renewal_branchcode {
931 my ($self, $params ) = @_;
933 my $interface = C4::Context->interface;
935 if ( $interface eq 'opac' ){
936 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
937 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
938 $branchcode = 'OPACRenew';
940 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
941 $branchcode = $self->homebranch;
943 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
944 $branchcode = $self->checkout->patron->branchcode;
946 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
947 $branchcode = $self->checkout->branchcode;
953 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
954 ? C4::Context->userenv->{branch} : $params->{branch};
961 Return the cover images associated with this item.
968 my $cover_image_rs = $self->_result->cover_images;
969 return unless $cover_image_rs;
970 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
973 =head3 columns_to_str
975 my $values = $items->columns_to_str;
977 Return a hashref with the string representation of the different attribute of the item.
979 This is meant to be used for display purpose only.
986 my $frameworkcode = $self->biblio->frameworkcode;
987 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
988 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
990 my $columns_info = $self->_result->result_source->columns_info;
992 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
994 for my $column ( keys %$columns_info ) {
996 next if $column eq 'more_subfields_xml';
998 my $value = $self->$column;
999 # 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
1001 if ( not defined $value or $value eq "" ) {
1002 $values->{$column} = $value;
1007 exists $mss->{"items.$column"}
1008 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1011 $values->{$column} =
1013 ? $subfield->{authorised_value}
1014 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1015 $subfield->{tagsubfield}, $value, '', $tagslib )
1021 $self->more_subfields_xml
1022 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1027 my ( $field ) = $marc_more->fields;
1028 for my $sf ( $field->subfields ) {
1029 my $subfield_code = $sf->[0];
1030 my $value = $sf->[1];
1031 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1032 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1034 $subfield->{authorised_value}
1035 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1036 $subfield->{tagsubfield}, $value, '', $tagslib )
1039 push @{$more_values->{$subfield_code}}, $value;
1042 while ( my ( $k, $v ) = each %$more_values ) {
1043 $values->{$k} = join ' | ', @$v;
1050 =head3 additional_attributes
1052 my $attributes = $item->additional_attributes;
1053 $attributes->{k} = 'new k';
1054 $item->update({ more_subfields => $attributes->to_marcxml });
1056 Returns a Koha::Item::Attributes object that represents the non-mapped
1057 attributes for this item.
1061 sub additional_attributes {
1064 return Koha::Item::Attributes->new_from_marcxml(
1065 $self->more_subfields_xml,
1069 =head3 _set_found_trigger
1071 $self->_set_found_trigger
1073 Finds the most recent lost item charge for this item and refunds the patron
1074 appropriately, taking into account any payments or writeoffs already applied
1077 Internal function, not exported, called only by Koha::Item->store.
1081 sub _set_found_trigger {
1082 my ( $self, $pre_mod_item ) = @_;
1084 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1085 my $no_refund_after_days =
1086 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1087 if ($no_refund_after_days) {
1088 my $today = dt_from_string();
1089 my $lost_age_in_days =
1090 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1093 return $self unless $lost_age_in_days < $no_refund_after_days;
1096 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1099 return_branch => C4::Context->userenv
1100 ? C4::Context->userenv->{'branch'}
1105 if ( $lostreturn_policy ) {
1107 # refund charge made for lost book
1108 my $lost_charge = Koha::Account::Lines->search(
1110 itemnumber => $self->itemnumber,
1111 debit_type_code => 'LOST',
1112 status => [ undef, { '<>' => 'FOUND' } ]
1115 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1120 if ( $lost_charge ) {
1122 my $patron = $lost_charge->patron;
1125 my $account = $patron->account;
1126 my $total_to_refund = 0;
1129 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1131 # some amount has been cancelled. collect the offsets that are not writeoffs
1132 # this works because the only way to subtract from this kind of a debt is
1133 # using the UI buttons 'Pay' and 'Write off'
1134 my $credit_offsets = $lost_charge->debit_offsets(
1136 'credit_id' => { '!=' => undef },
1137 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1139 { join => 'credit' }
1142 $total_to_refund = ( $credit_offsets->count > 0 )
1143 ? $credit_offsets->total * -1 # credits are negative on the DB
1147 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1150 if ( $credit_total > 0 ) {
1152 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1153 $credit = $account->add_credit(
1155 amount => $credit_total,
1156 description => 'Item found ' . $self->itemnumber,
1157 type => 'LOST_FOUND',
1158 interface => C4::Context->interface,
1159 library_id => $branchcode,
1160 item_id => $self->itemnumber,
1161 issue_id => $lost_charge->issue_id
1165 $credit->apply( { debits => [$lost_charge] } );
1169 message => 'lost_refunded',
1170 payload => { credit_id => $credit->id }
1175 # Update the account status
1176 $lost_charge->status('FOUND');
1177 $lost_charge->store();
1179 # Reconcile balances if required
1180 if ( C4::Context->preference('AccountAutoReconcile') ) {
1181 $account->reconcile_balance;
1186 # possibly restore fine for lost book
1187 my $lost_overdue = Koha::Account::Lines->search(
1189 itemnumber => $self->itemnumber,
1190 debit_type_code => 'OVERDUE',
1194 order_by => { '-desc' => 'date' },
1198 if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1200 my $patron = $lost_overdue->patron;
1202 my $account = $patron->account;
1204 # Update status of fine
1205 $lost_overdue->status('FOUND')->store();
1207 # Find related forgive credit
1208 my $refund = $lost_overdue->credits(
1210 credit_type_code => 'FORGIVEN',
1211 itemnumber => $self->itemnumber,
1212 status => [ { '!=' => 'VOID' }, undef ]
1214 { order_by => { '-desc' => 'date' }, rows => 1 }
1218 # Revert the forgive credit
1219 $refund->void({ interface => 'trigger' });
1223 message => 'lost_restored',
1224 payload => { refund_id => $refund->id }
1229 # Reconcile balances if required
1230 if ( C4::Context->preference('AccountAutoReconcile') ) {
1231 $account->reconcile_balance;
1235 } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1239 message => 'lost_charge',
1248 =head3 public_read_list
1250 This method returns the list of publicly readable database fields for both API and UI output purposes
1254 sub public_read_list {
1256 'itemnumber', 'biblionumber', 'homebranch',
1257 'holdingbranch', 'location', 'collectioncode',
1258 'itemcallnumber', 'copynumber', 'enumchron',
1259 'barcode', 'dateaccessioned', 'itemnotes',
1260 'onloan', 'uri', 'itype',
1261 'notforloan', 'damaged', 'itemlost',
1262 'withdrawn', 'restricted'
1268 Overloaded to_api method to ensure item-level itypes is adhered to.
1273 my ($self, $params) = @_;
1275 my $response = $self->SUPER::to_api($params);
1278 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1280 return { %$response, %$overrides };
1283 =head3 to_api_mapping
1285 This method returns the mapping for representing a Koha::Item object
1290 sub to_api_mapping {
1292 itemnumber => 'item_id',
1293 biblionumber => 'biblio_id',
1294 biblioitemnumber => undef,
1295 barcode => 'external_id',
1296 dateaccessioned => 'acquisition_date',
1297 booksellerid => 'acquisition_source',
1298 homebranch => 'home_library_id',
1299 price => 'purchase_price',
1300 replacementprice => 'replacement_price',
1301 replacementpricedate => 'replacement_price_date',
1302 datelastborrowed => 'last_checkout_date',
1303 datelastseen => 'last_seen_date',
1305 notforloan => 'not_for_loan_status',
1306 damaged => 'damaged_status',
1307 damaged_on => 'damaged_date',
1308 itemlost => 'lost_status',
1309 itemlost_on => 'lost_date',
1310 withdrawn => 'withdrawn',
1311 withdrawn_on => 'withdrawn_date',
1312 itemcallnumber => 'callnumber',
1313 coded_location_qualifier => 'coded_location_qualifier',
1314 issues => 'checkouts_count',
1315 renewals => 'renewals_count',
1316 reserves => 'holds_count',
1317 restricted => 'restricted_status',
1318 itemnotes => 'public_notes',
1319 itemnotes_nonpublic => 'internal_notes',
1320 holdingbranch => 'holding_library_id',
1321 timestamp => 'timestamp',
1322 location => 'location',
1323 permanent_location => 'permanent_location',
1324 onloan => 'checked_out_date',
1325 cn_source => 'call_number_source',
1326 cn_sort => 'call_number_sort',
1327 ccode => 'collection_code',
1328 materials => 'materials_notes',
1330 itype => 'item_type_id',
1331 more_subfields_xml => 'extended_subfields',
1332 enumchron => 'serial_issue_number',
1333 copynumber => 'copy_number',
1334 stocknumber => 'inventory_number',
1335 new_status => 'new_status'
1341 my $itemtype = $item->itemtype;
1343 Returns Koha object for effective itemtype
1349 return Koha::ItemTypes->find( $self->effective_itemtype );
1354 my $orders = $item->orders();
1356 Returns a Koha::Acquisition::Orders object
1363 my $orders = $self->_result->item_orders;
1364 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1367 =head3 tracked_links
1369 my $tracked_links = $item->tracked_links();
1371 Returns a Koha::TrackedLinks object
1378 my $tracked_links = $self->_result->linktrackers;
1379 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1382 =head3 move_to_biblio
1384 $item->move_to_biblio($to_biblio[, $params]);
1386 Move the item to another biblio and update any references in other tables.
1388 The final optional parameter, C<$params>, is expected to contain the
1389 'skip_record_index' key, which is relayed down to Koha::Item->store.
1390 There it prevents calling index_records, which takes most of the
1391 time in batch adds/deletes. The caller must take care of calling
1392 index_records separately.
1395 skip_record_index => 1|0
1397 Returns undef if the move failed or the biblionumber of the destination record otherwise
1401 sub move_to_biblio {
1402 my ( $self, $to_biblio, $params ) = @_;
1406 return if $self->biblionumber == $to_biblio->biblionumber;
1408 my $from_biblionumber = $self->biblionumber;
1409 my $to_biblionumber = $to_biblio->biblionumber;
1411 # Own biblionumber and biblioitemnumber
1413 biblionumber => $to_biblionumber,
1414 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1415 })->store({ skip_record_index => $params->{skip_record_index} });
1417 unless ($params->{skip_record_index}) {
1418 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1419 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1422 # Acquisition orders
1423 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1426 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1428 # hold_fill_target (there's no Koha object available yet)
1429 my $hold_fill_target = $self->_result->hold_fill_target;
1430 if ($hold_fill_target) {
1431 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1434 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1435 # and can't even fake one since the significant columns are nullable.
1436 my $storage = $self->_result->result_source->storage;
1439 my ($storage, $dbh, @cols) = @_;
1441 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1446 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1448 return $to_biblionumber;
1451 =head2 Internal methods
1453 =head3 _after_item_action_hooks
1455 Helper method that takes care of calling all plugin hooks
1459 sub _after_item_action_hooks {
1460 my ( $self, $params ) = @_;
1462 my $action = $params->{action};
1464 Koha::Plugins->call(
1465 'after_item_action',
1469 item_id => $self->itemnumber,
1476 my $recall = $item->recall;
1478 Return the relevant recall for this item
1484 my @recalls = Koha::Recalls->search(
1486 biblio_id => $self->biblionumber,
1489 { order_by => { -asc => 'created_date' } }
1491 foreach my $recall (@recalls) {
1492 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1496 # no item-level recall to return, so return earliest biblio-level
1497 # FIXME: eventually this will be based on priority
1501 =head3 can_be_recalled
1503 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1505 Does item-level checks and returns if items can be recalled by this borrower
1509 sub can_be_recalled {
1510 my ( $self, $params ) = @_;
1512 return 0 if !( C4::Context->preference('UseRecalls') );
1514 # check if this item is not for loan, withdrawn or lost
1515 return 0 if ( $self->notforloan != 0 );
1516 return 0 if ( $self->itemlost != 0 );
1517 return 0 if ( $self->withdrawn != 0 );
1519 # check if this item is not checked out - if not checked out, can't be recalled
1520 return 0 if ( !defined( $self->checkout ) );
1522 my $patron = $params->{patron};
1524 my $branchcode = C4::Context->userenv->{'branch'};
1526 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1529 # Check the circulation rule for each relevant itemtype for this item
1530 my $rule = Koha::CirculationRules->get_effective_rules({
1531 branchcode => $branchcode,
1532 categorycode => $patron ? $patron->categorycode : undef,
1533 itemtype => $self->effective_itemtype,
1536 'recalls_per_record',
1541 # check recalls allowed has been set and is not zero
1542 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1545 # check borrower has not reached open recalls allowed limit
1546 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1548 # check borrower has not reach open recalls allowed per record limit
1549 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1551 # check if this patron has already recalled this item
1552 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1554 # check if this patron has already checked out this item
1555 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1557 # check if this patron has already reserved this item
1558 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1561 # check item availability
1562 # items are unavailable for recall if they are lost, withdrawn or notforloan
1563 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1565 # if there are no available items at all, no recall can be placed
1566 return 0 if ( scalar @items == 0 );
1568 my $checked_out_count = 0;
1570 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1573 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1574 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1576 # can't recall if no items have been checked out
1577 return 0 if ( $checked_out_count == 0 );
1583 =head3 can_be_waiting_recall
1585 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1587 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1588 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1592 sub can_be_waiting_recall {
1595 return 0 if !( C4::Context->preference('UseRecalls') );
1597 # check if this item is not for loan, withdrawn or lost
1598 return 0 if ( $self->notforloan != 0 );
1599 return 0 if ( $self->itemlost != 0 );
1600 return 0 if ( $self->withdrawn != 0 );
1602 my $branchcode = $self->holdingbranch;
1603 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1604 $branchcode = C4::Context->userenv->{'branch'};
1606 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1609 # Check the circulation rule for each relevant itemtype for this item
1610 my $rule = Koha::CirculationRules->get_effective_rules({
1611 branchcode => $branchcode,
1612 categorycode => undef,
1613 itemtype => $self->effective_itemtype,
1619 # check recalls allowed has been set and is not zero
1620 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1626 =head3 check_recalls
1628 my $recall = $item->check_recalls;
1630 Get the most relevant recall for this item.
1637 my @recalls = Koha::Recalls->search(
1638 { biblio_id => $self->biblionumber,
1639 item_id => [ $self->itemnumber, undef ]
1641 { order_by => { -asc => 'created_date' } }
1642 )->filter_by_current->as_list;
1645 # iterate through relevant recalls to find the best one.
1646 # if we come across a waiting recall, use this one.
1647 # 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.
1648 foreach my $r ( @recalls ) {
1649 if ( $r->waiting ) {
1654 unless ( defined $recall ) {
1655 $recall = $recalls[0];
1671 Kyle M Hall <kyle@bywatersolutions.com>