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 my %updated_columns = $self->_result->get_dirty_columns;
139 if ( defined $self->location
140 and $self->location ne 'CART'
141 and $self->location ne 'PROC'
142 and not exists $updated_columns{permanent_location} )
144 $self->permanent_location( $self->location );
147 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
149 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
151 $self->_after_item_action_hooks({ action => 'modify' });
153 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
154 if $log_action && C4::Context->preference("CataloguingLog");
157 unless ( $self->dateaccessioned ) {
158 $self->dateaccessioned($today);
161 return $self->SUPER::store;
171 # FIXME check the item has no current issues
172 # i.e. raise the appropriate exception
174 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
176 $self->_after_item_action_hooks({ action => 'delete' });
178 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
179 if C4::Context->preference("CataloguingLog");
181 return $self->SUPER::delete;
191 my $safe_to_delete = $self->safe_to_delete;
192 return $safe_to_delete unless $safe_to_delete eq '1';
194 $self->move_to_deleted;
196 return $self->delete;
199 =head3 safe_to_delete
201 returns 1 if the item is safe to delete,
203 "book_on_loan" if the item is checked out,
205 "not_same_branch" if the item is blocked by independent branches,
207 "book_reserved" if the there are holds aganst the item, or
209 "linked_analytics" if the item has linked analytic records.
216 return "book_on_loan" if $self->checkout;
218 return "not_same_branch"
219 if defined C4::Context->userenv
220 and !C4::Context->IsSuperLibrarian()
221 and C4::Context->preference("IndependentBranches")
222 and ( C4::Context->userenv->{branch} ne $self->homebranch );
224 # check it doesn't have a waiting reserve
225 return "book_reserved"
226 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
228 return "linked_analytics"
229 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
234 =head3 move_to_deleted
236 my $is_moved = $item->move_to_deleted;
238 Move an item to the deleteditems table.
239 This can be done before deleting an item, to make sure the data are not completely deleted.
243 sub move_to_deleted {
245 my $item_infos = $self->unblessed;
246 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
247 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
251 =head3 effective_itemtype
253 Returns the itemtype for the item based on whether item level itemtypes are set or not.
257 sub effective_itemtype {
260 return $self->_result()->effective_itemtype();
270 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
272 return $self->{_home_branch};
275 =head3 holding_branch
282 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
284 return $self->{_holding_branch};
289 my $biblio = $item->biblio;
291 Return the bibliographic record of this item
297 my $biblio_rs = $self->_result->biblio;
298 return Koha::Biblio->_new_from_dbic( $biblio_rs );
303 my $biblioitem = $item->biblioitem;
305 Return the biblioitem record of this item
311 my $biblioitem_rs = $self->_result->biblioitem;
312 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
317 my $checkout = $item->checkout;
319 Return the checkout for this item
325 my $checkout_rs = $self->_result->issue;
326 return unless $checkout_rs;
327 return Koha::Checkout->_new_from_dbic( $checkout_rs );
332 my $holds = $item->holds();
333 my $holds = $item->holds($params);
334 my $holds = $item->holds({ found => 'W'});
336 Return holds attached to an item, optionally accept a hashref of params to pass to search
341 my ( $self,$params ) = @_;
342 my $holds_rs = $self->_result->reserves->search($params);
343 return Koha::Holds->_new_from_dbic( $holds_rs );
348 my $transfer = $item->get_transfer;
350 Return the transfer if the item is in transit or undef
356 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
357 return unless $transfer_rs;
358 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
361 =head3 last_returned_by
363 Gets and sets the last borrower to return an item.
365 Accepts and returns Koha::Patron objects
367 $item->last_returned_by( $borrowernumber );
369 $last_returned_by = $item->last_returned_by();
373 sub last_returned_by {
374 my ( $self, $borrower ) = @_;
376 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
379 return $items_last_returned_by_rs->update_or_create(
380 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
383 unless ( $self->{_last_returned_by} ) {
384 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
386 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
390 return $self->{_last_returned_by};
394 =head3 can_article_request
396 my $bool = $item->can_article_request( $borrower )
398 Returns true if item can be specifically requested
400 $borrower must be a Koha::Patron object
404 sub can_article_request {
405 my ( $self, $borrower ) = @_;
407 my $rule = $self->article_request_type($borrower);
409 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
413 =head3 hidden_in_opac
415 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
417 Returns true if item fields match the hidding criteria defined in $rules.
418 Returns false otherwise.
420 Takes HASHref that can have the following parameters:
422 $rules : { <field> => [ value_1, ... ], ... }
424 Note: $rules inherits its structure from the parsed YAML from reading
425 the I<OpacHiddenItems> system preference.
430 my ( $self, $params ) = @_;
432 my $rules = $params->{rules} // {};
435 if C4::Context->preference('hidelostitems') and
438 my $hidden_in_opac = 0;
440 foreach my $field ( keys %{$rules} ) {
442 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
448 return $hidden_in_opac;
451 =head3 can_be_transferred
453 $item->can_be_transferred({ to => $to_library, from => $from_library })
454 Checks if an item can be transferred to given library.
456 This feature is controlled by two system preferences:
457 UseBranchTransferLimits to enable / disable the feature
458 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
459 for setting the limitations
461 Takes HASHref that can have the following parameters:
462 MANDATORY PARAMETERS:
465 $from : Koha::Library # if not given, item holdingbranch
466 # will be used instead
468 Returns 1 if item can be transferred to $to_library, otherwise 0.
470 To find out whether at least one item of a Koha::Biblio can be transferred, please
471 see Koha::Biblio->can_be_transferred() instead of using this method for
472 multiple items of the same biblio.
476 sub can_be_transferred {
477 my ($self, $params) = @_;
479 my $to = $params->{to};
480 my $from = $params->{from};
482 $to = $to->branchcode;
483 $from = defined $from ? $from->branchcode : $self->holdingbranch;
485 return 1 if $from eq $to; # Transfer to current branch is allowed
486 return 1 unless C4::Context->preference('UseBranchTransferLimits');
488 my $limittype = C4::Context->preference('BranchTransferLimitsType');
489 return Koha::Item::Transfer::Limits->search({
492 $limittype => $limittype eq 'itemtype'
493 ? $self->effective_itemtype : $self->ccode
497 =head3 pickup_locations
499 @pickup_locations = $item->pickup_locations( {patron => $patron } )
501 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)
502 and if item can be transferred to each pickup location.
506 sub pickup_locations {
507 my ($self, $params) = @_;
509 my $patron = $params->{patron};
511 my $circ_control_branch =
512 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
514 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
517 if(defined $patron) {
518 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
519 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
522 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
523 @libs = $self->home_branch->get_hold_libraries;
524 push @libs, $self->home_branch unless scalar(@libs) > 0;
525 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
526 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
527 @libs = $plib->get_hold_libraries;
528 push @libs, $self->home_branch unless scalar(@libs) > 0;
529 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
530 push @libs, $self->home_branch;
531 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
532 push @libs, $self->holding_branch;
534 @libs = Koha::Libraries->search({
537 order_by => ['branchname']
541 my @pickup_locations;
542 foreach my $library (@libs) {
543 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
544 push @pickup_locations, $library;
548 return wantarray ? @pickup_locations : \@pickup_locations;
551 =head3 article_request_type
553 my $type = $item->article_request_type( $borrower )
555 returns 'yes', 'no', 'bib_only', or 'item_only'
557 $borrower must be a Koha::Patron object
561 sub article_request_type {
562 my ( $self, $borrower ) = @_;
564 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
566 $branch_control eq 'homebranch' ? $self->homebranch
567 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
569 my $borrowertype = $borrower->categorycode;
570 my $itemtype = $self->effective_itemtype();
571 my $rule = Koha::CirculationRules->get_effective_rule(
573 rule_name => 'article_requests',
574 categorycode => $borrowertype,
575 itemtype => $itemtype,
576 branchcode => $branchcode
580 return q{} unless $rule;
581 return $rule->rule_value || q{}
590 my $attributes = { order_by => 'priority' };
591 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
593 itemnumber => $self->itemnumber,
596 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
597 waitingdate => { '!=' => undef },
600 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
601 return Koha::Holds->_new_from_dbic($hold_rs);
610 my $hold_rs = $self->_result->reserves->search;
611 return Koha::Holds->_new_from_dbic($hold_rs);
614 =head3 stockrotationitem
616 my $sritem = Koha::Item->stockrotationitem;
618 Returns the stock rotation item associated with the current item.
622 sub stockrotationitem {
624 my $rs = $self->_result->stockrotationitem;
626 return Koha::StockRotationItem->_new_from_dbic( $rs );
631 my $item = $item->add_to_rota($rota_id);
633 Add this item to the rota identified by $ROTA_ID, which means associating it
634 with the first stage of that rota. Should this item already be associated
635 with a rota, then we will move it to the new rota.
640 my ( $self, $rota_id ) = @_;
641 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
645 =head3 has_pending_hold
647 my $is_pending_hold = $item->has_pending_hold();
649 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
653 sub has_pending_hold {
655 my $pending_hold = $self->_result->tmp_holdsqueues;
656 return $pending_hold->count ? 1: 0;
661 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
662 my $field = $item->as_marc_field({ [ mss => $mss ] });
664 This method returns a MARC::Field object representing the Koha::Item object
665 with the current mappings configuration.
670 my ( $self, $params ) = @_;
672 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
673 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
677 my @columns = $self->_result->result_source->columns;
679 foreach my $item_field ( @columns ) {
680 my $mapping = $mss->{ "items.$item_field"}[0];
681 my $tagfield = $mapping->{tagfield};
682 my $tagsubfield = $mapping->{tagsubfield};
683 next if !$tagfield; # TODO: Should we raise an exception instead?
684 # Feels like safe fallback is better
686 push @subfields, $tagsubfield => $self->$item_field;
689 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
690 push( @subfields, @{$unlinked_item_subfields} )
691 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
695 $field = MARC::Field->new(
696 "$item_tag", ' ', ' ', @subfields
702 =head3 to_api_mapping
704 This method returns the mapping for representing a Koha::Item object
711 itemnumber => 'item_id',
712 biblionumber => 'biblio_id',
713 biblioitemnumber => undef,
714 barcode => 'external_id',
715 dateaccessioned => 'acquisition_date',
716 booksellerid => 'acquisition_source',
717 homebranch => 'home_library_id',
718 price => 'purchase_price',
719 replacementprice => 'replacement_price',
720 replacementpricedate => 'replacement_price_date',
721 datelastborrowed => 'last_checkout_date',
722 datelastseen => 'last_seen_date',
724 notforloan => 'not_for_loan_status',
725 damaged => 'damaged_status',
726 damaged_on => 'damaged_date',
727 itemlost => 'lost_status',
728 itemlost_on => 'lost_date',
729 withdrawn => 'withdrawn',
730 withdrawn_on => 'withdrawn_date',
731 itemcallnumber => 'callnumber',
732 coded_location_qualifier => 'coded_location_qualifier',
733 issues => 'checkouts_count',
734 renewals => 'renewals_count',
735 reserves => 'holds_count',
736 restricted => 'restricted_status',
737 itemnotes => 'public_notes',
738 itemnotes_nonpublic => 'internal_notes',
739 holdingbranch => 'holding_library_id',
741 timestamp => 'timestamp',
742 location => 'location',
743 permanent_location => 'permanent_location',
744 onloan => 'checked_out_date',
745 cn_source => 'call_number_source',
746 cn_sort => 'call_number_sort',
747 ccode => 'collection_code',
748 materials => 'materials_notes',
750 itype => 'item_type',
751 more_subfields_xml => 'extended_subfields',
752 enumchron => 'serial_issue_number',
753 copynumber => 'copy_number',
754 stocknumber => 'inventory_number',
755 new_status => 'new_status'
759 =head2 Internal methods
761 =head3 _after_item_action_hooks
763 Helper method that takes care of calling all plugin hooks
767 sub _after_item_action_hooks {
768 my ( $self, $params ) = @_;
770 my $action = $params->{action};
772 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
774 my @plugins = Koha::Plugins->new->GetPlugins({
775 method => 'after_item_action',
780 foreach my $plugin ( @plugins ) {
782 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
802 Kyle M Hall <kyle@bywatersolutions.com>