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 $holds = $item->holds();
461 my $holds = $item->holds($params);
462 my $holds = $item->holds({ found => 'W'});
464 Return holds attached to an item, optionally accept a hashref of params to pass to search
469 my ( $self,$params ) = @_;
470 my $holds_rs = $self->_result->reserves->search($params);
471 return Koha::Holds->_new_from_dbic( $holds_rs );
474 =head3 request_transfer
476 my $transfer = $item->request_transfer(
480 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
484 Add a transfer request for this item to the given branch for the given reason.
486 An exception will be thrown if the BranchTransferLimits would prevent the requested
487 transfer, unless 'ignore_limits' is passed to override the limits.
489 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
490 The caller should catch such cases and retry the transfer request as appropriate passing
491 an appropriate override.
494 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
495 * replace - Used to replace the existing transfer request with your own.
499 sub request_transfer {
500 my ( $self, $params ) = @_;
502 # check for mandatory params
503 my @mandatory = ( 'to', 'reason' );
504 for my $param (@mandatory) {
505 unless ( defined( $params->{$param} ) ) {
506 Koha::Exceptions::MissingParameter->throw(
507 error => "The $param parameter is mandatory" );
511 Koha::Exceptions::Item::Transfer::Limit->throw()
512 unless ( $params->{ignore_limits}
513 || $self->can_be_transferred( { to => $params->{to} } ) );
515 my $request = $self->get_transfer;
516 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
517 if ( $request && !$params->{enqueue} && !$params->{replace} );
519 $request->cancel( { reason => $params->{reason}, force => 1 } )
520 if ( defined($request) && $params->{replace} );
522 my $transfer = Koha::Item::Transfer->new(
524 itemnumber => $self->itemnumber,
525 daterequested => dt_from_string,
526 frombranch => $self->holdingbranch,
527 tobranch => $params->{to}->branchcode,
528 reason => $params->{reason},
529 comments => $params->{comment}
538 my $transfer = $item->get_transfer;
540 Return the active transfer request or undef
542 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
543 whereby the most recently sent, but not received, transfer will be returned
544 if it exists, otherwise the oldest unsatisfied transfer will be returned.
546 This allows for transfers to queue, which is the case for stock rotation and
547 rotating collections where a manual transfer may need to take precedence but
548 we still expect the item to end up at a final location eventually.
554 my $transfer_rs = $self->_result->branchtransfers->search(
556 datearrived => undef,
557 datecancelled => undef
561 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
565 return unless $transfer_rs;
566 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
571 my $transfer = $item->get_transfers;
573 Return the list of outstanding transfers (i.e requested but not yet cancelled
576 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
577 whereby the most recently sent, but not received, transfer will be returned
578 first if it exists, otherwise requests are in oldest to newest request order.
580 This allows for transfers to queue, which is the case for stock rotation and
581 rotating collections where a manual transfer may need to take precedence but
582 we still expect the item to end up at a final location eventually.
588 my $transfer_rs = $self->_result->branchtransfers->search(
590 datearrived => undef,
591 datecancelled => undef
595 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
598 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
601 =head3 last_returned_by
603 Gets and sets the last borrower to return an item.
605 Accepts and returns Koha::Patron objects
607 $item->last_returned_by( $borrowernumber );
609 $last_returned_by = $item->last_returned_by();
613 sub last_returned_by {
614 my ( $self, $borrower ) = @_;
616 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
619 return $items_last_returned_by_rs->update_or_create(
620 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
623 unless ( $self->{_last_returned_by} ) {
624 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
626 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
630 return $self->{_last_returned_by};
634 =head3 can_article_request
636 my $bool = $item->can_article_request( $borrower )
638 Returns true if item can be specifically requested
640 $borrower must be a Koha::Patron object
644 sub can_article_request {
645 my ( $self, $borrower ) = @_;
647 my $rule = $self->article_request_type($borrower);
649 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
653 =head3 hidden_in_opac
655 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
657 Returns true if item fields match the hidding criteria defined in $rules.
658 Returns false otherwise.
660 Takes HASHref that can have the following parameters:
662 $rules : { <field> => [ value_1, ... ], ... }
664 Note: $rules inherits its structure from the parsed YAML from reading
665 the I<OpacHiddenItems> system preference.
670 my ( $self, $params ) = @_;
672 my $rules = $params->{rules} // {};
675 if C4::Context->preference('hidelostitems') and
678 my $hidden_in_opac = 0;
680 foreach my $field ( keys %{$rules} ) {
682 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
688 return $hidden_in_opac;
691 =head3 can_be_transferred
693 $item->can_be_transferred({ to => $to_library, from => $from_library })
694 Checks if an item can be transferred to given library.
696 This feature is controlled by two system preferences:
697 UseBranchTransferLimits to enable / disable the feature
698 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
699 for setting the limitations
701 Takes HASHref that can have the following parameters:
702 MANDATORY PARAMETERS:
705 $from : Koha::Library # if not given, item holdingbranch
706 # will be used instead
708 Returns 1 if item can be transferred to $to_library, otherwise 0.
710 To find out whether at least one item of a Koha::Biblio can be transferred, please
711 see Koha::Biblio->can_be_transferred() instead of using this method for
712 multiple items of the same biblio.
716 sub can_be_transferred {
717 my ($self, $params) = @_;
719 my $to = $params->{to};
720 my $from = $params->{from};
722 $to = $to->branchcode;
723 $from = defined $from ? $from->branchcode : $self->holdingbranch;
725 return 1 if $from eq $to; # Transfer to current branch is allowed
726 return 1 unless C4::Context->preference('UseBranchTransferLimits');
728 my $limittype = C4::Context->preference('BranchTransferLimitsType');
729 return Koha::Item::Transfer::Limits->search({
732 $limittype => $limittype eq 'itemtype'
733 ? $self->effective_itemtype : $self->ccode
738 =head3 pickup_locations
740 $pickup_locations = $item->pickup_locations( {patron => $patron } )
742 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)
743 and if item can be transferred to each pickup location.
747 sub pickup_locations {
748 my ($self, $params) = @_;
750 my $patron = $params->{patron};
752 my $circ_control_branch =
753 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
755 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
757 if(defined $patron) {
758 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
759 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
762 my $pickup_libraries = Koha::Libraries->search();
763 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
764 $pickup_libraries = $self->home_branch->get_hold_libraries;
765 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
766 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
767 $pickup_libraries = $plib->get_hold_libraries;
768 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
769 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
770 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
771 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
774 return $pickup_libraries->search(
779 order_by => ['branchname']
781 ) unless C4::Context->preference('UseBranchTransferLimits');
783 my $limittype = C4::Context->preference('BranchTransferLimitsType');
784 my ($ccode, $itype) = (undef, undef);
785 if( $limittype eq 'ccode' ){
786 $ccode = $self->ccode;
788 $itype = $self->itype;
790 my $limits = Koha::Item::Transfer::Limits->search(
792 fromBranch => $self->holdingbranch,
796 { columns => ['toBranch'] }
799 return $pickup_libraries->search(
801 pickup_location => 1,
803 '-not_in' => $limits->_resultset->as_query
807 order_by => ['branchname']
812 =head3 article_request_type
814 my $type = $item->article_request_type( $borrower )
816 returns 'yes', 'no', 'bib_only', or 'item_only'
818 $borrower must be a Koha::Patron object
822 sub article_request_type {
823 my ( $self, $borrower ) = @_;
825 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
827 $branch_control eq 'homebranch' ? $self->homebranch
828 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
830 my $borrowertype = $borrower->categorycode;
831 my $itemtype = $self->effective_itemtype();
832 my $rule = Koha::CirculationRules->get_effective_rule(
834 rule_name => 'article_requests',
835 categorycode => $borrowertype,
836 itemtype => $itemtype,
837 branchcode => $branchcode
841 return q{} unless $rule;
842 return $rule->rule_value || q{}
851 my $attributes = { order_by => 'priority' };
852 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
854 itemnumber => $self->itemnumber,
857 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
858 waitingdate => { '!=' => undef },
861 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
862 return Koha::Holds->_new_from_dbic($hold_rs);
865 =head3 stockrotationitem
867 my $sritem = Koha::Item->stockrotationitem;
869 Returns the stock rotation item associated with the current item.
873 sub stockrotationitem {
875 my $rs = $self->_result->stockrotationitem;
877 return Koha::StockRotationItem->_new_from_dbic( $rs );
882 my $item = $item->add_to_rota($rota_id);
884 Add this item to the rota identified by $ROTA_ID, which means associating it
885 with the first stage of that rota. Should this item already be associated
886 with a rota, then we will move it to the new rota.
891 my ( $self, $rota_id ) = @_;
892 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
896 =head3 has_pending_hold
898 my $is_pending_hold = $item->has_pending_hold();
900 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
904 sub has_pending_hold {
906 my $pending_hold = $self->_result->tmp_holdsqueues;
907 return $pending_hold->count ? 1: 0;
912 my $field = $item->as_marc_field;
914 This method returns a MARC::Field object representing the Koha::Item object
915 with the current mappings configuration.
922 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
924 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
928 my $item_field = $tagslib->{$itemtag};
930 my $more_subfields = $self->additional_attributes->to_hashref;
931 foreach my $subfield (
933 $a->{display_order} <=> $b->{display_order}
934 || $a->{subfield} cmp $b->{subfield}
935 } grep { ref($_) && %$_ } values %$item_field
938 my $kohafield = $subfield->{kohafield};
939 my $tagsubfield = $subfield->{tagsubfield};
941 if ( defined $kohafield ) {
942 next if $kohafield !~ m{^items\.}; # That would be weird!
943 ( my $attribute = $kohafield ) =~ s|^items\.||;
944 $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
945 if defined $self->$attribute and $self->$attribute ne '';
947 $value = $more_subfields->{$tagsubfield}
950 next unless defined $value
953 if ( $subfield->{repeatable} ) {
954 my @values = split '\|', $value;
955 push @subfields, ( $tagsubfield => $_ ) for @values;
958 push @subfields, ( $tagsubfield => $value );
963 return unless @subfields;
965 return MARC::Field->new(
966 "$itemtag", ' ', ' ', @subfields
970 =head3 renewal_branchcode
972 Returns the branchcode to be recorded in statistics renewal of the item
976 sub renewal_branchcode {
978 my ($self, $params ) = @_;
980 my $interface = C4::Context->interface;
982 if ( $interface eq 'opac' ){
983 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
984 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
985 $branchcode = 'OPACRenew';
987 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
988 $branchcode = $self->homebranch;
990 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
991 $branchcode = $self->checkout->patron->branchcode;
993 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
994 $branchcode = $self->checkout->branchcode;
1000 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1001 ? C4::Context->userenv->{branch} : $params->{branch};
1008 Return the cover images associated with this item.
1015 my $cover_image_rs = $self->_result->cover_images;
1016 return unless $cover_image_rs;
1017 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1020 =head3 columns_to_str
1022 my $values = $items->columns_to_str;
1024 Return a hashref with the string representation of the different attribute of the item.
1026 This is meant to be used for display purpose only.
1030 sub columns_to_str {
1033 my $frameworkcode = $self->biblio->frameworkcode;
1034 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1035 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1037 my $columns_info = $self->_result->result_source->columns_info;
1039 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1041 for my $column ( keys %$columns_info ) {
1043 next if $column eq 'more_subfields_xml';
1045 my $value = $self->$column;
1046 # 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
1048 if ( not defined $value or $value eq "" ) {
1049 $values->{$column} = $value;
1054 exists $mss->{"items.$column"}
1055 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1058 $values->{$column} =
1060 ? $subfield->{authorised_value}
1061 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1062 $subfield->{tagsubfield}, $value, '', $tagslib )
1068 $self->more_subfields_xml
1069 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1074 my ( $field ) = $marc_more->fields;
1075 for my $sf ( $field->subfields ) {
1076 my $subfield_code = $sf->[0];
1077 my $value = $sf->[1];
1078 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1079 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1081 $subfield->{authorised_value}
1082 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1083 $subfield->{tagsubfield}, $value, '', $tagslib )
1086 push @{$more_values->{$subfield_code}}, $value;
1089 while ( my ( $k, $v ) = each %$more_values ) {
1090 $values->{$k} = join ' | ', @$v;
1097 =head3 additional_attributes
1099 my $attributes = $item->additional_attributes;
1100 $attributes->{k} = 'new k';
1101 $item->update({ more_subfields => $attributes->to_marcxml });
1103 Returns a Koha::Item::Attributes object that represents the non-mapped
1104 attributes for this item.
1108 sub additional_attributes {
1111 return Koha::Item::Attributes->new_from_marcxml(
1112 $self->more_subfields_xml,
1116 =head3 _set_found_trigger
1118 $self->_set_found_trigger
1120 Finds the most recent lost item charge for this item and refunds the patron
1121 appropriately, taking into account any payments or writeoffs already applied
1124 Internal function, not exported, called only by Koha::Item->store.
1128 sub _set_found_trigger {
1129 my ( $self, $pre_mod_item ) = @_;
1131 # Reverse any lost item charges if necessary.
1132 my $no_refund_after_days =
1133 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1134 if ($no_refund_after_days) {
1135 my $today = dt_from_string();
1136 my $lost_age_in_days =
1137 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1140 return $self unless $lost_age_in_days < $no_refund_after_days;
1143 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1146 return_branch => C4::Context->userenv
1147 ? C4::Context->userenv->{'branch'}
1152 if ( $lostreturn_policy ) {
1154 # refund charge made for lost book
1155 my $lost_charge = Koha::Account::Lines->search(
1157 itemnumber => $self->itemnumber,
1158 debit_type_code => 'LOST',
1159 status => [ undef, { '<>' => 'FOUND' } ]
1162 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1167 if ( $lost_charge ) {
1169 my $patron = $lost_charge->patron;
1172 my $account = $patron->account;
1173 my $total_to_refund = 0;
1176 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1178 # some amount has been cancelled. collect the offsets that are not writeoffs
1179 # this works because the only way to subtract from this kind of a debt is
1180 # using the UI buttons 'Pay' and 'Write off'
1181 my $credit_offsets = $lost_charge->debit_offsets(
1183 'credit_id' => { '!=' => undef },
1184 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1186 { join => 'credit' }
1189 $total_to_refund = ( $credit_offsets->count > 0 )
1190 ? $credit_offsets->total * -1 # credits are negative on the DB
1194 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1197 if ( $credit_total > 0 ) {
1199 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1200 $credit = $account->add_credit(
1202 amount => $credit_total,
1203 description => 'Item found ' . $self->itemnumber,
1204 type => 'LOST_FOUND',
1205 interface => C4::Context->interface,
1206 library_id => $branchcode,
1207 item_id => $self->itemnumber,
1208 issue_id => $lost_charge->issue_id
1212 $credit->apply( { debits => [$lost_charge] } );
1216 message => 'lost_refunded',
1217 payload => { credit_id => $credit->id }
1222 # Update the account status
1223 $lost_charge->status('FOUND');
1224 $lost_charge->store();
1226 # Reconcile balances if required
1227 if ( C4::Context->preference('AccountAutoReconcile') ) {
1228 $account->reconcile_balance;
1233 # restore fine for lost book
1234 if ( $lostreturn_policy eq 'restore' ) {
1235 my $lost_overdue = Koha::Account::Lines->search(
1237 itemnumber => $self->itemnumber,
1238 debit_type_code => 'OVERDUE',
1242 order_by => { '-desc' => 'date' },
1247 if ( $lost_overdue ) {
1249 my $patron = $lost_overdue->patron;
1251 my $account = $patron->account;
1253 # Update status of fine
1254 $lost_overdue->status('FOUND')->store();
1256 # Find related forgive credit
1257 my $refund = $lost_overdue->credits(
1259 credit_type_code => 'FORGIVEN',
1260 itemnumber => $self->itemnumber,
1261 status => [ { '!=' => 'VOID' }, undef ]
1263 { order_by => { '-desc' => 'date' }, rows => 1 }
1267 # Revert the forgive credit
1268 $refund->void({ interface => 'trigger' });
1272 message => 'lost_restored',
1273 payload => { refund_id => $refund->id }
1278 # Reconcile balances if required
1279 if ( C4::Context->preference('AccountAutoReconcile') ) {
1280 $account->reconcile_balance;
1284 } elsif ( $lostreturn_policy eq 'charge' ) {
1288 message => 'lost_charge',
1297 =head3 public_read_list
1299 This method returns the list of publicly readable database fields for both API and UI output purposes
1303 sub public_read_list {
1305 'itemnumber', 'biblionumber', 'homebranch',
1306 'holdingbranch', 'location', 'collectioncode',
1307 'itemcallnumber', 'copynumber', 'enumchron',
1308 'barcode', 'dateaccessioned', 'itemnotes',
1309 'onloan', 'uri', 'itype',
1310 'notforloan', 'damaged', 'itemlost',
1311 'withdrawn', 'restricted'
1315 =head3 to_api_mapping
1317 This method returns the mapping for representing a Koha::Item object
1322 sub to_api_mapping {
1324 itemnumber => 'item_id',
1325 biblionumber => 'biblio_id',
1326 biblioitemnumber => undef,
1327 barcode => 'external_id',
1328 dateaccessioned => 'acquisition_date',
1329 booksellerid => 'acquisition_source',
1330 homebranch => 'home_library_id',
1331 price => 'purchase_price',
1332 replacementprice => 'replacement_price',
1333 replacementpricedate => 'replacement_price_date',
1334 datelastborrowed => 'last_checkout_date',
1335 datelastseen => 'last_seen_date',
1337 notforloan => 'not_for_loan_status',
1338 damaged => 'damaged_status',
1339 damaged_on => 'damaged_date',
1340 itemlost => 'lost_status',
1341 itemlost_on => 'lost_date',
1342 withdrawn => 'withdrawn',
1343 withdrawn_on => 'withdrawn_date',
1344 itemcallnumber => 'callnumber',
1345 coded_location_qualifier => 'coded_location_qualifier',
1346 issues => 'checkouts_count',
1347 renewals => 'renewals_count',
1348 reserves => 'holds_count',
1349 restricted => 'restricted_status',
1350 itemnotes => 'public_notes',
1351 itemnotes_nonpublic => 'internal_notes',
1352 holdingbranch => 'holding_library_id',
1353 timestamp => 'timestamp',
1354 location => 'location',
1355 permanent_location => 'permanent_location',
1356 onloan => 'checked_out_date',
1357 cn_source => 'call_number_source',
1358 cn_sort => 'call_number_sort',
1359 ccode => 'collection_code',
1360 materials => 'materials_notes',
1362 itype => 'item_type_id',
1363 more_subfields_xml => 'extended_subfields',
1364 enumchron => 'serial_issue_number',
1365 copynumber => 'copy_number',
1366 stocknumber => 'inventory_number',
1367 new_status => 'new_status'
1373 my $itemtype = $item->itemtype;
1375 Returns Koha object for effective itemtype
1381 return Koha::ItemTypes->find( $self->effective_itemtype );
1386 my $orders = $item->orders();
1388 Returns a Koha::Acquisition::Orders object
1395 my $orders = $self->_result->item_orders;
1396 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1399 =head3 tracked_links
1401 my $tracked_links = $item->tracked_links();
1403 Returns a Koha::TrackedLinks object
1410 my $tracked_links = $self->_result->linktrackers;
1411 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1414 =head3 move_to_biblio
1416 $item->move_to_biblio($to_biblio[, $params]);
1418 Move the item to another biblio and update any references in other tables.
1420 The final optional parameter, C<$params>, is expected to contain the
1421 'skip_record_index' key, which is relayed down to Koha::Item->store.
1422 There it prevents calling index_records, which takes most of the
1423 time in batch adds/deletes. The caller must take care of calling
1424 index_records separately.
1427 skip_record_index => 1|0
1429 Returns undef if the move failed or the biblionumber of the destination record otherwise
1433 sub move_to_biblio {
1434 my ( $self, $to_biblio, $params ) = @_;
1438 return if $self->biblionumber == $to_biblio->biblionumber;
1440 my $from_biblionumber = $self->biblionumber;
1441 my $to_biblionumber = $to_biblio->biblionumber;
1443 # Own biblionumber and biblioitemnumber
1445 biblionumber => $to_biblionumber,
1446 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1447 })->store({ skip_record_index => $params->{skip_record_index} });
1449 unless ($params->{skip_record_index}) {
1450 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1451 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1454 # Acquisition orders
1455 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1458 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1460 # hold_fill_target (there's no Koha object available yet)
1461 my $hold_fill_target = $self->_result->hold_fill_target;
1462 if ($hold_fill_target) {
1463 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1466 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1467 # and can't even fake one since the significant columns are nullable.
1468 my $storage = $self->_result->result_source->storage;
1471 my ($storage, $dbh, @cols) = @_;
1473 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1478 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1480 return $to_biblionumber;
1485 my $bundle_items = $item->bundle_items;
1487 Returns the items associated with this bundle
1494 if ( !$self->{_bundle_items_cached} ) {
1495 my $bundle_items = Koha::Items->search(
1496 { 'item_bundles_item.host' => $self->itemnumber },
1497 { join => 'item_bundles_item' } );
1498 $self->{_bundle_items} = $bundle_items;
1499 $self->{_bundle_items_cached} = 1;
1502 return $self->{_bundle_items};
1507 my $is_bundle = $item->is_bundle;
1509 Returns whether the item is a bundle or not
1515 return $self->bundle_items->count ? 1 : 0;
1520 my $bundle = $item->bundle_host;
1522 Returns the bundle item this item is attached to
1529 my $bundle_items_rs = $self->_result->item_bundles_item;
1530 return unless $bundle_items_rs;
1531 return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1536 my $in_bundle = $item->in_bundle;
1538 Returns whether this item is currently in a bundle
1544 return $self->bundle_host ? 1 : 0;
1547 =head3 add_to_bundle
1549 my $link = $item->add_to_bundle($bundle_item);
1551 Adds the bundle_item passed to this item
1556 my ( $self, $bundle_item ) = @_;
1558 my $schema = Koha::Database->new->schema;
1560 my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1565 $self->_result->add_to_item_bundles_hosts(
1566 { item => $bundle_item->itemnumber } );
1568 $bundle_item->notforloan($BundleNotLoanValue)->store();
1574 # 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 fro DBIx::Error
1575 if ( ref($_) eq 'DBIx::Class::Exception' ) {
1577 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1579 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1580 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1581 Koha::Exceptions::Object::FKConstraint->throw(
1582 error => 'Broken FK constraint',
1583 broken_fk => $+{column}
1588 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1590 Koha::Exceptions::Object::DuplicateID->throw(
1591 error => 'Duplicate ID',
1592 duplicate_id => $+{key}
1595 elsif ( $_->{msg} =~
1596 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1598 { # The optional \W in the regex might be a quote or backtick
1599 my $type = $+{type};
1600 my $value = $+{value};
1601 my $property = $+{property};
1602 $property =~ s/['`]//g;
1603 Koha::Exceptions::Object::BadValue->throw(
1606 property => $property =~ /(\w+\.\w+)$/
1609 , # results in table.column without quotes or backtics
1613 # Catch-all for foreign key breakages. It will help find other use cases
1622 =head3 remove_from_bundle
1624 Remove this item from any bundle it may have been attached to.
1628 sub remove_from_bundle {
1631 my $bundle_item_rs = $self->_result->item_bundles_item;
1632 if ( $bundle_item_rs ) {
1633 $bundle_item_rs->delete;
1634 $self->notforloan(0)->store();
1640 =head2 Internal methods
1642 =head3 _after_item_action_hooks
1644 Helper method that takes care of calling all plugin hooks
1648 sub _after_item_action_hooks {
1649 my ( $self, $params ) = @_;
1651 my $action = $params->{action};
1653 Koha::Plugins->call(
1654 'after_item_action',
1658 item_id => $self->itemnumber,
1665 my $recall = $item->recall;
1667 Return the relevant recall for this item
1673 my @recalls = Koha::Recalls->search(
1675 biblio_id => $self->biblionumber,
1678 { order_by => { -asc => 'created_date' } }
1680 foreach my $recall (@recalls) {
1681 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1685 # no item-level recall to return, so return earliest biblio-level
1686 # FIXME: eventually this will be based on priority
1690 =head3 can_be_recalled
1692 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1694 Does item-level checks and returns if items can be recalled by this borrower
1698 sub can_be_recalled {
1699 my ( $self, $params ) = @_;
1701 return 0 if !( C4::Context->preference('UseRecalls') );
1703 # check if this item is not for loan, withdrawn or lost
1704 return 0 if ( $self->notforloan != 0 );
1705 return 0 if ( $self->itemlost != 0 );
1706 return 0 if ( $self->withdrawn != 0 );
1708 # check if this item is not checked out - if not checked out, can't be recalled
1709 return 0 if ( !defined( $self->checkout ) );
1711 my $patron = $params->{patron};
1713 my $branchcode = C4::Context->userenv->{'branch'};
1715 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1718 # Check the circulation rule for each relevant itemtype for this item
1719 my $rule = Koha::CirculationRules->get_effective_rules({
1720 branchcode => $branchcode,
1721 categorycode => $patron ? $patron->categorycode : undef,
1722 itemtype => $self->effective_itemtype,
1725 'recalls_per_record',
1730 # check recalls allowed has been set and is not zero
1731 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1734 # check borrower has not reached open recalls allowed limit
1735 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1737 # check borrower has not reach open recalls allowed per record limit
1738 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1740 # check if this patron has already recalled this item
1741 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1743 # check if this patron has already checked out this item
1744 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1746 # check if this patron has already reserved this item
1747 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1750 # check item availability
1751 # items are unavailable for recall if they are lost, withdrawn or notforloan
1752 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1754 # if there are no available items at all, no recall can be placed
1755 return 0 if ( scalar @items == 0 );
1757 my $checked_out_count = 0;
1759 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1762 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1763 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1765 # can't recall if no items have been checked out
1766 return 0 if ( $checked_out_count == 0 );
1772 =head3 can_be_waiting_recall
1774 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1776 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1777 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1781 sub can_be_waiting_recall {
1784 return 0 if !( C4::Context->preference('UseRecalls') );
1786 # check if this item is not for loan, withdrawn or lost
1787 return 0 if ( $self->notforloan != 0 );
1788 return 0 if ( $self->itemlost != 0 );
1789 return 0 if ( $self->withdrawn != 0 );
1791 my $branchcode = $self->holdingbranch;
1792 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1793 $branchcode = C4::Context->userenv->{'branch'};
1795 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1798 # Check the circulation rule for each relevant itemtype for this item
1799 my $rule = Koha::CirculationRules->get_effective_rules({
1800 branchcode => $branchcode,
1801 categorycode => undef,
1802 itemtype => $self->effective_itemtype,
1808 # check recalls allowed has been set and is not zero
1809 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1815 =head3 check_recalls
1817 my $recall = $item->check_recalls;
1819 Get the most relevant recall for this item.
1826 my @recalls = Koha::Recalls->search(
1827 { biblio_id => $self->biblionumber,
1828 item_id => [ $self->itemnumber, undef ]
1830 { order_by => { -asc => 'created_date' } }
1831 )->filter_by_current->as_list;
1834 # iterate through relevant recalls to find the best one.
1835 # if we come across a waiting recall, use this one.
1836 # 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.
1837 foreach my $r ( @recalls ) {
1838 if ( $r->waiting ) {
1843 unless ( defined $recall ) {
1844 $recall = $recalls[0];
1850 =head3 is_notforloan
1852 my $is_notforloan = $item->is_notforloan;
1854 Determine whether or not this item is "notforloan" based on
1855 the item's notforloan status or its item type
1861 my $is_notforloan = 0;
1863 if ( $self->notforloan ){
1867 my $itemtype = $self->itemtype;
1869 if ( $itemtype->notforloan ){
1875 return $is_notforloan;
1888 Kyle M Hall <kyle@bywatersolutions.com>