3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::Exceptions::Item::Transfer;
40 use Koha::Item::Attributes;
41 use Koha::Item::Transfer::Limits;
42 use Koha::Item::Transfers;
47 use Koha::Result::Boolean;
48 use Koha::SearchEngine::Indexer;
49 use Koha::StockRotationItem;
50 use Koha::StockRotationRotas;
51 use Koha::TrackedLinks;
53 use base qw(Koha::Object);
57 Koha::Item - Koha Item object class
69 $params can take an optional 'skip_record_index' parameter.
70 If set, the reindexation process will not happen (index_records not called)
72 NOTE: This is a temporary fix to answer a performance issue when lot of items
73 are added (or modified) at the same time.
74 The correct way to fix this is to make the ES reindexation process async.
75 You should not turn it on if you do not understand what it is doing exactly.
81 my $params = @_ ? shift : {};
83 my $log_action = $params->{log_action} // 1;
85 # We do not want to oblige callers to pass this value
86 # Dev conveniences vs performance?
87 unless ( $self->biblioitemnumber ) {
88 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
91 # See related changes from C4::Items::AddItem
92 unless ( $self->itype ) {
93 $self->itype($self->biblio->biblioitem->itemtype);
96 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
98 my $today = dt_from_string;
99 my $action = 'create';
101 unless ( $self->in_storage ) { #AddItem
103 unless ( $self->permanent_location ) {
104 $self->permanent_location($self->location);
107 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
108 unless ( $self->location || !$default_location ) {
109 $self->permanent_location( $self->location || $default_location )
110 unless $self->permanent_location;
111 $self->location($default_location);
114 unless ( $self->replacementpricedate ) {
115 $self->replacementpricedate($today);
117 unless ( $self->datelastseen ) {
118 $self->datelastseen($today);
121 unless ( $self->dateaccessioned ) {
122 $self->dateaccessioned($today);
125 if ( $self->itemcallnumber
126 or $self->cn_source )
128 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
129 $self->cn_sort($cn_sort);
136 my %updated_columns = $self->_result->get_dirty_columns;
137 return $self->SUPER::store unless %updated_columns;
139 # Retrieve the item for comparison if we need to
141 exists $updated_columns{itemlost}
142 or exists $updated_columns{withdrawn}
143 or exists $updated_columns{damaged}
144 ) ? $self->get_from_storage : undef;
146 # Update *_on fields if needed
147 # FIXME: Why not for AddItem as well?
148 my @fields = qw( itemlost withdrawn damaged );
149 for my $field (@fields) {
151 # If the field is defined but empty or 0, we are
152 # removing/unsetting and thus need to clear out
154 if ( exists $updated_columns{$field}
155 && defined( $self->$field )
158 my $field_on = "${field}_on";
159 $self->$field_on(undef);
161 # If the field has changed otherwise, we much update
163 elsif (exists $updated_columns{$field}
164 && $updated_columns{$field}
165 && !$pre_mod_item->$field )
167 my $field_on = "${field}_on";
169 DateTime::Format::MySQL->format_datetime(
176 if ( exists $updated_columns{itemcallnumber}
177 or exists $updated_columns{cn_source} )
179 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
180 $self->cn_sort($cn_sort);
184 if ( exists $updated_columns{location}
185 and $self->location ne 'CART'
186 and $self->location ne 'PROC'
187 and not exists $updated_columns{permanent_location} )
189 $self->permanent_location( $self->location );
192 # If item was lost and has now been found,
193 # reverse any list item charges if necessary.
194 if ( exists $updated_columns{itemlost}
195 and $updated_columns{itemlost} <= 0
196 and $pre_mod_item->itemlost > 0 )
198 $self->_set_found_trigger($pre_mod_item);
203 my $result = $self->SUPER::store;
204 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
206 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
207 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
209 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
210 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
211 unless $params->{skip_record_index};
212 $self->get_from_storage->_after_item_action_hooks({ action => $action });
214 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
216 biblio_ids => [ $self->biblionumber ]
218 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
229 my $params = @_ ? shift : {};
231 # FIXME check the item has no current issues
232 # i.e. raise the appropriate exception
234 # Get the item group so we can delete it later if it has no items left
235 my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
237 my $result = $self->SUPER::delete;
239 # Delete the item gorup if it has no items left
240 $item_group->delete if ( $item_group && $item_group->items->count == 0 );
242 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
243 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
244 unless $params->{skip_record_index};
246 $self->_after_item_action_hooks({ action => 'delete' });
248 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
249 if C4::Context->preference("CataloguingLog");
251 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
253 biblio_ids => [ $self->biblionumber ]
255 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
266 my $params = @_ ? shift : {};
268 my $safe_to_delete = $self->safe_to_delete;
269 return $safe_to_delete unless $safe_to_delete;
271 $self->move_to_deleted;
273 return $self->delete($params);
276 =head3 safe_to_delete
278 returns 1 if the item is safe to delete,
280 "book_on_loan" if the item is checked out,
282 "not_same_branch" if the item is blocked by independent branches,
284 "book_reserved" if the there are holds aganst the item, or
286 "linked_analytics" if the item has linked analytic records.
288 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
297 $error = "book_on_loan" if $self->checkout;
299 $error = "not_same_branch"
300 if defined C4::Context->userenv
301 and !C4::Context->IsSuperLibrarian()
302 and C4::Context->preference("IndependentBranches")
303 and ( C4::Context->userenv->{branch} ne $self->homebranch );
305 # check it doesn't have a waiting reserve
306 $error = "book_reserved"
307 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
309 $error = "linked_analytics"
310 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
312 $error = "last_item_for_hold"
313 if $self->biblio->items->count == 1
314 && $self->biblio->holds->search(
321 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
324 return Koha::Result::Boolean->new(1);
327 =head3 move_to_deleted
329 my $is_moved = $item->move_to_deleted;
331 Move an item to the deleteditems table.
332 This can be done before deleting an item, to make sure the data are not completely deleted.
336 sub move_to_deleted {
338 my $item_infos = $self->unblessed;
339 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
340 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
344 =head3 effective_itemtype
346 Returns the itemtype for the item based on whether item level itemtypes are set or not.
350 sub effective_itemtype {
353 return $self->_result()->effective_itemtype();
363 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
365 return $self->{_home_branch};
368 =head3 holding_branch
375 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
377 return $self->{_holding_branch};
382 my $biblio = $item->biblio;
384 Return the bibliographic record of this item
390 my $biblio_rs = $self->_result->biblio;
391 return Koha::Biblio->_new_from_dbic( $biblio_rs );
396 my $biblioitem = $item->biblioitem;
398 Return the biblioitem record of this item
404 my $biblioitem_rs = $self->_result->biblioitem;
405 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
410 my $checkout = $item->checkout;
412 Return the checkout for this item
418 my $checkout_rs = $self->_result->issue;
419 return unless $checkout_rs;
420 return Koha::Checkout->_new_from_dbic( $checkout_rs );
425 my $item_group = $item->item_group;
427 Return the item group for this item
434 my $item_group_item = $self->_result->item_group_item;
435 return unless $item_group_item;
437 my $item_group_rs = $item_group_item->item_group;
438 return unless $item_group_rs;
440 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
446 my $return_claims = $item->return_claims;
448 Return any return_claims associated with this item
453 my ( $self, $params, $attrs ) = @_;
454 my $claims_rs = $self->_result->return_claims->search($params, $attrs);
455 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
460 my $return_claim = $item->return_claim;
462 Returns the most recent unresolved return_claims associated with this item
469 $self->_result->return_claims->search( { resolution => undef },
470 { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
471 return unless $claims_rs;
472 return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
477 my $holds = $item->holds();
478 my $holds = $item->holds($params);
479 my $holds = $item->holds({ found => 'W'});
481 Return holds attached to an item, optionally accept a hashref of params to pass to search
486 my ( $self,$params ) = @_;
487 my $holds_rs = $self->_result->reserves->search($params);
488 return Koha::Holds->_new_from_dbic( $holds_rs );
491 =head3 request_transfer
493 my $transfer = $item->request_transfer(
497 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
501 Add a transfer request for this item to the given branch for the given reason.
503 An exception will be thrown if the BranchTransferLimits would prevent the requested
504 transfer, unless 'ignore_limits' is passed to override the limits.
506 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
507 The caller should catch such cases and retry the transfer request as appropriate passing
508 an appropriate override.
511 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
512 * replace - Used to replace the existing transfer request with your own.
516 sub request_transfer {
517 my ( $self, $params ) = @_;
519 # check for mandatory params
520 my @mandatory = ( 'to', 'reason' );
521 for my $param (@mandatory) {
522 unless ( defined( $params->{$param} ) ) {
523 Koha::Exceptions::MissingParameter->throw(
524 error => "The $param parameter is mandatory" );
528 Koha::Exceptions::Item::Transfer::Limit->throw()
529 unless ( $params->{ignore_limits}
530 || $self->can_be_transferred( { to => $params->{to} } ) );
532 my $request = $self->get_transfer;
533 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
534 if ( $request && !$params->{enqueue} && !$params->{replace} );
536 $request->cancel( { reason => $params->{reason}, force => 1 } )
537 if ( defined($request) && $params->{replace} );
539 my $transfer = Koha::Item::Transfer->new(
541 itemnumber => $self->itemnumber,
542 daterequested => dt_from_string,
543 frombranch => $self->holdingbranch,
544 tobranch => $params->{to}->branchcode,
545 reason => $params->{reason},
546 comments => $params->{comment}
555 my $transfer = $item->get_transfer;
557 Return the active transfer request or undef
559 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
560 whereby the most recently sent, but not received, transfer will be returned
561 if it exists, otherwise the oldest unsatisfied transfer will be returned.
563 This allows for transfers to queue, which is the case for stock rotation and
564 rotating collections where a manual transfer may need to take precedence but
565 we still expect the item to end up at a final location eventually.
571 my $transfer_rs = $self->_result->branchtransfers->search(
573 datearrived => undef,
574 datecancelled => undef
578 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
582 return unless $transfer_rs;
583 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
588 my $transfer = $item->get_transfers;
590 Return the list of outstanding transfers (i.e requested but not yet cancelled
593 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
594 whereby the most recently sent, but not received, transfer will be returned
595 first if it exists, otherwise requests are in oldest to newest request order.
597 This allows for transfers to queue, which is the case for stock rotation and
598 rotating collections where a manual transfer may need to take precedence but
599 we still expect the item to end up at a final location eventually.
605 my $transfer_rs = $self->_result->branchtransfers->search(
607 datearrived => undef,
608 datecancelled => undef
612 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
615 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
618 =head3 last_returned_by
620 Gets and sets the last borrower to return an item.
622 Accepts and returns Koha::Patron objects
624 $item->last_returned_by( $borrowernumber );
626 $last_returned_by = $item->last_returned_by();
630 sub last_returned_by {
631 my ( $self, $borrower ) = @_;
633 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
636 return $items_last_returned_by_rs->update_or_create(
637 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
640 unless ( $self->{_last_returned_by} ) {
641 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
643 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
647 return $self->{_last_returned_by};
651 =head3 can_article_request
653 my $bool = $item->can_article_request( $borrower )
655 Returns true if item can be specifically requested
657 $borrower must be a Koha::Patron object
661 sub can_article_request {
662 my ( $self, $borrower ) = @_;
664 my $rule = $self->article_request_type($borrower);
666 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
670 =head3 hidden_in_opac
672 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
674 Returns true if item fields match the hidding criteria defined in $rules.
675 Returns false otherwise.
677 Takes HASHref that can have the following parameters:
679 $rules : { <field> => [ value_1, ... ], ... }
681 Note: $rules inherits its structure from the parsed YAML from reading
682 the I<OpacHiddenItems> system preference.
687 my ( $self, $params ) = @_;
689 my $rules = $params->{rules} // {};
692 if C4::Context->preference('hidelostitems') and
695 my $hidden_in_opac = 0;
697 foreach my $field ( keys %{$rules} ) {
699 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
705 return $hidden_in_opac;
708 =head3 can_be_transferred
710 $item->can_be_transferred({ to => $to_library, from => $from_library })
711 Checks if an item can be transferred to given library.
713 This feature is controlled by two system preferences:
714 UseBranchTransferLimits to enable / disable the feature
715 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
716 for setting the limitations
718 Takes HASHref that can have the following parameters:
719 MANDATORY PARAMETERS:
722 $from : Koha::Library # if not given, item holdingbranch
723 # will be used instead
725 Returns 1 if item can be transferred to $to_library, otherwise 0.
727 To find out whether at least one item of a Koha::Biblio can be transferred, please
728 see Koha::Biblio->can_be_transferred() instead of using this method for
729 multiple items of the same biblio.
733 sub can_be_transferred {
734 my ($self, $params) = @_;
736 my $to = $params->{to};
737 my $from = $params->{from};
739 $to = $to->branchcode;
740 $from = defined $from ? $from->branchcode : $self->holdingbranch;
742 return 1 if $from eq $to; # Transfer to current branch is allowed
743 return 1 unless C4::Context->preference('UseBranchTransferLimits');
745 my $limittype = C4::Context->preference('BranchTransferLimitsType');
746 return Koha::Item::Transfer::Limits->search({
749 $limittype => $limittype eq 'itemtype'
750 ? $self->effective_itemtype : $self->ccode
755 =head3 pickup_locations
757 $pickup_locations = $item->pickup_locations( {patron => $patron } )
759 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)
760 and if item can be transferred to each pickup location.
764 sub pickup_locations {
765 my ($self, $params) = @_;
767 my $patron = $params->{patron};
769 my $circ_control_branch =
770 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
772 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
774 if(defined $patron) {
775 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
776 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
779 my $pickup_libraries = Koha::Libraries->search();
780 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
781 $pickup_libraries = $self->home_branch->get_hold_libraries;
782 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
783 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
784 $pickup_libraries = $plib->get_hold_libraries;
785 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
786 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
787 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
788 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
791 return $pickup_libraries->search(
796 order_by => ['branchname']
798 ) unless C4::Context->preference('UseBranchTransferLimits');
800 my $limittype = C4::Context->preference('BranchTransferLimitsType');
801 my ($ccode, $itype) = (undef, undef);
802 if( $limittype eq 'ccode' ){
803 $ccode = $self->ccode;
805 $itype = $self->itype;
807 my $limits = Koha::Item::Transfer::Limits->search(
809 fromBranch => $self->holdingbranch,
813 { columns => ['toBranch'] }
816 return $pickup_libraries->search(
818 pickup_location => 1,
820 '-not_in' => $limits->_resultset->as_query
824 order_by => ['branchname']
829 =head3 article_request_type
831 my $type = $item->article_request_type( $borrower )
833 returns 'yes', 'no', 'bib_only', or 'item_only'
835 $borrower must be a Koha::Patron object
839 sub article_request_type {
840 my ( $self, $borrower ) = @_;
842 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
844 $branch_control eq 'homebranch' ? $self->homebranch
845 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
847 my $borrowertype = $borrower->categorycode;
848 my $itemtype = $self->effective_itemtype();
849 my $rule = Koha::CirculationRules->get_effective_rule(
851 rule_name => 'article_requests',
852 categorycode => $borrowertype,
853 itemtype => $itemtype,
854 branchcode => $branchcode
858 return q{} unless $rule;
859 return $rule->rule_value || q{}
868 my $attributes = { order_by => 'priority' };
869 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
871 itemnumber => $self->itemnumber,
874 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
875 waitingdate => { '!=' => undef },
878 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
879 return Koha::Holds->_new_from_dbic($hold_rs);
882 =head3 stockrotationitem
884 my $sritem = Koha::Item->stockrotationitem;
886 Returns the stock rotation item associated with the current item.
890 sub stockrotationitem {
892 my $rs = $self->_result->stockrotationitem;
894 return Koha::StockRotationItem->_new_from_dbic( $rs );
899 my $item = $item->add_to_rota($rota_id);
901 Add this item to the rota identified by $ROTA_ID, which means associating it
902 with the first stage of that rota. Should this item already be associated
903 with a rota, then we will move it to the new rota.
908 my ( $self, $rota_id ) = @_;
909 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
913 =head3 has_pending_hold
915 my $is_pending_hold = $item->has_pending_hold();
917 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
921 sub has_pending_hold {
923 my $pending_hold = $self->_result->tmp_holdsqueues;
924 return $pending_hold->count ? 1: 0;
929 my $field = $item->as_marc_field;
931 This method returns a MARC::Field object representing the Koha::Item object
932 with the current mappings configuration.
939 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
941 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
945 my $item_field = $tagslib->{$itemtag};
947 my $more_subfields = $self->additional_attributes->to_hashref;
948 foreach my $subfield (
950 $a->{display_order} <=> $b->{display_order}
951 || $a->{subfield} cmp $b->{subfield}
952 } grep { ref($_) && %$_ } values %$item_field
955 my $kohafield = $subfield->{kohafield};
956 my $tagsubfield = $subfield->{tagsubfield};
958 if ( defined $kohafield ) {
959 next if $kohafield !~ m{^items\.}; # That would be weird!
960 ( my $attribute = $kohafield ) =~ s|^items\.||;
961 $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
962 if defined $self->$attribute and $self->$attribute ne '';
964 $value = $more_subfields->{$tagsubfield}
967 next unless defined $value
970 if ( $subfield->{repeatable} ) {
971 my @values = split '\|', $value;
972 push @subfields, ( $tagsubfield => $_ ) for @values;
975 push @subfields, ( $tagsubfield => $value );
980 return unless @subfields;
982 return MARC::Field->new(
983 "$itemtag", ' ', ' ', @subfields
987 =head3 renewal_branchcode
989 Returns the branchcode to be recorded in statistics renewal of the item
993 sub renewal_branchcode {
995 my ($self, $params ) = @_;
997 my $interface = C4::Context->interface;
999 if ( $interface eq 'opac' ){
1000 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
1001 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
1002 $branchcode = 'OPACRenew';
1004 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1005 $branchcode = $self->homebranch;
1007 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1008 $branchcode = $self->checkout->patron->branchcode;
1010 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1011 $branchcode = $self->checkout->branchcode;
1017 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1018 ? C4::Context->userenv->{branch} : $params->{branch};
1025 Return the cover images associated with this item.
1032 my $cover_image_rs = $self->_result->cover_images;
1033 return unless $cover_image_rs;
1034 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1037 =head3 columns_to_str
1039 my $values = $items->columns_to_str;
1041 Return a hashref with the string representation of the different attribute of the item.
1043 This is meant to be used for display purpose only.
1047 sub columns_to_str {
1050 my $frameworkcode = $self->biblio->frameworkcode;
1051 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1052 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1054 my $columns_info = $self->_result->result_source->columns_info;
1056 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1058 for my $column ( keys %$columns_info ) {
1060 next if $column eq 'more_subfields_xml';
1062 my $value = $self->$column;
1063 # 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
1065 if ( not defined $value or $value eq "" ) {
1066 $values->{$column} = $value;
1071 exists $mss->{"items.$column"}
1072 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1075 $values->{$column} =
1077 ? $subfield->{authorised_value}
1078 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1079 $subfield->{tagsubfield}, $value, '', $tagslib )
1085 $self->more_subfields_xml
1086 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1091 my ( $field ) = $marc_more->fields;
1092 for my $sf ( $field->subfields ) {
1093 my $subfield_code = $sf->[0];
1094 my $value = $sf->[1];
1095 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1096 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1098 $subfield->{authorised_value}
1099 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1100 $subfield->{tagsubfield}, $value, '', $tagslib )
1103 push @{$more_values->{$subfield_code}}, $value;
1106 while ( my ( $k, $v ) = each %$more_values ) {
1107 $values->{$k} = join ' | ', @$v;
1114 =head3 additional_attributes
1116 my $attributes = $item->additional_attributes;
1117 $attributes->{k} = 'new k';
1118 $item->update({ more_subfields => $attributes->to_marcxml });
1120 Returns a Koha::Item::Attributes object that represents the non-mapped
1121 attributes for this item.
1125 sub additional_attributes {
1128 return Koha::Item::Attributes->new_from_marcxml(
1129 $self->more_subfields_xml,
1133 =head3 _set_found_trigger
1135 $self->_set_found_trigger
1137 Finds the most recent lost item charge for this item and refunds the patron
1138 appropriately, taking into account any payments or writeoffs already applied
1141 Internal function, not exported, called only by Koha::Item->store.
1145 sub _set_found_trigger {
1146 my ( $self, $pre_mod_item ) = @_;
1148 # Reverse any lost item charges if necessary.
1149 my $no_refund_after_days =
1150 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1151 if ($no_refund_after_days) {
1152 my $today = dt_from_string();
1153 my $lost_age_in_days =
1154 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1157 return $self unless $lost_age_in_days < $no_refund_after_days;
1160 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1163 return_branch => C4::Context->userenv
1164 ? C4::Context->userenv->{'branch'}
1169 if ( $lostreturn_policy ) {
1171 # refund charge made for lost book
1172 my $lost_charge = Koha::Account::Lines->search(
1174 itemnumber => $self->itemnumber,
1175 debit_type_code => 'LOST',
1176 status => [ undef, { '<>' => 'FOUND' } ]
1179 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1184 if ( $lost_charge ) {
1186 my $patron = $lost_charge->patron;
1189 my $account = $patron->account;
1190 my $total_to_refund = 0;
1193 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1195 # some amount has been cancelled. collect the offsets that are not writeoffs
1196 # this works because the only way to subtract from this kind of a debt is
1197 # using the UI buttons 'Pay' and 'Write off'
1198 my $credit_offsets = $lost_charge->debit_offsets(
1200 'credit_id' => { '!=' => undef },
1201 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1203 { join => 'credit' }
1206 $total_to_refund = ( $credit_offsets->count > 0 )
1207 ? $credit_offsets->total * -1 # credits are negative on the DB
1211 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1214 if ( $credit_total > 0 ) {
1216 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1217 $credit = $account->add_credit(
1219 amount => $credit_total,
1220 description => 'Item found ' . $self->itemnumber,
1221 type => 'LOST_FOUND',
1222 interface => C4::Context->interface,
1223 library_id => $branchcode,
1224 item_id => $self->itemnumber,
1225 issue_id => $lost_charge->issue_id
1229 $credit->apply( { debits => [$lost_charge] } );
1233 message => 'lost_refunded',
1234 payload => { credit_id => $credit->id }
1239 # Update the account status
1240 $lost_charge->status('FOUND');
1241 $lost_charge->store();
1243 # Reconcile balances if required
1244 if ( C4::Context->preference('AccountAutoReconcile') ) {
1245 $account->reconcile_balance;
1250 # restore fine for lost book
1251 if ( $lostreturn_policy eq 'restore' ) {
1252 my $lost_overdue = Koha::Account::Lines->search(
1254 itemnumber => $self->itemnumber,
1255 debit_type_code => 'OVERDUE',
1259 order_by => { '-desc' => 'date' },
1264 if ( $lost_overdue ) {
1266 my $patron = $lost_overdue->patron;
1268 my $account = $patron->account;
1270 # Update status of fine
1271 $lost_overdue->status('FOUND')->store();
1273 # Find related forgive credit
1274 my $refund = $lost_overdue->credits(
1276 credit_type_code => 'FORGIVEN',
1277 itemnumber => $self->itemnumber,
1278 status => [ { '!=' => 'VOID' }, undef ]
1280 { order_by => { '-desc' => 'date' }, rows => 1 }
1284 # Revert the forgive credit
1285 $refund->void({ interface => 'trigger' });
1289 message => 'lost_restored',
1290 payload => { refund_id => $refund->id }
1295 # Reconcile balances if required
1296 if ( C4::Context->preference('AccountAutoReconcile') ) {
1297 $account->reconcile_balance;
1301 } elsif ( $lostreturn_policy eq 'charge' ) {
1305 message => 'lost_charge',
1314 =head3 public_read_list
1316 This method returns the list of publicly readable database fields for both API and UI output purposes
1320 sub public_read_list {
1322 'itemnumber', 'biblionumber', 'homebranch',
1323 'holdingbranch', 'location', 'collectioncode',
1324 'itemcallnumber', 'copynumber', 'enumchron',
1325 'barcode', 'dateaccessioned', 'itemnotes',
1326 'onloan', 'uri', 'itype',
1327 'notforloan', 'damaged', 'itemlost',
1328 'withdrawn', 'restricted'
1334 Overloaded to_api method to ensure item-level itypes is adhered to.
1339 my ($self, $params) = @_;
1341 my $response = $self->SUPER::to_api($params);
1344 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1346 return { %$response, %$overrides };
1349 =head3 to_api_mapping
1351 This method returns the mapping for representing a Koha::Item object
1356 sub to_api_mapping {
1358 itemnumber => 'item_id',
1359 biblionumber => 'biblio_id',
1360 biblioitemnumber => undef,
1361 barcode => 'external_id',
1362 dateaccessioned => 'acquisition_date',
1363 booksellerid => 'acquisition_source',
1364 homebranch => 'home_library_id',
1365 price => 'purchase_price',
1366 replacementprice => 'replacement_price',
1367 replacementpricedate => 'replacement_price_date',
1368 datelastborrowed => 'last_checkout_date',
1369 datelastseen => 'last_seen_date',
1371 notforloan => 'not_for_loan_status',
1372 damaged => 'damaged_status',
1373 damaged_on => 'damaged_date',
1374 itemlost => 'lost_status',
1375 itemlost_on => 'lost_date',
1376 withdrawn => 'withdrawn',
1377 withdrawn_on => 'withdrawn_date',
1378 itemcallnumber => 'callnumber',
1379 coded_location_qualifier => 'coded_location_qualifier',
1380 issues => 'checkouts_count',
1381 renewals => 'renewals_count',
1382 reserves => 'holds_count',
1383 restricted => 'restricted_status',
1384 itemnotes => 'public_notes',
1385 itemnotes_nonpublic => 'internal_notes',
1386 holdingbranch => 'holding_library_id',
1387 timestamp => 'timestamp',
1388 location => 'location',
1389 permanent_location => 'permanent_location',
1390 onloan => 'checked_out_date',
1391 cn_source => 'call_number_source',
1392 cn_sort => 'call_number_sort',
1393 ccode => 'collection_code',
1394 materials => 'materials_notes',
1396 itype => 'item_type_id',
1397 more_subfields_xml => 'extended_subfields',
1398 enumchron => 'serial_issue_number',
1399 copynumber => 'copy_number',
1400 stocknumber => 'inventory_number',
1401 new_status => 'new_status'
1407 my $itemtype = $item->itemtype;
1409 Returns Koha object for effective itemtype
1415 return Koha::ItemTypes->find( $self->effective_itemtype );
1420 my $orders = $item->orders();
1422 Returns a Koha::Acquisition::Orders object
1429 my $orders = $self->_result->item_orders;
1430 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1433 =head3 tracked_links
1435 my $tracked_links = $item->tracked_links();
1437 Returns a Koha::TrackedLinks object
1444 my $tracked_links = $self->_result->linktrackers;
1445 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1448 =head3 move_to_biblio
1450 $item->move_to_biblio($to_biblio[, $params]);
1452 Move the item to another biblio and update any references in other tables.
1454 The final optional parameter, C<$params>, is expected to contain the
1455 'skip_record_index' key, which is relayed down to Koha::Item->store.
1456 There it prevents calling index_records, which takes most of the
1457 time in batch adds/deletes. The caller must take care of calling
1458 index_records separately.
1461 skip_record_index => 1|0
1463 Returns undef if the move failed or the biblionumber of the destination record otherwise
1467 sub move_to_biblio {
1468 my ( $self, $to_biblio, $params ) = @_;
1472 return if $self->biblionumber == $to_biblio->biblionumber;
1474 my $from_biblionumber = $self->biblionumber;
1475 my $to_biblionumber = $to_biblio->biblionumber;
1477 # Own biblionumber and biblioitemnumber
1479 biblionumber => $to_biblionumber,
1480 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1481 })->store({ skip_record_index => $params->{skip_record_index} });
1483 unless ($params->{skip_record_index}) {
1484 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1485 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1488 # Acquisition orders
1489 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1492 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1494 # hold_fill_target (there's no Koha object available yet)
1495 my $hold_fill_target = $self->_result->hold_fill_target;
1496 if ($hold_fill_target) {
1497 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1500 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1501 # and can't even fake one since the significant columns are nullable.
1502 my $storage = $self->_result->result_source->storage;
1505 my ($storage, $dbh, @cols) = @_;
1507 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1512 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1514 return $to_biblionumber;
1519 my $bundle_items = $item->bundle_items;
1521 Returns the items associated with this bundle
1528 if ( !$self->{_bundle_items_cached} ) {
1529 my $bundle_items = Koha::Items->search(
1530 { 'item_bundles_item.host' => $self->itemnumber },
1531 { join => 'item_bundles_item' } );
1532 $self->{_bundle_items} = $bundle_items;
1533 $self->{_bundle_items_cached} = 1;
1536 return $self->{_bundle_items};
1541 my $is_bundle = $item->is_bundle;
1543 Returns whether the item is a bundle or not
1549 return $self->bundle_items->count ? 1 : 0;
1554 my $bundle = $item->bundle_host;
1556 Returns the bundle item this item is attached to
1563 my $bundle_items_rs = $self->_result->item_bundles_item;
1564 return unless $bundle_items_rs;
1565 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1570 my $in_bundle = $item->in_bundle;
1572 Returns whether this item is currently in a bundle
1578 return $self->bundle_host ? 1 : 0;
1581 =head3 add_to_bundle
1583 my $link = $item->add_to_bundle($bundle_item);
1585 Adds the bundle_item passed to this item
1590 my ( $self, $bundle_item ) = @_;
1592 my $schema = Koha::Database->new->schema;
1594 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1599 $self->_result->add_to_item_bundles_hosts(
1600 { item => $bundle_item->itemnumber } );
1602 $bundle_item->notforloan($BundleNotLoanValue)->store();
1608 # FIXME: See if we can move the below copy/paste from Koha::Object::store into it's own class and catch at a lower level in the Schema instantiation, take inspiration from DBIx::Error
1609 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1611 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1613 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1614 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1615 Koha::Exceptions::Object::FKConstraint->throw(
1616 error => 'Broken FK constraint',
1617 broken_fk => $+{column}
1622 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1624 Koha::Exceptions::Object::DuplicateID->throw(
1625 error => 'Duplicate ID',
1626 duplicate_id => $+{key}
1629 elsif ( $_->{msg} =~
1630 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1632 { # The optional \W in the regex might be a quote or backtick
1633 my $type = $+{type};
1634 my $value = $+{value};
1635 my $property = $+{property};
1636 $property =~ s/['`]//g;
1637 Koha::Exceptions::Object::BadValue->throw(
1640 property => $property =~ /(\w+\.\w+)$/
1643 , # results in table.column without quotes or backtics
1647 # Catch-all for foreign key breakages. It will help find other use cases
1656 =head3 remove_from_bundle
1658 Remove this item from any bundle it may have been attached to.
1662 sub remove_from_bundle {
1665 my $bundle_item_rs = $self->_result->item_bundles_item;
1666 if ( $bundle_item_rs ) {
1667 $bundle_item_rs->delete;
1668 $self->notforloan(0)->store();
1674 =head2 Internal methods
1676 =head3 _after_item_action_hooks
1678 Helper method that takes care of calling all plugin hooks
1682 sub _after_item_action_hooks {
1683 my ( $self, $params ) = @_;
1685 my $action = $params->{action};
1687 Koha::Plugins->call(
1688 'after_item_action',
1692 item_id => $self->itemnumber,
1699 my $recall = $item->recall;
1701 Return the relevant recall for this item
1707 my @recalls = Koha::Recalls->search(
1709 biblio_id => $self->biblionumber,
1712 { order_by => { -asc => 'created_date' } }
1714 foreach my $recall (@recalls) {
1715 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1719 # no item-level recall to return, so return earliest biblio-level
1720 # FIXME: eventually this will be based on priority
1724 =head3 can_be_recalled
1726 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1728 Does item-level checks and returns if items can be recalled by this borrower
1732 sub can_be_recalled {
1733 my ( $self, $params ) = @_;
1735 return 0 if !( C4::Context->preference('UseRecalls') );
1737 # check if this item is not for loan, withdrawn or lost
1738 return 0 if ( $self->notforloan != 0 );
1739 return 0 if ( $self->itemlost != 0 );
1740 return 0 if ( $self->withdrawn != 0 );
1742 # check if this item is not checked out - if not checked out, can't be recalled
1743 return 0 if ( !defined( $self->checkout ) );
1745 my $patron = $params->{patron};
1747 my $branchcode = C4::Context->userenv->{'branch'};
1749 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1752 # Check the circulation rule for each relevant itemtype for this item
1753 my $rule = Koha::CirculationRules->get_effective_rules({
1754 branchcode => $branchcode,
1755 categorycode => $patron ? $patron->categorycode : undef,
1756 itemtype => $self->effective_itemtype,
1759 'recalls_per_record',
1764 # check recalls allowed has been set and is not zero
1765 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1768 # check borrower has not reached open recalls allowed limit
1769 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1771 # check borrower has not reach open recalls allowed per record limit
1772 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1774 # check if this patron has already recalled this item
1775 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1777 # check if this patron has already checked out this item
1778 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1780 # check if this patron has already reserved this item
1781 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1784 # check item availability
1785 # items are unavailable for recall if they are lost, withdrawn or notforloan
1786 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1788 # if there are no available items at all, no recall can be placed
1789 return 0 if ( scalar @items == 0 );
1791 my $checked_out_count = 0;
1793 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1796 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1797 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1799 # can't recall if no items have been checked out
1800 return 0 if ( $checked_out_count == 0 );
1806 =head3 can_be_waiting_recall
1808 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1810 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1811 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1815 sub can_be_waiting_recall {
1818 return 0 if !( C4::Context->preference('UseRecalls') );
1820 # check if this item is not for loan, withdrawn or lost
1821 return 0 if ( $self->notforloan != 0 );
1822 return 0 if ( $self->itemlost != 0 );
1823 return 0 if ( $self->withdrawn != 0 );
1825 my $branchcode = $self->holdingbranch;
1826 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1827 $branchcode = C4::Context->userenv->{'branch'};
1829 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1832 # Check the circulation rule for each relevant itemtype for this item
1833 my $rule = Koha::CirculationRules->get_effective_rules({
1834 branchcode => $branchcode,
1835 categorycode => undef,
1836 itemtype => $self->effective_itemtype,
1842 # check recalls allowed has been set and is not zero
1843 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1849 =head3 check_recalls
1851 my $recall = $item->check_recalls;
1853 Get the most relevant recall for this item.
1860 my @recalls = Koha::Recalls->search(
1861 { biblio_id => $self->biblionumber,
1862 item_id => [ $self->itemnumber, undef ]
1864 { order_by => { -asc => 'created_date' } }
1865 )->filter_by_current->as_list;
1868 # iterate through relevant recalls to find the best one.
1869 # if we come across a waiting recall, use this one.
1870 # 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.
1871 foreach my $r ( @recalls ) {
1872 if ( $r->waiting ) {
1877 unless ( defined $recall ) {
1878 $recall = $recalls[0];
1884 =head3 is_notforloan
1886 my $is_notforloan = $item->is_notforloan;
1888 Determine whether or not this item is "notforloan" based on
1889 the item's notforloan status or its item type
1895 my $is_notforloan = 0;
1897 if ( $self->notforloan ){
1901 my $itemtype = $self->itemtype;
1903 if ( $itemtype->notforloan ){
1909 return $is_notforloan;
1922 Kyle M Hall <kyle@bywatersolutions.com>