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 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
161 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
163 $self->_after_item_action_hooks({ action => 'modify' });
165 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
166 if $log_action && C4::Context->preference("CataloguingLog");
169 unless ( $self->dateaccessioned ) {
170 $self->dateaccessioned($today);
173 return $self->SUPER::store;
183 # FIXME check the item has no current issues
184 # i.e. raise the appropriate exception
186 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
188 $self->_after_item_action_hooks({ action => 'delete' });
190 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
191 if C4::Context->preference("CataloguingLog");
193 return $self->SUPER::delete;
203 my $safe_to_delete = $self->safe_to_delete;
204 return $safe_to_delete unless $safe_to_delete eq '1';
206 $self->move_to_deleted;
208 return $self->delete;
211 =head3 safe_to_delete
213 returns 1 if the item is safe to delete,
215 "book_on_loan" if the item is checked out,
217 "not_same_branch" if the item is blocked by independent branches,
219 "book_reserved" if the there are holds aganst the item, or
221 "linked_analytics" if the item has linked analytic records.
228 return "book_on_loan" if $self->checkout;
230 return "not_same_branch"
231 if defined C4::Context->userenv
232 and !C4::Context->IsSuperLibrarian()
233 and C4::Context->preference("IndependentBranches")
234 and ( C4::Context->userenv->{branch} ne $self->homebranch );
236 # check it doesn't have a waiting reserve
237 return "book_reserved"
238 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
240 return "linked_analytics"
241 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
246 =head3 move_to_deleted
248 my $is_moved = $item->move_to_deleted;
250 Move an item to the deleteditems table.
251 This can be done before deleting an item, to make sure the data are not completely deleted.
255 sub move_to_deleted {
257 my $item_infos = $self->unblessed;
258 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
259 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
263 =head3 effective_itemtype
265 Returns the itemtype for the item based on whether item level itemtypes are set or not.
269 sub effective_itemtype {
272 return $self->_result()->effective_itemtype();
282 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
284 return $self->{_home_branch};
287 =head3 holding_branch
294 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
296 return $self->{_holding_branch};
301 my $biblio = $item->biblio;
303 Return the bibliographic record of this item
309 my $biblio_rs = $self->_result->biblio;
310 return Koha::Biblio->_new_from_dbic( $biblio_rs );
315 my $biblioitem = $item->biblioitem;
317 Return the biblioitem record of this item
323 my $biblioitem_rs = $self->_result->biblioitem;
324 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
329 my $checkout = $item->checkout;
331 Return the checkout for this item
337 my $checkout_rs = $self->_result->issue;
338 return unless $checkout_rs;
339 return Koha::Checkout->_new_from_dbic( $checkout_rs );
344 my $holds = $item->holds();
345 my $holds = $item->holds($params);
346 my $holds = $item->holds({ found => 'W'});
348 Return holds attached to an item, optionally accept a hashref of params to pass to search
353 my ( $self,$params ) = @_;
354 my $holds_rs = $self->_result->reserves->search($params);
355 return Koha::Holds->_new_from_dbic( $holds_rs );
360 my $transfer = $item->get_transfer;
362 Return the transfer if the item is in transit or undef
368 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
369 return unless $transfer_rs;
370 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
373 =head3 last_returned_by
375 Gets and sets the last borrower to return an item.
377 Accepts and returns Koha::Patron objects
379 $item->last_returned_by( $borrowernumber );
381 $last_returned_by = $item->last_returned_by();
385 sub last_returned_by {
386 my ( $self, $borrower ) = @_;
388 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
391 return $items_last_returned_by_rs->update_or_create(
392 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
395 unless ( $self->{_last_returned_by} ) {
396 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
398 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
402 return $self->{_last_returned_by};
406 =head3 can_article_request
408 my $bool = $item->can_article_request( $borrower )
410 Returns true if item can be specifically requested
412 $borrower must be a Koha::Patron object
416 sub can_article_request {
417 my ( $self, $borrower ) = @_;
419 my $rule = $self->article_request_type($borrower);
421 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
425 =head3 hidden_in_opac
427 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
429 Returns true if item fields match the hidding criteria defined in $rules.
430 Returns false otherwise.
432 Takes HASHref that can have the following parameters:
434 $rules : { <field> => [ value_1, ... ], ... }
436 Note: $rules inherits its structure from the parsed YAML from reading
437 the I<OpacHiddenItems> system preference.
442 my ( $self, $params ) = @_;
444 my $rules = $params->{rules} // {};
447 if C4::Context->preference('hidelostitems') and
450 my $hidden_in_opac = 0;
452 foreach my $field ( keys %{$rules} ) {
454 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
460 return $hidden_in_opac;
463 =head3 can_be_transferred
465 $item->can_be_transferred({ to => $to_library, from => $from_library })
466 Checks if an item can be transferred to given library.
468 This feature is controlled by two system preferences:
469 UseBranchTransferLimits to enable / disable the feature
470 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
471 for setting the limitations
473 Takes HASHref that can have the following parameters:
474 MANDATORY PARAMETERS:
477 $from : Koha::Library # if not given, item holdingbranch
478 # will be used instead
480 Returns 1 if item can be transferred to $to_library, otherwise 0.
482 To find out whether at least one item of a Koha::Biblio can be transferred, please
483 see Koha::Biblio->can_be_transferred() instead of using this method for
484 multiple items of the same biblio.
488 sub can_be_transferred {
489 my ($self, $params) = @_;
491 my $to = $params->{to};
492 my $from = $params->{from};
494 $to = $to->branchcode;
495 $from = defined $from ? $from->branchcode : $self->holdingbranch;
497 return 1 if $from eq $to; # Transfer to current branch is allowed
498 return 1 unless C4::Context->preference('UseBranchTransferLimits');
500 my $limittype = C4::Context->preference('BranchTransferLimitsType');
501 return Koha::Item::Transfer::Limits->search({
504 $limittype => $limittype eq 'itemtype'
505 ? $self->effective_itemtype : $self->ccode
509 =head3 pickup_locations
511 @pickup_locations = $item->pickup_locations( {patron => $patron } )
513 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)
514 and if item can be transferred to each pickup location.
518 sub pickup_locations {
519 my ($self, $params) = @_;
521 my $patron = $params->{patron};
523 my $circ_control_branch =
524 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
526 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
529 if(defined $patron) {
530 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
531 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
534 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
535 @libs = $self->home_branch->get_hold_libraries;
536 push @libs, $self->home_branch unless scalar(@libs) > 0;
537 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
538 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
539 @libs = $plib->get_hold_libraries;
540 push @libs, $self->home_branch unless scalar(@libs) > 0;
541 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
542 push @libs, $self->home_branch;
543 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
544 push @libs, $self->holding_branch;
546 @libs = Koha::Libraries->search({
549 order_by => ['branchname']
553 my @pickup_locations;
554 foreach my $library (@libs) {
555 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
556 push @pickup_locations, $library;
560 return wantarray ? @pickup_locations : \@pickup_locations;
563 =head3 article_request_type
565 my $type = $item->article_request_type( $borrower )
567 returns 'yes', 'no', 'bib_only', or 'item_only'
569 $borrower must be a Koha::Patron object
573 sub article_request_type {
574 my ( $self, $borrower ) = @_;
576 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
578 $branch_control eq 'homebranch' ? $self->homebranch
579 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
581 my $borrowertype = $borrower->categorycode;
582 my $itemtype = $self->effective_itemtype();
583 my $rule = Koha::CirculationRules->get_effective_rule(
585 rule_name => 'article_requests',
586 categorycode => $borrowertype,
587 itemtype => $itemtype,
588 branchcode => $branchcode
592 return q{} unless $rule;
593 return $rule->rule_value || q{}
602 my $attributes = { order_by => 'priority' };
603 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
605 itemnumber => $self->itemnumber,
608 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
609 waitingdate => { '!=' => undef },
612 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
613 return Koha::Holds->_new_from_dbic($hold_rs);
616 =head3 stockrotationitem
618 my $sritem = Koha::Item->stockrotationitem;
620 Returns the stock rotation item associated with the current item.
624 sub stockrotationitem {
626 my $rs = $self->_result->stockrotationitem;
628 return Koha::StockRotationItem->_new_from_dbic( $rs );
633 my $item = $item->add_to_rota($rota_id);
635 Add this item to the rota identified by $ROTA_ID, which means associating it
636 with the first stage of that rota. Should this item already be associated
637 with a rota, then we will move it to the new rota.
642 my ( $self, $rota_id ) = @_;
643 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
647 =head3 has_pending_hold
649 my $is_pending_hold = $item->has_pending_hold();
651 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
655 sub has_pending_hold {
657 my $pending_hold = $self->_result->tmp_holdsqueues;
658 return $pending_hold->count ? 1: 0;
663 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
664 my $field = $item->as_marc_field({ [ mss => $mss ] });
666 This method returns a MARC::Field object representing the Koha::Item object
667 with the current mappings configuration.
672 my ( $self, $params ) = @_;
674 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
675 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
679 my @columns = $self->_result->result_source->columns;
681 foreach my $item_field ( @columns ) {
682 my $mapping = $mss->{ "items.$item_field"}[0];
683 my $tagfield = $mapping->{tagfield};
684 my $tagsubfield = $mapping->{tagsubfield};
685 next if !$tagfield; # TODO: Should we raise an exception instead?
686 # Feels like safe fallback is better
688 push @subfields, $tagsubfield => $self->$item_field;
691 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
692 push( @subfields, @{$unlinked_item_subfields} )
693 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
697 $field = MARC::Field->new(
698 "$item_tag", ' ', ' ', @subfields
704 =head3 renewal_branchcode
706 Returns the branchcode to be recorded in statistics renewal of the item
710 sub renewal_branchcode {
712 my ($self, $params ) = @_;
714 my $interface = C4::Context->interface;
716 if ( $interface eq 'opac' ){
717 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
718 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
719 $branchcode = 'OPACRenew';
721 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
722 $branchcode = $self->homebranch;
724 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
725 $branchcode = $self->checkout->patron->branchcode;
727 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
728 $branchcode = $self->checkout->branchcode;
734 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
735 ? C4::Context->userenv->{branch} : $params->{branch};
740 =head3 to_api_mapping
742 This method returns the mapping for representing a Koha::Item object
749 itemnumber => 'item_id',
750 biblionumber => 'biblio_id',
751 biblioitemnumber => undef,
752 barcode => 'external_id',
753 dateaccessioned => 'acquisition_date',
754 booksellerid => 'acquisition_source',
755 homebranch => 'home_library_id',
756 price => 'purchase_price',
757 replacementprice => 'replacement_price',
758 replacementpricedate => 'replacement_price_date',
759 datelastborrowed => 'last_checkout_date',
760 datelastseen => 'last_seen_date',
762 notforloan => 'not_for_loan_status',
763 damaged => 'damaged_status',
764 damaged_on => 'damaged_date',
765 itemlost => 'lost_status',
766 itemlost_on => 'lost_date',
767 withdrawn => 'withdrawn',
768 withdrawn_on => 'withdrawn_date',
769 itemcallnumber => 'callnumber',
770 coded_location_qualifier => 'coded_location_qualifier',
771 issues => 'checkouts_count',
772 renewals => 'renewals_count',
773 reserves => 'holds_count',
774 restricted => 'restricted_status',
775 itemnotes => 'public_notes',
776 itemnotes_nonpublic => 'internal_notes',
777 holdingbranch => 'holding_library_id',
779 timestamp => 'timestamp',
780 location => 'location',
781 permanent_location => 'permanent_location',
782 onloan => 'checked_out_date',
783 cn_source => 'call_number_source',
784 cn_sort => 'call_number_sort',
785 ccode => 'collection_code',
786 materials => 'materials_notes',
788 itype => 'item_type',
789 more_subfields_xml => 'extended_subfields',
790 enumchron => 'serial_issue_number',
791 copynumber => 'copy_number',
792 stocknumber => 'inventory_number',
793 new_status => 'new_status'
797 =head2 Internal methods
799 =head3 _after_item_action_hooks
801 Helper method that takes care of calling all plugin hooks
805 sub _after_item_action_hooks {
806 my ( $self, $params ) = @_;
808 my $action = $params->{action};
810 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
812 my @plugins = Koha::Plugins->new->GetPlugins({
813 method => 'after_item_action',
818 foreach my $plugin ( @plugins ) {
820 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
840 Kyle M Hall <kyle@bywatersolutions.com>