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 if ( defined $self->location and $self->location ne 'CART' and $self->location ne 'PROC' and not $self->permanent_location ) {
139 $self->permanent_location($self->location);
142 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
144 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
146 $self->_after_item_action_hooks({ action => 'modify' });
148 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
149 if $log_action && C4::Context->preference("CataloguingLog");
152 unless ( $self->dateaccessioned ) {
153 $self->dateaccessioned($today);
156 return $self->SUPER::store;
166 # FIXME check the item has no current issues
167 # i.e. raise the appropriate exception
169 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
171 $self->_after_item_action_hooks({ action => 'delete' });
173 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
174 if C4::Context->preference("CataloguingLog");
176 return $self->SUPER::delete;
186 my $safe_to_delete = $self->safe_to_delete;
187 return $safe_to_delete unless $safe_to_delete eq '1';
189 $self->move_to_deleted;
191 return $self->delete;
194 =head3 safe_to_delete
196 returns 1 if the item is safe to delete,
198 "book_on_loan" if the item is checked out,
200 "not_same_branch" if the item is blocked by independent branches,
202 "book_reserved" if the there are holds aganst the item, or
204 "linked_analytics" if the item has linked analytic records.
211 return "book_on_loan" if $self->checkout;
213 return "not_same_branch"
214 if defined C4::Context->userenv
215 and !C4::Context->IsSuperLibrarian()
216 and C4::Context->preference("IndependentBranches")
217 and ( C4::Context->userenv->{branch} ne $self->homebranch );
219 # check it doesn't have a waiting reserve
220 return "book_reserved"
221 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
223 return "linked_analytics"
224 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
229 =head3 move_to_deleted
231 my $is_moved = $item->move_to_deleted;
233 Move an item to the deleteditems table.
234 This can be done before deleting an item, to make sure the data are not completely deleted.
238 sub move_to_deleted {
240 my $item_infos = $self->unblessed;
241 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
242 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
246 =head3 effective_itemtype
248 Returns the itemtype for the item based on whether item level itemtypes are set or not.
252 sub effective_itemtype {
255 return $self->_result()->effective_itemtype();
265 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
267 return $self->{_home_branch};
270 =head3 holding_branch
277 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
279 return $self->{_holding_branch};
284 my $biblio = $item->biblio;
286 Return the bibliographic record of this item
292 my $biblio_rs = $self->_result->biblio;
293 return Koha::Biblio->_new_from_dbic( $biblio_rs );
298 my $biblioitem = $item->biblioitem;
300 Return the biblioitem record of this item
306 my $biblioitem_rs = $self->_result->biblioitem;
307 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
312 my $checkout = $item->checkout;
314 Return the checkout for this item
320 my $checkout_rs = $self->_result->issue;
321 return unless $checkout_rs;
322 return Koha::Checkout->_new_from_dbic( $checkout_rs );
327 my $holds = $item->holds();
328 my $holds = $item->holds($params);
329 my $holds = $item->holds({ found => 'W'});
331 Return holds attached to an item, optionally accept a hashref of params to pass to search
336 my ( $self,$params ) = @_;
337 my $holds_rs = $self->_result->reserves->search($params);
338 return Koha::Holds->_new_from_dbic( $holds_rs );
343 my $transfer = $item->get_transfer;
345 Return the transfer if the item is in transit or undef
351 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
352 return unless $transfer_rs;
353 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
356 =head3 last_returned_by
358 Gets and sets the last borrower to return an item.
360 Accepts and returns Koha::Patron objects
362 $item->last_returned_by( $borrowernumber );
364 $last_returned_by = $item->last_returned_by();
368 sub last_returned_by {
369 my ( $self, $borrower ) = @_;
371 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
374 return $items_last_returned_by_rs->update_or_create(
375 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
378 unless ( $self->{_last_returned_by} ) {
379 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
381 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
385 return $self->{_last_returned_by};
389 =head3 can_article_request
391 my $bool = $item->can_article_request( $borrower )
393 Returns true if item can be specifically requested
395 $borrower must be a Koha::Patron object
399 sub can_article_request {
400 my ( $self, $borrower ) = @_;
402 my $rule = $self->article_request_type($borrower);
404 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
408 =head3 hidden_in_opac
410 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
412 Returns true if item fields match the hidding criteria defined in $rules.
413 Returns false otherwise.
415 Takes HASHref that can have the following parameters:
417 $rules : { <field> => [ value_1, ... ], ... }
419 Note: $rules inherits its structure from the parsed YAML from reading
420 the I<OpacHiddenItems> system preference.
425 my ( $self, $params ) = @_;
427 my $rules = $params->{rules} // {};
430 if C4::Context->preference('hidelostitems') and
433 my $hidden_in_opac = 0;
435 foreach my $field ( keys %{$rules} ) {
437 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
443 return $hidden_in_opac;
446 =head3 can_be_transferred
448 $item->can_be_transferred({ to => $to_library, from => $from_library })
449 Checks if an item can be transferred to given library.
451 This feature is controlled by two system preferences:
452 UseBranchTransferLimits to enable / disable the feature
453 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
454 for setting the limitations
456 Takes HASHref that can have the following parameters:
457 MANDATORY PARAMETERS:
460 $from : Koha::Library # if not given, item holdingbranch
461 # will be used instead
463 Returns 1 if item can be transferred to $to_library, otherwise 0.
465 To find out whether at least one item of a Koha::Biblio can be transferred, please
466 see Koha::Biblio->can_be_transferred() instead of using this method for
467 multiple items of the same biblio.
471 sub can_be_transferred {
472 my ($self, $params) = @_;
474 my $to = $params->{to};
475 my $from = $params->{from};
477 $to = $to->branchcode;
478 $from = defined $from ? $from->branchcode : $self->holdingbranch;
480 return 1 if $from eq $to; # Transfer to current branch is allowed
481 return 1 unless C4::Context->preference('UseBranchTransferLimits');
483 my $limittype = C4::Context->preference('BranchTransferLimitsType');
484 return Koha::Item::Transfer::Limits->search({
487 $limittype => $limittype eq 'itemtype'
488 ? $self->effective_itemtype : $self->ccode
492 =head3 pickup_locations
494 @pickup_locations = $item->pickup_locations( {patron => $patron } )
496 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)
497 and if item can be transferred to each pickup location.
501 sub pickup_locations {
502 my ($self, $params) = @_;
504 my $patron = $params->{patron};
506 my $circ_control_branch =
507 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
509 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
512 if(defined $patron) {
513 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
514 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
517 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
518 @libs = $self->home_branch->get_hold_libraries;
519 push @libs, $self->home_branch unless scalar(@libs) > 0;
520 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
521 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
522 @libs = $plib->get_hold_libraries;
523 push @libs, $self->home_branch unless scalar(@libs) > 0;
524 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
525 push @libs, $self->home_branch;
526 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
527 push @libs, $self->holding_branch;
529 @libs = Koha::Libraries->search({
532 order_by => ['branchname']
536 my @pickup_locations;
537 foreach my $library (@libs) {
538 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
539 push @pickup_locations, $library;
543 return wantarray ? @pickup_locations : \@pickup_locations;
546 =head3 article_request_type
548 my $type = $item->article_request_type( $borrower )
550 returns 'yes', 'no', 'bib_only', or 'item_only'
552 $borrower must be a Koha::Patron object
556 sub article_request_type {
557 my ( $self, $borrower ) = @_;
559 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
561 $branch_control eq 'homebranch' ? $self->homebranch
562 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
564 my $borrowertype = $borrower->categorycode;
565 my $itemtype = $self->effective_itemtype();
566 my $rule = Koha::CirculationRules->get_effective_rule(
568 rule_name => 'article_requests',
569 categorycode => $borrowertype,
570 itemtype => $itemtype,
571 branchcode => $branchcode
575 return q{} unless $rule;
576 return $rule->rule_value || q{}
585 my $attributes = { order_by => 'priority' };
586 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
588 itemnumber => $self->itemnumber,
591 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
592 waitingdate => { '!=' => undef },
595 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
596 return Koha::Holds->_new_from_dbic($hold_rs);
605 my $hold_rs = $self->_result->reserves->search;
606 return Koha::Holds->_new_from_dbic($hold_rs);
609 =head3 stockrotationitem
611 my $sritem = Koha::Item->stockrotationitem;
613 Returns the stock rotation item associated with the current item.
617 sub stockrotationitem {
619 my $rs = $self->_result->stockrotationitem;
621 return Koha::StockRotationItem->_new_from_dbic( $rs );
626 my $item = $item->add_to_rota($rota_id);
628 Add this item to the rota identified by $ROTA_ID, which means associating it
629 with the first stage of that rota. Should this item already be associated
630 with a rota, then we will move it to the new rota.
635 my ( $self, $rota_id ) = @_;
636 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
640 =head3 has_pending_hold
642 my $is_pending_hold = $item->has_pending_hold();
644 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
648 sub has_pending_hold {
650 my $pending_hold = $self->_result->tmp_holdsqueues;
651 return $pending_hold->count ? 1: 0;
656 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
657 my $field = $item->as_marc_field({ [ mss => $mss ] });
659 This method returns a MARC::Field object representing the Koha::Item object
660 with the current mappings configuration.
665 my ( $self, $params ) = @_;
667 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
668 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
672 my @columns = $self->_result->result_source->columns;
674 foreach my $item_field ( @columns ) {
675 my $mapping = $mss->{ "items.$item_field"}[0];
676 my $tagfield = $mapping->{tagfield};
677 my $tagsubfield = $mapping->{tagsubfield};
678 next if !$tagfield; # TODO: Should we raise an exception instead?
679 # Feels like safe fallback is better
681 push @subfields, $tagsubfield => $self->$item_field;
684 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
685 push( @subfields, @{$unlinked_item_subfields} )
686 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
690 $field = MARC::Field->new(
691 "$item_tag", ' ', ' ', @subfields
697 =head3 to_api_mapping
699 This method returns the mapping for representing a Koha::Item object
706 itemnumber => 'item_id',
707 biblionumber => 'biblio_id',
708 biblioitemnumber => undef,
709 barcode => 'external_id',
710 dateaccessioned => 'acquisition_date',
711 booksellerid => 'acquisition_source',
712 homebranch => 'home_library_id',
713 price => 'purchase_price',
714 replacementprice => 'replacement_price',
715 replacementpricedate => 'replacement_price_date',
716 datelastborrowed => 'last_checkout_date',
717 datelastseen => 'last_seen_date',
719 notforloan => 'not_for_loan_status',
720 damaged => 'damaged_status',
721 damaged_on => 'damaged_date',
722 itemlost => 'lost_status',
723 itemlost_on => 'lost_date',
724 withdrawn => 'withdrawn',
725 withdrawn_on => 'withdrawn_date',
726 itemcallnumber => 'callnumber',
727 coded_location_qualifier => 'coded_location_qualifier',
728 issues => 'checkouts_count',
729 renewals => 'renewals_count',
730 reserves => 'holds_count',
731 restricted => 'restricted_status',
732 itemnotes => 'public_notes',
733 itemnotes_nonpublic => 'internal_notes',
734 holdingbranch => 'holding_library_id',
736 timestamp => 'timestamp',
737 location => 'location',
738 permanent_location => 'permanent_location',
739 onloan => 'checked_out_date',
740 cn_source => 'call_number_source',
741 cn_sort => 'call_number_sort',
742 ccode => 'collection_code',
743 materials => 'materials_notes',
745 itype => 'item_type',
746 more_subfields_xml => 'extended_subfields',
747 enumchron => 'serial_issue_number',
748 copynumber => 'copy_number',
749 stocknumber => 'inventory_number',
750 new_status => 'new_status'
754 =head2 Internal methods
756 =head3 _after_item_action_hooks
758 Helper method that takes care of calling all plugin hooks
762 sub _after_item_action_hooks {
763 my ( $self, $params ) = @_;
765 my $action = $params->{action};
767 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
769 my @plugins = Koha::Plugins->new->GetPlugins({
770 method => 'after_item_action',
775 foreach my $plugin ( @plugins ) {
777 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
797 Kyle M Hall <kyle@bywatersolutions.com>