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);
612 my $hold_rs = $self->_result->reserves->search;
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 to_api_mapping
706 This method returns the mapping for representing a Koha::Item object
713 itemnumber => 'item_id',
714 biblionumber => 'biblio_id',
715 biblioitemnumber => undef,
716 barcode => 'external_id',
717 dateaccessioned => 'acquisition_date',
718 booksellerid => 'acquisition_source',
719 homebranch => 'home_library_id',
720 price => 'purchase_price',
721 replacementprice => 'replacement_price',
722 replacementpricedate => 'replacement_price_date',
723 datelastborrowed => 'last_checkout_date',
724 datelastseen => 'last_seen_date',
726 notforloan => 'not_for_loan_status',
727 damaged => 'damaged_status',
728 damaged_on => 'damaged_date',
729 itemlost => 'lost_status',
730 itemlost_on => 'lost_date',
731 withdrawn => 'withdrawn',
732 withdrawn_on => 'withdrawn_date',
733 itemcallnumber => 'callnumber',
734 coded_location_qualifier => 'coded_location_qualifier',
735 issues => 'checkouts_count',
736 renewals => 'renewals_count',
737 reserves => 'holds_count',
738 restricted => 'restricted_status',
739 itemnotes => 'public_notes',
740 itemnotes_nonpublic => 'internal_notes',
741 holdingbranch => 'holding_library_id',
743 timestamp => 'timestamp',
744 location => 'location',
745 permanent_location => 'permanent_location',
746 onloan => 'checked_out_date',
747 cn_source => 'call_number_source',
748 cn_sort => 'call_number_sort',
749 ccode => 'collection_code',
750 materials => 'materials_notes',
752 itype => 'item_type',
753 more_subfields_xml => 'extended_subfields',
754 enumchron => 'serial_issue_number',
755 copynumber => 'copy_number',
756 stocknumber => 'inventory_number',
757 new_status => 'new_status'
761 =head2 Internal methods
763 =head3 _after_item_action_hooks
765 Helper method that takes care of calling all plugin hooks
769 sub _after_item_action_hooks {
770 my ( $self, $params ) = @_;
772 my $action = $params->{action};
774 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
776 my @plugins = Koha::Plugins->new->GetPlugins({
777 method => 'after_item_action',
782 foreach my $plugin ( @plugins ) {
784 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
804 Kyle M Hall <kyle@bywatersolutions.com>