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::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
34 use C4::ClassSource; # FIXME We would like to avoid that
35 use C4::Log qw( logaction );
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
48 use base qw(Koha::Object);
52 Koha::Item - Koha Item object class
64 $params can take an optional 'skip_modzebra_update' parameter.
65 If set, the reindexation process will not happen (ModZebra not called)
67 NOTE: This is a temporary fix to answer a performance issue when lot of items
68 are added (or modified) at the same time.
69 The correct way to fix this is to make the ES reindexation process async.
70 You should not turn it on if you do not understand what it is doing exactly.
76 my $params = @_ ? shift : {};
78 my $log_action = $params->{log_action} // 1;
80 # We do not want to oblige callers to pass this value
81 # Dev conveniences vs performance?
82 unless ( $self->biblioitemnumber ) {
83 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
86 # See related changes from C4::Items::AddItem
87 unless ( $self->itype ) {
88 $self->itype($self->biblio->biblioitem->itemtype);
91 my $today = dt_from_string;
92 unless ( $self->in_storage ) { #AddItem
93 unless ( $self->permanent_location ) {
94 $self->permanent_location($self->location);
96 unless ( $self->replacementpricedate ) {
97 $self->replacementpricedate($today);
99 unless ( $self->datelastseen ) {
100 $self->datelastseen($today);
103 unless ( $self->dateaccessioned ) {
104 $self->dateaccessioned($today);
107 if ( $self->itemcallnumber
108 or $self->cn_source )
110 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
111 $self->cn_sort($cn_sort);
114 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
115 unless $params->{skip_modzebra_update};
117 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
118 if $log_action && C4::Context->preference("CataloguingLog");
120 $self->_after_item_action_hooks({ action => 'create' });
124 { # Update *_on fields if needed
125 # Why not for AddItem as well?
126 my @fields = qw( itemlost withdrawn damaged );
128 # Only retrieve the item if we need to set an "on" date field
129 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
130 my $pre_mod_item = $self->get_from_storage;
131 for my $field (@fields) {
133 and not $pre_mod_item->$field )
135 my $field_on = "${field}_on";
137 DateTime::Format::MySQL->format_datetime( dt_from_string() )
143 # If the field is defined but empty, we are removing and,
144 # and thus need to clear out the 'on' field as well
145 for my $field (@fields) {
146 if ( defined( $self->$field ) && !$self->$field ) {
147 my $field_on = "${field}_on";
148 $self->$field_on(undef);
153 my %updated_columns = $self->_result->get_dirty_columns;
154 return $self->SUPER::store unless %updated_columns;
156 if ( exists $updated_columns{itemcallnumber}
157 or exists $updated_columns{cn_source} )
159 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
160 $self->cn_sort($cn_sort);
164 if ( exists $updated_columns{location}
165 and $self->location ne 'CART'
166 and $self->location ne 'PROC'
167 and not exists $updated_columns{permanent_location} )
169 $self->permanent_location( $self->location );
172 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
173 unless $params->{skip_modzebra_update};
175 $self->_after_item_action_hooks({ action => 'modify' });
177 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
178 if $log_action && C4::Context->preference("CataloguingLog");
181 unless ( $self->dateaccessioned ) {
182 $self->dateaccessioned($today);
185 return $self->SUPER::store;
194 my $params = @_ ? shift : {};
196 # FIXME check the item has no current issues
197 # i.e. raise the appropriate exception
199 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
200 unless $params->{skip_modzebra_update};
202 $self->_after_item_action_hooks({ action => 'delete' });
204 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
205 if C4::Context->preference("CataloguingLog");
207 return $self->SUPER::delete;
216 my $params = @_ ? shift : {};
218 my $safe_to_delete = $self->safe_to_delete;
219 return $safe_to_delete unless $safe_to_delete eq '1';
221 $self->move_to_deleted;
223 return $self->delete($params);
226 =head3 safe_to_delete
228 returns 1 if the item is safe to delete,
230 "book_on_loan" if the item is checked out,
232 "not_same_branch" if the item is blocked by independent branches,
234 "book_reserved" if the there are holds aganst the item, or
236 "linked_analytics" if the item has linked analytic records.
238 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
245 return "book_on_loan" if $self->checkout;
247 return "not_same_branch"
248 if defined C4::Context->userenv
249 and !C4::Context->IsSuperLibrarian()
250 and C4::Context->preference("IndependentBranches")
251 and ( C4::Context->userenv->{branch} ne $self->homebranch );
253 # check it doesn't have a waiting reserve
254 return "book_reserved"
255 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
257 return "linked_analytics"
258 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
260 return "last_item_for_hold"
261 if $self->biblio->items->count == 1
262 && $self->biblio->holds->search(
271 =head3 move_to_deleted
273 my $is_moved = $item->move_to_deleted;
275 Move an item to the deleteditems table.
276 This can be done before deleting an item, to make sure the data are not completely deleted.
280 sub move_to_deleted {
282 my $item_infos = $self->unblessed;
283 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
284 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
288 =head3 effective_itemtype
290 Returns the itemtype for the item based on whether item level itemtypes are set or not.
294 sub effective_itemtype {
297 return $self->_result()->effective_itemtype();
307 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
309 return $self->{_home_branch};
312 =head3 holding_branch
319 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
321 return $self->{_holding_branch};
326 my $biblio = $item->biblio;
328 Return the bibliographic record of this item
334 my $biblio_rs = $self->_result->biblio;
335 return Koha::Biblio->_new_from_dbic( $biblio_rs );
340 my $biblioitem = $item->biblioitem;
342 Return the biblioitem record of this item
348 my $biblioitem_rs = $self->_result->biblioitem;
349 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
354 my $checkout = $item->checkout;
356 Return the checkout for this item
362 my $checkout_rs = $self->_result->issue;
363 return unless $checkout_rs;
364 return Koha::Checkout->_new_from_dbic( $checkout_rs );
369 my $holds = $item->holds();
370 my $holds = $item->holds($params);
371 my $holds = $item->holds({ found => 'W'});
373 Return holds attached to an item, optionally accept a hashref of params to pass to search
378 my ( $self,$params ) = @_;
379 my $holds_rs = $self->_result->reserves->search($params);
380 return Koha::Holds->_new_from_dbic( $holds_rs );
385 my $transfer = $item->get_transfer;
387 Return the transfer if the item is in transit or undef
393 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
394 return unless $transfer_rs;
395 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
398 =head3 last_returned_by
400 Gets and sets the last borrower to return an item.
402 Accepts and returns Koha::Patron objects
404 $item->last_returned_by( $borrowernumber );
406 $last_returned_by = $item->last_returned_by();
410 sub last_returned_by {
411 my ( $self, $borrower ) = @_;
413 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
416 return $items_last_returned_by_rs->update_or_create(
417 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
420 unless ( $self->{_last_returned_by} ) {
421 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
423 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
427 return $self->{_last_returned_by};
431 =head3 can_article_request
433 my $bool = $item->can_article_request( $borrower )
435 Returns true if item can be specifically requested
437 $borrower must be a Koha::Patron object
441 sub can_article_request {
442 my ( $self, $borrower ) = @_;
444 my $rule = $self->article_request_type($borrower);
446 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
450 =head3 hidden_in_opac
452 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
454 Returns true if item fields match the hidding criteria defined in $rules.
455 Returns false otherwise.
457 Takes HASHref that can have the following parameters:
459 $rules : { <field> => [ value_1, ... ], ... }
461 Note: $rules inherits its structure from the parsed YAML from reading
462 the I<OpacHiddenItems> system preference.
467 my ( $self, $params ) = @_;
469 my $rules = $params->{rules} // {};
472 if C4::Context->preference('hidelostitems') and
475 my $hidden_in_opac = 0;
477 foreach my $field ( keys %{$rules} ) {
479 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
485 return $hidden_in_opac;
488 =head3 can_be_transferred
490 $item->can_be_transferred({ to => $to_library, from => $from_library })
491 Checks if an item can be transferred to given library.
493 This feature is controlled by two system preferences:
494 UseBranchTransferLimits to enable / disable the feature
495 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
496 for setting the limitations
498 Takes HASHref that can have the following parameters:
499 MANDATORY PARAMETERS:
502 $from : Koha::Library # if not given, item holdingbranch
503 # will be used instead
505 Returns 1 if item can be transferred to $to_library, otherwise 0.
507 To find out whether at least one item of a Koha::Biblio can be transferred, please
508 see Koha::Biblio->can_be_transferred() instead of using this method for
509 multiple items of the same biblio.
513 sub can_be_transferred {
514 my ($self, $params) = @_;
516 my $to = $params->{to};
517 my $from = $params->{from};
519 $to = $to->branchcode;
520 $from = defined $from ? $from->branchcode : $self->holdingbranch;
522 return 1 if $from eq $to; # Transfer to current branch is allowed
523 return 1 unless C4::Context->preference('UseBranchTransferLimits');
525 my $limittype = C4::Context->preference('BranchTransferLimitsType');
526 return Koha::Item::Transfer::Limits->search({
529 $limittype => $limittype eq 'itemtype'
530 ? $self->effective_itemtype : $self->ccode
534 =head3 pickup_locations
536 $pickup_locations = $item->pickup_locations( {patron => $patron } )
538 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)
539 and if item can be transferred to each pickup location.
543 sub pickup_locations {
544 my ($self, $params) = @_;
546 my $patron = $params->{patron};
548 my $circ_control_branch =
549 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
551 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
554 if(defined $patron) {
555 return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
556 return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
559 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
560 @libs = $self->home_branch->get_hold_libraries;
561 push @libs, $self->home_branch unless scalar(@libs) > 0;
562 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
563 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
564 @libs = $plib->get_hold_libraries;
565 push @libs, $self->home_branch unless scalar(@libs) > 0;
566 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
567 push @libs, $self->home_branch;
568 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
569 push @libs, $self->holding_branch;
571 @libs = Koha::Libraries->search({
574 order_by => ['branchname']
578 my @pickup_locations;
579 foreach my $library (@libs) {
580 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
581 push @pickup_locations, $library;
585 return \@pickup_locations;
588 =head3 article_request_type
590 my $type = $item->article_request_type( $borrower )
592 returns 'yes', 'no', 'bib_only', or 'item_only'
594 $borrower must be a Koha::Patron object
598 sub article_request_type {
599 my ( $self, $borrower ) = @_;
601 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
603 $branch_control eq 'homebranch' ? $self->homebranch
604 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
606 my $borrowertype = $borrower->categorycode;
607 my $itemtype = $self->effective_itemtype();
608 my $rule = Koha::CirculationRules->get_effective_rule(
610 rule_name => 'article_requests',
611 categorycode => $borrowertype,
612 itemtype => $itemtype,
613 branchcode => $branchcode
617 return q{} unless $rule;
618 return $rule->rule_value || q{}
627 my $attributes = { order_by => 'priority' };
628 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
630 itemnumber => $self->itemnumber,
633 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
634 waitingdate => { '!=' => undef },
637 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
638 return Koha::Holds->_new_from_dbic($hold_rs);
641 =head3 stockrotationitem
643 my $sritem = Koha::Item->stockrotationitem;
645 Returns the stock rotation item associated with the current item.
649 sub stockrotationitem {
651 my $rs = $self->_result->stockrotationitem;
653 return Koha::StockRotationItem->_new_from_dbic( $rs );
658 my $item = $item->add_to_rota($rota_id);
660 Add this item to the rota identified by $ROTA_ID, which means associating it
661 with the first stage of that rota. Should this item already be associated
662 with a rota, then we will move it to the new rota.
667 my ( $self, $rota_id ) = @_;
668 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
672 =head3 has_pending_hold
674 my $is_pending_hold = $item->has_pending_hold();
676 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
680 sub has_pending_hold {
682 my $pending_hold = $self->_result->tmp_holdsqueues;
683 return $pending_hold->count ? 1: 0;
688 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
689 my $field = $item->as_marc_field({ [ mss => $mss ] });
691 This method returns a MARC::Field object representing the Koha::Item object
692 with the current mappings configuration.
697 my ( $self, $params ) = @_;
699 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
700 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
704 my @columns = $self->_result->result_source->columns;
706 foreach my $item_field ( @columns ) {
707 my $mapping = $mss->{ "items.$item_field"}[0];
708 my $tagfield = $mapping->{tagfield};
709 my $tagsubfield = $mapping->{tagsubfield};
710 next if !$tagfield; # TODO: Should we raise an exception instead?
711 # Feels like safe fallback is better
713 push @subfields, $tagsubfield => $self->$item_field
714 if defined $self->$item_field and $item_field ne '';
717 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
718 push( @subfields, @{$unlinked_item_subfields} )
719 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
723 $field = MARC::Field->new(
724 "$item_tag", ' ', ' ', @subfields
730 =head3 renewal_branchcode
732 Returns the branchcode to be recorded in statistics renewal of the item
736 sub renewal_branchcode {
738 my ($self, $params ) = @_;
740 my $interface = C4::Context->interface;
742 if ( $interface eq 'opac' ){
743 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
744 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
745 $branchcode = 'OPACRenew';
747 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
748 $branchcode = $self->homebranch;
750 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
751 $branchcode = $self->checkout->patron->branchcode;
753 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
754 $branchcode = $self->checkout->branchcode;
760 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
761 ? C4::Context->userenv->{branch} : $params->{branch};
767 my ($self, $params) = @_;
769 my $holdingbranch = $params->{holdingbranch} || $self->holdingbranch;
770 my $borrowernumber = $params->{borrowernumber} || undef;
772 ## If item was lost, it has now been found, reverse any list item charges if necessary.
774 my $no_refund_after_days =
775 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
776 if ($no_refund_after_days) {
777 my $today = dt_from_string();
778 my $lost_age_in_days =
779 dt_from_string( $self->itemlost_on )->delta_days($today)
782 $refund = 0 unless ( $lost_age_in_days < $no_refund_after_days );
788 && Koha::CirculationRules->get_lostreturn_policy(
790 current_branch => C4::Context->userenv->{branch},
796 _FixAccountForLostAndFound( $self->itemnumber, borrowernumber, $self->barcode );
803 =head3 to_api_mapping
805 This method returns the mapping for representing a Koha::Item object
812 itemnumber => 'item_id',
813 biblionumber => 'biblio_id',
814 biblioitemnumber => undef,
815 barcode => 'external_id',
816 dateaccessioned => 'acquisition_date',
817 booksellerid => 'acquisition_source',
818 homebranch => 'home_library_id',
819 price => 'purchase_price',
820 replacementprice => 'replacement_price',
821 replacementpricedate => 'replacement_price_date',
822 datelastborrowed => 'last_checkout_date',
823 datelastseen => 'last_seen_date',
825 notforloan => 'not_for_loan_status',
826 damaged => 'damaged_status',
827 damaged_on => 'damaged_date',
828 itemlost => 'lost_status',
829 itemlost_on => 'lost_date',
830 withdrawn => 'withdrawn',
831 withdrawn_on => 'withdrawn_date',
832 itemcallnumber => 'callnumber',
833 coded_location_qualifier => 'coded_location_qualifier',
834 issues => 'checkouts_count',
835 renewals => 'renewals_count',
836 reserves => 'holds_count',
837 restricted => 'restricted_status',
838 itemnotes => 'public_notes',
839 itemnotes_nonpublic => 'internal_notes',
840 holdingbranch => 'holding_library_id',
842 timestamp => 'timestamp',
843 location => 'location',
844 permanent_location => 'permanent_location',
845 onloan => 'checked_out_date',
846 cn_source => 'call_number_source',
847 cn_sort => 'call_number_sort',
848 ccode => 'collection_code',
849 materials => 'materials_notes',
851 itype => 'item_type',
852 more_subfields_xml => 'extended_subfields',
853 enumchron => 'serial_issue_number',
854 copynumber => 'copy_number',
855 stocknumber => 'inventory_number',
856 new_status => 'new_status'
862 my $itemtype = $item->itemtype;
864 Returns Koha object for effective itemtype
870 return Koha::ItemTypes->find( $self->effective_itemtype );
873 =head2 Internal methods
875 =head3 _after_item_action_hooks
877 Helper method that takes care of calling all plugin hooks
881 sub _after_item_action_hooks {
882 my ( $self, $params ) = @_;
884 my $action = $params->{action};
891 item_id => $self->itemnumber,
906 Kyle M Hall <kyle@bywatersolutions.com>