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 %updated_columns = $self->_result->get_dirty_columns;
80 if ( exists $updated_columns{itemcallnumber} ) {
81 my $cn_sort = GetClassSort($self->cn_source, $self->itemcallnumber, "");
82 $self->cn_sort($cn_sort);
85 my $today = dt_from_string;
86 unless ( $self->in_storage ) { #AddItem
87 unless ( $self->permanent_location ) {
88 $self->permanent_location($self->location);
90 unless ( $self->replacementpricedate ) {
91 $self->replacementpricedate($today);
93 unless ( $self->datelastseen ) {
94 $self->datelastseen($today);
97 unless ( $self->dateaccessioned ) {
98 $self->dateaccessioned($today);
101 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
103 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
104 if $log_action && C4::Context->preference("CataloguingLog");
106 $self->_after_item_action_hooks({ action => 'create' });
110 { # Update *_on fields if needed
111 # Why not for AddItem as well?
112 my @fields = qw( itemlost withdrawn damaged );
114 # Only retrieve the item if we need to set an "on" date field
115 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
116 my $pre_mod_item = $self->get_from_storage;
117 for my $field (@fields) {
119 and not $pre_mod_item->$field )
121 my $field_on = "${field}_on";
123 DateTime::Format::MySQL->format_datetime( dt_from_string() )
129 # If the field is defined but empty, we are removing and,
130 # and thus need to clear out the 'on' field as well
131 for my $field (@fields) {
132 if ( defined( $self->$field ) && !$self->$field ) {
133 my $field_on = "${field}_on";
134 $self->$field_on(undef);
139 %updated_columns = $self->_result->get_dirty_columns;
140 return $self->SUPER::store unless %updated_columns;
141 if ( exists $updated_columns{location}
142 and $self->location ne 'CART'
143 and $self->location ne 'PROC'
144 and not exists $updated_columns{permanent_location} )
146 $self->permanent_location( $self->location );
149 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
151 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
153 $self->_after_item_action_hooks({ action => 'modify' });
155 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
156 if $log_action && C4::Context->preference("CataloguingLog");
159 unless ( $self->dateaccessioned ) {
160 $self->dateaccessioned($today);
163 return $self->SUPER::store;
173 # FIXME check the item has no current issues
174 # i.e. raise the appropriate exception
176 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
178 $self->_after_item_action_hooks({ action => 'delete' });
180 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
181 if C4::Context->preference("CataloguingLog");
183 return $self->SUPER::delete;
193 my $safe_to_delete = $self->safe_to_delete;
194 return $safe_to_delete unless $safe_to_delete eq '1';
196 $self->move_to_deleted;
198 return $self->delete;
201 =head3 safe_to_delete
203 returns 1 if the item is safe to delete,
205 "book_on_loan" if the item is checked out,
207 "not_same_branch" if the item is blocked by independent branches,
209 "book_reserved" if the there are holds aganst the item, or
211 "linked_analytics" if the item has linked analytic records.
218 return "book_on_loan" if $self->checkout;
220 return "not_same_branch"
221 if defined C4::Context->userenv
222 and !C4::Context->IsSuperLibrarian()
223 and C4::Context->preference("IndependentBranches")
224 and ( C4::Context->userenv->{branch} ne $self->homebranch );
226 # check it doesn't have a waiting reserve
227 return "book_reserved"
228 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
230 return "linked_analytics"
231 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
236 =head3 move_to_deleted
238 my $is_moved = $item->move_to_deleted;
240 Move an item to the deleteditems table.
241 This can be done before deleting an item, to make sure the data are not completely deleted.
245 sub move_to_deleted {
247 my $item_infos = $self->unblessed;
248 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
249 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
253 =head3 effective_itemtype
255 Returns the itemtype for the item based on whether item level itemtypes are set or not.
259 sub effective_itemtype {
262 return $self->_result()->effective_itemtype();
272 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
274 return $self->{_home_branch};
277 =head3 holding_branch
284 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
286 return $self->{_holding_branch};
291 my $biblio = $item->biblio;
293 Return the bibliographic record of this item
299 my $biblio_rs = $self->_result->biblio;
300 return Koha::Biblio->_new_from_dbic( $biblio_rs );
305 my $biblioitem = $item->biblioitem;
307 Return the biblioitem record of this item
313 my $biblioitem_rs = $self->_result->biblioitem;
314 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
319 my $checkout = $item->checkout;
321 Return the checkout for this item
327 my $checkout_rs = $self->_result->issue;
328 return unless $checkout_rs;
329 return Koha::Checkout->_new_from_dbic( $checkout_rs );
334 my $holds = $item->holds();
335 my $holds = $item->holds($params);
336 my $holds = $item->holds({ found => 'W'});
338 Return holds attached to an item, optionally accept a hashref of params to pass to search
343 my ( $self,$params ) = @_;
344 my $holds_rs = $self->_result->reserves->search($params);
345 return Koha::Holds->_new_from_dbic( $holds_rs );
350 my $transfer = $item->get_transfer;
352 Return the transfer if the item is in transit or undef
358 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
359 return unless $transfer_rs;
360 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
363 =head3 last_returned_by
365 Gets and sets the last borrower to return an item.
367 Accepts and returns Koha::Patron objects
369 $item->last_returned_by( $borrowernumber );
371 $last_returned_by = $item->last_returned_by();
375 sub last_returned_by {
376 my ( $self, $borrower ) = @_;
378 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
381 return $items_last_returned_by_rs->update_or_create(
382 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
385 unless ( $self->{_last_returned_by} ) {
386 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
388 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
392 return $self->{_last_returned_by};
396 =head3 can_article_request
398 my $bool = $item->can_article_request( $borrower )
400 Returns true if item can be specifically requested
402 $borrower must be a Koha::Patron object
406 sub can_article_request {
407 my ( $self, $borrower ) = @_;
409 my $rule = $self->article_request_type($borrower);
411 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
415 =head3 hidden_in_opac
417 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
419 Returns true if item fields match the hidding criteria defined in $rules.
420 Returns false otherwise.
422 Takes HASHref that can have the following parameters:
424 $rules : { <field> => [ value_1, ... ], ... }
426 Note: $rules inherits its structure from the parsed YAML from reading
427 the I<OpacHiddenItems> system preference.
432 my ( $self, $params ) = @_;
434 my $rules = $params->{rules} // {};
437 if C4::Context->preference('hidelostitems') and
440 my $hidden_in_opac = 0;
442 foreach my $field ( keys %{$rules} ) {
444 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
450 return $hidden_in_opac;
453 =head3 can_be_transferred
455 $item->can_be_transferred({ to => $to_library, from => $from_library })
456 Checks if an item can be transferred to given library.
458 This feature is controlled by two system preferences:
459 UseBranchTransferLimits to enable / disable the feature
460 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
461 for setting the limitations
463 Takes HASHref that can have the following parameters:
464 MANDATORY PARAMETERS:
467 $from : Koha::Library # if not given, item holdingbranch
468 # will be used instead
470 Returns 1 if item can be transferred to $to_library, otherwise 0.
472 To find out whether at least one item of a Koha::Biblio can be transferred, please
473 see Koha::Biblio->can_be_transferred() instead of using this method for
474 multiple items of the same biblio.
478 sub can_be_transferred {
479 my ($self, $params) = @_;
481 my $to = $params->{to};
482 my $from = $params->{from};
484 $to = $to->branchcode;
485 $from = defined $from ? $from->branchcode : $self->holdingbranch;
487 return 1 if $from eq $to; # Transfer to current branch is allowed
488 return 1 unless C4::Context->preference('UseBranchTransferLimits');
490 my $limittype = C4::Context->preference('BranchTransferLimitsType');
491 return Koha::Item::Transfer::Limits->search({
494 $limittype => $limittype eq 'itemtype'
495 ? $self->effective_itemtype : $self->ccode
499 =head3 pickup_locations
501 @pickup_locations = $item->pickup_locations( {patron => $patron } )
503 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)
504 and if item can be transferred to each pickup location.
508 sub pickup_locations {
509 my ($self, $params) = @_;
511 my $patron = $params->{patron};
513 my $circ_control_branch =
514 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
516 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
519 if(defined $patron) {
520 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
521 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
524 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
525 @libs = $self->home_branch->get_hold_libraries;
526 push @libs, $self->home_branch unless scalar(@libs) > 0;
527 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
528 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
529 @libs = $plib->get_hold_libraries;
530 push @libs, $self->home_branch unless scalar(@libs) > 0;
531 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
532 push @libs, $self->home_branch;
533 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
534 push @libs, $self->holding_branch;
536 @libs = Koha::Libraries->search({
539 order_by => ['branchname']
543 my @pickup_locations;
544 foreach my $library (@libs) {
545 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
546 push @pickup_locations, $library;
550 return wantarray ? @pickup_locations : \@pickup_locations;
553 =head3 article_request_type
555 my $type = $item->article_request_type( $borrower )
557 returns 'yes', 'no', 'bib_only', or 'item_only'
559 $borrower must be a Koha::Patron object
563 sub article_request_type {
564 my ( $self, $borrower ) = @_;
566 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
568 $branch_control eq 'homebranch' ? $self->homebranch
569 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
571 my $borrowertype = $borrower->categorycode;
572 my $itemtype = $self->effective_itemtype();
573 my $rule = Koha::CirculationRules->get_effective_rule(
575 rule_name => 'article_requests',
576 categorycode => $borrowertype,
577 itemtype => $itemtype,
578 branchcode => $branchcode
582 return q{} unless $rule;
583 return $rule->rule_value || q{}
592 my $attributes = { order_by => 'priority' };
593 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
595 itemnumber => $self->itemnumber,
598 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
599 waitingdate => { '!=' => undef },
602 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
603 return Koha::Holds->_new_from_dbic($hold_rs);
606 =head3 stockrotationitem
608 my $sritem = Koha::Item->stockrotationitem;
610 Returns the stock rotation item associated with the current item.
614 sub stockrotationitem {
616 my $rs = $self->_result->stockrotationitem;
618 return Koha::StockRotationItem->_new_from_dbic( $rs );
623 my $item = $item->add_to_rota($rota_id);
625 Add this item to the rota identified by $ROTA_ID, which means associating it
626 with the first stage of that rota. Should this item already be associated
627 with a rota, then we will move it to the new rota.
632 my ( $self, $rota_id ) = @_;
633 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
637 =head3 has_pending_hold
639 my $is_pending_hold = $item->has_pending_hold();
641 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
645 sub has_pending_hold {
647 my $pending_hold = $self->_result->tmp_holdsqueues;
648 return $pending_hold->count ? 1: 0;
653 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
654 my $field = $item->as_marc_field({ [ mss => $mss ] });
656 This method returns a MARC::Field object representing the Koha::Item object
657 with the current mappings configuration.
662 my ( $self, $params ) = @_;
664 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
665 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
669 my @columns = $self->_result->result_source->columns;
671 foreach my $item_field ( @columns ) {
672 my $mapping = $mss->{ "items.$item_field"}[0];
673 my $tagfield = $mapping->{tagfield};
674 my $tagsubfield = $mapping->{tagsubfield};
675 next if !$tagfield; # TODO: Should we raise an exception instead?
676 # Feels like safe fallback is better
678 push @subfields, $tagsubfield => $self->$item_field;
681 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
682 push( @subfields, @{$unlinked_item_subfields} )
683 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
687 $field = MARC::Field->new(
688 "$item_tag", ' ', ' ', @subfields
694 =head3 to_api_mapping
696 This method returns the mapping for representing a Koha::Item object
703 itemnumber => 'item_id',
704 biblionumber => 'biblio_id',
705 biblioitemnumber => undef,
706 barcode => 'external_id',
707 dateaccessioned => 'acquisition_date',
708 booksellerid => 'acquisition_source',
709 homebranch => 'home_library_id',
710 price => 'purchase_price',
711 replacementprice => 'replacement_price',
712 replacementpricedate => 'replacement_price_date',
713 datelastborrowed => 'last_checkout_date',
714 datelastseen => 'last_seen_date',
716 notforloan => 'not_for_loan_status',
717 damaged => 'damaged_status',
718 damaged_on => 'damaged_date',
719 itemlost => 'lost_status',
720 itemlost_on => 'lost_date',
721 withdrawn => 'withdrawn',
722 withdrawn_on => 'withdrawn_date',
723 itemcallnumber => 'callnumber',
724 coded_location_qualifier => 'coded_location_qualifier',
725 issues => 'checkouts_count',
726 renewals => 'renewals_count',
727 reserves => 'holds_count',
728 restricted => 'restricted_status',
729 itemnotes => 'public_notes',
730 itemnotes_nonpublic => 'internal_notes',
731 holdingbranch => 'holding_library_id',
733 timestamp => 'timestamp',
734 location => 'location',
735 permanent_location => 'permanent_location',
736 onloan => 'checked_out_date',
737 cn_source => 'call_number_source',
738 cn_sort => 'call_number_sort',
739 ccode => 'collection_code',
740 materials => 'materials_notes',
742 itype => 'item_type',
743 more_subfields_xml => 'extended_subfields',
744 enumchron => 'serial_issue_number',
745 copynumber => 'copy_number',
746 stocknumber => 'inventory_number',
747 new_status => 'new_status'
751 =head2 Internal methods
753 =head3 _after_item_action_hooks
755 Helper method that takes care of calling all plugin hooks
759 sub _after_item_action_hooks {
760 my ( $self, $params ) = @_;
762 my $action = $params->{action};
764 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
766 my @plugins = Koha::Plugins->new->GetPlugins({
767 method => 'after_item_action',
772 foreach my $plugin ( @plugins ) {
774 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
794 Kyle M Hall <kyle@bywatersolutions.com>