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 or exists $updated_columns{cn_source} )
83 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
84 $self->cn_sort($cn_sort);
87 my $today = dt_from_string;
88 unless ( $self->in_storage ) { #AddItem
89 unless ( $self->permanent_location ) {
90 $self->permanent_location($self->location);
92 unless ( $self->replacementpricedate ) {
93 $self->replacementpricedate($today);
95 unless ( $self->datelastseen ) {
96 $self->datelastseen($today);
99 unless ( $self->dateaccessioned ) {
100 $self->dateaccessioned($today);
103 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
105 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
106 if $log_action && C4::Context->preference("CataloguingLog");
108 $self->_after_item_action_hooks({ action => 'create' });
112 { # Update *_on fields if needed
113 # Why not for AddItem as well?
114 my @fields = qw( itemlost withdrawn damaged );
116 # Only retrieve the item if we need to set an "on" date field
117 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
118 my $pre_mod_item = $self->get_from_storage;
119 for my $field (@fields) {
121 and not $pre_mod_item->$field )
123 my $field_on = "${field}_on";
125 DateTime::Format::MySQL->format_datetime( dt_from_string() )
131 # If the field is defined but empty, we are removing and,
132 # and thus need to clear out the 'on' field as well
133 for my $field (@fields) {
134 if ( defined( $self->$field ) && !$self->$field ) {
135 my $field_on = "${field}_on";
136 $self->$field_on(undef);
141 %updated_columns = $self->_result->get_dirty_columns;
142 return $self->SUPER::store unless %updated_columns;
143 if ( exists $updated_columns{location}
144 and $self->location ne 'CART'
145 and $self->location ne 'PROC'
146 and not exists $updated_columns{permanent_location} )
148 $self->permanent_location( $self->location );
151 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
153 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
155 $self->_after_item_action_hooks({ action => 'modify' });
157 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
158 if $log_action && C4::Context->preference("CataloguingLog");
161 unless ( $self->dateaccessioned ) {
162 $self->dateaccessioned($today);
165 return $self->SUPER::store;
175 # FIXME check the item has no current issues
176 # i.e. raise the appropriate exception
178 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
180 $self->_after_item_action_hooks({ action => 'delete' });
182 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
183 if C4::Context->preference("CataloguingLog");
185 return $self->SUPER::delete;
195 my $safe_to_delete = $self->safe_to_delete;
196 return $safe_to_delete unless $safe_to_delete eq '1';
198 $self->move_to_deleted;
200 return $self->delete;
203 =head3 safe_to_delete
205 returns 1 if the item is safe to delete,
207 "book_on_loan" if the item is checked out,
209 "not_same_branch" if the item is blocked by independent branches,
211 "book_reserved" if the there are holds aganst the item, or
213 "linked_analytics" if the item has linked analytic records.
220 return "book_on_loan" if $self->checkout;
222 return "not_same_branch"
223 if defined C4::Context->userenv
224 and !C4::Context->IsSuperLibrarian()
225 and C4::Context->preference("IndependentBranches")
226 and ( C4::Context->userenv->{branch} ne $self->homebranch );
228 # check it doesn't have a waiting reserve
229 return "book_reserved"
230 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
232 return "linked_analytics"
233 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
238 =head3 move_to_deleted
240 my $is_moved = $item->move_to_deleted;
242 Move an item to the deleteditems table.
243 This can be done before deleting an item, to make sure the data are not completely deleted.
247 sub move_to_deleted {
249 my $item_infos = $self->unblessed;
250 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
251 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
255 =head3 effective_itemtype
257 Returns the itemtype for the item based on whether item level itemtypes are set or not.
261 sub effective_itemtype {
264 return $self->_result()->effective_itemtype();
274 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
276 return $self->{_home_branch};
279 =head3 holding_branch
286 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
288 return $self->{_holding_branch};
293 my $biblio = $item->biblio;
295 Return the bibliographic record of this item
301 my $biblio_rs = $self->_result->biblio;
302 return Koha::Biblio->_new_from_dbic( $biblio_rs );
307 my $biblioitem = $item->biblioitem;
309 Return the biblioitem record of this item
315 my $biblioitem_rs = $self->_result->biblioitem;
316 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
321 my $checkout = $item->checkout;
323 Return the checkout for this item
329 my $checkout_rs = $self->_result->issue;
330 return unless $checkout_rs;
331 return Koha::Checkout->_new_from_dbic( $checkout_rs );
336 my $holds = $item->holds();
337 my $holds = $item->holds($params);
338 my $holds = $item->holds({ found => 'W'});
340 Return holds attached to an item, optionally accept a hashref of params to pass to search
345 my ( $self,$params ) = @_;
346 my $holds_rs = $self->_result->reserves->search($params);
347 return Koha::Holds->_new_from_dbic( $holds_rs );
352 my $transfer = $item->get_transfer;
354 Return the transfer if the item is in transit or undef
360 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
361 return unless $transfer_rs;
362 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
365 =head3 last_returned_by
367 Gets and sets the last borrower to return an item.
369 Accepts and returns Koha::Patron objects
371 $item->last_returned_by( $borrowernumber );
373 $last_returned_by = $item->last_returned_by();
377 sub last_returned_by {
378 my ( $self, $borrower ) = @_;
380 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
383 return $items_last_returned_by_rs->update_or_create(
384 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
387 unless ( $self->{_last_returned_by} ) {
388 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
390 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
394 return $self->{_last_returned_by};
398 =head3 can_article_request
400 my $bool = $item->can_article_request( $borrower )
402 Returns true if item can be specifically requested
404 $borrower must be a Koha::Patron object
408 sub can_article_request {
409 my ( $self, $borrower ) = @_;
411 my $rule = $self->article_request_type($borrower);
413 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
417 =head3 hidden_in_opac
419 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
421 Returns true if item fields match the hidding criteria defined in $rules.
422 Returns false otherwise.
424 Takes HASHref that can have the following parameters:
426 $rules : { <field> => [ value_1, ... ], ... }
428 Note: $rules inherits its structure from the parsed YAML from reading
429 the I<OpacHiddenItems> system preference.
434 my ( $self, $params ) = @_;
436 my $rules = $params->{rules} // {};
439 if C4::Context->preference('hidelostitems') and
442 my $hidden_in_opac = 0;
444 foreach my $field ( keys %{$rules} ) {
446 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
452 return $hidden_in_opac;
455 =head3 can_be_transferred
457 $item->can_be_transferred({ to => $to_library, from => $from_library })
458 Checks if an item can be transferred to given library.
460 This feature is controlled by two system preferences:
461 UseBranchTransferLimits to enable / disable the feature
462 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
463 for setting the limitations
465 Takes HASHref that can have the following parameters:
466 MANDATORY PARAMETERS:
469 $from : Koha::Library # if not given, item holdingbranch
470 # will be used instead
472 Returns 1 if item can be transferred to $to_library, otherwise 0.
474 To find out whether at least one item of a Koha::Biblio can be transferred, please
475 see Koha::Biblio->can_be_transferred() instead of using this method for
476 multiple items of the same biblio.
480 sub can_be_transferred {
481 my ($self, $params) = @_;
483 my $to = $params->{to};
484 my $from = $params->{from};
486 $to = $to->branchcode;
487 $from = defined $from ? $from->branchcode : $self->holdingbranch;
489 return 1 if $from eq $to; # Transfer to current branch is allowed
490 return 1 unless C4::Context->preference('UseBranchTransferLimits');
492 my $limittype = C4::Context->preference('BranchTransferLimitsType');
493 return Koha::Item::Transfer::Limits->search({
496 $limittype => $limittype eq 'itemtype'
497 ? $self->effective_itemtype : $self->ccode
501 =head3 pickup_locations
503 @pickup_locations = $item->pickup_locations( {patron => $patron } )
505 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)
506 and if item can be transferred to each pickup location.
510 sub pickup_locations {
511 my ($self, $params) = @_;
513 my $patron = $params->{patron};
515 my $circ_control_branch =
516 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
518 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
521 if(defined $patron) {
522 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
523 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
526 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
527 @libs = $self->home_branch->get_hold_libraries;
528 push @libs, $self->home_branch unless scalar(@libs) > 0;
529 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
530 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
531 @libs = $plib->get_hold_libraries;
532 push @libs, $self->home_branch unless scalar(@libs) > 0;
533 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
534 push @libs, $self->home_branch;
535 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
536 push @libs, $self->holding_branch;
538 @libs = Koha::Libraries->search({
541 order_by => ['branchname']
545 my @pickup_locations;
546 foreach my $library (@libs) {
547 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
548 push @pickup_locations, $library;
552 return wantarray ? @pickup_locations : \@pickup_locations;
555 =head3 article_request_type
557 my $type = $item->article_request_type( $borrower )
559 returns 'yes', 'no', 'bib_only', or 'item_only'
561 $borrower must be a Koha::Patron object
565 sub article_request_type {
566 my ( $self, $borrower ) = @_;
568 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
570 $branch_control eq 'homebranch' ? $self->homebranch
571 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
573 my $borrowertype = $borrower->categorycode;
574 my $itemtype = $self->effective_itemtype();
575 my $rule = Koha::CirculationRules->get_effective_rule(
577 rule_name => 'article_requests',
578 categorycode => $borrowertype,
579 itemtype => $itemtype,
580 branchcode => $branchcode
584 return q{} unless $rule;
585 return $rule->rule_value || q{}
594 my $attributes = { order_by => 'priority' };
595 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
597 itemnumber => $self->itemnumber,
600 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
601 waitingdate => { '!=' => undef },
604 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
605 return Koha::Holds->_new_from_dbic($hold_rs);
608 =head3 stockrotationitem
610 my $sritem = Koha::Item->stockrotationitem;
612 Returns the stock rotation item associated with the current item.
616 sub stockrotationitem {
618 my $rs = $self->_result->stockrotationitem;
620 return Koha::StockRotationItem->_new_from_dbic( $rs );
625 my $item = $item->add_to_rota($rota_id);
627 Add this item to the rota identified by $ROTA_ID, which means associating it
628 with the first stage of that rota. Should this item already be associated
629 with a rota, then we will move it to the new rota.
634 my ( $self, $rota_id ) = @_;
635 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
639 =head3 has_pending_hold
641 my $is_pending_hold = $item->has_pending_hold();
643 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
647 sub has_pending_hold {
649 my $pending_hold = $self->_result->tmp_holdsqueues;
650 return $pending_hold->count ? 1: 0;
655 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
656 my $field = $item->as_marc_field({ [ mss => $mss ] });
658 This method returns a MARC::Field object representing the Koha::Item object
659 with the current mappings configuration.
664 my ( $self, $params ) = @_;
666 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
667 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
671 my @columns = $self->_result->result_source->columns;
673 foreach my $item_field ( @columns ) {
674 my $mapping = $mss->{ "items.$item_field"}[0];
675 my $tagfield = $mapping->{tagfield};
676 my $tagsubfield = $mapping->{tagsubfield};
677 next if !$tagfield; # TODO: Should we raise an exception instead?
678 # Feels like safe fallback is better
680 push @subfields, $tagsubfield => $self->$item_field;
683 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
684 push( @subfields, @{$unlinked_item_subfields} )
685 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
689 $field = MARC::Field->new(
690 "$item_tag", ' ', ' ', @subfields
696 =head3 to_api_mapping
698 This method returns the mapping for representing a Koha::Item object
705 itemnumber => 'item_id',
706 biblionumber => 'biblio_id',
707 biblioitemnumber => undef,
708 barcode => 'external_id',
709 dateaccessioned => 'acquisition_date',
710 booksellerid => 'acquisition_source',
711 homebranch => 'home_library_id',
712 price => 'purchase_price',
713 replacementprice => 'replacement_price',
714 replacementpricedate => 'replacement_price_date',
715 datelastborrowed => 'last_checkout_date',
716 datelastseen => 'last_seen_date',
718 notforloan => 'not_for_loan_status',
719 damaged => 'damaged_status',
720 damaged_on => 'damaged_date',
721 itemlost => 'lost_status',
722 itemlost_on => 'lost_date',
723 withdrawn => 'withdrawn',
724 withdrawn_on => 'withdrawn_date',
725 itemcallnumber => 'callnumber',
726 coded_location_qualifier => 'coded_location_qualifier',
727 issues => 'checkouts_count',
728 renewals => 'renewals_count',
729 reserves => 'holds_count',
730 restricted => 'restricted_status',
731 itemnotes => 'public_notes',
732 itemnotes_nonpublic => 'internal_notes',
733 holdingbranch => 'holding_library_id',
735 timestamp => 'timestamp',
736 location => 'location',
737 permanent_location => 'permanent_location',
738 onloan => 'checked_out_date',
739 cn_source => 'call_number_source',
740 cn_sort => 'call_number_sort',
741 ccode => 'collection_code',
742 materials => 'materials_notes',
744 itype => 'item_type',
745 more_subfields_xml => 'extended_subfields',
746 enumchron => 'serial_issue_number',
747 copynumber => 'copy_number',
748 stocknumber => 'inventory_number',
749 new_status => 'new_status'
753 =head2 Internal methods
755 =head3 _after_item_action_hooks
757 Helper method that takes care of calling all plugin hooks
761 sub _after_item_action_hooks {
762 my ( $self, $params ) = @_;
764 my $action = $params->{action};
766 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
768 my @plugins = Koha::Plugins->new->GetPlugins({
769 method => 'after_item_action',
774 foreach my $plugin ( @plugins ) {
776 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
796 Kyle M Hall <kyle@bywatersolutions.com>