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::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
37 use Koha::CirculationRules;
38 use Koha::SearchEngine::Indexer;
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
63 $params can take an optional 'skip_record_index' parameter.
64 If set, the reindexation process will not happen (index_records not called)
66 NOTE: This is a temporary fix to answer a performance issue when lot of items
67 are added (or modified) at the same time.
68 The correct way to fix this is to make the ES reindexation process async.
69 You should not turn it on if you do not understand what it is doing exactly.
75 my $params = @_ ? shift : {};
77 my $log_action = $params->{log_action} // 1;
79 # We do not want to oblige callers to pass this value
80 # Dev conveniences vs performance?
81 unless ( $self->biblioitemnumber ) {
82 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
85 # See related changes from C4::Items::AddItem
86 unless ( $self->itype ) {
87 $self->itype($self->biblio->biblioitem->itemtype);
90 my $today = dt_from_string;
91 my $action = 'create';
93 unless ( $self->in_storage ) { #AddItem
94 unless ( $self->permanent_location ) {
95 $self->permanent_location($self->location);
97 unless ( $self->replacementpricedate ) {
98 $self->replacementpricedate($today);
100 unless ( $self->datelastseen ) {
101 $self->datelastseen($today);
104 unless ( $self->dateaccessioned ) {
105 $self->dateaccessioned($today);
108 if ( $self->itemcallnumber
109 or $self->cn_source )
111 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
112 $self->cn_sort($cn_sort);
119 { # Update *_on fields if needed
120 # Why not for AddItem as well?
121 my @fields = qw( itemlost withdrawn damaged );
123 # Only retrieve the item if we need to set an "on" date field
124 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
125 my $pre_mod_item = $self->get_from_storage;
126 for my $field (@fields) {
128 and not $pre_mod_item->$field )
130 my $field_on = "${field}_on";
132 DateTime::Format::MySQL->format_datetime( dt_from_string() )
138 # If the field is defined but empty, we are removing and,
139 # and thus need to clear out the 'on' field as well
140 for my $field (@fields) {
141 if ( defined( $self->$field ) && !$self->$field ) {
142 my $field_on = "${field}_on";
143 $self->$field_on(undef);
148 my %updated_columns = $self->_result->get_dirty_columns;
149 return $self->SUPER::store unless %updated_columns;
151 if ( exists $updated_columns{itemcallnumber}
152 or exists $updated_columns{cn_source} )
154 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
155 $self->cn_sort($cn_sort);
159 if ( exists $updated_columns{location}
160 and $self->location ne 'CART'
161 and $self->location ne 'PROC'
162 and not exists $updated_columns{permanent_location} )
164 $self->permanent_location( $self->location );
169 unless ( $self->dateaccessioned ) {
170 $self->dateaccessioned($today);
173 my $result = $self->SUPER::store;
174 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
176 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
177 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
179 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
180 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
181 unless $params->{skip_record_index};
182 $self->get_from_storage->_after_item_action_hooks({ action => $action });
192 my $params = @_ ? shift : {};
194 # FIXME check the item has no current issues
195 # i.e. raise the appropriate exception
197 my $result = $self->SUPER::delete;
199 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
200 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
201 unless $params->{skip_record_index};
203 $self->_after_item_action_hooks({ action => 'delete' });
205 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
206 if C4::Context->preference("CataloguingLog");
217 my $params = @_ ? shift : {};
219 my $safe_to_delete = $self->safe_to_delete;
220 return $safe_to_delete unless $safe_to_delete eq '1';
222 $self->move_to_deleted;
224 return $self->delete($params);
227 =head3 safe_to_delete
229 returns 1 if the item is safe to delete,
231 "book_on_loan" if the item is checked out,
233 "not_same_branch" if the item is blocked by independent branches,
235 "book_reserved" if the there are holds aganst the item, or
237 "linked_analytics" if the item has linked analytic records.
239 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
246 return "book_on_loan" if $self->checkout;
248 return "not_same_branch"
249 if defined C4::Context->userenv
250 and !C4::Context->IsSuperLibrarian()
251 and C4::Context->preference("IndependentBranches")
252 and ( C4::Context->userenv->{branch} ne $self->homebranch );
254 # check it doesn't have a waiting reserve
255 return "book_reserved"
256 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
258 return "linked_analytics"
259 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
261 return "last_item_for_hold"
262 if $self->biblio->items->count == 1
263 && $self->biblio->holds->search(
272 =head3 move_to_deleted
274 my $is_moved = $item->move_to_deleted;
276 Move an item to the deleteditems table.
277 This can be done before deleting an item, to make sure the data are not completely deleted.
281 sub move_to_deleted {
283 my $item_infos = $self->unblessed;
284 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
285 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
289 =head3 effective_itemtype
291 Returns the itemtype for the item based on whether item level itemtypes are set or not.
295 sub effective_itemtype {
298 return $self->_result()->effective_itemtype();
308 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
310 return $self->{_home_branch};
313 =head3 holding_branch
320 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
322 return $self->{_holding_branch};
327 my $biblio = $item->biblio;
329 Return the bibliographic record of this item
335 my $biblio_rs = $self->_result->biblio;
336 return Koha::Biblio->_new_from_dbic( $biblio_rs );
341 my $biblioitem = $item->biblioitem;
343 Return the biblioitem record of this item
349 my $biblioitem_rs = $self->_result->biblioitem;
350 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
355 my $checkout = $item->checkout;
357 Return the checkout for this item
363 my $checkout_rs = $self->_result->issue;
364 return unless $checkout_rs;
365 return Koha::Checkout->_new_from_dbic( $checkout_rs );
370 my $holds = $item->holds();
371 my $holds = $item->holds($params);
372 my $holds = $item->holds({ found => 'W'});
374 Return holds attached to an item, optionally accept a hashref of params to pass to search
379 my ( $self,$params ) = @_;
380 my $holds_rs = $self->_result->reserves->search($params);
381 return Koha::Holds->_new_from_dbic( $holds_rs );
386 my $transfer = $item->get_transfer;
388 Return the transfer if the item is in transit or undef
394 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
395 return unless $transfer_rs;
396 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
399 =head3 last_returned_by
401 Gets and sets the last borrower to return an item.
403 Accepts and returns Koha::Patron objects
405 $item->last_returned_by( $borrowernumber );
407 $last_returned_by = $item->last_returned_by();
411 sub last_returned_by {
412 my ( $self, $borrower ) = @_;
414 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
417 return $items_last_returned_by_rs->update_or_create(
418 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
421 unless ( $self->{_last_returned_by} ) {
422 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
424 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
428 return $self->{_last_returned_by};
432 =head3 can_article_request
434 my $bool = $item->can_article_request( $borrower )
436 Returns true if item can be specifically requested
438 $borrower must be a Koha::Patron object
442 sub can_article_request {
443 my ( $self, $borrower ) = @_;
445 my $rule = $self->article_request_type($borrower);
447 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
451 =head3 hidden_in_opac
453 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
455 Returns true if item fields match the hidding criteria defined in $rules.
456 Returns false otherwise.
458 Takes HASHref that can have the following parameters:
460 $rules : { <field> => [ value_1, ... ], ... }
462 Note: $rules inherits its structure from the parsed YAML from reading
463 the I<OpacHiddenItems> system preference.
468 my ( $self, $params ) = @_;
470 my $rules = $params->{rules} // {};
473 if C4::Context->preference('hidelostitems') and
476 my $hidden_in_opac = 0;
478 foreach my $field ( keys %{$rules} ) {
480 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
486 return $hidden_in_opac;
489 =head3 can_be_transferred
491 $item->can_be_transferred({ to => $to_library, from => $from_library })
492 Checks if an item can be transferred to given library.
494 This feature is controlled by two system preferences:
495 UseBranchTransferLimits to enable / disable the feature
496 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
497 for setting the limitations
499 Takes HASHref that can have the following parameters:
500 MANDATORY PARAMETERS:
503 $from : Koha::Library # if not given, item holdingbranch
504 # will be used instead
506 Returns 1 if item can be transferred to $to_library, otherwise 0.
508 To find out whether at least one item of a Koha::Biblio can be transferred, please
509 see Koha::Biblio->can_be_transferred() instead of using this method for
510 multiple items of the same biblio.
514 sub can_be_transferred {
515 my ($self, $params) = @_;
517 my $to = $params->{to};
518 my $from = $params->{from};
520 $to = $to->branchcode;
521 $from = defined $from ? $from->branchcode : $self->holdingbranch;
523 return 1 if $from eq $to; # Transfer to current branch is allowed
524 return 1 unless C4::Context->preference('UseBranchTransferLimits');
526 my $limittype = C4::Context->preference('BranchTransferLimitsType');
527 return Koha::Item::Transfer::Limits->search({
530 $limittype => $limittype eq 'itemtype'
531 ? $self->effective_itemtype : $self->ccode
536 =head3 pickup_locations
538 $pickup_locations = $item->pickup_locations( {patron => $patron } )
540 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)
541 and if item can be transferred to each pickup location.
545 sub pickup_locations {
546 my ($self, $params) = @_;
548 my $patron = $params->{patron};
550 my $circ_control_branch =
551 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
553 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
555 if(defined $patron) {
556 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
557 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
560 my $pickup_libraries = Koha::Libraries->search();
561 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
562 $pickup_libraries = $self->home_branch->get_hold_libraries;
563 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
564 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
565 $pickup_libraries = $plib->get_hold_libraries;
566 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
567 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
568 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
569 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
572 return $pickup_libraries->search(
577 order_by => ['branchname']
579 ) unless C4::Context->preference('UseBranchTransferLimits');
581 my $limittype = C4::Context->preference('BranchTransferLimitsType');
582 my ($ccode, $itype) = (undef, undef);
583 if( $limittype eq 'ccode' ){
584 $ccode = $self->ccode;
586 $itype = $self->itype;
588 my $limits = Koha::Item::Transfer::Limits->search(
590 fromBranch => $self->holdingbranch,
594 { columns => ['toBranch'] }
597 return $pickup_libraries->search(
599 pickup_location => 1,
601 '-not_in' => $limits->_resultset->as_query
605 order_by => ['branchname']
610 =head3 article_request_type
612 my $type = $item->article_request_type( $borrower )
614 returns 'yes', 'no', 'bib_only', or 'item_only'
616 $borrower must be a Koha::Patron object
620 sub article_request_type {
621 my ( $self, $borrower ) = @_;
623 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
625 $branch_control eq 'homebranch' ? $self->homebranch
626 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
628 my $borrowertype = $borrower->categorycode;
629 my $itemtype = $self->effective_itemtype();
630 my $rule = Koha::CirculationRules->get_effective_rule(
632 rule_name => 'article_requests',
633 categorycode => $borrowertype,
634 itemtype => $itemtype,
635 branchcode => $branchcode
639 return q{} unless $rule;
640 return $rule->rule_value || q{}
649 my $attributes = { order_by => 'priority' };
650 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
652 itemnumber => $self->itemnumber,
655 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
656 waitingdate => { '!=' => undef },
659 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
660 return Koha::Holds->_new_from_dbic($hold_rs);
663 =head3 stockrotationitem
665 my $sritem = Koha::Item->stockrotationitem;
667 Returns the stock rotation item associated with the current item.
671 sub stockrotationitem {
673 my $rs = $self->_result->stockrotationitem;
675 return Koha::StockRotationItem->_new_from_dbic( $rs );
680 my $item = $item->add_to_rota($rota_id);
682 Add this item to the rota identified by $ROTA_ID, which means associating it
683 with the first stage of that rota. Should this item already be associated
684 with a rota, then we will move it to the new rota.
689 my ( $self, $rota_id ) = @_;
690 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
694 =head3 has_pending_hold
696 my $is_pending_hold = $item->has_pending_hold();
698 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
702 sub has_pending_hold {
704 my $pending_hold = $self->_result->tmp_holdsqueues;
705 return $pending_hold->count ? 1: 0;
710 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
711 my $field = $item->as_marc_field({ [ mss => $mss ] });
713 This method returns a MARC::Field object representing the Koha::Item object
714 with the current mappings configuration.
719 my ( $self, $params ) = @_;
721 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
722 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
726 my @columns = $self->_result->result_source->columns;
728 foreach my $item_field ( @columns ) {
729 my $mapping = $mss->{ "items.$item_field"}[0];
730 my $tagfield = $mapping->{tagfield};
731 my $tagsubfield = $mapping->{tagsubfield};
732 next if !$tagfield; # TODO: Should we raise an exception instead?
733 # Feels like safe fallback is better
735 push @subfields, $tagsubfield => $self->$item_field
736 if defined $self->$item_field and $item_field ne '';
739 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
740 push( @subfields, @{$unlinked_item_subfields} )
741 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
745 $field = MARC::Field->new(
746 "$item_tag", ' ', ' ', @subfields
752 =head3 renewal_branchcode
754 Returns the branchcode to be recorded in statistics renewal of the item
758 sub renewal_branchcode {
760 my ($self, $params ) = @_;
762 my $interface = C4::Context->interface;
764 if ( $interface eq 'opac' ){
765 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
766 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
767 $branchcode = 'OPACRenew';
769 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
770 $branchcode = $self->homebranch;
772 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
773 $branchcode = $self->checkout->patron->branchcode;
775 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
776 $branchcode = $self->checkout->branchcode;
782 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
783 ? C4::Context->userenv->{branch} : $params->{branch};
788 =head3 to_api_mapping
790 This method returns the mapping for representing a Koha::Item object
797 itemnumber => 'item_id',
798 biblionumber => 'biblio_id',
799 biblioitemnumber => undef,
800 barcode => 'external_id',
801 dateaccessioned => 'acquisition_date',
802 booksellerid => 'acquisition_source',
803 homebranch => 'home_library_id',
804 price => 'purchase_price',
805 replacementprice => 'replacement_price',
806 replacementpricedate => 'replacement_price_date',
807 datelastborrowed => 'last_checkout_date',
808 datelastseen => 'last_seen_date',
810 notforloan => 'not_for_loan_status',
811 damaged => 'damaged_status',
812 damaged_on => 'damaged_date',
813 itemlost => 'lost_status',
814 itemlost_on => 'lost_date',
815 withdrawn => 'withdrawn',
816 withdrawn_on => 'withdrawn_date',
817 itemcallnumber => 'callnumber',
818 coded_location_qualifier => 'coded_location_qualifier',
819 issues => 'checkouts_count',
820 renewals => 'renewals_count',
821 reserves => 'holds_count',
822 restricted => 'restricted_status',
823 itemnotes => 'public_notes',
824 itemnotes_nonpublic => 'internal_notes',
825 holdingbranch => 'holding_library_id',
827 timestamp => 'timestamp',
828 location => 'location',
829 permanent_location => 'permanent_location',
830 onloan => 'checked_out_date',
831 cn_source => 'call_number_source',
832 cn_sort => 'call_number_sort',
833 ccode => 'collection_code',
834 materials => 'materials_notes',
836 itype => 'item_type',
837 more_subfields_xml => 'extended_subfields',
838 enumchron => 'serial_issue_number',
839 copynumber => 'copy_number',
840 stocknumber => 'inventory_number',
841 new_status => 'new_status'
845 =head2 Internal methods
847 =head3 _after_item_action_hooks
849 Helper method that takes care of calling all plugin hooks
853 sub _after_item_action_hooks {
854 my ( $self, $params ) = @_;
856 my $action = $params->{action};
858 if ( C4::Context->config("enable_plugins") ) {
860 my @plugins = Koha::Plugins->new->GetPlugins({
861 method => 'after_item_action',
866 foreach my $plugin ( @plugins ) {
868 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
888 Kyle M Hall <kyle@bywatersolutions.com>