Bug 24828: Add ability to specify cash register in SIP config
[koha.git] / Koha / Item.pm
blob567a294911dfe240fdf5304afa58410846aab7ad
1 package Koha::Item;
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>.
20 use Modern::Perl;
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
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 );
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Patrons;
42 use Koha::Plugins;
43 use Koha::Libraries;
44 use Koha::StockRotationItem;
45 use Koha::StockRotationRotas;
47 use base qw(Koha::Object);
49 =head1 NAME
51 Koha::Item - Koha Item object class
53 =head1 API
55 =head2 Class methods
57 =cut
59 =head3 store
61 =cut
63 sub store {
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
96 or $self->cn_source )
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' });
109 } else { # ModItem
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) {
119 if ( $self->$field
120 and not $pre_mod_item->$field )
122 my $field_on = "${field}_on";
123 $self->$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 $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
161 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
163 $self->_after_item_action_hooks({ action => 'modify' });
165 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
166 if $log_action && C4::Context->preference("CataloguingLog");
169 unless ( $self->dateaccessioned ) {
170 $self->dateaccessioned($today);
173 return $self->SUPER::store;
176 =head3 delete
178 =cut
180 sub delete {
181 my ( $self ) = @_;
183 # FIXME check the item has no current issues
184 # i.e. raise the appropriate exception
186 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
188 $self->_after_item_action_hooks({ action => 'delete' });
190 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
191 if C4::Context->preference("CataloguingLog");
193 return $self->SUPER::delete;
196 =head3 safe_delete
198 =cut
200 sub safe_delete {
201 my ($self) = @_;
203 my $safe_to_delete = $self->safe_to_delete;
204 return $safe_to_delete unless $safe_to_delete eq '1';
206 $self->move_to_deleted;
208 return $self->delete;
211 =head3 safe_to_delete
213 returns 1 if the item is safe to delete,
215 "book_on_loan" if the item is checked out,
217 "not_same_branch" if the item is blocked by independent branches,
219 "book_reserved" if the there are holds aganst the item, or
221 "linked_analytics" if the item has linked analytic records.
223 =cut
225 sub safe_to_delete {
226 my ($self) = @_;
228 return "book_on_loan" if $self->checkout;
230 return "not_same_branch"
231 if defined C4::Context->userenv
232 and !C4::Context->IsSuperLibrarian()
233 and C4::Context->preference("IndependentBranches")
234 and ( C4::Context->userenv->{branch} ne $self->homebranch );
236 # check it doesn't have a waiting reserve
237 return "book_reserved"
238 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
240 return "linked_analytics"
241 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
243 return 1;
246 =head3 move_to_deleted
248 my $is_moved = $item->move_to_deleted;
250 Move an item to the deleteditems table.
251 This can be done before deleting an item, to make sure the data are not completely deleted.
253 =cut
255 sub move_to_deleted {
256 my ($self) = @_;
257 my $item_infos = $self->unblessed;
258 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
259 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
263 =head3 effective_itemtype
265 Returns the itemtype for the item based on whether item level itemtypes are set or not.
267 =cut
269 sub effective_itemtype {
270 my ( $self ) = @_;
272 return $self->_result()->effective_itemtype();
275 =head3 home_branch
277 =cut
279 sub home_branch {
280 my ($self) = @_;
282 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
284 return $self->{_home_branch};
287 =head3 holding_branch
289 =cut
291 sub holding_branch {
292 my ($self) = @_;
294 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
296 return $self->{_holding_branch};
299 =head3 biblio
301 my $biblio = $item->biblio;
303 Return the bibliographic record of this item
305 =cut
307 sub biblio {
308 my ( $self ) = @_;
309 my $biblio_rs = $self->_result->biblio;
310 return Koha::Biblio->_new_from_dbic( $biblio_rs );
313 =head3 biblioitem
315 my $biblioitem = $item->biblioitem;
317 Return the biblioitem record of this item
319 =cut
321 sub biblioitem {
322 my ( $self ) = @_;
323 my $biblioitem_rs = $self->_result->biblioitem;
324 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
327 =head3 checkout
329 my $checkout = $item->checkout;
331 Return the checkout for this item
333 =cut
335 sub checkout {
336 my ( $self ) = @_;
337 my $checkout_rs = $self->_result->issue;
338 return unless $checkout_rs;
339 return Koha::Checkout->_new_from_dbic( $checkout_rs );
342 =head3 holds
344 my $holds = $item->holds();
345 my $holds = $item->holds($params);
346 my $holds = $item->holds({ found => 'W'});
348 Return holds attached to an item, optionally accept a hashref of params to pass to search
350 =cut
352 sub holds {
353 my ( $self,$params ) = @_;
354 my $holds_rs = $self->_result->reserves->search($params);
355 return Koha::Holds->_new_from_dbic( $holds_rs );
358 =head3 get_transfer
360 my $transfer = $item->get_transfer;
362 Return the transfer if the item is in transit or undef
364 =cut
366 sub get_transfer {
367 my ( $self ) = @_;
368 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
369 return unless $transfer_rs;
370 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
373 =head3 last_returned_by
375 Gets and sets the last borrower to return an item.
377 Accepts and returns Koha::Patron objects
379 $item->last_returned_by( $borrowernumber );
381 $last_returned_by = $item->last_returned_by();
383 =cut
385 sub last_returned_by {
386 my ( $self, $borrower ) = @_;
388 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
390 if ($borrower) {
391 return $items_last_returned_by_rs->update_or_create(
392 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
394 else {
395 unless ( $self->{_last_returned_by} ) {
396 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
397 if ($result) {
398 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
402 return $self->{_last_returned_by};
406 =head3 can_article_request
408 my $bool = $item->can_article_request( $borrower )
410 Returns true if item can be specifically requested
412 $borrower must be a Koha::Patron object
414 =cut
416 sub can_article_request {
417 my ( $self, $borrower ) = @_;
419 my $rule = $self->article_request_type($borrower);
421 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
422 return q{};
425 =head3 hidden_in_opac
427 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
429 Returns true if item fields match the hidding criteria defined in $rules.
430 Returns false otherwise.
432 Takes HASHref that can have the following parameters:
433 OPTIONAL PARAMETERS:
434 $rules : { <field> => [ value_1, ... ], ... }
436 Note: $rules inherits its structure from the parsed YAML from reading
437 the I<OpacHiddenItems> system preference.
439 =cut
441 sub hidden_in_opac {
442 my ( $self, $params ) = @_;
444 my $rules = $params->{rules} // {};
446 return 1
447 if C4::Context->preference('hidelostitems') and
448 $self->itemlost > 0;
450 my $hidden_in_opac = 0;
452 foreach my $field ( keys %{$rules} ) {
454 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
455 $hidden_in_opac = 1;
456 last;
460 return $hidden_in_opac;
463 =head3 can_be_transferred
465 $item->can_be_transferred({ to => $to_library, from => $from_library })
466 Checks if an item can be transferred to given library.
468 This feature is controlled by two system preferences:
469 UseBranchTransferLimits to enable / disable the feature
470 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
471 for setting the limitations
473 Takes HASHref that can have the following parameters:
474 MANDATORY PARAMETERS:
475 $to : Koha::Library
476 OPTIONAL PARAMETERS:
477 $from : Koha::Library # if not given, item holdingbranch
478 # will be used instead
480 Returns 1 if item can be transferred to $to_library, otherwise 0.
482 To find out whether at least one item of a Koha::Biblio can be transferred, please
483 see Koha::Biblio->can_be_transferred() instead of using this method for
484 multiple items of the same biblio.
486 =cut
488 sub can_be_transferred {
489 my ($self, $params) = @_;
491 my $to = $params->{to};
492 my $from = $params->{from};
494 $to = $to->branchcode;
495 $from = defined $from ? $from->branchcode : $self->holdingbranch;
497 return 1 if $from eq $to; # Transfer to current branch is allowed
498 return 1 unless C4::Context->preference('UseBranchTransferLimits');
500 my $limittype = C4::Context->preference('BranchTransferLimitsType');
501 return Koha::Item::Transfer::Limits->search({
502 toBranch => $to,
503 fromBranch => $from,
504 $limittype => $limittype eq 'itemtype'
505 ? $self->effective_itemtype : $self->ccode
506 })->count ? 0 : 1;
509 =head3 pickup_locations
511 @pickup_locations = $item->pickup_locations( {patron => $patron } )
513 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)
514 and if item can be transferred to each pickup location.
516 =cut
518 sub pickup_locations {
519 my ($self, $params) = @_;
521 my $patron = $params->{patron};
523 my $circ_control_branch =
524 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
525 my $branchitemrule =
526 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
528 my @libs;
529 if(defined $patron) {
530 return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
531 return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
534 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
535 @libs = $self->home_branch->get_hold_libraries;
536 push @libs, $self->home_branch unless scalar(@libs) > 0;
537 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
538 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
539 @libs = $plib->get_hold_libraries;
540 push @libs, $self->home_branch unless scalar(@libs) > 0;
541 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
542 push @libs, $self->home_branch;
543 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
544 push @libs, $self->holding_branch;
545 } else {
546 @libs = Koha::Libraries->search({
547 pickup_location => 1
548 }, {
549 order_by => ['branchname']
550 })->as_list;
553 my @pickup_locations;
554 foreach my $library (@libs) {
555 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
556 push @pickup_locations, $library;
560 return wantarray ? @pickup_locations : \@pickup_locations;
563 =head3 article_request_type
565 my $type = $item->article_request_type( $borrower )
567 returns 'yes', 'no', 'bib_only', or 'item_only'
569 $borrower must be a Koha::Patron object
571 =cut
573 sub article_request_type {
574 my ( $self, $borrower ) = @_;
576 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
577 my $branchcode =
578 $branch_control eq 'homebranch' ? $self->homebranch
579 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
580 : undef;
581 my $borrowertype = $borrower->categorycode;
582 my $itemtype = $self->effective_itemtype();
583 my $rule = Koha::CirculationRules->get_effective_rule(
585 rule_name => 'article_requests',
586 categorycode => $borrowertype,
587 itemtype => $itemtype,
588 branchcode => $branchcode
592 return q{} unless $rule;
593 return $rule->rule_value || q{}
596 =head3 current_holds
598 =cut
600 sub current_holds {
601 my ( $self ) = @_;
602 my $attributes = { order_by => 'priority' };
603 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
604 my $params = {
605 itemnumber => $self->itemnumber,
606 suspend => 0,
607 -or => [
608 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
609 waitingdate => { '!=' => undef },
612 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
613 return Koha::Holds->_new_from_dbic($hold_rs);
616 =head3 stockrotationitem
618 my $sritem = Koha::Item->stockrotationitem;
620 Returns the stock rotation item associated with the current item.
622 =cut
624 sub stockrotationitem {
625 my ( $self ) = @_;
626 my $rs = $self->_result->stockrotationitem;
627 return 0 if !$rs;
628 return Koha::StockRotationItem->_new_from_dbic( $rs );
631 =head3 add_to_rota
633 my $item = $item->add_to_rota($rota_id);
635 Add this item to the rota identified by $ROTA_ID, which means associating it
636 with the first stage of that rota. Should this item already be associated
637 with a rota, then we will move it to the new rota.
639 =cut
641 sub add_to_rota {
642 my ( $self, $rota_id ) = @_;
643 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
644 return $self;
647 =head3 has_pending_hold
649 my $is_pending_hold = $item->has_pending_hold();
651 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 =cut
655 sub has_pending_hold {
656 my ( $self ) = @_;
657 my $pending_hold = $self->_result->tmp_holdsqueues;
658 return $pending_hold->count ? 1: 0;
661 =head3 as_marc_field
663 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
664 my $field = $item->as_marc_field({ [ mss => $mss ] });
666 This method returns a MARC::Field object representing the Koha::Item object
667 with the current mappings configuration.
669 =cut
671 sub as_marc_field {
672 my ( $self, $params ) = @_;
674 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
675 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
677 my @subfields;
679 my @columns = $self->_result->result_source->columns;
681 foreach my $item_field ( @columns ) {
682 my $mapping = $mss->{ "items.$item_field"}[0];
683 my $tagfield = $mapping->{tagfield};
684 my $tagsubfield = $mapping->{tagsubfield};
685 next if !$tagfield; # TODO: Should we raise an exception instead?
686 # Feels like safe fallback is better
688 push @subfields, $tagsubfield => $self->$item_field;
691 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
692 push( @subfields, @{$unlinked_item_subfields} )
693 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
695 my $field;
697 $field = MARC::Field->new(
698 "$item_tag", ' ', ' ', @subfields
699 ) if @subfields;
701 return $field;
704 =head3 renewal_branchcode
706 Returns the branchcode to be recorded in statistics renewal of the item
708 =cut
710 sub renewal_branchcode {
712 my ($self, $params ) = @_;
714 my $interface = C4::Context->interface;
715 my $branchcode;
716 if ( $interface eq 'opac' ){
717 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
718 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
719 $branchcode = 'OPACRenew';
721 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
722 $branchcode = $self->homebranch;
724 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
725 $branchcode = $self->checkout->patron->branchcode;
727 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
728 $branchcode = $self->checkout->branchcode;
730 else {
731 $branchcode = "";
733 } else {
734 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
735 ? C4::Context->userenv->{branch} : $params->{branch};
737 return $branchcode;
740 =head3 to_api_mapping
742 This method returns the mapping for representing a Koha::Item object
743 on the API.
745 =cut
747 sub to_api_mapping {
748 return {
749 itemnumber => 'item_id',
750 biblionumber => 'biblio_id',
751 biblioitemnumber => undef,
752 barcode => 'external_id',
753 dateaccessioned => 'acquisition_date',
754 booksellerid => 'acquisition_source',
755 homebranch => 'home_library_id',
756 price => 'purchase_price',
757 replacementprice => 'replacement_price',
758 replacementpricedate => 'replacement_price_date',
759 datelastborrowed => 'last_checkout_date',
760 datelastseen => 'last_seen_date',
761 stack => undef,
762 notforloan => 'not_for_loan_status',
763 damaged => 'damaged_status',
764 damaged_on => 'damaged_date',
765 itemlost => 'lost_status',
766 itemlost_on => 'lost_date',
767 withdrawn => 'withdrawn',
768 withdrawn_on => 'withdrawn_date',
769 itemcallnumber => 'callnumber',
770 coded_location_qualifier => 'coded_location_qualifier',
771 issues => 'checkouts_count',
772 renewals => 'renewals_count',
773 reserves => 'holds_count',
774 restricted => 'restricted_status',
775 itemnotes => 'public_notes',
776 itemnotes_nonpublic => 'internal_notes',
777 holdingbranch => 'holding_library_id',
778 paidfor => undef,
779 timestamp => 'timestamp',
780 location => 'location',
781 permanent_location => 'permanent_location',
782 onloan => 'checked_out_date',
783 cn_source => 'call_number_source',
784 cn_sort => 'call_number_sort',
785 ccode => 'collection_code',
786 materials => 'materials_notes',
787 uri => 'uri',
788 itype => 'item_type',
789 more_subfields_xml => 'extended_subfields',
790 enumchron => 'serial_issue_number',
791 copynumber => 'copy_number',
792 stocknumber => 'inventory_number',
793 new_status => 'new_status'
797 =head2 Internal methods
799 =head3 _after_item_action_hooks
801 Helper method that takes care of calling all plugin hooks
803 =cut
805 sub _after_item_action_hooks {
806 my ( $self, $params ) = @_;
808 my $action = $params->{action};
810 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
812 my @plugins = Koha::Plugins->new->GetPlugins({
813 method => 'after_item_action',
816 if (@plugins) {
818 foreach my $plugin ( @plugins ) {
819 try {
820 $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
822 catch {
823 warn "$_";
830 =head3 _type
832 =cut
834 sub _type {
835 return 'Item';
838 =head1 AUTHOR
840 Kyle M Hall <kyle@bywatersolutions.com>
842 =cut