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;
44 use Koha::StockRotationItem;
45 use Koha::StockRotationRotas;
47 use base qw(Koha::Object);
51 Koha::Item - Koha Item object class
64 my ($self, $params) = @_;
66 my $log_action = $params->{log_action} // 1;
68 # We do not want to oblige callers to pass this value
69 # Dev conveniences vs performance?
70 unless ( $self->biblioitemnumber ) {
71 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
74 # See related changes from C4::Items::AddItem
75 unless ( $self->itype ) {
76 $self->itype($self->biblio->biblioitem->itemtype);
79 my $today = dt_from_string;
80 unless ( $self->in_storage ) { #AddItem
81 unless ( $self->permanent_location ) {
82 $self->permanent_location($self->location);
84 unless ( $self->replacementpricedate ) {
85 $self->replacementpricedate($today);
87 unless ( $self->datelastseen ) {
88 $self->datelastseen($today);
91 unless ( $self->dateaccessioned ) {
92 $self->dateaccessioned($today);
95 if ( $self->itemcallnumber
98 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
99 $self->cn_sort($cn_sort);
102 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
104 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
105 if $log_action && C4::Context->preference("CataloguingLog");
107 $self->_after_item_action_hooks({ action => 'create' });
111 { # Update *_on fields if needed
112 # Why not for AddItem as well?
113 my @fields = qw( itemlost withdrawn damaged );
115 # Only retrieve the item if we need to set an "on" date field
116 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
117 my $pre_mod_item = $self->get_from_storage;
118 for my $field (@fields) {
120 and not $pre_mod_item->$field )
122 my $field_on = "${field}_on";
124 DateTime::Format::MySQL->format_datetime( dt_from_string() )
130 # If the field is defined but empty, we are removing and,
131 # and thus need to clear out the 'on' field as well
132 for my $field (@fields) {
133 if ( defined( $self->$field ) && !$self->$field ) {
134 my $field_on = "${field}_on";
135 $self->$field_on(undef);
140 my %updated_columns = $self->_result->get_dirty_columns;
141 return $self->SUPER::store unless %updated_columns;
143 if ( exists $updated_columns{itemcallnumber}
144 or exists $updated_columns{cn_source} )
146 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
147 $self->cn_sort($cn_sort);
151 if ( exists $updated_columns{location}
152 and $self->location ne 'CART'
153 and $self->location ne 'PROC'
154 and not exists $updated_columns{permanent_location} )
156 $self->permanent_location( $self->location );
159 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
161 $self->_after_item_action_hooks({ action => 'modify' });
163 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
164 if $log_action && C4::Context->preference("CataloguingLog");
167 unless ( $self->dateaccessioned ) {
168 $self->dateaccessioned($today);
171 return $self->SUPER::store;
181 # FIXME check the item has no current issues
182 # i.e. raise the appropriate exception
184 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
186 $self->_after_item_action_hooks({ action => 'delete' });
188 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
189 if C4::Context->preference("CataloguingLog");
191 return $self->SUPER::delete;
201 my $safe_to_delete = $self->safe_to_delete;
202 return $safe_to_delete unless $safe_to_delete eq '1';
204 $self->move_to_deleted;
206 return $self->delete;
209 =head3 safe_to_delete
211 returns 1 if the item is safe to delete,
213 "book_on_loan" if the item is checked out,
215 "not_same_branch" if the item is blocked by independent branches,
217 "book_reserved" if the there are holds aganst the item, or
219 "linked_analytics" if the item has linked analytic records.
226 return "book_on_loan" if $self->checkout;
228 return "not_same_branch"
229 if defined C4::Context->userenv
230 and !C4::Context->IsSuperLibrarian()
231 and C4::Context->preference("IndependentBranches")
232 and ( C4::Context->userenv->{branch} ne $self->homebranch );
234 # check it doesn't have a waiting reserve
235 return "book_reserved"
236 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
238 return "linked_analytics"
239 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
244 =head3 move_to_deleted
246 my $is_moved = $item->move_to_deleted;
248 Move an item to the deleteditems table.
249 This can be done before deleting an item, to make sure the data are not completely deleted.
253 sub move_to_deleted {
255 my $item_infos = $self->unblessed;
256 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
257 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
261 =head3 effective_itemtype
263 Returns the itemtype for the item based on whether item level itemtypes are set or not.
267 sub effective_itemtype {
270 return $self->_result()->effective_itemtype();
280 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
282 return $self->{_home_branch};
285 =head3 holding_branch
292 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
294 return $self->{_holding_branch};
299 my $biblio = $item->biblio;
301 Return the bibliographic record of this item
307 my $biblio_rs = $self->_result->biblio;
308 return Koha::Biblio->_new_from_dbic( $biblio_rs );
313 my $biblioitem = $item->biblioitem;
315 Return the biblioitem record of this item
321 my $biblioitem_rs = $self->_result->biblioitem;
322 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
327 my $checkout = $item->checkout;
329 Return the checkout for this item
335 my $checkout_rs = $self->_result->issue;
336 return unless $checkout_rs;
337 return Koha::Checkout->_new_from_dbic( $checkout_rs );
342 my $holds = $item->holds();
343 my $holds = $item->holds($params);
344 my $holds = $item->holds({ found => 'W'});
346 Return holds attached to an item, optionally accept a hashref of params to pass to search
351 my ( $self,$params ) = @_;
352 my $holds_rs = $self->_result->reserves->search($params);
353 return Koha::Holds->_new_from_dbic( $holds_rs );
358 my $transfer = $item->get_transfer;
360 Return the transfer if the item is in transit or undef
366 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
367 return unless $transfer_rs;
368 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
371 =head3 last_returned_by
373 Gets and sets the last borrower to return an item.
375 Accepts and returns Koha::Patron objects
377 $item->last_returned_by( $borrowernumber );
379 $last_returned_by = $item->last_returned_by();
383 sub last_returned_by {
384 my ( $self, $borrower ) = @_;
386 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
389 return $items_last_returned_by_rs->update_or_create(
390 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
393 unless ( $self->{_last_returned_by} ) {
394 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
396 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
400 return $self->{_last_returned_by};
404 =head3 can_article_request
406 my $bool = $item->can_article_request( $borrower )
408 Returns true if item can be specifically requested
410 $borrower must be a Koha::Patron object
414 sub can_article_request {
415 my ( $self, $borrower ) = @_;
417 my $rule = $self->article_request_type($borrower);
419 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
423 =head3 hidden_in_opac
425 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
427 Returns true if item fields match the hidding criteria defined in $rules.
428 Returns false otherwise.
430 Takes HASHref that can have the following parameters:
432 $rules : { <field> => [ value_1, ... ], ... }
434 Note: $rules inherits its structure from the parsed YAML from reading
435 the I<OpacHiddenItems> system preference.
440 my ( $self, $params ) = @_;
442 my $rules = $params->{rules} // {};
445 if C4::Context->preference('hidelostitems') and
448 my $hidden_in_opac = 0;
450 foreach my $field ( keys %{$rules} ) {
452 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
458 return $hidden_in_opac;
461 =head3 can_be_transferred
463 $item->can_be_transferred({ to => $to_library, from => $from_library })
464 Checks if an item can be transferred to given library.
466 This feature is controlled by two system preferences:
467 UseBranchTransferLimits to enable / disable the feature
468 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
469 for setting the limitations
471 Takes HASHref that can have the following parameters:
472 MANDATORY PARAMETERS:
475 $from : Koha::Library # if not given, item holdingbranch
476 # will be used instead
478 Returns 1 if item can be transferred to $to_library, otherwise 0.
480 To find out whether at least one item of a Koha::Biblio can be transferred, please
481 see Koha::Biblio->can_be_transferred() instead of using this method for
482 multiple items of the same biblio.
486 sub can_be_transferred {
487 my ($self, $params) = @_;
489 my $to = $params->{to};
490 my $from = $params->{from};
492 $to = $to->branchcode;
493 $from = defined $from ? $from->branchcode : $self->holdingbranch;
495 return 1 if $from eq $to; # Transfer to current branch is allowed
496 return 1 unless C4::Context->preference('UseBranchTransferLimits');
498 my $limittype = C4::Context->preference('BranchTransferLimitsType');
499 return Koha::Item::Transfer::Limits->search({
502 $limittype => $limittype eq 'itemtype'
503 ? $self->effective_itemtype : $self->ccode
507 =head3 pickup_locations
509 @pickup_locations = $item->pickup_locations( {patron => $patron } )
511 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)
512 and if item can be transferred to each pickup location.
516 sub pickup_locations {
517 my ($self, $params) = @_;
519 my $patron = $params->{patron};
521 my $circ_control_branch =
522 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
524 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
527 if(defined $patron) {
528 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
529 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
532 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
533 @libs = $self->home_branch->get_hold_libraries;
534 push @libs, $self->home_branch unless scalar(@libs) > 0;
535 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
536 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
537 @libs = $plib->get_hold_libraries;
538 push @libs, $self->home_branch unless scalar(@libs) > 0;
539 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
540 push @libs, $self->home_branch;
541 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
542 push @libs, $self->holding_branch;
544 @libs = Koha::Libraries->search({
547 order_by => ['branchname']
551 my @pickup_locations;
552 foreach my $library (@libs) {
553 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
554 push @pickup_locations, $library;
558 return wantarray ? @pickup_locations : \@pickup_locations;
561 =head3 article_request_type
563 my $type = $item->article_request_type( $borrower )
565 returns 'yes', 'no', 'bib_only', or 'item_only'
567 $borrower must be a Koha::Patron object
571 sub article_request_type {
572 my ( $self, $borrower ) = @_;
574 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
576 $branch_control eq 'homebranch' ? $self->homebranch
577 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
579 my $borrowertype = $borrower->categorycode;
580 my $itemtype = $self->effective_itemtype();
581 my $rule = Koha::CirculationRules->get_effective_rule(
583 rule_name => 'article_requests',
584 categorycode => $borrowertype,
585 itemtype => $itemtype,
586 branchcode => $branchcode
590 return q{} unless $rule;
591 return $rule->rule_value || q{}
600 my $attributes = { order_by => 'priority' };
601 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
603 itemnumber => $self->itemnumber,
606 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
607 waitingdate => { '!=' => undef },
610 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
611 return Koha::Holds->_new_from_dbic($hold_rs);
614 =head3 stockrotationitem
616 my $sritem = Koha::Item->stockrotationitem;
618 Returns the stock rotation item associated with the current item.
622 sub stockrotationitem {
624 my $rs = $self->_result->stockrotationitem;
626 return Koha::StockRotationItem->_new_from_dbic( $rs );
631 my $item = $item->add_to_rota($rota_id);
633 Add this item to the rota identified by $ROTA_ID, which means associating it
634 with the first stage of that rota. Should this item already be associated
635 with a rota, then we will move it to the new rota.
640 my ( $self, $rota_id ) = @_;
641 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
645 =head3 has_pending_hold
647 my $is_pending_hold = $item->has_pending_hold();
649 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
653 sub has_pending_hold {
655 my $pending_hold = $self->_result->tmp_holdsqueues;
656 return $pending_hold->count ? 1: 0;
661 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
662 my $field = $item->as_marc_field({ [ mss => $mss ] });
664 This method returns a MARC::Field object representing the Koha::Item object
665 with the current mappings configuration.
670 my ( $self, $params ) = @_;
672 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
673 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
677 my @columns = $self->_result->result_source->columns;
679 foreach my $item_field ( @columns ) {
680 my $mapping = $mss->{ "items.$item_field"}[0];
681 my $tagfield = $mapping->{tagfield};
682 my $tagsubfield = $mapping->{tagsubfield};
683 next if !$tagfield; # TODO: Should we raise an exception instead?
684 # Feels like safe fallback is better
686 push @subfields, $tagsubfield => $self->$item_field;
689 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
690 push( @subfields, @{$unlinked_item_subfields} )
691 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
695 $field = MARC::Field->new(
696 "$item_tag", ' ', ' ', @subfields
702 =head3 renewal_branchcode
704 Returns the branchcode to be recorded in statistics renewal of the item
708 sub renewal_branchcode {
710 my ($self, $params ) = @_;
712 my $interface = C4::Context->interface;
714 if ( $interface eq 'opac' ){
715 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
716 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
717 $branchcode = 'OPACRenew';
719 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
720 $branchcode = $self->homebranch;
722 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
723 $branchcode = $self->checkout->patron->branchcode;
725 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
726 $branchcode = $self->checkout->branchcode;
732 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
733 ? C4::Context->userenv->{branch} : $params->{branch};
738 =head3 to_api_mapping
740 This method returns the mapping for representing a Koha::Item object
747 itemnumber => 'item_id',
748 biblionumber => 'biblio_id',
749 biblioitemnumber => undef,
750 barcode => 'external_id',
751 dateaccessioned => 'acquisition_date',
752 booksellerid => 'acquisition_source',
753 homebranch => 'home_library_id',
754 price => 'purchase_price',
755 replacementprice => 'replacement_price',
756 replacementpricedate => 'replacement_price_date',
757 datelastborrowed => 'last_checkout_date',
758 datelastseen => 'last_seen_date',
760 notforloan => 'not_for_loan_status',
761 damaged => 'damaged_status',
762 damaged_on => 'damaged_date',
763 itemlost => 'lost_status',
764 itemlost_on => 'lost_date',
765 withdrawn => 'withdrawn',
766 withdrawn_on => 'withdrawn_date',
767 itemcallnumber => 'callnumber',
768 coded_location_qualifier => 'coded_location_qualifier',
769 issues => 'checkouts_count',
770 renewals => 'renewals_count',
771 reserves => 'holds_count',
772 restricted => 'restricted_status',
773 itemnotes => 'public_notes',
774 itemnotes_nonpublic => 'internal_notes',
775 holdingbranch => 'holding_library_id',
777 timestamp => 'timestamp',
778 location => 'location',
779 permanent_location => 'permanent_location',
780 onloan => 'checked_out_date',
781 cn_source => 'call_number_source',
782 cn_sort => 'call_number_sort',
783 ccode => 'collection_code',
784 materials => 'materials_notes',
786 itype => 'item_type',
787 more_subfields_xml => 'extended_subfields',
788 enumchron => 'serial_issue_number',
789 copynumber => 'copy_number',
790 stocknumber => 'inventory_number',
791 new_status => 'new_status'
795 =head2 Internal methods
797 =head3 _after_item_action_hooks
799 Helper method that takes care of calling all plugin hooks
803 sub _after_item_action_hooks {
804 my ( $self, $params ) = @_;
806 my $action = $params->{action};
808 if ( C4::Context->config("enable_plugins") ) {
810 my @plugins = Koha::Plugins->new->GetPlugins({
811 method => 'after_item_action',
816 foreach my $plugin ( @plugins ) {
818 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
838 Kyle M Hall <kyle@bywatersolutions.com>