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);
27 use Koha::DateUtils qw( dt_from_string );
32 use C4::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
37 use Koha::CirculationRules;
38 use Koha::Item::Transfer::Limits;
39 use Koha::Item::Transfers;
42 use Koha::StockRotationItem;
43 use Koha::StockRotationRotas;
45 use base qw(Koha::Object);
49 Koha::Item - Koha Item object class
62 my ($self, $params) = @_;
64 my $log_action = $params->{log_action} // 1;
66 # We do not want to oblige callers to pass this value
67 # Dev conveniences vs performance?
68 unless ( $self->biblioitemnumber ) {
69 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
72 # See related changes from C4::Items::AddItem
73 unless ( $self->itype ) {
74 $self->itype($self->biblio->biblioitem->itemtype);
77 if ( $self->itemcallnumber ) { # This could be improved, we should recalculate it only if changed
78 my $cn_sort = GetClassSort($self->cn_source, $self->itemcallnumber, "");
79 $self->cn_sort($cn_sort);
82 my $today = dt_from_string;
83 unless ( $self->in_storage ) { #AddItem
84 unless ( $self->permanent_location ) {
85 $self->permanent_location($self->location);
87 unless ( $self->replacementpricedate ) {
88 $self->replacementpricedate($today);
90 unless ( $self->datelastseen ) {
91 $self->datelastseen($today);
94 unless ( $self->dateaccessioned ) {
95 $self->dateaccessioned($today);
98 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
100 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
101 if $log_action && C4::Context->preference("CataloguingLog");
103 $self->_after_item_action_hooks({ action => 'create' });
107 { # Update *_on fields if needed
108 # Why not for AddItem as well?
109 my @fields = qw( itemlost withdrawn damaged );
111 # Only retrieve the item if we need to set an "on" date field
112 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
113 my $pre_mod_item = $self->get_from_storage;
114 for my $field (@fields) {
116 and not $pre_mod_item->$field )
118 my $field_on = "${field}_on";
120 DateTime::Format::MySQL->format_datetime( dt_from_string() )
126 # If the field is defined but empty, we are removing and,
127 # and thus need to clear out the 'on' field as well
128 for my $field (@fields) {
129 if ( defined( $self->$field ) && !$self->$field ) {
130 my $field_on = "${field}_on";
131 $self->$field_on(undef);
136 if ( defined $self->location and $self->location ne 'CART' and $self->location ne 'PROC' and not $self->permanent_location ) {
137 $self->permanent_location($self->location);
140 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
142 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
144 $self->_after_item_action_hooks({ action => 'modify' });
146 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
147 if $log_action && C4::Context->preference("CataloguingLog");
151 unless ( $self->dateaccessioned ) {
152 $self->dateaccessioned($today);
155 return $self->SUPER::store;
158 =head3 effective_itemtype
160 Returns the itemtype for the item based on whether item level itemtypes are set or not.
164 sub effective_itemtype {
167 return $self->_result()->effective_itemtype();
177 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
179 return $self->{_home_branch};
182 =head3 holding_branch
189 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
191 return $self->{_holding_branch};
196 my $biblio = $item->biblio;
198 Return the bibliographic record of this item
204 my $biblio_rs = $self->_result->biblio;
205 return Koha::Biblio->_new_from_dbic( $biblio_rs );
210 my $biblioitem = $item->biblioitem;
212 Return the biblioitem record of this item
218 my $biblioitem_rs = $self->_result->biblioitem;
219 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
224 my $checkout = $item->checkout;
226 Return the checkout for this item
232 my $checkout_rs = $self->_result->issue;
233 return unless $checkout_rs;
234 return Koha::Checkout->_new_from_dbic( $checkout_rs );
239 my $holds = $item->holds();
240 my $holds = $item->holds($params);
241 my $holds = $item->holds({ found => 'W'});
243 Return holds attached to an item, optionally accept a hashref of params to pass to search
248 my ( $self,$params ) = @_;
249 my $holds_rs = $self->_result->reserves->search($params);
250 return Koha::Holds->_new_from_dbic( $holds_rs );
255 my $transfer = $item->get_transfer;
257 Return the transfer if the item is in transit or undef
263 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
264 return unless $transfer_rs;
265 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
268 =head3 last_returned_by
270 Gets and sets the last borrower to return an item.
272 Accepts and returns Koha::Patron objects
274 $item->last_returned_by( $borrowernumber );
276 $last_returned_by = $item->last_returned_by();
280 sub last_returned_by {
281 my ( $self, $borrower ) = @_;
283 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
286 return $items_last_returned_by_rs->update_or_create(
287 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
290 unless ( $self->{_last_returned_by} ) {
291 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
293 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
297 return $self->{_last_returned_by};
301 =head3 can_article_request
303 my $bool = $item->can_article_request( $borrower )
305 Returns true if item can be specifically requested
307 $borrower must be a Koha::Patron object
311 sub can_article_request {
312 my ( $self, $borrower ) = @_;
314 my $rule = $self->article_request_type($borrower);
316 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
320 =head3 hidden_in_opac
322 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
324 Returns true if item fields match the hidding criteria defined in $rules.
325 Returns false otherwise.
327 Takes HASHref that can have the following parameters:
329 $rules : { <field> => [ value_1, ... ], ... }
331 Note: $rules inherits its structure from the parsed YAML from reading
332 the I<OpacHiddenItems> system preference.
337 my ( $self, $params ) = @_;
339 my $rules = $params->{rules} // {};
342 if C4::Context->preference('hidelostitems') and
345 my $hidden_in_opac = 0;
347 foreach my $field ( keys %{$rules} ) {
349 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
355 return $hidden_in_opac;
358 =head3 can_be_transferred
360 $item->can_be_transferred({ to => $to_library, from => $from_library })
361 Checks if an item can be transferred to given library.
363 This feature is controlled by two system preferences:
364 UseBranchTransferLimits to enable / disable the feature
365 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
366 for setting the limitations
368 Takes HASHref that can have the following parameters:
369 MANDATORY PARAMETERS:
372 $from : Koha::Library # if not given, item holdingbranch
373 # will be used instead
375 Returns 1 if item can be transferred to $to_library, otherwise 0.
377 To find out whether at least one item of a Koha::Biblio can be transferred, please
378 see Koha::Biblio->can_be_transferred() instead of using this method for
379 multiple items of the same biblio.
383 sub can_be_transferred {
384 my ($self, $params) = @_;
386 my $to = $params->{to};
387 my $from = $params->{from};
389 $to = $to->branchcode;
390 $from = defined $from ? $from->branchcode : $self->holdingbranch;
392 return 1 if $from eq $to; # Transfer to current branch is allowed
393 return 1 unless C4::Context->preference('UseBranchTransferLimits');
395 my $limittype = C4::Context->preference('BranchTransferLimitsType');
396 return Koha::Item::Transfer::Limits->search({
399 $limittype => $limittype eq 'itemtype'
400 ? $self->effective_itemtype : $self->ccode
404 =head3 pickup_locations
406 @pickup_locations = $item->pickup_locations( {patron => $patron } )
408 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)
409 and if item can be transferred to each pickup location.
413 sub pickup_locations {
414 my ($self, $params) = @_;
416 my $patron = $params->{patron};
418 my $circ_control_branch =
419 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
421 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
424 if(defined $patron) {
425 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
426 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
429 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
430 @libs = $self->home_branch->get_hold_libraries;
431 push @libs, $self->home_branch unless scalar(@libs) > 0;
432 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
433 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
434 @libs = $plib->get_hold_libraries;
435 push @libs, $self->home_branch unless scalar(@libs) > 0;
436 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
437 push @libs, $self->home_branch;
438 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
439 push @libs, $self->holding_branch;
441 @libs = Koha::Libraries->search({
444 order_by => ['branchname']
448 my @pickup_locations;
449 foreach my $library (@libs) {
450 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
451 push @pickup_locations, $library;
455 return wantarray ? @pickup_locations : \@pickup_locations;
458 =head3 article_request_type
460 my $type = $item->article_request_type( $borrower )
462 returns 'yes', 'no', 'bib_only', or 'item_only'
464 $borrower must be a Koha::Patron object
468 sub article_request_type {
469 my ( $self, $borrower ) = @_;
471 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
473 $branch_control eq 'homebranch' ? $self->homebranch
474 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
476 my $borrowertype = $borrower->categorycode;
477 my $itemtype = $self->effective_itemtype();
478 my $rule = Koha::CirculationRules->get_effective_rule(
480 rule_name => 'article_requests',
481 categorycode => $borrowertype,
482 itemtype => $itemtype,
483 branchcode => $branchcode
487 return q{} unless $rule;
488 return $rule->rule_value || q{}
497 my $attributes = { order_by => 'priority' };
498 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
500 itemnumber => $self->itemnumber,
503 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
504 waitingdate => { '!=' => undef },
507 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
508 return Koha::Holds->_new_from_dbic($hold_rs);
511 =head3 stockrotationitem
513 my $sritem = Koha::Item->stockrotationitem;
515 Returns the stock rotation item associated with the current item.
519 sub stockrotationitem {
521 my $rs = $self->_result->stockrotationitem;
523 return Koha::StockRotationItem->_new_from_dbic( $rs );
528 my $item = $item->add_to_rota($rota_id);
530 Add this item to the rota identified by $ROTA_ID, which means associating it
531 with the first stage of that rota. Should this item already be associated
532 with a rota, then we will move it to the new rota.
537 my ( $self, $rota_id ) = @_;
538 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
542 =head3 has_pending_hold
544 my $is_pending_hold = $item->has_pending_hold();
546 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
550 sub has_pending_hold {
552 my $pending_hold = $self->_result->tmp_holdsqueues;
553 return $pending_hold->count ? 1: 0;
558 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
559 my $field = $item->as_marc_field({ [ mss => $mss ] });
561 This method returns a MARC::Field object representing the Koha::Item object
562 with the current mappings configuration.
567 my ( $self, $params ) = @_;
569 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
570 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
574 my @columns = $self->_result->result_source->columns;
576 foreach my $item_field ( @columns ) {
577 my $mapping = $mss->{ "items.$item_field"}[0];
578 my $tagfield = $mapping->{tagfield};
579 my $tagsubfield = $mapping->{tagsubfield};
580 next if !$tagfield; # TODO: Should we raise an exception instead?
581 # Feels like safe fallback is better
583 push @subfields, $tagsubfield => $self->$item_field;
586 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
587 push( @subfields, @{$unlinked_item_subfields} )
588 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
592 $field = MARC::Field->new(
593 "$item_tag", ' ', ' ', @subfields
599 =head3 to_api_mapping
601 This method returns the mapping for representing a Koha::Item object
608 itemnumber => 'item_id',
609 biblionumber => 'biblio_id',
610 biblioitemnumber => undef,
611 barcode => 'external_id',
612 dateaccessioned => 'acquisition_date',
613 booksellerid => 'acquisition_source',
614 homebranch => 'home_library_id',
615 price => 'purchase_price',
616 replacementprice => 'replacement_price',
617 replacementpricedate => 'replacement_price_date',
618 datelastborrowed => 'last_checkout_date',
619 datelastseen => 'last_seen_date',
621 notforloan => 'not_for_loan_status',
622 damaged => 'damaged_status',
623 damaged_on => 'damaged_date',
624 itemlost => 'lost_status',
625 itemlost_on => 'lost_date',
626 withdrawn => 'withdrawn',
627 withdrawn_on => 'withdrawn_date',
628 itemcallnumber => 'callnumber',
629 coded_location_qualifier => 'coded_location_qualifier',
630 issues => 'checkouts_count',
631 renewals => 'renewals_count',
632 reserves => 'holds_count',
633 restricted => 'restricted_status',
634 itemnotes => 'public_notes',
635 itemnotes_nonpublic => 'internal_notes',
636 holdingbranch => 'holding_library_id',
638 timestamp => 'timestamp',
639 location => 'location',
640 permanent_location => 'permanent_location',
641 onloan => 'checked_out_date',
642 cn_source => 'call_number_source',
643 cn_sort => 'call_number_sort',
644 ccode => 'collection_code',
645 materials => 'materials_notes',
647 itype => 'item_type',
648 more_subfields_xml => 'extended_subfields',
649 enumchron => 'serial_issue_number',
650 copynumber => 'copy_number',
651 stocknumber => 'inventory_number',
652 new_status => 'new_status'
656 =head2 Internal methods
668 Kyle M Hall <kyle@bywatersolutions.com>