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>.
23 use List::MoreUtils qw(any);
28 use Koha::DateUtils qw( dt_from_string );
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
37 use Koha::CirculationRules;
38 use Koha::SearchEngine::Indexer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
44 use Koha::StockRotationItem;
45 use Koha::StockRotationRotas;
47 use base qw(Koha::Object);
51 Koha::Item - Koha Item object class
63 $params can take an optional 'skip_record_index' parameter.
64 If set, the reindexation process will not happen (index_records not called)
66 NOTE: This is a temporary fix to answer a performance issue when lot of items
67 are added (or modified) at the same time.
68 The correct way to fix this is to make the ES reindexation process async.
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 my $today = dt_from_string;
91 my $action = 'create';
93 unless ( $self->in_storage ) { #AddItem
94 unless ( $self->permanent_location ) {
95 $self->permanent_location($self->location);
97 unless ( $self->replacementpricedate ) {
98 $self->replacementpricedate($today);
100 unless ( $self->datelastseen ) {
101 $self->datelastseen($today);
104 unless ( $self->dateaccessioned ) {
105 $self->dateaccessioned($today);
108 if ( $self->itemcallnumber
109 or $self->cn_source )
111 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
112 $self->cn_sort($cn_sort);
119 { # Update *_on fields if needed
120 # Why not for AddItem as well?
121 my @fields = qw( itemlost withdrawn damaged );
123 # Only retrieve the item if we need to set an "on" date field
124 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
125 my $pre_mod_item = $self->get_from_storage;
126 for my $field (@fields) {
128 and not $pre_mod_item->$field )
130 my $field_on = "${field}_on";
132 DateTime::Format::MySQL->format_datetime( dt_from_string() )
138 # If the field is defined but empty, we are removing and,
139 # and thus need to clear out the 'on' field as well
140 for my $field (@fields) {
141 if ( defined( $self->$field ) && !$self->$field ) {
142 my $field_on = "${field}_on";
143 $self->$field_on(undef);
148 my %updated_columns = $self->_result->get_dirty_columns;
149 return $self->SUPER::store unless %updated_columns;
151 if ( exists $updated_columns{itemcallnumber}
152 or exists $updated_columns{cn_source} )
154 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
155 $self->cn_sort($cn_sort);
159 if ( exists $updated_columns{location}
160 and $self->location ne 'CART'
161 and $self->location ne 'PROC'
162 and not exists $updated_columns{permanent_location} )
164 $self->permanent_location( $self->location );
167 $self->_after_item_action_hooks({ action => 'modify' });
171 unless ( $self->dateaccessioned ) {
172 $self->dateaccessioned($today);
175 my $result = $self->SUPER::store;
176 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
178 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
179 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
181 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
182 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
183 unless $params->{skip_record_index};
194 my $params = @_ ? shift : {};
196 # FIXME check the item has no current issues
197 # i.e. raise the appropriate exception
199 my $result = $self->SUPER::delete;
201 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
202 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
203 unless $params->{skip_record_index};
205 $self->_after_item_action_hooks({ action => 'delete' });
207 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
208 if C4::Context->preference("CataloguingLog");
219 my $params = @_ ? shift : {};
221 my $safe_to_delete = $self->safe_to_delete;
222 return $safe_to_delete unless $safe_to_delete eq '1';
224 $self->move_to_deleted;
226 return $self->delete($params);
229 =head3 safe_to_delete
231 returns 1 if the item is safe to delete,
233 "book_on_loan" if the item is checked out,
235 "not_same_branch" if the item is blocked by independent branches,
237 "book_reserved" if the there are holds aganst the item, or
239 "linked_analytics" if the item has linked analytic records.
241 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
248 return "book_on_loan" if $self->checkout;
250 return "not_same_branch"
251 if defined C4::Context->userenv
252 and !C4::Context->IsSuperLibrarian()
253 and C4::Context->preference("IndependentBranches")
254 and ( C4::Context->userenv->{branch} ne $self->homebranch );
256 # check it doesn't have a waiting reserve
257 return "book_reserved"
258 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
260 return "linked_analytics"
261 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
263 return "last_item_for_hold"
264 if $self->biblio->items->count == 1
265 && $self->biblio->holds->search(
274 =head3 move_to_deleted
276 my $is_moved = $item->move_to_deleted;
278 Move an item to the deleteditems table.
279 This can be done before deleting an item, to make sure the data are not completely deleted.
283 sub move_to_deleted {
285 my $item_infos = $self->unblessed;
286 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
287 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
291 =head3 effective_itemtype
293 Returns the itemtype for the item based on whether item level itemtypes are set or not.
297 sub effective_itemtype {
300 return $self->_result()->effective_itemtype();
310 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
312 return $self->{_home_branch};
315 =head3 holding_branch
322 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
324 return $self->{_holding_branch};
329 my $biblio = $item->biblio;
331 Return the bibliographic record of this item
337 my $biblio_rs = $self->_result->biblio;
338 return Koha::Biblio->_new_from_dbic( $biblio_rs );
343 my $biblioitem = $item->biblioitem;
345 Return the biblioitem record of this item
351 my $biblioitem_rs = $self->_result->biblioitem;
352 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
357 my $checkout = $item->checkout;
359 Return the checkout for this item
365 my $checkout_rs = $self->_result->issue;
366 return unless $checkout_rs;
367 return Koha::Checkout->_new_from_dbic( $checkout_rs );
372 my $holds = $item->holds();
373 my $holds = $item->holds($params);
374 my $holds = $item->holds({ found => 'W'});
376 Return holds attached to an item, optionally accept a hashref of params to pass to search
381 my ( $self,$params ) = @_;
382 my $holds_rs = $self->_result->reserves->search($params);
383 return Koha::Holds->_new_from_dbic( $holds_rs );
388 my $transfer = $item->get_transfer;
390 Return the transfer if the item is in transit or undef
396 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
397 return unless $transfer_rs;
398 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
401 =head3 last_returned_by
403 Gets and sets the last borrower to return an item.
405 Accepts and returns Koha::Patron objects
407 $item->last_returned_by( $borrowernumber );
409 $last_returned_by = $item->last_returned_by();
413 sub last_returned_by {
414 my ( $self, $borrower ) = @_;
416 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
419 return $items_last_returned_by_rs->update_or_create(
420 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
423 unless ( $self->{_last_returned_by} ) {
424 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
426 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
430 return $self->{_last_returned_by};
434 =head3 can_article_request
436 my $bool = $item->can_article_request( $borrower )
438 Returns true if item can be specifically requested
440 $borrower must be a Koha::Patron object
444 sub can_article_request {
445 my ( $self, $borrower ) = @_;
447 my $rule = $self->article_request_type($borrower);
449 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
453 =head3 hidden_in_opac
455 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
457 Returns true if item fields match the hidding criteria defined in $rules.
458 Returns false otherwise.
460 Takes HASHref that can have the following parameters:
462 $rules : { <field> => [ value_1, ... ], ... }
464 Note: $rules inherits its structure from the parsed YAML from reading
465 the I<OpacHiddenItems> system preference.
470 my ( $self, $params ) = @_;
472 my $rules = $params->{rules} // {};
475 if C4::Context->preference('hidelostitems') and
478 my $hidden_in_opac = 0;
480 foreach my $field ( keys %{$rules} ) {
482 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
488 return $hidden_in_opac;
491 =head3 can_be_transferred
493 $item->can_be_transferred({ to => $to_library, from => $from_library })
494 Checks if an item can be transferred to given library.
496 This feature is controlled by two system preferences:
497 UseBranchTransferLimits to enable / disable the feature
498 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
499 for setting the limitations
501 Takes HASHref that can have the following parameters:
502 MANDATORY PARAMETERS:
505 $from : Koha::Library # if not given, item holdingbranch
506 # will be used instead
508 Returns 1 if item can be transferred to $to_library, otherwise 0.
510 To find out whether at least one item of a Koha::Biblio can be transferred, please
511 see Koha::Biblio->can_be_transferred() instead of using this method for
512 multiple items of the same biblio.
516 sub can_be_transferred {
517 my ($self, $params) = @_;
519 my $to = $params->{to};
520 my $from = $params->{from};
522 $to = $to->branchcode;
523 $from = defined $from ? $from->branchcode : $self->holdingbranch;
525 return 1 if $from eq $to; # Transfer to current branch is allowed
526 return 1 unless C4::Context->preference('UseBranchTransferLimits');
528 my $limittype = C4::Context->preference('BranchTransferLimitsType');
529 return Koha::Item::Transfer::Limits->search({
532 $limittype => $limittype eq 'itemtype'
533 ? $self->effective_itemtype : $self->ccode
538 =head3 pickup_locations
540 $pickup_locations = $item->pickup_locations( {patron => $patron } )
542 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)
543 and if item can be transferred to each pickup location.
547 sub pickup_locations {
548 my ($self, $params) = @_;
550 my $patron = $params->{patron};
552 my $circ_control_branch =
553 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
555 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
557 if(defined $patron) {
558 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
559 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
562 my $pickup_libraries = Koha::Libraries->search();
563 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
564 $pickup_libraries = $self->home_branch->get_hold_libraries;
565 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
566 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
567 $pickup_libraries = $plib->get_hold_libraries;
568 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
569 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
570 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
571 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
574 return $pickup_libraries->search(
579 order_by => ['branchname']
581 ) unless C4::Context->preference('UseBranchTransferLimits');
583 my $limittype = C4::Context->preference('BranchTransferLimitsType');
584 my ($ccode, $itype) = (undef, undef);
585 if( $limittype eq 'ccode' ){
586 $ccode = $self->ccode;
588 $itype = $self->itype;
590 my $limits = Koha::Item::Transfer::Limits->search(
592 fromBranch => $self->holdingbranch,
596 { columns => ['toBranch'] }
599 return $pickup_libraries->search(
601 pickup_location => 1,
603 '-not_in' => $limits->_resultset->as_query
607 order_by => ['branchname']
612 =head3 article_request_type
614 my $type = $item->article_request_type( $borrower )
616 returns 'yes', 'no', 'bib_only', or 'item_only'
618 $borrower must be a Koha::Patron object
622 sub article_request_type {
623 my ( $self, $borrower ) = @_;
625 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
627 $branch_control eq 'homebranch' ? $self->homebranch
628 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
630 my $borrowertype = $borrower->categorycode;
631 my $itemtype = $self->effective_itemtype();
632 my $rule = Koha::CirculationRules->get_effective_rule(
634 rule_name => 'article_requests',
635 categorycode => $borrowertype,
636 itemtype => $itemtype,
637 branchcode => $branchcode
641 return q{} unless $rule;
642 return $rule->rule_value || q{}
651 my $attributes = { order_by => 'priority' };
652 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
654 itemnumber => $self->itemnumber,
657 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
658 waitingdate => { '!=' => undef },
661 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
662 return Koha::Holds->_new_from_dbic($hold_rs);
665 =head3 stockrotationitem
667 my $sritem = Koha::Item->stockrotationitem;
669 Returns the stock rotation item associated with the current item.
673 sub stockrotationitem {
675 my $rs = $self->_result->stockrotationitem;
677 return Koha::StockRotationItem->_new_from_dbic( $rs );
682 my $item = $item->add_to_rota($rota_id);
684 Add this item to the rota identified by $ROTA_ID, which means associating it
685 with the first stage of that rota. Should this item already be associated
686 with a rota, then we will move it to the new rota.
691 my ( $self, $rota_id ) = @_;
692 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
696 =head3 has_pending_hold
698 my $is_pending_hold = $item->has_pending_hold();
700 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
704 sub has_pending_hold {
706 my $pending_hold = $self->_result->tmp_holdsqueues;
707 return $pending_hold->count ? 1: 0;
712 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
713 my $field = $item->as_marc_field({ [ mss => $mss ] });
715 This method returns a MARC::Field object representing the Koha::Item object
716 with the current mappings configuration.
721 my ( $self, $params ) = @_;
723 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
724 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
728 my @columns = $self->_result->result_source->columns;
730 foreach my $item_field ( @columns ) {
731 my $mapping = $mss->{ "items.$item_field"}[0];
732 my $tagfield = $mapping->{tagfield};
733 my $tagsubfield = $mapping->{tagsubfield};
734 next if !$tagfield; # TODO: Should we raise an exception instead?
735 # Feels like safe fallback is better
737 push @subfields, $tagsubfield => $self->$item_field
738 if defined $self->$item_field and $item_field ne '';
741 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
742 push( @subfields, @{$unlinked_item_subfields} )
743 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
747 $field = MARC::Field->new(
748 "$item_tag", ' ', ' ', @subfields
754 =head3 renewal_branchcode
756 Returns the branchcode to be recorded in statistics renewal of the item
760 sub renewal_branchcode {
762 my ($self, $params ) = @_;
764 my $interface = C4::Context->interface;
766 if ( $interface eq 'opac' ){
767 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
768 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
769 $branchcode = 'OPACRenew';
771 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
772 $branchcode = $self->homebranch;
774 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
775 $branchcode = $self->checkout->patron->branchcode;
777 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
778 $branchcode = $self->checkout->branchcode;
784 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
785 ? C4::Context->userenv->{branch} : $params->{branch};
790 =head3 to_api_mapping
792 This method returns the mapping for representing a Koha::Item object
799 itemnumber => 'item_id',
800 biblionumber => 'biblio_id',
801 biblioitemnumber => undef,
802 barcode => 'external_id',
803 dateaccessioned => 'acquisition_date',
804 booksellerid => 'acquisition_source',
805 homebranch => 'home_library_id',
806 price => 'purchase_price',
807 replacementprice => 'replacement_price',
808 replacementpricedate => 'replacement_price_date',
809 datelastborrowed => 'last_checkout_date',
810 datelastseen => 'last_seen_date',
812 notforloan => 'not_for_loan_status',
813 damaged => 'damaged_status',
814 damaged_on => 'damaged_date',
815 itemlost => 'lost_status',
816 itemlost_on => 'lost_date',
817 withdrawn => 'withdrawn',
818 withdrawn_on => 'withdrawn_date',
819 itemcallnumber => 'callnumber',
820 coded_location_qualifier => 'coded_location_qualifier',
821 issues => 'checkouts_count',
822 renewals => 'renewals_count',
823 reserves => 'holds_count',
824 restricted => 'restricted_status',
825 itemnotes => 'public_notes',
826 itemnotes_nonpublic => 'internal_notes',
827 holdingbranch => 'holding_library_id',
829 timestamp => 'timestamp',
830 location => 'location',
831 permanent_location => 'permanent_location',
832 onloan => 'checked_out_date',
833 cn_source => 'call_number_source',
834 cn_sort => 'call_number_sort',
835 ccode => 'collection_code',
836 materials => 'materials_notes',
838 itype => 'item_type',
839 more_subfields_xml => 'extended_subfields',
840 enumchron => 'serial_issue_number',
841 copynumber => 'copy_number',
842 stocknumber => 'inventory_number',
843 new_status => 'new_status'
847 =head2 Internal methods
849 =head3 _after_item_action_hooks
851 Helper method that takes care of calling all plugin hooks
855 sub _after_item_action_hooks {
856 my ( $self, $params ) = @_;
858 my $action = $params->{action};
860 if ( C4::Context->config("enable_plugins") ) {
862 my @plugins = Koha::Plugins->new->GetPlugins({
863 method => 'after_item_action',
868 foreach my $plugin ( @plugins ) {
870 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
890 Kyle M Hall <kyle@bywatersolutions.com>