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>.
22 use List::MoreUtils qw( any );
25 use Koha::DateUtils qw( dt_from_string output_pref );
28 use C4::Circulation qw( barcodedecode GetBranchItemRule );
30 use C4::ClassSource qw( GetClassSort );
31 use C4::Log qw( logaction );
33 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
34 use Koha::Biblio::ItemGroups;
36 use Koha::CirculationRules;
37 use Koha::CoverImages;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Attributes;
40 use Koha::Item::Transfer::Limits;
41 use Koha::Item::Transfers;
46 use Koha::Result::Boolean;
47 use Koha::SearchEngine::Indexer;
48 use Koha::StockRotationItem;
49 use Koha::StockRotationRotas;
50 use Koha::TrackedLinks;
52 use base qw(Koha::Object);
56 Koha::Item - Koha Item object class
68 $params can take an optional 'skip_record_index' parameter.
69 If set, the reindexation process will not happen (index_records not called)
71 NOTE: This is a temporary fix to answer a performance issue when lot of items
72 are added (or modified) at the same time.
73 The correct way to fix this is to make the ES reindexation process async.
74 You should not turn it on if you do not understand what it is doing exactly.
80 my $params = @_ ? shift : {};
82 my $log_action = $params->{log_action} // 1;
84 # We do not want to oblige callers to pass this value
85 # Dev conveniences vs performance?
86 unless ( $self->biblioitemnumber ) {
87 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
90 # See related changes from C4::Items::AddItem
91 unless ( $self->itype ) {
92 $self->itype($self->biblio->biblioitem->itemtype);
95 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
97 my $today = dt_from_string;
98 my $action = 'create';
100 unless ( $self->in_storage ) { #AddItem
102 unless ( $self->permanent_location ) {
103 $self->permanent_location($self->location);
106 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
107 unless ( $self->location || !$default_location ) {
108 $self->permanent_location( $self->location || $default_location )
109 unless $self->permanent_location;
110 $self->location($default_location);
113 unless ( $self->replacementpricedate ) {
114 $self->replacementpricedate($today);
116 unless ( $self->datelastseen ) {
117 $self->datelastseen($today);
120 unless ( $self->dateaccessioned ) {
121 $self->dateaccessioned($today);
124 if ( $self->itemcallnumber
125 or $self->cn_source )
127 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
128 $self->cn_sort($cn_sort);
135 my %updated_columns = $self->_result->get_dirty_columns;
136 return $self->SUPER::store unless %updated_columns;
138 # Retrieve the item for comparison if we need to
140 exists $updated_columns{itemlost}
141 or exists $updated_columns{withdrawn}
142 or exists $updated_columns{damaged}
143 ) ? $self->get_from_storage : undef;
145 # Update *_on fields if needed
146 # FIXME: Why not for AddItem as well?
147 my @fields = qw( itemlost withdrawn damaged );
148 for my $field (@fields) {
150 # If the field is defined but empty or 0, we are
151 # removing/unsetting and thus need to clear out
153 if ( exists $updated_columns{$field}
154 && defined( $self->$field )
157 my $field_on = "${field}_on";
158 $self->$field_on(undef);
160 # If the field has changed otherwise, we much update
162 elsif (exists $updated_columns{$field}
163 && $updated_columns{$field}
164 && !$pre_mod_item->$field )
166 my $field_on = "${field}_on";
168 DateTime::Format::MySQL->format_datetime(
175 if ( exists $updated_columns{itemcallnumber}
176 or exists $updated_columns{cn_source} )
178 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
179 $self->cn_sort($cn_sort);
183 if ( exists $updated_columns{location}
184 and $self->location ne 'CART'
185 and $self->location ne 'PROC'
186 and not exists $updated_columns{permanent_location} )
188 $self->permanent_location( $self->location );
191 # If item was lost and has now been found,
192 # reverse any list item charges if necessary.
193 if ( exists $updated_columns{itemlost}
194 and $updated_columns{itemlost} <= 0
195 and $pre_mod_item->itemlost > 0 )
197 $self->_set_found_trigger($pre_mod_item);
202 my $result = $self->SUPER::store;
203 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
205 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
206 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
208 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
209 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
210 unless $params->{skip_record_index};
211 $self->get_from_storage->_after_item_action_hooks({ action => $action });
213 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
215 biblio_ids => [ $self->biblionumber ]
217 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
228 my $params = @_ ? shift : {};
230 # FIXME check the item has no current issues
231 # i.e. raise the appropriate exception
233 my $result = $self->SUPER::delete;
235 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
236 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
237 unless $params->{skip_record_index};
239 $self->_after_item_action_hooks({ action => 'delete' });
241 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
242 if C4::Context->preference("CataloguingLog");
244 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
246 biblio_ids => [ $self->biblionumber ]
248 ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
259 my $params = @_ ? shift : {};
261 my $safe_to_delete = $self->safe_to_delete;
262 return $safe_to_delete unless $safe_to_delete;
264 $self->move_to_deleted;
266 return $self->delete($params);
269 =head3 safe_to_delete
271 returns 1 if the item is safe to delete,
273 "book_on_loan" if the item is checked out,
275 "not_same_branch" if the item is blocked by independent branches,
277 "book_reserved" if the there are holds aganst the item, or
279 "linked_analytics" if the item has linked analytic records.
281 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
290 $error = "book_on_loan" if $self->checkout;
292 $error = "not_same_branch"
293 if defined C4::Context->userenv
294 and !C4::Context->IsSuperLibrarian()
295 and C4::Context->preference("IndependentBranches")
296 and ( C4::Context->userenv->{branch} ne $self->homebranch );
298 # check it doesn't have a waiting reserve
299 $error = "book_reserved"
300 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
302 $error = "linked_analytics"
303 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
305 $error = "last_item_for_hold"
306 if $self->biblio->items->count == 1
307 && $self->biblio->holds->search(
314 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
317 return Koha::Result::Boolean->new(1);
320 =head3 move_to_deleted
322 my $is_moved = $item->move_to_deleted;
324 Move an item to the deleteditems table.
325 This can be done before deleting an item, to make sure the data are not completely deleted.
329 sub move_to_deleted {
331 my $item_infos = $self->unblessed;
332 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
333 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
337 =head3 effective_itemtype
339 Returns the itemtype for the item based on whether item level itemtypes are set or not.
343 sub effective_itemtype {
346 return $self->_result()->effective_itemtype();
356 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
358 return $self->{_home_branch};
361 =head3 holding_branch
368 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
370 return $self->{_holding_branch};
375 my $biblio = $item->biblio;
377 Return the bibliographic record of this item
383 my $biblio_rs = $self->_result->biblio;
384 return Koha::Biblio->_new_from_dbic( $biblio_rs );
389 my $biblioitem = $item->biblioitem;
391 Return the biblioitem record of this item
397 my $biblioitem_rs = $self->_result->biblioitem;
398 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
403 my $checkout = $item->checkout;
405 Return the checkout for this item
411 my $checkout_rs = $self->_result->issue;
412 return unless $checkout_rs;
413 return Koha::Checkout->_new_from_dbic( $checkout_rs );
418 my $item_group = $item->item_group;
420 Return the item group for this item
427 my $item_group_item = $self->_result->item_group_item;
428 return unless $item_group_item;
430 my $item_group_rs = $item_group_item->item_group;
431 return unless $item_group_rs;
433 my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
439 my $holds = $item->holds();
440 my $holds = $item->holds($params);
441 my $holds = $item->holds({ found => 'W'});
443 Return holds attached to an item, optionally accept a hashref of params to pass to search
448 my ( $self,$params ) = @_;
449 my $holds_rs = $self->_result->reserves->search($params);
450 return Koha::Holds->_new_from_dbic( $holds_rs );
453 =head3 request_transfer
455 my $transfer = $item->request_transfer(
459 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
463 Add a transfer request for this item to the given branch for the given reason.
465 An exception will be thrown if the BranchTransferLimits would prevent the requested
466 transfer, unless 'ignore_limits' is passed to override the limits.
468 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
469 The caller should catch such cases and retry the transfer request as appropriate passing
470 an appropriate override.
473 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
474 * replace - Used to replace the existing transfer request with your own.
478 sub request_transfer {
479 my ( $self, $params ) = @_;
481 # check for mandatory params
482 my @mandatory = ( 'to', 'reason' );
483 for my $param (@mandatory) {
484 unless ( defined( $params->{$param} ) ) {
485 Koha::Exceptions::MissingParameter->throw(
486 error => "The $param parameter is mandatory" );
490 Koha::Exceptions::Item::Transfer::Limit->throw()
491 unless ( $params->{ignore_limits}
492 || $self->can_be_transferred( { to => $params->{to} } ) );
494 my $request = $self->get_transfer;
495 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
496 if ( $request && !$params->{enqueue} && !$params->{replace} );
498 $request->cancel( { reason => $params->{reason}, force => 1 } )
499 if ( defined($request) && $params->{replace} );
501 my $transfer = Koha::Item::Transfer->new(
503 itemnumber => $self->itemnumber,
504 daterequested => dt_from_string,
505 frombranch => $self->holdingbranch,
506 tobranch => $params->{to}->branchcode,
507 reason => $params->{reason},
508 comments => $params->{comment}
517 my $transfer = $item->get_transfer;
519 Return the active transfer request or undef
521 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
522 whereby the most recently sent, but not received, transfer will be returned
523 if it exists, otherwise the oldest unsatisfied transfer will be returned.
525 This allows for transfers to queue, which is the case for stock rotation and
526 rotating collections where a manual transfer may need to take precedence but
527 we still expect the item to end up at a final location eventually.
533 my $transfer_rs = $self->_result->branchtransfers->search(
535 datearrived => undef,
536 datecancelled => undef
540 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
544 return unless $transfer_rs;
545 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
550 my $transfer = $item->get_transfers;
552 Return the list of outstanding transfers (i.e requested but not yet cancelled
555 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
556 whereby the most recently sent, but not received, transfer will be returned
557 first if it exists, otherwise requests are in oldest to newest request order.
559 This allows for transfers to queue, which is the case for stock rotation and
560 rotating collections where a manual transfer may need to take precedence but
561 we still expect the item to end up at a final location eventually.
567 my $transfer_rs = $self->_result->branchtransfers->search(
569 datearrived => undef,
570 datecancelled => undef
574 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
577 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
580 =head3 last_returned_by
582 Gets and sets the last borrower to return an item.
584 Accepts and returns Koha::Patron objects
586 $item->last_returned_by( $borrowernumber );
588 $last_returned_by = $item->last_returned_by();
592 sub last_returned_by {
593 my ( $self, $borrower ) = @_;
595 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
598 return $items_last_returned_by_rs->update_or_create(
599 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
602 unless ( $self->{_last_returned_by} ) {
603 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
605 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
609 return $self->{_last_returned_by};
613 =head3 can_article_request
615 my $bool = $item->can_article_request( $borrower )
617 Returns true if item can be specifically requested
619 $borrower must be a Koha::Patron object
623 sub can_article_request {
624 my ( $self, $borrower ) = @_;
626 my $rule = $self->article_request_type($borrower);
628 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
632 =head3 hidden_in_opac
634 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
636 Returns true if item fields match the hidding criteria defined in $rules.
637 Returns false otherwise.
639 Takes HASHref that can have the following parameters:
641 $rules : { <field> => [ value_1, ... ], ... }
643 Note: $rules inherits its structure from the parsed YAML from reading
644 the I<OpacHiddenItems> system preference.
649 my ( $self, $params ) = @_;
651 my $rules = $params->{rules} // {};
654 if C4::Context->preference('hidelostitems') and
657 my $hidden_in_opac = 0;
659 foreach my $field ( keys %{$rules} ) {
661 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
667 return $hidden_in_opac;
670 =head3 can_be_transferred
672 $item->can_be_transferred({ to => $to_library, from => $from_library })
673 Checks if an item can be transferred to given library.
675 This feature is controlled by two system preferences:
676 UseBranchTransferLimits to enable / disable the feature
677 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
678 for setting the limitations
680 Takes HASHref that can have the following parameters:
681 MANDATORY PARAMETERS:
684 $from : Koha::Library # if not given, item holdingbranch
685 # will be used instead
687 Returns 1 if item can be transferred to $to_library, otherwise 0.
689 To find out whether at least one item of a Koha::Biblio can be transferred, please
690 see Koha::Biblio->can_be_transferred() instead of using this method for
691 multiple items of the same biblio.
695 sub can_be_transferred {
696 my ($self, $params) = @_;
698 my $to = $params->{to};
699 my $from = $params->{from};
701 $to = $to->branchcode;
702 $from = defined $from ? $from->branchcode : $self->holdingbranch;
704 return 1 if $from eq $to; # Transfer to current branch is allowed
705 return 1 unless C4::Context->preference('UseBranchTransferLimits');
707 my $limittype = C4::Context->preference('BranchTransferLimitsType');
708 return Koha::Item::Transfer::Limits->search({
711 $limittype => $limittype eq 'itemtype'
712 ? $self->effective_itemtype : $self->ccode
717 =head3 pickup_locations
719 $pickup_locations = $item->pickup_locations( {patron => $patron } )
721 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)
722 and if item can be transferred to each pickup location.
726 sub pickup_locations {
727 my ($self, $params) = @_;
729 my $patron = $params->{patron};
731 my $circ_control_branch =
732 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
734 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
736 if(defined $patron) {
737 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
738 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
741 my $pickup_libraries = Koha::Libraries->search();
742 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
743 $pickup_libraries = $self->home_branch->get_hold_libraries;
744 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
745 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
746 $pickup_libraries = $plib->get_hold_libraries;
747 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
748 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
749 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
750 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
753 return $pickup_libraries->search(
758 order_by => ['branchname']
760 ) unless C4::Context->preference('UseBranchTransferLimits');
762 my $limittype = C4::Context->preference('BranchTransferLimitsType');
763 my ($ccode, $itype) = (undef, undef);
764 if( $limittype eq 'ccode' ){
765 $ccode = $self->ccode;
767 $itype = $self->itype;
769 my $limits = Koha::Item::Transfer::Limits->search(
771 fromBranch => $self->holdingbranch,
775 { columns => ['toBranch'] }
778 return $pickup_libraries->search(
780 pickup_location => 1,
782 '-not_in' => $limits->_resultset->as_query
786 order_by => ['branchname']
791 =head3 article_request_type
793 my $type = $item->article_request_type( $borrower )
795 returns 'yes', 'no', 'bib_only', or 'item_only'
797 $borrower must be a Koha::Patron object
801 sub article_request_type {
802 my ( $self, $borrower ) = @_;
804 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
806 $branch_control eq 'homebranch' ? $self->homebranch
807 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
809 my $borrowertype = $borrower->categorycode;
810 my $itemtype = $self->effective_itemtype();
811 my $rule = Koha::CirculationRules->get_effective_rule(
813 rule_name => 'article_requests',
814 categorycode => $borrowertype,
815 itemtype => $itemtype,
816 branchcode => $branchcode
820 return q{} unless $rule;
821 return $rule->rule_value || q{}
830 my $attributes = { order_by => 'priority' };
831 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
833 itemnumber => $self->itemnumber,
836 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
837 waitingdate => { '!=' => undef },
840 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
841 return Koha::Holds->_new_from_dbic($hold_rs);
844 =head3 stockrotationitem
846 my $sritem = Koha::Item->stockrotationitem;
848 Returns the stock rotation item associated with the current item.
852 sub stockrotationitem {
854 my $rs = $self->_result->stockrotationitem;
856 return Koha::StockRotationItem->_new_from_dbic( $rs );
861 my $item = $item->add_to_rota($rota_id);
863 Add this item to the rota identified by $ROTA_ID, which means associating it
864 with the first stage of that rota. Should this item already be associated
865 with a rota, then we will move it to the new rota.
870 my ( $self, $rota_id ) = @_;
871 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
875 =head3 has_pending_hold
877 my $is_pending_hold = $item->has_pending_hold();
879 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
883 sub has_pending_hold {
885 my $pending_hold = $self->_result->tmp_holdsqueues;
886 return $pending_hold->count ? 1: 0;
891 my $field = $item->as_marc_field;
893 This method returns a MARC::Field object representing the Koha::Item object
894 with the current mappings configuration.
901 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
903 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
907 my $item_field = $tagslib->{$itemtag};
909 my $more_subfields = $self->additional_attributes->to_hashref;
910 foreach my $subfield (
912 $a->{display_order} <=> $b->{display_order}
913 || $a->{subfield} cmp $b->{subfield}
914 } grep { ref($_) && %$_ } values %$item_field
917 my $kohafield = $subfield->{kohafield};
918 my $tagsubfield = $subfield->{tagsubfield};
920 if ( defined $kohafield ) {
921 next if $kohafield !~ m{^items\.}; # That would be weird!
922 ( my $attribute = $kohafield ) =~ s|^items\.||;
923 $value = $self->$attribute # This call may fail if a kohafield is not a DB column but we don't want to add extra work for that there
924 if defined $self->$attribute and $self->$attribute ne '';
926 $value = $more_subfields->{$tagsubfield}
929 next unless defined $value
932 if ( $subfield->{repeatable} ) {
933 my @values = split '\|', $value;
934 push @subfields, ( $tagsubfield => $_ ) for @values;
937 push @subfields, ( $tagsubfield => $value );
942 return unless @subfields;
944 return MARC::Field->new(
945 "$itemtag", ' ', ' ', @subfields
949 =head3 renewal_branchcode
951 Returns the branchcode to be recorded in statistics renewal of the item
955 sub renewal_branchcode {
957 my ($self, $params ) = @_;
959 my $interface = C4::Context->interface;
961 if ( $interface eq 'opac' ){
962 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
963 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
964 $branchcode = 'OPACRenew';
966 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
967 $branchcode = $self->homebranch;
969 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
970 $branchcode = $self->checkout->patron->branchcode;
972 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
973 $branchcode = $self->checkout->branchcode;
979 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
980 ? C4::Context->userenv->{branch} : $params->{branch};
987 Return the cover images associated with this item.
994 my $cover_image_rs = $self->_result->cover_images;
995 return unless $cover_image_rs;
996 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
999 =head3 columns_to_str
1001 my $values = $items->columns_to_str;
1003 Return a hashref with the string representation of the different attribute of the item.
1005 This is meant to be used for display purpose only.
1009 sub columns_to_str {
1012 my $frameworkcode = $self->biblio->frameworkcode;
1013 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
1014 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1016 my $columns_info = $self->_result->result_source->columns_info;
1018 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1020 for my $column ( keys %$columns_info ) {
1022 next if $column eq 'more_subfields_xml';
1024 my $value = $self->$column;
1025 # Maybe we need to deal with datetime columns here, but so far we have damaged_on, itemlost_on and withdrawn_on, and they are not linked with kohafield
1027 if ( not defined $value or $value eq "" ) {
1028 $values->{$column} = $value;
1033 exists $mss->{"items.$column"}
1034 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1037 $values->{$column} =
1039 ? $subfield->{authorised_value}
1040 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1041 $subfield->{tagsubfield}, $value, '', $tagslib )
1047 $self->more_subfields_xml
1048 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1053 my ( $field ) = $marc_more->fields;
1054 for my $sf ( $field->subfields ) {
1055 my $subfield_code = $sf->[0];
1056 my $value = $sf->[1];
1057 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1058 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1060 $subfield->{authorised_value}
1061 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1062 $subfield->{tagsubfield}, $value, '', $tagslib )
1065 push @{$more_values->{$subfield_code}}, $value;
1068 while ( my ( $k, $v ) = each %$more_values ) {
1069 $values->{$k} = join ' | ', @$v;
1076 =head3 additional_attributes
1078 my $attributes = $item->additional_attributes;
1079 $attributes->{k} = 'new k';
1080 $item->update({ more_subfields => $attributes->to_marcxml });
1082 Returns a Koha::Item::Attributes object that represents the non-mapped
1083 attributes for this item.
1087 sub additional_attributes {
1090 return Koha::Item::Attributes->new_from_marcxml(
1091 $self->more_subfields_xml,
1095 =head3 _set_found_trigger
1097 $self->_set_found_trigger
1099 Finds the most recent lost item charge for this item and refunds the patron
1100 appropriately, taking into account any payments or writeoffs already applied
1103 Internal function, not exported, called only by Koha::Item->store.
1107 sub _set_found_trigger {
1108 my ( $self, $pre_mod_item ) = @_;
1110 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1111 my $no_refund_after_days =
1112 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1113 if ($no_refund_after_days) {
1114 my $today = dt_from_string();
1115 my $lost_age_in_days =
1116 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1119 return $self unless $lost_age_in_days < $no_refund_after_days;
1122 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1125 return_branch => C4::Context->userenv
1126 ? C4::Context->userenv->{'branch'}
1131 if ( $lostreturn_policy ) {
1133 # refund charge made for lost book
1134 my $lost_charge = Koha::Account::Lines->search(
1136 itemnumber => $self->itemnumber,
1137 debit_type_code => 'LOST',
1138 status => [ undef, { '<>' => 'FOUND' } ]
1141 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1146 if ( $lost_charge ) {
1148 my $patron = $lost_charge->patron;
1151 my $account = $patron->account;
1152 my $total_to_refund = 0;
1155 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1157 # some amount has been cancelled. collect the offsets that are not writeoffs
1158 # this works because the only way to subtract from this kind of a debt is
1159 # using the UI buttons 'Pay' and 'Write off'
1160 my $credit_offsets = $lost_charge->debit_offsets(
1162 'credit_id' => { '!=' => undef },
1163 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1165 { join => 'credit' }
1168 $total_to_refund = ( $credit_offsets->count > 0 )
1169 ? $credit_offsets->total * -1 # credits are negative on the DB
1173 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1176 if ( $credit_total > 0 ) {
1178 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1179 $credit = $account->add_credit(
1181 amount => $credit_total,
1182 description => 'Item found ' . $self->itemnumber,
1183 type => 'LOST_FOUND',
1184 interface => C4::Context->interface,
1185 library_id => $branchcode,
1186 item_id => $self->itemnumber,
1187 issue_id => $lost_charge->issue_id
1191 $credit->apply( { debits => [$lost_charge] } );
1195 message => 'lost_refunded',
1196 payload => { credit_id => $credit->id }
1201 # Update the account status
1202 $lost_charge->status('FOUND');
1203 $lost_charge->store();
1205 # Reconcile balances if required
1206 if ( C4::Context->preference('AccountAutoReconcile') ) {
1207 $account->reconcile_balance;
1212 # restore fine for lost book
1213 if ( $lostreturn_policy eq 'restore' ) {
1214 my $lost_overdue = Koha::Account::Lines->search(
1216 itemnumber => $self->itemnumber,
1217 debit_type_code => 'OVERDUE',
1221 order_by => { '-desc' => 'date' },
1226 if ( $lost_overdue ) {
1228 my $patron = $lost_overdue->patron;
1230 my $account = $patron->account;
1232 # Update status of fine
1233 $lost_overdue->status('FOUND')->store();
1235 # Find related forgive credit
1236 my $refund = $lost_overdue->credits(
1238 credit_type_code => 'FORGIVEN',
1239 itemnumber => $self->itemnumber,
1240 status => [ { '!=' => 'VOID' }, undef ]
1242 { order_by => { '-desc' => 'date' }, rows => 1 }
1246 # Revert the forgive credit
1247 $refund->void({ interface => 'trigger' });
1251 message => 'lost_restored',
1252 payload => { refund_id => $refund->id }
1257 # Reconcile balances if required
1258 if ( C4::Context->preference('AccountAutoReconcile') ) {
1259 $account->reconcile_balance;
1263 } elsif ( $lostreturn_policy eq 'charge' ) {
1267 message => 'lost_charge',
1276 =head3 public_read_list
1278 This method returns the list of publicly readable database fields for both API and UI output purposes
1282 sub public_read_list {
1284 'itemnumber', 'biblionumber', 'homebranch',
1285 'holdingbranch', 'location', 'collectioncode',
1286 'itemcallnumber', 'copynumber', 'enumchron',
1287 'barcode', 'dateaccessioned', 'itemnotes',
1288 'onloan', 'uri', 'itype',
1289 'notforloan', 'damaged', 'itemlost',
1290 'withdrawn', 'restricted'
1294 =head3 to_api_mapping
1296 This method returns the mapping for representing a Koha::Item object
1301 sub to_api_mapping {
1303 itemnumber => 'item_id',
1304 biblionumber => 'biblio_id',
1305 biblioitemnumber => undef,
1306 barcode => 'external_id',
1307 dateaccessioned => 'acquisition_date',
1308 booksellerid => 'acquisition_source',
1309 homebranch => 'home_library_id',
1310 price => 'purchase_price',
1311 replacementprice => 'replacement_price',
1312 replacementpricedate => 'replacement_price_date',
1313 datelastborrowed => 'last_checkout_date',
1314 datelastseen => 'last_seen_date',
1316 notforloan => 'not_for_loan_status',
1317 damaged => 'damaged_status',
1318 damaged_on => 'damaged_date',
1319 itemlost => 'lost_status',
1320 itemlost_on => 'lost_date',
1321 withdrawn => 'withdrawn',
1322 withdrawn_on => 'withdrawn_date',
1323 itemcallnumber => 'callnumber',
1324 coded_location_qualifier => 'coded_location_qualifier',
1325 issues => 'checkouts_count',
1326 renewals => 'renewals_count',
1327 reserves => 'holds_count',
1328 restricted => 'restricted_status',
1329 itemnotes => 'public_notes',
1330 itemnotes_nonpublic => 'internal_notes',
1331 holdingbranch => 'holding_library_id',
1332 timestamp => 'timestamp',
1333 location => 'location',
1334 permanent_location => 'permanent_location',
1335 onloan => 'checked_out_date',
1336 cn_source => 'call_number_source',
1337 cn_sort => 'call_number_sort',
1338 ccode => 'collection_code',
1339 materials => 'materials_notes',
1341 itype => 'item_type_id',
1342 more_subfields_xml => 'extended_subfields',
1343 enumchron => 'serial_issue_number',
1344 copynumber => 'copy_number',
1345 stocknumber => 'inventory_number',
1346 new_status => 'new_status'
1352 my $itemtype = $item->itemtype;
1354 Returns Koha object for effective itemtype
1360 return Koha::ItemTypes->find( $self->effective_itemtype );
1365 my $orders = $item->orders();
1367 Returns a Koha::Acquisition::Orders object
1374 my $orders = $self->_result->item_orders;
1375 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1378 =head3 tracked_links
1380 my $tracked_links = $item->tracked_links();
1382 Returns a Koha::TrackedLinks object
1389 my $tracked_links = $self->_result->linktrackers;
1390 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1393 =head3 move_to_biblio
1395 $item->move_to_biblio($to_biblio[, $params]);
1397 Move the item to another biblio and update any references in other tables.
1399 The final optional parameter, C<$params>, is expected to contain the
1400 'skip_record_index' key, which is relayed down to Koha::Item->store.
1401 There it prevents calling index_records, which takes most of the
1402 time in batch adds/deletes. The caller must take care of calling
1403 index_records separately.
1406 skip_record_index => 1|0
1408 Returns undef if the move failed or the biblionumber of the destination record otherwise
1412 sub move_to_biblio {
1413 my ( $self, $to_biblio, $params ) = @_;
1417 return if $self->biblionumber == $to_biblio->biblionumber;
1419 my $from_biblionumber = $self->biblionumber;
1420 my $to_biblionumber = $to_biblio->biblionumber;
1422 # Own biblionumber and biblioitemnumber
1424 biblionumber => $to_biblionumber,
1425 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1426 })->store({ skip_record_index => $params->{skip_record_index} });
1428 unless ($params->{skip_record_index}) {
1429 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1430 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1433 # Acquisition orders
1434 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1437 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1439 # hold_fill_target (there's no Koha object available yet)
1440 my $hold_fill_target = $self->_result->hold_fill_target;
1441 if ($hold_fill_target) {
1442 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1445 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1446 # and can't even fake one since the significant columns are nullable.
1447 my $storage = $self->_result->result_source->storage;
1450 my ($storage, $dbh, @cols) = @_;
1452 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1457 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1459 return $to_biblionumber;
1462 =head2 Internal methods
1464 =head3 _after_item_action_hooks
1466 Helper method that takes care of calling all plugin hooks
1470 sub _after_item_action_hooks {
1471 my ( $self, $params ) = @_;
1473 my $action = $params->{action};
1475 Koha::Plugins->call(
1476 'after_item_action',
1480 item_id => $self->itemnumber,
1487 my $recall = $item->recall;
1489 Return the relevant recall for this item
1495 my @recalls = Koha::Recalls->search(
1497 biblio_id => $self->biblionumber,
1500 { order_by => { -asc => 'created_date' } }
1502 foreach my $recall (@recalls) {
1503 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1507 # no item-level recall to return, so return earliest biblio-level
1508 # FIXME: eventually this will be based on priority
1512 =head3 can_be_recalled
1514 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1516 Does item-level checks and returns if items can be recalled by this borrower
1520 sub can_be_recalled {
1521 my ( $self, $params ) = @_;
1523 return 0 if !( C4::Context->preference('UseRecalls') );
1525 # check if this item is not for loan, withdrawn or lost
1526 return 0 if ( $self->notforloan != 0 );
1527 return 0 if ( $self->itemlost != 0 );
1528 return 0 if ( $self->withdrawn != 0 );
1530 # check if this item is not checked out - if not checked out, can't be recalled
1531 return 0 if ( !defined( $self->checkout ) );
1533 my $patron = $params->{patron};
1535 my $branchcode = C4::Context->userenv->{'branch'};
1537 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1540 # Check the circulation rule for each relevant itemtype for this item
1541 my $rule = Koha::CirculationRules->get_effective_rules({
1542 branchcode => $branchcode,
1543 categorycode => $patron ? $patron->categorycode : undef,
1544 itemtype => $self->effective_itemtype,
1547 'recalls_per_record',
1552 # check recalls allowed has been set and is not zero
1553 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1556 # check borrower has not reached open recalls allowed limit
1557 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1559 # check borrower has not reach open recalls allowed per record limit
1560 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1562 # check if this patron has already recalled this item
1563 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1565 # check if this patron has already checked out this item
1566 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1568 # check if this patron has already reserved this item
1569 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1572 # check item availability
1573 # items are unavailable for recall if they are lost, withdrawn or notforloan
1574 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1576 # if there are no available items at all, no recall can be placed
1577 return 0 if ( scalar @items == 0 );
1579 my $checked_out_count = 0;
1581 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1584 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1585 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1587 # can't recall if no items have been checked out
1588 return 0 if ( $checked_out_count == 0 );
1594 =head3 can_be_waiting_recall
1596 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1598 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1599 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1603 sub can_be_waiting_recall {
1606 return 0 if !( C4::Context->preference('UseRecalls') );
1608 # check if this item is not for loan, withdrawn or lost
1609 return 0 if ( $self->notforloan != 0 );
1610 return 0 if ( $self->itemlost != 0 );
1611 return 0 if ( $self->withdrawn != 0 );
1613 my $branchcode = $self->holdingbranch;
1614 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1615 $branchcode = C4::Context->userenv->{'branch'};
1617 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1620 # Check the circulation rule for each relevant itemtype for this item
1621 my $rule = Koha::CirculationRules->get_effective_rules({
1622 branchcode => $branchcode,
1623 categorycode => undef,
1624 itemtype => $self->effective_itemtype,
1630 # check recalls allowed has been set and is not zero
1631 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1637 =head3 check_recalls
1639 my $recall = $item->check_recalls;
1641 Get the most relevant recall for this item.
1648 my @recalls = Koha::Recalls->search(
1649 { biblio_id => $self->biblionumber,
1650 item_id => [ $self->itemnumber, undef ]
1652 { order_by => { -asc => 'created_date' } }
1653 )->filter_by_current->as_list;
1656 # iterate through relevant recalls to find the best one.
1657 # if we come across a waiting recall, use this one.
1658 # if we have iterated through all recalls and not found a waiting recall, use the first recall in the array, which should be the oldest recall.
1659 foreach my $r ( @recalls ) {
1660 if ( $r->waiting ) {
1665 unless ( defined $recall ) {
1666 $recall = $recalls[0];
1672 =head3 is_notforloan
1674 my $is_notforloan = $item->is_notforloan;
1676 Determine whether or not this item is "notforloan" based on
1677 the item's notforloan status or its item type
1683 my $is_notforloan = 0;
1685 if ( $self->notforloan ){
1689 my $itemtype = $self->itemtype;
1691 if ( $itemtype->notforloan ){
1697 return $is_notforloan;
1710 Kyle M Hall <kyle@bywatersolutions.com>