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
63 $params can take an optional 'skip_modzebra_update' parameter.
64 If set, the reindexation process will not happen (ModZebra 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 unless ( $self->in_storage ) { #AddItem
92 unless ( $self->permanent_location ) {
93 $self->permanent_location($self->location);
95 unless ( $self->replacementpricedate ) {
96 $self->replacementpricedate($today);
98 unless ( $self->datelastseen ) {
99 $self->datelastseen($today);
102 unless ( $self->dateaccessioned ) {
103 $self->dateaccessioned($today);
106 if ( $self->itemcallnumber
107 or $self->cn_source )
109 my $cn_sort = GetClassSort
( $self->cn_source, $self->itemcallnumber, "" );
110 $self->cn_sort($cn_sort);
113 C4
::Biblio
::ModZebra
( $self->biblionumber, "specialUpdate", "biblioserver" )
114 unless $params->{skip_modzebra_update
};
116 logaction
( "CATALOGUING", "ADD", $self->itemnumber, "item" )
117 if $log_action && C4
::Context
->preference("CataloguingLog");
119 $self->_after_item_action_hooks({ action
=> 'create' });
123 { # Update *_on fields if needed
124 # Why not for AddItem as well?
125 my @fields = qw( itemlost withdrawn damaged );
127 # Only retrieve the item if we need to set an "on" date field
128 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
129 my $pre_mod_item = $self->get_from_storage;
130 for my $field (@fields) {
132 and not $pre_mod_item->$field )
134 my $field_on = "${field}_on";
136 DateTime
::Format
::MySQL
->format_datetime( dt_from_string
() )
142 # If the field is defined but empty, we are removing and,
143 # and thus need to clear out the 'on' field as well
144 for my $field (@fields) {
145 if ( defined( $self->$field ) && !$self->$field ) {
146 my $field_on = "${field}_on";
147 $self->$field_on(undef);
152 my %updated_columns = $self->_result->get_dirty_columns;
153 return $self->SUPER::store
unless %updated_columns;
155 if ( exists $updated_columns{itemcallnumber
}
156 or exists $updated_columns{cn_source
} )
158 my $cn_sort = GetClassSort
( $self->cn_source, $self->itemcallnumber, "" );
159 $self->cn_sort($cn_sort);
163 if ( exists $updated_columns{location
}
164 and $self->location ne 'CART'
165 and $self->location ne 'PROC'
166 and not exists $updated_columns{permanent_location
} )
168 $self->permanent_location( $self->location );
171 C4
::Biblio
::ModZebra
( $self->biblionumber, "specialUpdate", "biblioserver" )
172 unless $params->{skip_modzebra_update
};
174 $self->_after_item_action_hooks({ action
=> 'modify' });
176 logaction
( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper
($self->unblessed) )
177 if $log_action && C4
::Context
->preference("CataloguingLog");
180 unless ( $self->dateaccessioned ) {
181 $self->dateaccessioned($today);
184 return $self->SUPER::store
;
193 my $params = @_ ?
shift : {};
195 # FIXME check the item has no current issues
196 # i.e. raise the appropriate exception
198 C4
::Biblio
::ModZebra
( $self->biblionumber, "specialUpdate", "biblioserver" )
199 unless $params->{skip_modzebra_update
};
201 $self->_after_item_action_hooks({ action
=> 'delete' });
203 logaction
( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
204 if C4
::Context
->preference("CataloguingLog");
206 return $self->SUPER::delete;
215 my $params = @_ ?
shift : {};
217 my $safe_to_delete = $self->safe_to_delete;
218 return $safe_to_delete unless $safe_to_delete eq '1';
220 $self->move_to_deleted;
222 return $self->delete($params);
225 =head3 safe_to_delete
227 returns 1 if the item is safe to delete,
229 "book_on_loan" if the item is checked out,
231 "not_same_branch" if the item is blocked by independent branches,
233 "book_reserved" if the there are holds aganst the item, or
235 "linked_analytics" if the item has linked analytic records.
237 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
244 return "book_on_loan" if $self->checkout;
246 return "not_same_branch"
247 if defined C4
::Context
->userenv
248 and !C4
::Context
->IsSuperLibrarian()
249 and C4
::Context
->preference("IndependentBranches")
250 and ( C4
::Context
->userenv->{branch
} ne $self->homebranch );
252 # check it doesn't have a waiting reserve
253 return "book_reserved"
254 if $self->holds->search( { found
=> [ 'W', 'T' ] } )->count;
256 return "linked_analytics"
257 if C4
::Items
::GetAnalyticsCount
( $self->itemnumber ) > 0;
259 return "last_item_for_hold"
260 if $self->biblio->items->count == 1
261 && $self->biblio->holds->search(
270 =head3 move_to_deleted
272 my $is_moved = $item->move_to_deleted;
274 Move an item to the deleteditems table.
275 This can be done before deleting an item, to make sure the data are not completely deleted.
279 sub move_to_deleted
{
281 my $item_infos = $self->unblessed;
282 delete $item_infos->{timestamp
}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
283 return Koha
::Database
->new->schema->resultset('Deleteditem')->create($item_infos);
287 =head3 effective_itemtype
289 Returns the itemtype for the item based on whether item level itemtypes are set or not.
293 sub effective_itemtype
{
296 return $self->_result()->effective_itemtype();
306 $self->{_home_branch
} ||= Koha
::Libraries
->find( $self->homebranch() );
308 return $self->{_home_branch
};
311 =head3 holding_branch
318 $self->{_holding_branch
} ||= Koha
::Libraries
->find( $self->holdingbranch() );
320 return $self->{_holding_branch
};
325 my $biblio = $item->biblio;
327 Return the bibliographic record of this item
333 my $biblio_rs = $self->_result->biblio;
334 return Koha
::Biblio
->_new_from_dbic( $biblio_rs );
339 my $biblioitem = $item->biblioitem;
341 Return the biblioitem record of this item
347 my $biblioitem_rs = $self->_result->biblioitem;
348 return Koha
::Biblioitem
->_new_from_dbic( $biblioitem_rs );
353 my $checkout = $item->checkout;
355 Return the checkout for this item
361 my $checkout_rs = $self->_result->issue;
362 return unless $checkout_rs;
363 return Koha
::Checkout
->_new_from_dbic( $checkout_rs );
368 my $holds = $item->holds();
369 my $holds = $item->holds($params);
370 my $holds = $item->holds({ found => 'W'});
372 Return holds attached to an item, optionally accept a hashref of params to pass to search
377 my ( $self,$params ) = @_;
378 my $holds_rs = $self->_result->reserves->search($params);
379 return Koha
::Holds
->_new_from_dbic( $holds_rs );
384 my $transfer = $item->get_transfer;
386 Return the transfer if the item is in transit or undef
392 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived
=> undef })->first;
393 return unless $transfer_rs;
394 return Koha
::Item
::Transfer
->_new_from_dbic( $transfer_rs );
397 =head3 last_returned_by
399 Gets and sets the last borrower to return an item.
401 Accepts and returns Koha::Patron objects
403 $item->last_returned_by( $borrowernumber );
405 $last_returned_by = $item->last_returned_by();
409 sub last_returned_by
{
410 my ( $self, $borrower ) = @_;
412 my $items_last_returned_by_rs = Koha
::Database
->new()->schema()->resultset('ItemsLastBorrower');
415 return $items_last_returned_by_rs->update_or_create(
416 { borrowernumber
=> $borrower->borrowernumber, itemnumber
=> $self->id } );
419 unless ( $self->{_last_returned_by
} ) {
420 my $result = $items_last_returned_by_rs->single( { itemnumber
=> $self->id } );
422 $self->{_last_returned_by
} = Koha
::Patrons
->find( $result->get_column('borrowernumber') );
426 return $self->{_last_returned_by
};
430 =head3 can_article_request
432 my $bool = $item->can_article_request( $borrower )
434 Returns true if item can be specifically requested
436 $borrower must be a Koha::Patron object
440 sub can_article_request
{
441 my ( $self, $borrower ) = @_;
443 my $rule = $self->article_request_type($borrower);
445 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
449 =head3 hidden_in_opac
451 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
453 Returns true if item fields match the hidding criteria defined in $rules.
454 Returns false otherwise.
456 Takes HASHref that can have the following parameters:
458 $rules : { <field> => [ value_1, ... ], ... }
460 Note: $rules inherits its structure from the parsed YAML from reading
461 the I<OpacHiddenItems> system preference.
466 my ( $self, $params ) = @_;
468 my $rules = $params->{rules
} // {};
471 if C4
::Context
->preference('hidelostitems') and
474 my $hidden_in_opac = 0;
476 foreach my $field ( keys %{$rules} ) {
478 if ( any
{ $self->$field eq $_ } @
{ $rules->{$field} } ) {
484 return $hidden_in_opac;
487 =head3 can_be_transferred
489 $item->can_be_transferred({ to => $to_library, from => $from_library })
490 Checks if an item can be transferred to given library.
492 This feature is controlled by two system preferences:
493 UseBranchTransferLimits to enable / disable the feature
494 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
495 for setting the limitations
497 Takes HASHref that can have the following parameters:
498 MANDATORY PARAMETERS:
501 $from : Koha::Library # if not given, item holdingbranch
502 # will be used instead
504 Returns 1 if item can be transferred to $to_library, otherwise 0.
506 To find out whether at least one item of a Koha::Biblio can be transferred, please
507 see Koha::Biblio->can_be_transferred() instead of using this method for
508 multiple items of the same biblio.
512 sub can_be_transferred
{
513 my ($self, $params) = @_;
515 my $to = $params->{to
};
516 my $from = $params->{from
};
518 $to = $to->branchcode;
519 $from = defined $from ?
$from->branchcode : $self->holdingbranch;
521 return 1 if $from eq $to; # Transfer to current branch is allowed
522 return 1 unless C4
::Context
->preference('UseBranchTransferLimits');
524 my $limittype = C4
::Context
->preference('BranchTransferLimitsType');
525 return Koha
::Item
::Transfer
::Limits
->search({
528 $limittype => $limittype eq 'itemtype'
529 ?
$self->effective_itemtype : $self->ccode
533 =head3 pickup_locations
535 $pickup_locations = $item->pickup_locations( {patron => $patron } )
537 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)
538 and if item can be transferred to each pickup location.
542 sub pickup_locations
{
543 my ($self, $params) = @_;
545 my $patron = $params->{patron
};
547 my $circ_control_branch =
548 C4
::Reserves
::GetReservesControlBranch
( $self->unblessed(), $patron->unblessed );
550 C4
::Circulation
::GetBranchItemRule
( $circ_control_branch, $self->itype );
553 if(defined $patron) {
554 return \
@libs if $branchitemrule->{holdallowed
} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode
=> $patron->branchcode} );
555 return \
@libs if $branchitemrule->{holdallowed
} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
558 if ($branchitemrule->{hold_fulfillment_policy
} eq 'holdgroup') {
559 @libs = $self->home_branch->get_hold_libraries;
560 push @libs, $self->home_branch unless scalar(@libs) > 0;
561 } elsif ($branchitemrule->{hold_fulfillment_policy
} eq 'patrongroup') {
562 my $plib = Koha
::Libraries
->find({ branchcode
=> $patron->branchcode});
563 @libs = $plib->get_hold_libraries;
564 push @libs, $self->home_branch unless scalar(@libs) > 0;
565 } elsif ($branchitemrule->{hold_fulfillment_policy
} eq 'homebranch') {
566 push @libs, $self->home_branch;
567 } elsif ($branchitemrule->{hold_fulfillment_policy
} eq 'holdingbranch') {
568 push @libs, $self->holding_branch;
570 @libs = Koha
::Libraries
->search({
573 order_by
=> ['branchname']
577 my @pickup_locations;
578 foreach my $library (@libs) {
579 if ($library->pickup_location && $self->can_be_transferred({ to
=> $library })) {
580 push @pickup_locations, $library;
584 return \
@pickup_locations;
587 =head3 article_request_type
589 my $type = $item->article_request_type( $borrower )
591 returns 'yes', 'no', 'bib_only', or 'item_only'
593 $borrower must be a Koha::Patron object
597 sub article_request_type
{
598 my ( $self, $borrower ) = @_;
600 my $branch_control = C4
::Context
->preference('HomeOrHoldingBranch');
602 $branch_control eq 'homebranch' ?
$self->homebranch
603 : $branch_control eq 'holdingbranch' ?
$self->holdingbranch
605 my $borrowertype = $borrower->categorycode;
606 my $itemtype = $self->effective_itemtype();
607 my $rule = Koha
::CirculationRules
->get_effective_rule(
609 rule_name
=> 'article_requests',
610 categorycode
=> $borrowertype,
611 itemtype
=> $itemtype,
612 branchcode
=> $branchcode
616 return q{} unless $rule;
617 return $rule->rule_value || q{}
626 my $attributes = { order_by
=> 'priority' };
627 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
629 itemnumber
=> $self->itemnumber,
632 reservedate
=> { '<=' => $dtf->format_date(dt_from_string
) },
633 waitingdate
=> { '!=' => undef },
636 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
637 return Koha
::Holds
->_new_from_dbic($hold_rs);
640 =head3 stockrotationitem
642 my $sritem = Koha::Item->stockrotationitem;
644 Returns the stock rotation item associated with the current item.
648 sub stockrotationitem
{
650 my $rs = $self->_result->stockrotationitem;
652 return Koha
::StockRotationItem
->_new_from_dbic( $rs );
657 my $item = $item->add_to_rota($rota_id);
659 Add this item to the rota identified by $ROTA_ID, which means associating it
660 with the first stage of that rota. Should this item already be associated
661 with a rota, then we will move it to the new rota.
666 my ( $self, $rota_id ) = @_;
667 Koha
::StockRotationRotas
->find($rota_id)->add_item($self->itemnumber);
671 =head3 has_pending_hold
673 my $is_pending_hold = $item->has_pending_hold();
675 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
679 sub has_pending_hold
{
681 my $pending_hold = $self->_result->tmp_holdsqueues;
682 return $pending_hold->count ?
1: 0;
687 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
688 my $field = $item->as_marc_field({ [ mss => $mss ] });
690 This method returns a MARC::Field object representing the Koha::Item object
691 with the current mappings configuration.
696 my ( $self, $params ) = @_;
698 my $mss = $params->{mss
} // C4
::Biblio
::GetMarcSubfieldStructure
( '', { unsafe
=> 1 } );
699 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield
};
703 my @columns = $self->_result->result_source->columns;
705 foreach my $item_field ( @columns ) {
706 my $mapping = $mss->{ "items.$item_field"}[0];
707 my $tagfield = $mapping->{tagfield
};
708 my $tagsubfield = $mapping->{tagsubfield
};
709 next if !$tagfield; # TODO: Should we raise an exception instead?
710 # Feels like safe fallback is better
712 push @subfields, $tagsubfield => $self->$item_field
713 if defined $self->$item_field and $item_field ne '';
716 my $unlinked_item_subfields = C4
::Items
::_parse_unlinked_item_subfields_from_xml
($self->more_subfields_xml);
717 push( @subfields, @
{$unlinked_item_subfields} )
718 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
722 $field = MARC
::Field
->new(
723 "$item_tag", ' ', ' ', @subfields
729 =head3 renewal_branchcode
731 Returns the branchcode to be recorded in statistics renewal of the item
735 sub renewal_branchcode
{
737 my ($self, $params ) = @_;
739 my $interface = C4
::Context
->interface;
741 if ( $interface eq 'opac' ){
742 my $renewal_branchcode = C4
::Context
->preference('OpacRenewalBranch');
743 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
744 $branchcode = 'OPACRenew';
746 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
747 $branchcode = $self->homebranch;
749 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
750 $branchcode = $self->checkout->patron->branchcode;
752 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
753 $branchcode = $self->checkout->branchcode;
759 $branchcode = ( C4
::Context
->userenv && defined C4
::Context
->userenv->{branch
} )
760 ? C4
::Context
->userenv->{branch
} : $params->{branch
};
765 =head3 to_api_mapping
767 This method returns the mapping for representing a Koha::Item object
774 itemnumber
=> 'item_id',
775 biblionumber
=> 'biblio_id',
776 biblioitemnumber
=> undef,
777 barcode
=> 'external_id',
778 dateaccessioned
=> 'acquisition_date',
779 booksellerid
=> 'acquisition_source',
780 homebranch
=> 'home_library_id',
781 price
=> 'purchase_price',
782 replacementprice
=> 'replacement_price',
783 replacementpricedate
=> 'replacement_price_date',
784 datelastborrowed
=> 'last_checkout_date',
785 datelastseen
=> 'last_seen_date',
787 notforloan
=> 'not_for_loan_status',
788 damaged
=> 'damaged_status',
789 damaged_on
=> 'damaged_date',
790 itemlost
=> 'lost_status',
791 itemlost_on
=> 'lost_date',
792 withdrawn
=> 'withdrawn',
793 withdrawn_on
=> 'withdrawn_date',
794 itemcallnumber
=> 'callnumber',
795 coded_location_qualifier
=> 'coded_location_qualifier',
796 issues
=> 'checkouts_count',
797 renewals
=> 'renewals_count',
798 reserves
=> 'holds_count',
799 restricted
=> 'restricted_status',
800 itemnotes
=> 'public_notes',
801 itemnotes_nonpublic
=> 'internal_notes',
802 holdingbranch
=> 'holding_library_id',
804 timestamp
=> 'timestamp',
805 location
=> 'location',
806 permanent_location
=> 'permanent_location',
807 onloan
=> 'checked_out_date',
808 cn_source
=> 'call_number_source',
809 cn_sort
=> 'call_number_sort',
810 ccode
=> 'collection_code',
811 materials
=> 'materials_notes',
813 itype
=> 'item_type',
814 more_subfields_xml
=> 'extended_subfields',
815 enumchron
=> 'serial_issue_number',
816 copynumber
=> 'copy_number',
817 stocknumber
=> 'inventory_number',
818 new_status
=> 'new_status'
822 =head2 Internal methods
824 =head3 _after_item_action_hooks
826 Helper method that takes care of calling all plugin hooks
830 sub _after_item_action_hooks
{
831 my ( $self, $params ) = @_;
833 my $action = $params->{action
};
835 if ( C4
::Context
->config("enable_plugins") ) {
837 my @plugins = Koha
::Plugins
->new->GetPlugins({
838 method
=> 'after_item_action',
843 foreach my $plugin ( @plugins ) {
845 $plugin->after_item_action({ action
=> $action, item
=> $self, item_id
=> $self->itemnumber });
865 Kyle M Hall <kyle@bywatersolutions.com>