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;
771 ## If item was lost, it has now been found, reverse any list item charges if necessary.
773 my $no_refund_after_days =
774 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
775 if ($no_refund_after_days) {
776 my $today = dt_from_string();
777 my $lost_age_in_days =
778 dt_from_string( $self->itemlost_on )->delta_days($today)
781 $refund = 0 unless ( $lost_age_in_days < $no_refund_after_days );
787 && Koha::CirculationRules->get_lostreturn_policy(
789 current_branch => C4::Context->userenv->{branch},
795 C4::Circulation::_FixAccountForLostAndFound( $self->itemnumber, $self->barcode );
802 =head3 to_api_mapping
804 This method returns the mapping for representing a Koha::Item object
811 itemnumber => 'item_id',
812 biblionumber => 'biblio_id',
813 biblioitemnumber => undef,
814 barcode => 'external_id',
815 dateaccessioned => 'acquisition_date',
816 booksellerid => 'acquisition_source',
817 homebranch => 'home_library_id',
818 price => 'purchase_price',
819 replacementprice => 'replacement_price',
820 replacementpricedate => 'replacement_price_date',
821 datelastborrowed => 'last_checkout_date',
822 datelastseen => 'last_seen_date',
824 notforloan => 'not_for_loan_status',
825 damaged => 'damaged_status',
826 damaged_on => 'damaged_date',
827 itemlost => 'lost_status',
828 itemlost_on => 'lost_date',
829 withdrawn => 'withdrawn',
830 withdrawn_on => 'withdrawn_date',
831 itemcallnumber => 'callnumber',
832 coded_location_qualifier => 'coded_location_qualifier',
833 issues => 'checkouts_count',
834 renewals => 'renewals_count',
835 reserves => 'holds_count',
836 restricted => 'restricted_status',
837 itemnotes => 'public_notes',
838 itemnotes_nonpublic => 'internal_notes',
839 holdingbranch => 'holding_library_id',
841 timestamp => 'timestamp',
842 location => 'location',
843 permanent_location => 'permanent_location',
844 onloan => 'checked_out_date',
845 cn_source => 'call_number_source',
846 cn_sort => 'call_number_sort',
847 ccode => 'collection_code',
848 materials => 'materials_notes',
850 itype => 'item_type',
851 more_subfields_xml => 'extended_subfields',
852 enumchron => 'serial_issue_number',
853 copynumber => 'copy_number',
854 stocknumber => 'inventory_number',
855 new_status => 'new_status'
861 my $itemtype = $item->itemtype;
863 Returns Koha object for effective itemtype
869 return Koha::ItemTypes->find( $self->effective_itemtype );
872 =head2 Internal methods
874 =head3 _after_item_action_hooks
876 Helper method that takes care of calling all plugin hooks
880 sub _after_item_action_hooks {
881 my ( $self, $params ) = @_;
883 my $action = $params->{action};
890 item_id => $self->itemnumber,
905 Kyle M Hall <kyle@bywatersolutions.com>