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;
179 =head3 move_to_deleted
181 my $is_moved = $item->move_to_deleted;
183 Move an item to the deleteditems table.
184 This can be done before deleting an item, to make sure the data are not completely deleted.
188 sub move_to_deleted {
190 my $item_infos = $self->unblessed;
191 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
192 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
196 =head3 effective_itemtype
198 Returns the itemtype for the item based on whether item level itemtypes are set or not.
202 sub effective_itemtype {
205 return $self->_result()->effective_itemtype();
215 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
217 return $self->{_home_branch};
220 =head3 holding_branch
227 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
229 return $self->{_holding_branch};
234 my $biblio = $item->biblio;
236 Return the bibliographic record of this item
242 my $biblio_rs = $self->_result->biblio;
243 return Koha::Biblio->_new_from_dbic( $biblio_rs );
248 my $biblioitem = $item->biblioitem;
250 Return the biblioitem record of this item
256 my $biblioitem_rs = $self->_result->biblioitem;
257 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
262 my $checkout = $item->checkout;
264 Return the checkout for this item
270 my $checkout_rs = $self->_result->issue;
271 return unless $checkout_rs;
272 return Koha::Checkout->_new_from_dbic( $checkout_rs );
277 my $holds = $item->holds();
278 my $holds = $item->holds($params);
279 my $holds = $item->holds({ found => 'W'});
281 Return holds attached to an item, optionally accept a hashref of params to pass to search
286 my ( $self,$params ) = @_;
287 my $holds_rs = $self->_result->reserves->search($params);
288 return Koha::Holds->_new_from_dbic( $holds_rs );
293 my $transfer = $item->get_transfer;
295 Return the transfer if the item is in transit or undef
301 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
302 return unless $transfer_rs;
303 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
306 =head3 last_returned_by
308 Gets and sets the last borrower to return an item.
310 Accepts and returns Koha::Patron objects
312 $item->last_returned_by( $borrowernumber );
314 $last_returned_by = $item->last_returned_by();
318 sub last_returned_by {
319 my ( $self, $borrower ) = @_;
321 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
324 return $items_last_returned_by_rs->update_or_create(
325 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
328 unless ( $self->{_last_returned_by} ) {
329 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
331 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
335 return $self->{_last_returned_by};
339 =head3 can_article_request
341 my $bool = $item->can_article_request( $borrower )
343 Returns true if item can be specifically requested
345 $borrower must be a Koha::Patron object
349 sub can_article_request {
350 my ( $self, $borrower ) = @_;
352 my $rule = $self->article_request_type($borrower);
354 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
358 =head3 hidden_in_opac
360 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
362 Returns true if item fields match the hidding criteria defined in $rules.
363 Returns false otherwise.
365 Takes HASHref that can have the following parameters:
367 $rules : { <field> => [ value_1, ... ], ... }
369 Note: $rules inherits its structure from the parsed YAML from reading
370 the I<OpacHiddenItems> system preference.
375 my ( $self, $params ) = @_;
377 my $rules = $params->{rules} // {};
380 if C4::Context->preference('hidelostitems') and
383 my $hidden_in_opac = 0;
385 foreach my $field ( keys %{$rules} ) {
387 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
393 return $hidden_in_opac;
396 =head3 can_be_transferred
398 $item->can_be_transferred({ to => $to_library, from => $from_library })
399 Checks if an item can be transferred to given library.
401 This feature is controlled by two system preferences:
402 UseBranchTransferLimits to enable / disable the feature
403 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
404 for setting the limitations
406 Takes HASHref that can have the following parameters:
407 MANDATORY PARAMETERS:
410 $from : Koha::Library # if not given, item holdingbranch
411 # will be used instead
413 Returns 1 if item can be transferred to $to_library, otherwise 0.
415 To find out whether at least one item of a Koha::Biblio can be transferred, please
416 see Koha::Biblio->can_be_transferred() instead of using this method for
417 multiple items of the same biblio.
421 sub can_be_transferred {
422 my ($self, $params) = @_;
424 my $to = $params->{to};
425 my $from = $params->{from};
427 $to = $to->branchcode;
428 $from = defined $from ? $from->branchcode : $self->holdingbranch;
430 return 1 if $from eq $to; # Transfer to current branch is allowed
431 return 1 unless C4::Context->preference('UseBranchTransferLimits');
433 my $limittype = C4::Context->preference('BranchTransferLimitsType');
434 return Koha::Item::Transfer::Limits->search({
437 $limittype => $limittype eq 'itemtype'
438 ? $self->effective_itemtype : $self->ccode
442 =head3 pickup_locations
444 @pickup_locations = $item->pickup_locations( {patron => $patron } )
446 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)
447 and if item can be transferred to each pickup location.
451 sub pickup_locations {
452 my ($self, $params) = @_;
454 my $patron = $params->{patron};
456 my $circ_control_branch =
457 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
459 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
462 if(defined $patron) {
463 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
464 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
467 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
468 @libs = $self->home_branch->get_hold_libraries;
469 push @libs, $self->home_branch unless scalar(@libs) > 0;
470 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
471 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
472 @libs = $plib->get_hold_libraries;
473 push @libs, $self->home_branch unless scalar(@libs) > 0;
474 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
475 push @libs, $self->home_branch;
476 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
477 push @libs, $self->holding_branch;
479 @libs = Koha::Libraries->search({
482 order_by => ['branchname']
486 my @pickup_locations;
487 foreach my $library (@libs) {
488 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
489 push @pickup_locations, $library;
493 return wantarray ? @pickup_locations : \@pickup_locations;
496 =head3 article_request_type
498 my $type = $item->article_request_type( $borrower )
500 returns 'yes', 'no', 'bib_only', or 'item_only'
502 $borrower must be a Koha::Patron object
506 sub article_request_type {
507 my ( $self, $borrower ) = @_;
509 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
511 $branch_control eq 'homebranch' ? $self->homebranch
512 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
514 my $borrowertype = $borrower->categorycode;
515 my $itemtype = $self->effective_itemtype();
516 my $rule = Koha::CirculationRules->get_effective_rule(
518 rule_name => 'article_requests',
519 categorycode => $borrowertype,
520 itemtype => $itemtype,
521 branchcode => $branchcode
525 return q{} unless $rule;
526 return $rule->rule_value || q{}
535 my $attributes = { order_by => 'priority' };
536 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
538 itemnumber => $self->itemnumber,
541 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
542 waitingdate => { '!=' => undef },
545 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
546 return Koha::Holds->_new_from_dbic($hold_rs);
549 =head3 stockrotationitem
551 my $sritem = Koha::Item->stockrotationitem;
553 Returns the stock rotation item associated with the current item.
557 sub stockrotationitem {
559 my $rs = $self->_result->stockrotationitem;
561 return Koha::StockRotationItem->_new_from_dbic( $rs );
566 my $item = $item->add_to_rota($rota_id);
568 Add this item to the rota identified by $ROTA_ID, which means associating it
569 with the first stage of that rota. Should this item already be associated
570 with a rota, then we will move it to the new rota.
575 my ( $self, $rota_id ) = @_;
576 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
580 =head3 has_pending_hold
582 my $is_pending_hold = $item->has_pending_hold();
584 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
588 sub has_pending_hold {
590 my $pending_hold = $self->_result->tmp_holdsqueues;
591 return $pending_hold->count ? 1: 0;
596 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
597 my $field = $item->as_marc_field({ [ mss => $mss ] });
599 This method returns a MARC::Field object representing the Koha::Item object
600 with the current mappings configuration.
605 my ( $self, $params ) = @_;
607 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
608 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
612 my @columns = $self->_result->result_source->columns;
614 foreach my $item_field ( @columns ) {
615 my $mapping = $mss->{ "items.$item_field"}[0];
616 my $tagfield = $mapping->{tagfield};
617 my $tagsubfield = $mapping->{tagsubfield};
618 next if !$tagfield; # TODO: Should we raise an exception instead?
619 # Feels like safe fallback is better
621 push @subfields, $tagsubfield => $self->$item_field;
624 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
625 push( @subfields, @{$unlinked_item_subfields} )
626 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
630 $field = MARC::Field->new(
631 "$item_tag", ' ', ' ', @subfields
637 =head3 to_api_mapping
639 This method returns the mapping for representing a Koha::Item object
646 itemnumber => 'item_id',
647 biblionumber => 'biblio_id',
648 biblioitemnumber => undef,
649 barcode => 'external_id',
650 dateaccessioned => 'acquisition_date',
651 booksellerid => 'acquisition_source',
652 homebranch => 'home_library_id',
653 price => 'purchase_price',
654 replacementprice => 'replacement_price',
655 replacementpricedate => 'replacement_price_date',
656 datelastborrowed => 'last_checkout_date',
657 datelastseen => 'last_seen_date',
659 notforloan => 'not_for_loan_status',
660 damaged => 'damaged_status',
661 damaged_on => 'damaged_date',
662 itemlost => 'lost_status',
663 itemlost_on => 'lost_date',
664 withdrawn => 'withdrawn',
665 withdrawn_on => 'withdrawn_date',
666 itemcallnumber => 'callnumber',
667 coded_location_qualifier => 'coded_location_qualifier',
668 issues => 'checkouts_count',
669 renewals => 'renewals_count',
670 reserves => 'holds_count',
671 restricted => 'restricted_status',
672 itemnotes => 'public_notes',
673 itemnotes_nonpublic => 'internal_notes',
674 holdingbranch => 'holding_library_id',
676 timestamp => 'timestamp',
677 location => 'location',
678 permanent_location => 'permanent_location',
679 onloan => 'checked_out_date',
680 cn_source => 'call_number_source',
681 cn_sort => 'call_number_sort',
682 ccode => 'collection_code',
683 materials => 'materials_notes',
685 itype => 'item_type',
686 more_subfields_xml => 'extended_subfields',
687 enumchron => 'serial_issue_number',
688 copynumber => 'copy_number',
689 stocknumber => 'inventory_number',
690 new_status => 'new_status'
694 =head2 Internal methods
696 =head3 _after_item_action_hooks
698 Helper method that takes care of calling all plugin hooks
702 sub _after_item_action_hooks {
703 my ( $self, $params ) = @_;
705 my $action = $params->{action};
707 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
709 my @plugins = Koha::Plugins->new->GetPlugins({
710 method => 'after_item_action',
715 foreach my $plugin ( @plugins ) {
717 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
737 Kyle M Hall <kyle@bywatersolutions.com>