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 if ( $self->itemcallnumber ) { # This could be improved, we should recalculate it only if changed
80 my $cn_sort = GetClassSort($self->cn_source, $self->itemcallnumber, "");
81 $self->cn_sort($cn_sort);
84 my $today = dt_from_string;
85 unless ( $self->in_storage ) { #AddItem
86 unless ( $self->permanent_location ) {
87 $self->permanent_location($self->location);
89 unless ( $self->replacementpricedate ) {
90 $self->replacementpricedate($today);
92 unless ( $self->datelastseen ) {
93 $self->datelastseen($today);
96 unless ( $self->dateaccessioned ) {
97 $self->dateaccessioned($today);
100 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
102 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
103 if $log_action && C4::Context->preference("CataloguingLog");
105 $self->_after_item_action_hooks({ action => 'create' });
109 { # Update *_on fields if needed
110 # Why not for AddItem as well?
111 my @fields = qw( itemlost withdrawn damaged );
113 # Only retrieve the item if we need to set an "on" date field
114 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
115 my $pre_mod_item = $self->get_from_storage;
116 for my $field (@fields) {
118 and not $pre_mod_item->$field )
120 my $field_on = "${field}_on";
122 DateTime::Format::MySQL->format_datetime( dt_from_string() )
128 # If the field is defined but empty, we are removing and,
129 # and thus need to clear out the 'on' field as well
130 for my $field (@fields) {
131 if ( defined( $self->$field ) && !$self->$field ) {
132 my $field_on = "${field}_on";
133 $self->$field_on(undef);
138 my %updated_columns = $self->_result->get_dirty_columns;
139 return $self->SUPER::store unless %updated_columns;
140 if ( defined $self->location
141 and $self->location ne 'CART'
142 and $self->location ne 'PROC'
143 and not exists $updated_columns{permanent_location} )
145 $self->permanent_location( $self->location );
148 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
150 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
152 $self->_after_item_action_hooks({ action => 'modify' });
154 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
155 if $log_action && C4::Context->preference("CataloguingLog");
158 unless ( $self->dateaccessioned ) {
159 $self->dateaccessioned($today);
162 return $self->SUPER::store;
172 # FIXME check the item has no current issues
173 # i.e. raise the appropriate exception
175 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
177 $self->_after_item_action_hooks({ action => 'delete' });
179 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
180 if C4::Context->preference("CataloguingLog");
182 return $self->SUPER::delete;
192 my $safe_to_delete = $self->safe_to_delete;
193 return $safe_to_delete unless $safe_to_delete eq '1';
195 $self->move_to_deleted;
197 return $self->delete;
200 =head3 safe_to_delete
202 returns 1 if the item is safe to delete,
204 "book_on_loan" if the item is checked out,
206 "not_same_branch" if the item is blocked by independent branches,
208 "book_reserved" if the there are holds aganst the item, or
210 "linked_analytics" if the item has linked analytic records.
217 return "book_on_loan" if $self->checkout;
219 return "not_same_branch"
220 if defined C4::Context->userenv
221 and !C4::Context->IsSuperLibrarian()
222 and C4::Context->preference("IndependentBranches")
223 and ( C4::Context->userenv->{branch} ne $self->homebranch );
225 # check it doesn't have a waiting reserve
226 return "book_reserved"
227 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
229 return "linked_analytics"
230 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
235 =head3 move_to_deleted
237 my $is_moved = $item->move_to_deleted;
239 Move an item to the deleteditems table.
240 This can be done before deleting an item, to make sure the data are not completely deleted.
244 sub move_to_deleted {
246 my $item_infos = $self->unblessed;
247 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
248 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
252 =head3 effective_itemtype
254 Returns the itemtype for the item based on whether item level itemtypes are set or not.
258 sub effective_itemtype {
261 return $self->_result()->effective_itemtype();
271 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
273 return $self->{_home_branch};
276 =head3 holding_branch
283 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
285 return $self->{_holding_branch};
290 my $biblio = $item->biblio;
292 Return the bibliographic record of this item
298 my $biblio_rs = $self->_result->biblio;
299 return Koha::Biblio->_new_from_dbic( $biblio_rs );
304 my $biblioitem = $item->biblioitem;
306 Return the biblioitem record of this item
312 my $biblioitem_rs = $self->_result->biblioitem;
313 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
318 my $checkout = $item->checkout;
320 Return the checkout for this item
326 my $checkout_rs = $self->_result->issue;
327 return unless $checkout_rs;
328 return Koha::Checkout->_new_from_dbic( $checkout_rs );
333 my $holds = $item->holds();
334 my $holds = $item->holds($params);
335 my $holds = $item->holds({ found => 'W'});
337 Return holds attached to an item, optionally accept a hashref of params to pass to search
342 my ( $self,$params ) = @_;
343 my $holds_rs = $self->_result->reserves->search($params);
344 return Koha::Holds->_new_from_dbic( $holds_rs );
349 my $transfer = $item->get_transfer;
351 Return the transfer if the item is in transit or undef
357 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
358 return unless $transfer_rs;
359 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
362 =head3 last_returned_by
364 Gets and sets the last borrower to return an item.
366 Accepts and returns Koha::Patron objects
368 $item->last_returned_by( $borrowernumber );
370 $last_returned_by = $item->last_returned_by();
374 sub last_returned_by {
375 my ( $self, $borrower ) = @_;
377 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
380 return $items_last_returned_by_rs->update_or_create(
381 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
384 unless ( $self->{_last_returned_by} ) {
385 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
387 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
391 return $self->{_last_returned_by};
395 =head3 can_article_request
397 my $bool = $item->can_article_request( $borrower )
399 Returns true if item can be specifically requested
401 $borrower must be a Koha::Patron object
405 sub can_article_request {
406 my ( $self, $borrower ) = @_;
408 my $rule = $self->article_request_type($borrower);
410 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
414 =head3 hidden_in_opac
416 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
418 Returns true if item fields match the hidding criteria defined in $rules.
419 Returns false otherwise.
421 Takes HASHref that can have the following parameters:
423 $rules : { <field> => [ value_1, ... ], ... }
425 Note: $rules inherits its structure from the parsed YAML from reading
426 the I<OpacHiddenItems> system preference.
431 my ( $self, $params ) = @_;
433 my $rules = $params->{rules} // {};
436 if C4::Context->preference('hidelostitems') and
439 my $hidden_in_opac = 0;
441 foreach my $field ( keys %{$rules} ) {
443 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
449 return $hidden_in_opac;
452 =head3 can_be_transferred
454 $item->can_be_transferred({ to => $to_library, from => $from_library })
455 Checks if an item can be transferred to given library.
457 This feature is controlled by two system preferences:
458 UseBranchTransferLimits to enable / disable the feature
459 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
460 for setting the limitations
462 Takes HASHref that can have the following parameters:
463 MANDATORY PARAMETERS:
466 $from : Koha::Library # if not given, item holdingbranch
467 # will be used instead
469 Returns 1 if item can be transferred to $to_library, otherwise 0.
471 To find out whether at least one item of a Koha::Biblio can be transferred, please
472 see Koha::Biblio->can_be_transferred() instead of using this method for
473 multiple items of the same biblio.
477 sub can_be_transferred {
478 my ($self, $params) = @_;
480 my $to = $params->{to};
481 my $from = $params->{from};
483 $to = $to->branchcode;
484 $from = defined $from ? $from->branchcode : $self->holdingbranch;
486 return 1 if $from eq $to; # Transfer to current branch is allowed
487 return 1 unless C4::Context->preference('UseBranchTransferLimits');
489 my $limittype = C4::Context->preference('BranchTransferLimitsType');
490 return Koha::Item::Transfer::Limits->search({
493 $limittype => $limittype eq 'itemtype'
494 ? $self->effective_itemtype : $self->ccode
498 =head3 pickup_locations
500 @pickup_locations = $item->pickup_locations( {patron => $patron } )
502 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)
503 and if item can be transferred to each pickup location.
507 sub pickup_locations {
508 my ($self, $params) = @_;
510 my $patron = $params->{patron};
512 my $circ_control_branch =
513 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
515 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
518 if(defined $patron) {
519 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
520 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
523 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
524 @libs = $self->home_branch->get_hold_libraries;
525 push @libs, $self->home_branch unless scalar(@libs) > 0;
526 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
527 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
528 @libs = $plib->get_hold_libraries;
529 push @libs, $self->home_branch unless scalar(@libs) > 0;
530 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
531 push @libs, $self->home_branch;
532 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
533 push @libs, $self->holding_branch;
535 @libs = Koha::Libraries->search({
538 order_by => ['branchname']
542 my @pickup_locations;
543 foreach my $library (@libs) {
544 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
545 push @pickup_locations, $library;
549 return wantarray ? @pickup_locations : \@pickup_locations;
552 =head3 article_request_type
554 my $type = $item->article_request_type( $borrower )
556 returns 'yes', 'no', 'bib_only', or 'item_only'
558 $borrower must be a Koha::Patron object
562 sub article_request_type {
563 my ( $self, $borrower ) = @_;
565 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
567 $branch_control eq 'homebranch' ? $self->homebranch
568 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
570 my $borrowertype = $borrower->categorycode;
571 my $itemtype = $self->effective_itemtype();
572 my $rule = Koha::CirculationRules->get_effective_rule(
574 rule_name => 'article_requests',
575 categorycode => $borrowertype,
576 itemtype => $itemtype,
577 branchcode => $branchcode
581 return q{} unless $rule;
582 return $rule->rule_value || q{}
591 my $attributes = { order_by => 'priority' };
592 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
594 itemnumber => $self->itemnumber,
597 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
598 waitingdate => { '!=' => undef },
601 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
602 return Koha::Holds->_new_from_dbic($hold_rs);
611 my $hold_rs = $self->_result->reserves->search;
612 return Koha::Holds->_new_from_dbic($hold_rs);
615 =head3 stockrotationitem
617 my $sritem = Koha::Item->stockrotationitem;
619 Returns the stock rotation item associated with the current item.
623 sub stockrotationitem {
625 my $rs = $self->_result->stockrotationitem;
627 return Koha::StockRotationItem->_new_from_dbic( $rs );
632 my $item = $item->add_to_rota($rota_id);
634 Add this item to the rota identified by $ROTA_ID, which means associating it
635 with the first stage of that rota. Should this item already be associated
636 with a rota, then we will move it to the new rota.
641 my ( $self, $rota_id ) = @_;
642 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
646 =head3 has_pending_hold
648 my $is_pending_hold = $item->has_pending_hold();
650 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
654 sub has_pending_hold {
656 my $pending_hold = $self->_result->tmp_holdsqueues;
657 return $pending_hold->count ? 1: 0;
662 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
663 my $field = $item->as_marc_field({ [ mss => $mss ] });
665 This method returns a MARC::Field object representing the Koha::Item object
666 with the current mappings configuration.
671 my ( $self, $params ) = @_;
673 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
674 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
678 my @columns = $self->_result->result_source->columns;
680 foreach my $item_field ( @columns ) {
681 my $mapping = $mss->{ "items.$item_field"}[0];
682 my $tagfield = $mapping->{tagfield};
683 my $tagsubfield = $mapping->{tagsubfield};
684 next if !$tagfield; # TODO: Should we raise an exception instead?
685 # Feels like safe fallback is better
687 push @subfields, $tagsubfield => $self->$item_field;
690 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
691 push( @subfields, @{$unlinked_item_subfields} )
692 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
696 $field = MARC::Field->new(
697 "$item_tag", ' ', ' ', @subfields
703 =head3 to_api_mapping
705 This method returns the mapping for representing a Koha::Item object
712 itemnumber => 'item_id',
713 biblionumber => 'biblio_id',
714 biblioitemnumber => undef,
715 barcode => 'external_id',
716 dateaccessioned => 'acquisition_date',
717 booksellerid => 'acquisition_source',
718 homebranch => 'home_library_id',
719 price => 'purchase_price',
720 replacementprice => 'replacement_price',
721 replacementpricedate => 'replacement_price_date',
722 datelastborrowed => 'last_checkout_date',
723 datelastseen => 'last_seen_date',
725 notforloan => 'not_for_loan_status',
726 damaged => 'damaged_status',
727 damaged_on => 'damaged_date',
728 itemlost => 'lost_status',
729 itemlost_on => 'lost_date',
730 withdrawn => 'withdrawn',
731 withdrawn_on => 'withdrawn_date',
732 itemcallnumber => 'callnumber',
733 coded_location_qualifier => 'coded_location_qualifier',
734 issues => 'checkouts_count',
735 renewals => 'renewals_count',
736 reserves => 'holds_count',
737 restricted => 'restricted_status',
738 itemnotes => 'public_notes',
739 itemnotes_nonpublic => 'internal_notes',
740 holdingbranch => 'holding_library_id',
742 timestamp => 'timestamp',
743 location => 'location',
744 permanent_location => 'permanent_location',
745 onloan => 'checked_out_date',
746 cn_source => 'call_number_source',
747 cn_sort => 'call_number_sort',
748 ccode => 'collection_code',
749 materials => 'materials_notes',
751 itype => 'item_type',
752 more_subfields_xml => 'extended_subfields',
753 enumchron => 'serial_issue_number',
754 copynumber => 'copy_number',
755 stocknumber => 'inventory_number',
756 new_status => 'new_status'
760 =head2 Internal methods
762 =head3 _after_item_action_hooks
764 Helper method that takes care of calling all plugin hooks
768 sub _after_item_action_hooks {
769 my ( $self, $params ) = @_;
771 my $action = $params->{action};
773 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
775 my @plugins = Koha::Plugins->new->GetPlugins({
776 method => 'after_item_action',
781 foreach my $plugin ( @plugins ) {
783 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
803 Kyle M Hall <kyle@bywatersolutions.com>