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;
159 =head3 effective_itemtype
161 Returns the itemtype for the item based on whether item level itemtypes are set or not.
165 sub effective_itemtype {
168 return $self->_result()->effective_itemtype();
178 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
180 return $self->{_home_branch};
183 =head3 holding_branch
190 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
192 return $self->{_holding_branch};
197 my $biblio = $item->biblio;
199 Return the bibliographic record of this item
205 my $biblio_rs = $self->_result->biblio;
206 return Koha::Biblio->_new_from_dbic( $biblio_rs );
211 my $biblioitem = $item->biblioitem;
213 Return the biblioitem record of this item
219 my $biblioitem_rs = $self->_result->biblioitem;
220 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
225 my $checkout = $item->checkout;
227 Return the checkout for this item
233 my $checkout_rs = $self->_result->issue;
234 return unless $checkout_rs;
235 return Koha::Checkout->_new_from_dbic( $checkout_rs );
240 my $holds = $item->holds();
241 my $holds = $item->holds($params);
242 my $holds = $item->holds({ found => 'W'});
244 Return holds attached to an item, optionally accept a hashref of params to pass to search
249 my ( $self,$params ) = @_;
250 my $holds_rs = $self->_result->reserves->search($params);
251 return Koha::Holds->_new_from_dbic( $holds_rs );
256 my $transfer = $item->get_transfer;
258 Return the transfer if the item is in transit or undef
264 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
265 return unless $transfer_rs;
266 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
269 =head3 last_returned_by
271 Gets and sets the last borrower to return an item.
273 Accepts and returns Koha::Patron objects
275 $item->last_returned_by( $borrowernumber );
277 $last_returned_by = $item->last_returned_by();
281 sub last_returned_by {
282 my ( $self, $borrower ) = @_;
284 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
287 return $items_last_returned_by_rs->update_or_create(
288 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
291 unless ( $self->{_last_returned_by} ) {
292 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
294 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
298 return $self->{_last_returned_by};
302 =head3 can_article_request
304 my $bool = $item->can_article_request( $borrower )
306 Returns true if item can be specifically requested
308 $borrower must be a Koha::Patron object
312 sub can_article_request {
313 my ( $self, $borrower ) = @_;
315 my $rule = $self->article_request_type($borrower);
317 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
321 =head3 hidden_in_opac
323 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
325 Returns true if item fields match the hidding criteria defined in $rules.
326 Returns false otherwise.
328 Takes HASHref that can have the following parameters:
330 $rules : { <field> => [ value_1, ... ], ... }
332 Note: $rules inherits its structure from the parsed YAML from reading
333 the I<OpacHiddenItems> system preference.
338 my ( $self, $params ) = @_;
340 my $rules = $params->{rules} // {};
343 if C4::Context->preference('hidelostitems') and
346 my $hidden_in_opac = 0;
348 foreach my $field ( keys %{$rules} ) {
350 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
356 return $hidden_in_opac;
359 =head3 can_be_transferred
361 $item->can_be_transferred({ to => $to_library, from => $from_library })
362 Checks if an item can be transferred to given library.
364 This feature is controlled by two system preferences:
365 UseBranchTransferLimits to enable / disable the feature
366 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
367 for setting the limitations
369 Takes HASHref that can have the following parameters:
370 MANDATORY PARAMETERS:
373 $from : Koha::Library # if not given, item holdingbranch
374 # will be used instead
376 Returns 1 if item can be transferred to $to_library, otherwise 0.
378 To find out whether at least one item of a Koha::Biblio can be transferred, please
379 see Koha::Biblio->can_be_transferred() instead of using this method for
380 multiple items of the same biblio.
384 sub can_be_transferred {
385 my ($self, $params) = @_;
387 my $to = $params->{to};
388 my $from = $params->{from};
390 $to = $to->branchcode;
391 $from = defined $from ? $from->branchcode : $self->holdingbranch;
393 return 1 if $from eq $to; # Transfer to current branch is allowed
394 return 1 unless C4::Context->preference('UseBranchTransferLimits');
396 my $limittype = C4::Context->preference('BranchTransferLimitsType');
397 return Koha::Item::Transfer::Limits->search({
400 $limittype => $limittype eq 'itemtype'
401 ? $self->effective_itemtype : $self->ccode
405 =head3 pickup_locations
407 @pickup_locations = $item->pickup_locations( {patron => $patron } )
409 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)
410 and if item can be transferred to each pickup location.
414 sub pickup_locations {
415 my ($self, $params) = @_;
417 my $patron = $params->{patron};
419 my $circ_control_branch =
420 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
422 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
425 if(defined $patron) {
426 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
427 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
430 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
431 @libs = $self->home_branch->get_hold_libraries;
432 push @libs, $self->home_branch unless scalar(@libs) > 0;
433 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
434 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
435 @libs = $plib->get_hold_libraries;
436 push @libs, $self->home_branch unless scalar(@libs) > 0;
437 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
438 push @libs, $self->home_branch;
439 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
440 push @libs, $self->holding_branch;
442 @libs = Koha::Libraries->search({
445 order_by => ['branchname']
449 my @pickup_locations;
450 foreach my $library (@libs) {
451 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
452 push @pickup_locations, $library;
456 return wantarray ? @pickup_locations : \@pickup_locations;
459 =head3 article_request_type
461 my $type = $item->article_request_type( $borrower )
463 returns 'yes', 'no', 'bib_only', or 'item_only'
465 $borrower must be a Koha::Patron object
469 sub article_request_type {
470 my ( $self, $borrower ) = @_;
472 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
474 $branch_control eq 'homebranch' ? $self->homebranch
475 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
477 my $borrowertype = $borrower->categorycode;
478 my $itemtype = $self->effective_itemtype();
479 my $rule = Koha::CirculationRules->get_effective_rule(
481 rule_name => 'article_requests',
482 categorycode => $borrowertype,
483 itemtype => $itemtype,
484 branchcode => $branchcode
488 return q{} unless $rule;
489 return $rule->rule_value || q{}
498 my $attributes = { order_by => 'priority' };
499 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
501 itemnumber => $self->itemnumber,
504 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
505 waitingdate => { '!=' => undef },
508 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
509 return Koha::Holds->_new_from_dbic($hold_rs);
512 =head3 stockrotationitem
514 my $sritem = Koha::Item->stockrotationitem;
516 Returns the stock rotation item associated with the current item.
520 sub stockrotationitem {
522 my $rs = $self->_result->stockrotationitem;
524 return Koha::StockRotationItem->_new_from_dbic( $rs );
529 my $item = $item->add_to_rota($rota_id);
531 Add this item to the rota identified by $ROTA_ID, which means associating it
532 with the first stage of that rota. Should this item already be associated
533 with a rota, then we will move it to the new rota.
538 my ( $self, $rota_id ) = @_;
539 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
543 =head3 has_pending_hold
545 my $is_pending_hold = $item->has_pending_hold();
547 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
551 sub has_pending_hold {
553 my $pending_hold = $self->_result->tmp_holdsqueues;
554 return $pending_hold->count ? 1: 0;
559 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
560 my $field = $item->as_marc_field({ [ mss => $mss ] });
562 This method returns a MARC::Field object representing the Koha::Item object
563 with the current mappings configuration.
568 my ( $self, $params ) = @_;
570 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
571 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
575 my @columns = $self->_result->result_source->columns;
577 foreach my $item_field ( @columns ) {
578 my $mapping = $mss->{ "items.$item_field"}[0];
579 my $tagfield = $mapping->{tagfield};
580 my $tagsubfield = $mapping->{tagsubfield};
581 next if !$tagfield; # TODO: Should we raise an exception instead?
582 # Feels like safe fallback is better
584 push @subfields, $tagsubfield => $self->$item_field;
587 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
588 push( @subfields, @{$unlinked_item_subfields} )
589 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
593 $field = MARC::Field->new(
594 "$item_tag", ' ', ' ', @subfields
600 =head3 to_api_mapping
602 This method returns the mapping for representing a Koha::Item object
609 itemnumber => 'item_id',
610 biblionumber => 'biblio_id',
611 biblioitemnumber => undef,
612 barcode => 'external_id',
613 dateaccessioned => 'acquisition_date',
614 booksellerid => 'acquisition_source',
615 homebranch => 'home_library_id',
616 price => 'purchase_price',
617 replacementprice => 'replacement_price',
618 replacementpricedate => 'replacement_price_date',
619 datelastborrowed => 'last_checkout_date',
620 datelastseen => 'last_seen_date',
622 notforloan => 'not_for_loan_status',
623 damaged => 'damaged_status',
624 damaged_on => 'damaged_date',
625 itemlost => 'lost_status',
626 itemlost_on => 'lost_date',
627 withdrawn => 'withdrawn',
628 withdrawn_on => 'withdrawn_date',
629 itemcallnumber => 'callnumber',
630 coded_location_qualifier => 'coded_location_qualifier',
631 issues => 'checkouts_count',
632 renewals => 'renewals_count',
633 reserves => 'holds_count',
634 restricted => 'restricted_status',
635 itemnotes => 'public_notes',
636 itemnotes_nonpublic => 'internal_notes',
637 holdingbranch => 'holding_library_id',
639 timestamp => 'timestamp',
640 location => 'location',
641 permanent_location => 'permanent_location',
642 onloan => 'checked_out_date',
643 cn_source => 'call_number_source',
644 cn_sort => 'call_number_sort',
645 ccode => 'collection_code',
646 materials => 'materials_notes',
648 itype => 'item_type',
649 more_subfields_xml => 'extended_subfields',
650 enumchron => 'serial_issue_number',
651 copynumber => 'copy_number',
652 stocknumber => 'inventory_number',
653 new_status => 'new_status'
657 =head2 Internal methods
659 =head3 _after_item_action_hooks
661 Helper method that takes care of calling all plugin hooks
665 sub _after_item_action_hooks {
666 my ( $self, $params ) = @_;
668 my $action = $params->{action};
670 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
672 my @plugins = Koha::Plugins->new->GetPlugins({
673 method => 'after_item_action',
678 foreach my $plugin ( @plugins ) {
680 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
700 Kyle M Hall <kyle@bywatersolutions.com>