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 if ( $self->timestamp ) {
152 $self->timestamp(dt_from_string); # Maybe move this to Koha::Object->store?
155 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
157 $self->_after_item_action_hooks({ action => 'modify' });
159 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
160 if $log_action && C4::Context->preference("CataloguingLog");
163 unless ( $self->dateaccessioned ) {
164 $self->dateaccessioned($today);
167 return $self->SUPER::store;
177 # FIXME check the item has no current issues
178 # i.e. raise the appropriate exception
180 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
182 $self->_after_item_action_hooks({ action => 'delete' });
184 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
185 if C4::Context->preference("CataloguingLog");
187 return $self->SUPER::delete;
197 my $safe_to_delete = $self->safe_to_delete;
198 return $safe_to_delete unless $safe_to_delete eq '1';
200 $self->move_to_deleted;
202 return $self->delete;
205 =head3 safe_to_delete
207 returns 1 if the item is safe to delete,
209 "book_on_loan" if the item is checked out,
211 "not_same_branch" if the item is blocked by independent branches,
213 "book_reserved" if the there are holds aganst the item, or
215 "linked_analytics" if the item has linked analytic records.
222 return "book_on_loan" if $self->checkout;
224 return "not_same_branch"
225 if defined C4::Context->userenv
226 and !C4::Context->IsSuperLibrarian()
227 and C4::Context->preference("IndependentBranches")
228 and ( C4::Context->userenv->{branch} ne $self->homebranch );
230 # check it doesn't have a waiting reserve
231 return "book_reserved"
232 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
234 return "linked_analytics"
235 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
240 =head3 move_to_deleted
242 my $is_moved = $item->move_to_deleted;
244 Move an item to the deleteditems table.
245 This can be done before deleting an item, to make sure the data are not completely deleted.
249 sub move_to_deleted {
251 my $item_infos = $self->unblessed;
252 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
253 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
257 =head3 effective_itemtype
259 Returns the itemtype for the item based on whether item level itemtypes are set or not.
263 sub effective_itemtype {
266 return $self->_result()->effective_itemtype();
276 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
278 return $self->{_home_branch};
281 =head3 holding_branch
288 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
290 return $self->{_holding_branch};
295 my $biblio = $item->biblio;
297 Return the bibliographic record of this item
303 my $biblio_rs = $self->_result->biblio;
304 return Koha::Biblio->_new_from_dbic( $biblio_rs );
309 my $biblioitem = $item->biblioitem;
311 Return the biblioitem record of this item
317 my $biblioitem_rs = $self->_result->biblioitem;
318 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
323 my $checkout = $item->checkout;
325 Return the checkout for this item
331 my $checkout_rs = $self->_result->issue;
332 return unless $checkout_rs;
333 return Koha::Checkout->_new_from_dbic( $checkout_rs );
338 my $holds = $item->holds();
339 my $holds = $item->holds($params);
340 my $holds = $item->holds({ found => 'W'});
342 Return holds attached to an item, optionally accept a hashref of params to pass to search
347 my ( $self,$params ) = @_;
348 my $holds_rs = $self->_result->reserves->search($params);
349 return Koha::Holds->_new_from_dbic( $holds_rs );
354 my $transfer = $item->get_transfer;
356 Return the transfer if the item is in transit or undef
362 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
363 return unless $transfer_rs;
364 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
367 =head3 last_returned_by
369 Gets and sets the last borrower to return an item.
371 Accepts and returns Koha::Patron objects
373 $item->last_returned_by( $borrowernumber );
375 $last_returned_by = $item->last_returned_by();
379 sub last_returned_by {
380 my ( $self, $borrower ) = @_;
382 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
385 return $items_last_returned_by_rs->update_or_create(
386 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
389 unless ( $self->{_last_returned_by} ) {
390 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
392 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
396 return $self->{_last_returned_by};
400 =head3 can_article_request
402 my $bool = $item->can_article_request( $borrower )
404 Returns true if item can be specifically requested
406 $borrower must be a Koha::Patron object
410 sub can_article_request {
411 my ( $self, $borrower ) = @_;
413 my $rule = $self->article_request_type($borrower);
415 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
419 =head3 hidden_in_opac
421 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
423 Returns true if item fields match the hidding criteria defined in $rules.
424 Returns false otherwise.
426 Takes HASHref that can have the following parameters:
428 $rules : { <field> => [ value_1, ... ], ... }
430 Note: $rules inherits its structure from the parsed YAML from reading
431 the I<OpacHiddenItems> system preference.
436 my ( $self, $params ) = @_;
438 my $rules = $params->{rules} // {};
441 if C4::Context->preference('hidelostitems') and
444 my $hidden_in_opac = 0;
446 foreach my $field ( keys %{$rules} ) {
448 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
454 return $hidden_in_opac;
457 =head3 can_be_transferred
459 $item->can_be_transferred({ to => $to_library, from => $from_library })
460 Checks if an item can be transferred to given library.
462 This feature is controlled by two system preferences:
463 UseBranchTransferLimits to enable / disable the feature
464 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
465 for setting the limitations
467 Takes HASHref that can have the following parameters:
468 MANDATORY PARAMETERS:
471 $from : Koha::Library # if not given, item holdingbranch
472 # will be used instead
474 Returns 1 if item can be transferred to $to_library, otherwise 0.
476 To find out whether at least one item of a Koha::Biblio can be transferred, please
477 see Koha::Biblio->can_be_transferred() instead of using this method for
478 multiple items of the same biblio.
482 sub can_be_transferred {
483 my ($self, $params) = @_;
485 my $to = $params->{to};
486 my $from = $params->{from};
488 $to = $to->branchcode;
489 $from = defined $from ? $from->branchcode : $self->holdingbranch;
491 return 1 if $from eq $to; # Transfer to current branch is allowed
492 return 1 unless C4::Context->preference('UseBranchTransferLimits');
494 my $limittype = C4::Context->preference('BranchTransferLimitsType');
495 return Koha::Item::Transfer::Limits->search({
498 $limittype => $limittype eq 'itemtype'
499 ? $self->effective_itemtype : $self->ccode
503 =head3 pickup_locations
505 @pickup_locations = $item->pickup_locations( {patron => $patron } )
507 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)
508 and if item can be transferred to each pickup location.
512 sub pickup_locations {
513 my ($self, $params) = @_;
515 my $patron = $params->{patron};
517 my $circ_control_branch =
518 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
520 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
523 if(defined $patron) {
524 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
525 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
528 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
529 @libs = $self->home_branch->get_hold_libraries;
530 push @libs, $self->home_branch unless scalar(@libs) > 0;
531 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
532 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
533 @libs = $plib->get_hold_libraries;
534 push @libs, $self->home_branch unless scalar(@libs) > 0;
535 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
536 push @libs, $self->home_branch;
537 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
538 push @libs, $self->holding_branch;
540 @libs = Koha::Libraries->search({
543 order_by => ['branchname']
547 my @pickup_locations;
548 foreach my $library (@libs) {
549 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
550 push @pickup_locations, $library;
554 return wantarray ? @pickup_locations : \@pickup_locations;
557 =head3 article_request_type
559 my $type = $item->article_request_type( $borrower )
561 returns 'yes', 'no', 'bib_only', or 'item_only'
563 $borrower must be a Koha::Patron object
567 sub article_request_type {
568 my ( $self, $borrower ) = @_;
570 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
572 $branch_control eq 'homebranch' ? $self->homebranch
573 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
575 my $borrowertype = $borrower->categorycode;
576 my $itemtype = $self->effective_itemtype();
577 my $rule = Koha::CirculationRules->get_effective_rule(
579 rule_name => 'article_requests',
580 categorycode => $borrowertype,
581 itemtype => $itemtype,
582 branchcode => $branchcode
586 return q{} unless $rule;
587 return $rule->rule_value || q{}
596 my $attributes = { order_by => 'priority' };
597 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
599 itemnumber => $self->itemnumber,
602 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
603 waitingdate => { '!=' => undef },
606 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
607 return Koha::Holds->_new_from_dbic($hold_rs);
610 =head3 stockrotationitem
612 my $sritem = Koha::Item->stockrotationitem;
614 Returns the stock rotation item associated with the current item.
618 sub stockrotationitem {
620 my $rs = $self->_result->stockrotationitem;
622 return Koha::StockRotationItem->_new_from_dbic( $rs );
627 my $item = $item->add_to_rota($rota_id);
629 Add this item to the rota identified by $ROTA_ID, which means associating it
630 with the first stage of that rota. Should this item already be associated
631 with a rota, then we will move it to the new rota.
636 my ( $self, $rota_id ) = @_;
637 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
641 =head3 has_pending_hold
643 my $is_pending_hold = $item->has_pending_hold();
645 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
649 sub has_pending_hold {
651 my $pending_hold = $self->_result->tmp_holdsqueues;
652 return $pending_hold->count ? 1: 0;
657 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
658 my $field = $item->as_marc_field({ [ mss => $mss ] });
660 This method returns a MARC::Field object representing the Koha::Item object
661 with the current mappings configuration.
666 my ( $self, $params ) = @_;
668 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
669 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
673 my @columns = $self->_result->result_source->columns;
675 foreach my $item_field ( @columns ) {
676 my $mapping = $mss->{ "items.$item_field"}[0];
677 my $tagfield = $mapping->{tagfield};
678 my $tagsubfield = $mapping->{tagsubfield};
679 next if !$tagfield; # TODO: Should we raise an exception instead?
680 # Feels like safe fallback is better
682 push @subfields, $tagsubfield => $self->$item_field;
685 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
686 push( @subfields, @{$unlinked_item_subfields} )
687 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
691 $field = MARC::Field->new(
692 "$item_tag", ' ', ' ', @subfields
698 =head3 to_api_mapping
700 This method returns the mapping for representing a Koha::Item object
707 itemnumber => 'item_id',
708 biblionumber => 'biblio_id',
709 biblioitemnumber => undef,
710 barcode => 'external_id',
711 dateaccessioned => 'acquisition_date',
712 booksellerid => 'acquisition_source',
713 homebranch => 'home_library_id',
714 price => 'purchase_price',
715 replacementprice => 'replacement_price',
716 replacementpricedate => 'replacement_price_date',
717 datelastborrowed => 'last_checkout_date',
718 datelastseen => 'last_seen_date',
720 notforloan => 'not_for_loan_status',
721 damaged => 'damaged_status',
722 damaged_on => 'damaged_date',
723 itemlost => 'lost_status',
724 itemlost_on => 'lost_date',
725 withdrawn => 'withdrawn',
726 withdrawn_on => 'withdrawn_date',
727 itemcallnumber => 'callnumber',
728 coded_location_qualifier => 'coded_location_qualifier',
729 issues => 'checkouts_count',
730 renewals => 'renewals_count',
731 reserves => 'holds_count',
732 restricted => 'restricted_status',
733 itemnotes => 'public_notes',
734 itemnotes_nonpublic => 'internal_notes',
735 holdingbranch => 'holding_library_id',
737 timestamp => 'timestamp',
738 location => 'location',
739 permanent_location => 'permanent_location',
740 onloan => 'checked_out_date',
741 cn_source => 'call_number_source',
742 cn_sort => 'call_number_sort',
743 ccode => 'collection_code',
744 materials => 'materials_notes',
746 itype => 'item_type',
747 more_subfields_xml => 'extended_subfields',
748 enumchron => 'serial_issue_number',
749 copynumber => 'copy_number',
750 stocknumber => 'inventory_number',
751 new_status => 'new_status'
755 =head2 Internal methods
757 =head3 _after_item_action_hooks
759 Helper method that takes care of calling all plugin hooks
763 sub _after_item_action_hooks {
764 my ( $self, $params ) = @_;
766 my $action = $params->{action};
768 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
770 my @plugins = Koha::Plugins->new->GetPlugins({
771 method => 'after_item_action',
776 foreach my $plugin ( @plugins ) {
778 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
798 Kyle M Hall <kyle@bywatersolutions.com>