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 $today = dt_from_string;
80 unless ( $self->in_storage ) { #AddItem
81 unless ( $self->permanent_location ) {
82 $self->permanent_location($self->location);
84 unless ( $self->replacementpricedate ) {
85 $self->replacementpricedate($today);
87 unless ( $self->datelastseen ) {
88 $self->datelastseen($today);
91 unless ( $self->dateaccessioned ) {
92 $self->dateaccessioned($today);
95 if ( $self->itemcallnumber
98 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
99 $self->cn_sort($cn_sort);
102 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
104 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
105 if $log_action && C4::Context->preference("CataloguingLog");
107 $self->_after_item_action_hooks({ action => 'create' });
111 { # Update *_on fields if needed
112 # Why not for AddItem as well?
113 my @fields = qw( itemlost withdrawn damaged );
115 # Only retrieve the item if we need to set an "on" date field
116 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
117 my $pre_mod_item = $self->get_from_storage;
118 for my $field (@fields) {
120 and not $pre_mod_item->$field )
122 my $field_on = "${field}_on";
124 DateTime::Format::MySQL->format_datetime( dt_from_string() )
130 # If the field is defined but empty, we are removing and,
131 # and thus need to clear out the 'on' field as well
132 for my $field (@fields) {
133 if ( defined( $self->$field ) && !$self->$field ) {
134 my $field_on = "${field}_on";
135 $self->$field_on(undef);
140 my %updated_columns = $self->_result->get_dirty_columns;
141 return $self->SUPER::store unless %updated_columns;
143 if ( exists $updated_columns{itemcallnumber}
144 or exists $updated_columns{cn_source} )
146 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
147 $self->cn_sort($cn_sort);
151 if ( exists $updated_columns{location}
152 and $self->location ne 'CART'
153 and $self->location ne 'PROC'
154 and not exists $updated_columns{permanent_location} )
156 $self->permanent_location( $self->location );
159 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
161 $self->_after_item_action_hooks({ action => 'modify' });
163 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
164 if $log_action && C4::Context->preference("CataloguingLog");
167 unless ( $self->dateaccessioned ) {
168 $self->dateaccessioned($today);
171 return $self->SUPER::store;
181 # FIXME check the item has no current issues
182 # i.e. raise the appropriate exception
184 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
186 $self->_after_item_action_hooks({ action => 'delete' });
188 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
189 if C4::Context->preference("CataloguingLog");
191 return $self->SUPER::delete;
201 my $safe_to_delete = $self->safe_to_delete;
202 return $safe_to_delete unless $safe_to_delete eq '1';
204 $self->move_to_deleted;
206 return $self->delete;
209 =head3 safe_to_delete
211 returns 1 if the item is safe to delete,
213 "book_on_loan" if the item is checked out,
215 "not_same_branch" if the item is blocked by independent branches,
217 "book_reserved" if the there are holds aganst the item, or
219 "linked_analytics" if the item has linked analytic records.
226 return "book_on_loan" if $self->checkout;
228 return "not_same_branch"
229 if defined C4::Context->userenv
230 and !C4::Context->IsSuperLibrarian()
231 and C4::Context->preference("IndependentBranches")
232 and ( C4::Context->userenv->{branch} ne $self->homebranch );
234 # check it doesn't have a waiting reserve
235 return "book_reserved"
236 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
238 return "linked_analytics"
239 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
244 =head3 move_to_deleted
246 my $is_moved = $item->move_to_deleted;
248 Move an item to the deleteditems table.
249 This can be done before deleting an item, to make sure the data are not completely deleted.
253 sub move_to_deleted {
255 my $item_infos = $self->unblessed;
256 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
257 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
261 =head3 effective_itemtype
263 Returns the itemtype for the item based on whether item level itemtypes are set or not.
267 sub effective_itemtype {
270 return $self->_result()->effective_itemtype();
280 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
282 return $self->{_home_branch};
285 =head3 holding_branch
292 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
294 return $self->{_holding_branch};
299 my $biblio = $item->biblio;
301 Return the bibliographic record of this item
307 my $biblio_rs = $self->_result->biblio;
308 return Koha::Biblio->_new_from_dbic( $biblio_rs );
313 my $biblioitem = $item->biblioitem;
315 Return the biblioitem record of this item
321 my $biblioitem_rs = $self->_result->biblioitem;
322 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
327 my $checkout = $item->checkout;
329 Return the checkout for this item
335 my $checkout_rs = $self->_result->issue;
336 return unless $checkout_rs;
337 return Koha::Checkout->_new_from_dbic( $checkout_rs );
342 my $holds = $item->holds();
343 my $holds = $item->holds($params);
344 my $holds = $item->holds({ found => 'W'});
346 Return holds attached to an item, optionally accept a hashref of params to pass to search
351 my ( $self,$params ) = @_;
352 my $holds_rs = $self->_result->reserves->search($params);
353 return Koha::Holds->_new_from_dbic( $holds_rs );
358 my $transfer = $item->get_transfer;
360 Return the transfer if the item is in transit or undef
366 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
367 return unless $transfer_rs;
368 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
371 =head3 last_returned_by
373 Gets and sets the last borrower to return an item.
375 Accepts and returns Koha::Patron objects
377 $item->last_returned_by( $borrowernumber );
379 $last_returned_by = $item->last_returned_by();
383 sub last_returned_by {
384 my ( $self, $borrower ) = @_;
386 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
389 return $items_last_returned_by_rs->update_or_create(
390 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
393 unless ( $self->{_last_returned_by} ) {
394 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
396 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
400 return $self->{_last_returned_by};
404 =head3 can_article_request
406 my $bool = $item->can_article_request( $borrower )
408 Returns true if item can be specifically requested
410 $borrower must be a Koha::Patron object
414 sub can_article_request {
415 my ( $self, $borrower ) = @_;
417 my $rule = $self->article_request_type($borrower);
419 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
423 =head3 hidden_in_opac
425 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
427 Returns true if item fields match the hidding criteria defined in $rules.
428 Returns false otherwise.
430 Takes HASHref that can have the following parameters:
432 $rules : { <field> => [ value_1, ... ], ... }
434 Note: $rules inherits its structure from the parsed YAML from reading
435 the I<OpacHiddenItems> system preference.
440 my ( $self, $params ) = @_;
442 my $rules = $params->{rules} // {};
445 if C4::Context->preference('hidelostitems') and
448 my $hidden_in_opac = 0;
450 foreach my $field ( keys %{$rules} ) {
452 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
458 return $hidden_in_opac;
461 =head3 can_be_transferred
463 $item->can_be_transferred({ to => $to_library, from => $from_library })
464 Checks if an item can be transferred to given library.
466 This feature is controlled by two system preferences:
467 UseBranchTransferLimits to enable / disable the feature
468 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
469 for setting the limitations
471 Takes HASHref that can have the following parameters:
472 MANDATORY PARAMETERS:
475 $from : Koha::Library # if not given, item holdingbranch
476 # will be used instead
478 Returns 1 if item can be transferred to $to_library, otherwise 0.
480 To find out whether at least one item of a Koha::Biblio can be transferred, please
481 see Koha::Biblio->can_be_transferred() instead of using this method for
482 multiple items of the same biblio.
486 sub can_be_transferred {
487 my ($self, $params) = @_;
489 my $to = $params->{to};
490 my $from = $params->{from};
492 $to = $to->branchcode;
493 $from = defined $from ? $from->branchcode : $self->holdingbranch;
495 return 1 if $from eq $to; # Transfer to current branch is allowed
496 return 1 unless C4::Context->preference('UseBranchTransferLimits');
498 my $limittype = C4::Context->preference('BranchTransferLimitsType');
499 return Koha::Item::Transfer::Limits->search({
502 $limittype => $limittype eq 'itemtype'
503 ? $self->effective_itemtype : $self->ccode
507 =head3 pickup_locations
509 @pickup_locations = $item->pickup_locations( {patron => $patron } )
511 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)
512 and if item can be transferred to each pickup location.
516 sub pickup_locations {
517 my ($self, $params) = @_;
519 my $patron = $params->{patron};
521 my $circ_control_branch =
522 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
524 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
527 if(defined $patron) {
528 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
529 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
532 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
533 @libs = $self->home_branch->get_hold_libraries;
534 push @libs, $self->home_branch unless scalar(@libs) > 0;
535 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
536 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
537 @libs = $plib->get_hold_libraries;
538 push @libs, $self->home_branch unless scalar(@libs) > 0;
539 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
540 push @libs, $self->home_branch;
541 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
542 push @libs, $self->holding_branch;
544 @libs = Koha::Libraries->search({
547 order_by => ['branchname']
551 my @pickup_locations;
552 foreach my $library (@libs) {
553 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
554 push @pickup_locations, $library;
558 return wantarray ? @pickup_locations : \@pickup_locations;
561 =head3 article_request_type
563 my $type = $item->article_request_type( $borrower )
565 returns 'yes', 'no', 'bib_only', or 'item_only'
567 $borrower must be a Koha::Patron object
571 sub article_request_type {
572 my ( $self, $borrower ) = @_;
574 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
576 $branch_control eq 'homebranch' ? $self->homebranch
577 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
579 my $borrowertype = $borrower->categorycode;
580 my $itemtype = $self->effective_itemtype();
581 my $rule = Koha::CirculationRules->get_effective_rule(
583 rule_name => 'article_requests',
584 categorycode => $borrowertype,
585 itemtype => $itemtype,
586 branchcode => $branchcode
590 return q{} unless $rule;
591 return $rule->rule_value || q{}
600 my $attributes = { order_by => 'priority' };
601 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
603 itemnumber => $self->itemnumber,
606 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
607 waitingdate => { '!=' => undef },
610 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
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
687 if defined $self->$item_field and $item_field ne '';
690 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
691 push( @subfields, @{$unlinked_item_subfields} )
692 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
696 $field = MARC::Field->new(
697 "$item_tag", ' ', ' ', @subfields
703 =head3 renewal_branchcode
705 Returns the branchcode to be recorded in statistics renewal of the item
709 sub renewal_branchcode {
711 my ($self, $params ) = @_;
713 my $interface = C4::Context->interface;
715 if ( $interface eq 'opac' ){
716 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
717 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
718 $branchcode = 'OPACRenew';
720 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
721 $branchcode = $self->homebranch;
723 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
724 $branchcode = $self->checkout->patron->branchcode;
726 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
727 $branchcode = $self->checkout->branchcode;
733 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
734 ? C4::Context->userenv->{branch} : $params->{branch};
739 =head3 to_api_mapping
741 This method returns the mapping for representing a Koha::Item object
748 itemnumber => 'item_id',
749 biblionumber => 'biblio_id',
750 biblioitemnumber => undef,
751 barcode => 'external_id',
752 dateaccessioned => 'acquisition_date',
753 booksellerid => 'acquisition_source',
754 homebranch => 'home_library_id',
755 price => 'purchase_price',
756 replacementprice => 'replacement_price',
757 replacementpricedate => 'replacement_price_date',
758 datelastborrowed => 'last_checkout_date',
759 datelastseen => 'last_seen_date',
761 notforloan => 'not_for_loan_status',
762 damaged => 'damaged_status',
763 damaged_on => 'damaged_date',
764 itemlost => 'lost_status',
765 itemlost_on => 'lost_date',
766 withdrawn => 'withdrawn',
767 withdrawn_on => 'withdrawn_date',
768 itemcallnumber => 'callnumber',
769 coded_location_qualifier => 'coded_location_qualifier',
770 issues => 'checkouts_count',
771 renewals => 'renewals_count',
772 reserves => 'holds_count',
773 restricted => 'restricted_status',
774 itemnotes => 'public_notes',
775 itemnotes_nonpublic => 'internal_notes',
776 holdingbranch => 'holding_library_id',
778 timestamp => 'timestamp',
779 location => 'location',
780 permanent_location => 'permanent_location',
781 onloan => 'checked_out_date',
782 cn_source => 'call_number_source',
783 cn_sort => 'call_number_sort',
784 ccode => 'collection_code',
785 materials => 'materials_notes',
787 itype => 'item_type',
788 more_subfields_xml => 'extended_subfields',
789 enumchron => 'serial_issue_number',
790 copynumber => 'copy_number',
791 stocknumber => 'inventory_number',
792 new_status => 'new_status'
796 =head2 Internal methods
798 =head3 _after_item_action_hooks
800 Helper method that takes care of calling all plugin hooks
804 sub _after_item_action_hooks {
805 my ( $self, $params ) = @_;
807 my $action = $params->{action};
809 if ( C4::Context->config("enable_plugins") ) {
811 my @plugins = Koha::Plugins->new->GetPlugins({
812 method => 'after_item_action',
817 foreach my $plugin ( @plugins ) {
819 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
839 Kyle M Hall <kyle@bywatersolutions.com>