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 );
23 use Data::Dumper qw( Dumper );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
35 use Koha::CirculationRules;
36 use Koha::CoverImages;
37 use Koha::SearchEngine::Indexer;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Item::Attributes;
46 use Koha::StockRotationItem;
47 use Koha::StockRotationRotas;
48 use Koha::TrackedLinks;
50 use base qw(Koha::Object);
54 Koha::Item - Koha Item object class
66 $params can take an optional 'skip_record_index' parameter.
67 If set, the reindexation process will not happen (index_records not called)
69 NOTE: This is a temporary fix to answer a performance issue when lot of items
70 are added (or modified) at the same time.
71 The correct way to fix this is to make the ES reindexation process async.
72 You should not turn it on if you do not understand what it is doing exactly.
78 my $params = @_ ? shift : {};
80 my $log_action = $params->{log_action} // 1;
82 # We do not want to oblige callers to pass this value
83 # Dev conveniences vs performance?
84 unless ( $self->biblioitemnumber ) {
85 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
88 # See related changes from C4::Items::AddItem
89 unless ( $self->itype ) {
90 $self->itype($self->biblio->biblioitem->itemtype);
93 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
95 my $today = dt_from_string;
96 my $action = 'create';
98 unless ( $self->in_storage ) { #AddItem
100 unless ( $self->permanent_location ) {
101 $self->permanent_location($self->location);
104 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
105 unless ( $self->location || !$default_location ) {
106 $self->permanent_location( $self->location || $default_location )
107 unless $self->permanent_location;
108 $self->location($default_location);
111 unless ( $self->replacementpricedate ) {
112 $self->replacementpricedate($today);
114 unless ( $self->datelastseen ) {
115 $self->datelastseen($today);
118 unless ( $self->dateaccessioned ) {
119 $self->dateaccessioned($today);
122 if ( $self->itemcallnumber
123 or $self->cn_source )
125 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
126 $self->cn_sort($cn_sort);
133 my %updated_columns = $self->_result->get_dirty_columns;
134 return $self->SUPER::store unless %updated_columns;
136 # Retrieve the item for comparison if we need to
138 exists $updated_columns{itemlost}
139 or exists $updated_columns{withdrawn}
140 or exists $updated_columns{damaged}
141 ) ? $self->get_from_storage : undef;
143 # Update *_on fields if needed
144 # FIXME: Why not for AddItem as well?
145 my @fields = qw( itemlost withdrawn damaged );
146 for my $field (@fields) {
148 # If the field is defined but empty or 0, we are
149 # removing/unsetting and thus need to clear out
151 if ( exists $updated_columns{$field}
152 && defined( $self->$field )
155 my $field_on = "${field}_on";
156 $self->$field_on(undef);
158 # If the field has changed otherwise, we much update
160 elsif (exists $updated_columns{$field}
161 && $updated_columns{$field}
162 && !$pre_mod_item->$field )
164 my $field_on = "${field}_on";
166 DateTime::Format::MySQL->format_datetime(
173 if ( exists $updated_columns{itemcallnumber}
174 or exists $updated_columns{cn_source} )
176 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177 $self->cn_sort($cn_sort);
181 if ( exists $updated_columns{location}
182 and $self->location ne 'CART'
183 and $self->location ne 'PROC'
184 and not exists $updated_columns{permanent_location} )
186 $self->permanent_location( $self->location );
189 # If item was lost and has now been found,
190 # reverse any list item charges if necessary.
191 if ( exists $updated_columns{itemlost}
192 and $updated_columns{itemlost} <= 0
193 and $pre_mod_item->itemlost > 0 )
195 $self->_set_found_trigger($pre_mod_item);
200 unless ( $self->dateaccessioned ) {
201 $self->dateaccessioned($today);
204 my $result = $self->SUPER::store;
205 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
207 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
208 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
210 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
211 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
212 unless $params->{skip_record_index};
213 $self->get_from_storage->_after_item_action_hooks({ action => $action });
224 my $params = @_ ? shift : {};
226 # FIXME check the item has no current issues
227 # i.e. raise the appropriate exception
229 my $result = $self->SUPER::delete;
231 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
232 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
233 unless $params->{skip_record_index};
235 $self->_after_item_action_hooks({ action => 'delete' });
237 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
238 if C4::Context->preference("CataloguingLog");
249 my $params = @_ ? shift : {};
251 my $safe_to_delete = $self->safe_to_delete;
252 return $safe_to_delete unless $safe_to_delete eq '1';
254 $self->move_to_deleted;
256 return $self->delete($params);
259 =head3 safe_to_delete
261 returns 1 if the item is safe to delete,
263 "book_on_loan" if the item is checked out,
265 "not_same_branch" if the item is blocked by independent branches,
267 "book_reserved" if the there are holds aganst the item, or
269 "linked_analytics" if the item has linked analytic records.
271 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
278 return "book_on_loan" if $self->checkout;
280 return "not_same_branch"
281 if defined C4::Context->userenv
282 and !C4::Context->IsSuperLibrarian()
283 and C4::Context->preference("IndependentBranches")
284 and ( C4::Context->userenv->{branch} ne $self->homebranch );
286 # check it doesn't have a waiting reserve
287 return "book_reserved"
288 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
290 return "linked_analytics"
291 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
293 return "last_item_for_hold"
294 if $self->biblio->items->count == 1
295 && $self->biblio->holds->search(
304 =head3 move_to_deleted
306 my $is_moved = $item->move_to_deleted;
308 Move an item to the deleteditems table.
309 This can be done before deleting an item, to make sure the data are not completely deleted.
313 sub move_to_deleted {
315 my $item_infos = $self->unblessed;
316 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
317 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
321 =head3 effective_itemtype
323 Returns the itemtype for the item based on whether item level itemtypes are set or not.
327 sub effective_itemtype {
330 return $self->_result()->effective_itemtype();
340 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
342 return $self->{_home_branch};
345 =head3 holding_branch
352 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
354 return $self->{_holding_branch};
359 my $biblio = $item->biblio;
361 Return the bibliographic record of this item
367 my $biblio_rs = $self->_result->biblio;
368 return Koha::Biblio->_new_from_dbic( $biblio_rs );
373 my $biblioitem = $item->biblioitem;
375 Return the biblioitem record of this item
381 my $biblioitem_rs = $self->_result->biblioitem;
382 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
387 my $checkout = $item->checkout;
389 Return the checkout for this item
395 my $checkout_rs = $self->_result->issue;
396 return unless $checkout_rs;
397 return Koha::Checkout->_new_from_dbic( $checkout_rs );
402 my $holds = $item->holds();
403 my $holds = $item->holds($params);
404 my $holds = $item->holds({ found => 'W'});
406 Return holds attached to an item, optionally accept a hashref of params to pass to search
411 my ( $self,$params ) = @_;
412 my $holds_rs = $self->_result->reserves->search($params);
413 return Koha::Holds->_new_from_dbic( $holds_rs );
416 =head3 request_transfer
418 my $transfer = $item->request_transfer(
422 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
426 Add a transfer request for this item to the given branch for the given reason.
428 An exception will be thrown if the BranchTransferLimits would prevent the requested
429 transfer, unless 'ignore_limits' is passed to override the limits.
431 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
432 The caller should catch such cases and retry the transfer request as appropriate passing
433 an appropriate override.
436 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
437 * replace - Used to replace the existing transfer request with your own.
441 sub request_transfer {
442 my ( $self, $params ) = @_;
444 # check for mandatory params
445 my @mandatory = ( 'to', 'reason' );
446 for my $param (@mandatory) {
447 unless ( defined( $params->{$param} ) ) {
448 Koha::Exceptions::MissingParameter->throw(
449 error => "The $param parameter is mandatory" );
453 Koha::Exceptions::Item::Transfer::Limit->throw()
454 unless ( $params->{ignore_limits}
455 || $self->can_be_transferred( { to => $params->{to} } ) );
457 my $request = $self->get_transfer;
458 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
459 if ( $request && !$params->{enqueue} && !$params->{replace} );
461 $request->cancel( { reason => $params->{reason}, force => 1 } )
462 if ( defined($request) && $params->{replace} );
464 my $transfer = Koha::Item::Transfer->new(
466 itemnumber => $self->itemnumber,
467 daterequested => dt_from_string,
468 frombranch => $self->holdingbranch,
469 tobranch => $params->{to}->branchcode,
470 reason => $params->{reason},
471 comments => $params->{comment}
480 my $transfer = $item->get_transfer;
482 Return the active transfer request or undef
484 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
485 whereby the most recently sent, but not received, transfer will be returned
486 if it exists, otherwise the oldest unsatisfied transfer will be returned.
488 This allows for transfers to queue, which is the case for stock rotation and
489 rotating collections where a manual transfer may need to take precedence but
490 we still expect the item to end up at a final location eventually.
496 my $transfer_rs = $self->_result->branchtransfers->search(
498 datearrived => undef,
499 datecancelled => undef
503 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
507 return unless $transfer_rs;
508 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
513 my $transfer = $item->get_transfers;
515 Return the list of outstanding transfers (i.e requested but not yet cancelled
518 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
519 whereby the most recently sent, but not received, transfer will be returned
520 first if it exists, otherwise requests are in oldest to newest request order.
522 This allows for transfers to queue, which is the case for stock rotation and
523 rotating collections where a manual transfer may need to take precedence but
524 we still expect the item to end up at a final location eventually.
530 my $transfer_rs = $self->_result->branchtransfers->search(
532 datearrived => undef,
533 datecancelled => undef
537 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
540 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
543 =head3 last_returned_by
545 Gets and sets the last borrower to return an item.
547 Accepts and returns Koha::Patron objects
549 $item->last_returned_by( $borrowernumber );
551 $last_returned_by = $item->last_returned_by();
555 sub last_returned_by {
556 my ( $self, $borrower ) = @_;
558 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
561 return $items_last_returned_by_rs->update_or_create(
562 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
565 unless ( $self->{_last_returned_by} ) {
566 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
568 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
572 return $self->{_last_returned_by};
576 =head3 can_article_request
578 my $bool = $item->can_article_request( $borrower )
580 Returns true if item can be specifically requested
582 $borrower must be a Koha::Patron object
586 sub can_article_request {
587 my ( $self, $borrower ) = @_;
589 my $rule = $self->article_request_type($borrower);
591 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
595 =head3 hidden_in_opac
597 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
599 Returns true if item fields match the hidding criteria defined in $rules.
600 Returns false otherwise.
602 Takes HASHref that can have the following parameters:
604 $rules : { <field> => [ value_1, ... ], ... }
606 Note: $rules inherits its structure from the parsed YAML from reading
607 the I<OpacHiddenItems> system preference.
612 my ( $self, $params ) = @_;
614 my $rules = $params->{rules} // {};
617 if C4::Context->preference('hidelostitems') and
620 my $hidden_in_opac = 0;
622 foreach my $field ( keys %{$rules} ) {
624 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
630 return $hidden_in_opac;
633 =head3 can_be_transferred
635 $item->can_be_transferred({ to => $to_library, from => $from_library })
636 Checks if an item can be transferred to given library.
638 This feature is controlled by two system preferences:
639 UseBranchTransferLimits to enable / disable the feature
640 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
641 for setting the limitations
643 Takes HASHref that can have the following parameters:
644 MANDATORY PARAMETERS:
647 $from : Koha::Library # if not given, item holdingbranch
648 # will be used instead
650 Returns 1 if item can be transferred to $to_library, otherwise 0.
652 To find out whether at least one item of a Koha::Biblio can be transferred, please
653 see Koha::Biblio->can_be_transferred() instead of using this method for
654 multiple items of the same biblio.
658 sub can_be_transferred {
659 my ($self, $params) = @_;
661 my $to = $params->{to};
662 my $from = $params->{from};
664 $to = $to->branchcode;
665 $from = defined $from ? $from->branchcode : $self->holdingbranch;
667 return 1 if $from eq $to; # Transfer to current branch is allowed
668 return 1 unless C4::Context->preference('UseBranchTransferLimits');
670 my $limittype = C4::Context->preference('BranchTransferLimitsType');
671 return Koha::Item::Transfer::Limits->search({
674 $limittype => $limittype eq 'itemtype'
675 ? $self->effective_itemtype : $self->ccode
680 =head3 pickup_locations
682 $pickup_locations = $item->pickup_locations( {patron => $patron } )
684 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)
685 and if item can be transferred to each pickup location.
689 sub pickup_locations {
690 my ($self, $params) = @_;
692 my $patron = $params->{patron};
694 my $circ_control_branch =
695 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
697 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
699 if(defined $patron) {
700 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
701 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
704 my $pickup_libraries = Koha::Libraries->search();
705 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
706 $pickup_libraries = $self->home_branch->get_hold_libraries;
707 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
708 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
709 $pickup_libraries = $plib->get_hold_libraries;
710 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
711 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
712 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
713 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
716 return $pickup_libraries->search(
721 order_by => ['branchname']
723 ) unless C4::Context->preference('UseBranchTransferLimits');
725 my $limittype = C4::Context->preference('BranchTransferLimitsType');
726 my ($ccode, $itype) = (undef, undef);
727 if( $limittype eq 'ccode' ){
728 $ccode = $self->ccode;
730 $itype = $self->itype;
732 my $limits = Koha::Item::Transfer::Limits->search(
734 fromBranch => $self->holdingbranch,
738 { columns => ['toBranch'] }
741 return $pickup_libraries->search(
743 pickup_location => 1,
745 '-not_in' => $limits->_resultset->as_query
749 order_by => ['branchname']
754 =head3 article_request_type
756 my $type = $item->article_request_type( $borrower )
758 returns 'yes', 'no', 'bib_only', or 'item_only'
760 $borrower must be a Koha::Patron object
764 sub article_request_type {
765 my ( $self, $borrower ) = @_;
767 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
769 $branch_control eq 'homebranch' ? $self->homebranch
770 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
772 my $borrowertype = $borrower->categorycode;
773 my $itemtype = $self->effective_itemtype();
774 my $rule = Koha::CirculationRules->get_effective_rule(
776 rule_name => 'article_requests',
777 categorycode => $borrowertype,
778 itemtype => $itemtype,
779 branchcode => $branchcode
783 return q{} unless $rule;
784 return $rule->rule_value || q{}
793 my $attributes = { order_by => 'priority' };
794 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
796 itemnumber => $self->itemnumber,
799 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
800 waitingdate => { '!=' => undef },
803 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
804 return Koha::Holds->_new_from_dbic($hold_rs);
807 =head3 stockrotationitem
809 my $sritem = Koha::Item->stockrotationitem;
811 Returns the stock rotation item associated with the current item.
815 sub stockrotationitem {
817 my $rs = $self->_result->stockrotationitem;
819 return Koha::StockRotationItem->_new_from_dbic( $rs );
824 my $item = $item->add_to_rota($rota_id);
826 Add this item to the rota identified by $ROTA_ID, which means associating it
827 with the first stage of that rota. Should this item already be associated
828 with a rota, then we will move it to the new rota.
833 my ( $self, $rota_id ) = @_;
834 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
838 =head3 has_pending_hold
840 my $is_pending_hold = $item->has_pending_hold();
842 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
846 sub has_pending_hold {
848 my $pending_hold = $self->_result->tmp_holdsqueues;
849 return $pending_hold->count ? 1: 0;
854 my $field = $item->as_marc_field;
856 This method returns a MARC::Field object representing the Koha::Item object
857 with the current mappings configuration.
864 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
866 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
870 my $item_field = $tagslib->{$itemtag};
872 my $more_subfields = $self->additional_attributes->to_hashref;
873 foreach my $subfield (
875 $a->{display_order} <=> $b->{display_order}
876 || $a->{subfield} cmp $b->{subfield}
877 } grep { ref($_) && %$_ } values %$item_field
880 my $kohafield = $subfield->{kohafield};
881 my $tagsubfield = $subfield->{tagsubfield};
883 if ( defined $kohafield ) {
884 next if $kohafield !~ m{^items\.}; # That would be weird!
885 ( my $attribute = $kohafield ) =~ s|^items\.||;
886 $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
887 if defined $self->$attribute and $self->$attribute ne '';
889 $value = $more_subfields->{$tagsubfield}
892 next unless defined $value
895 if ( $subfield->{repeatable} ) {
896 my @values = split '\|', $value;
897 push @subfields, ( $tagsubfield => $_ ) for @values;
900 push @subfields, ( $tagsubfield => $value );
905 return unless @subfields;
907 return MARC::Field->new(
908 "$itemtag", ' ', ' ', @subfields
912 =head3 renewal_branchcode
914 Returns the branchcode to be recorded in statistics renewal of the item
918 sub renewal_branchcode {
920 my ($self, $params ) = @_;
922 my $interface = C4::Context->interface;
924 if ( $interface eq 'opac' ){
925 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
926 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
927 $branchcode = 'OPACRenew';
929 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
930 $branchcode = $self->homebranch;
932 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
933 $branchcode = $self->checkout->patron->branchcode;
935 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
936 $branchcode = $self->checkout->branchcode;
942 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
943 ? C4::Context->userenv->{branch} : $params->{branch};
950 Return the cover images associated with this item.
957 my $cover_image_rs = $self->_result->cover_images;
958 return unless $cover_image_rs;
959 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
962 =head3 columns_to_str
964 my $values = $items->columns_to_str;
966 Return a hashref with the string representation of the different attribute of the item.
968 This is meant to be used for display purpose only.
975 my $frameworkcode = $self->biblio->frameworkcode;
976 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
977 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
979 my $columns_info = $self->_result->result_source->columns_info;
981 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
983 for my $column ( keys %$columns_info ) {
985 next if $column eq 'more_subfields_xml';
987 my $value = $self->$column;
988 # 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
990 if ( not defined $value or $value eq "" ) {
991 $values->{$column} = $value;
996 exists $mss->{"items.$column"}
997 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1000 $values->{$column} =
1002 ? $subfield->{authorised_value}
1003 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1004 $subfield->{tagsubfield}, $value, '', $tagslib )
1010 $self->more_subfields_xml
1011 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1016 my ( $field ) = $marc_more->fields;
1017 for my $sf ( $field->subfields ) {
1018 my $subfield_code = $sf->[0];
1019 my $value = $sf->[1];
1020 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1021 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1023 $subfield->{authorised_value}
1024 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1025 $subfield->{tagsubfield}, $value, '', $tagslib )
1028 push @{$more_values->{$subfield_code}}, $value;
1031 while ( my ( $k, $v ) = each %$more_values ) {
1032 $values->{$k} = join ' | ', @$v;
1039 =head3 additional_attributes
1041 my $attributes = $item->additional_attributes;
1042 $attributes->{k} = 'new k';
1043 $item->update({ more_subfields => $attributes->to_marcxml });
1045 Returns a Koha::Item::Attributes object that represents the non-mapped
1046 attributes for this item.
1050 sub additional_attributes {
1053 return Koha::Item::Attributes->new_from_marcxml(
1054 $self->more_subfields_xml,
1058 =head3 _set_found_trigger
1060 $self->_set_found_trigger
1062 Finds the most recent lost item charge for this item and refunds the patron
1063 appropriately, taking into account any payments or writeoffs already applied
1066 Internal function, not exported, called only by Koha::Item->store.
1070 sub _set_found_trigger {
1071 my ( $self, $pre_mod_item ) = @_;
1073 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1074 my $no_refund_after_days =
1075 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1076 if ($no_refund_after_days) {
1077 my $today = dt_from_string();
1078 my $lost_age_in_days =
1079 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1082 return $self unless $lost_age_in_days < $no_refund_after_days;
1085 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1088 return_branch => C4::Context->userenv
1089 ? C4::Context->userenv->{'branch'}
1094 if ( $lostreturn_policy ) {
1096 # refund charge made for lost book
1097 my $lost_charge = Koha::Account::Lines->search(
1099 itemnumber => $self->itemnumber,
1100 debit_type_code => 'LOST',
1101 status => [ undef, { '<>' => 'FOUND' } ]
1104 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1109 if ( $lost_charge ) {
1111 my $patron = $lost_charge->patron;
1114 my $account = $patron->account;
1115 my $total_to_refund = 0;
1118 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1120 # some amount has been cancelled. collect the offsets that are not writeoffs
1121 # this works because the only way to subtract from this kind of a debt is
1122 # using the UI buttons 'Pay' and 'Write off'
1123 my $credit_offsets = $lost_charge->debit_offsets(
1125 'credit_id' => { '!=' => undef },
1126 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1128 { join => 'credit' }
1131 $total_to_refund = ( $credit_offsets->count > 0 )
1132 ? $credit_offsets->total * -1 # credits are negative on the DB
1136 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1139 if ( $credit_total > 0 ) {
1141 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1142 $credit = $account->add_credit(
1144 amount => $credit_total,
1145 description => 'Item found ' . $self->itemnumber,
1146 type => 'LOST_FOUND',
1147 interface => C4::Context->interface,
1148 library_id => $branchcode,
1149 item_id => $self->itemnumber,
1150 issue_id => $lost_charge->issue_id
1154 $credit->apply( { debits => [$lost_charge] } );
1155 $self->{_refunded} = 1;
1158 # Update the account status
1159 $lost_charge->status('FOUND');
1160 $lost_charge->store();
1162 # Reconcile balances if required
1163 if ( C4::Context->preference('AccountAutoReconcile') ) {
1164 $account->reconcile_balance;
1169 # restore fine for lost book
1170 if ( $lostreturn_policy eq 'restore' ) {
1171 my $lost_overdue = Koha::Account::Lines->search(
1173 itemnumber => $self->itemnumber,
1174 debit_type_code => 'OVERDUE',
1178 order_by => { '-desc' => 'date' },
1183 if ( $lost_overdue ) {
1185 my $patron = $lost_overdue->patron;
1187 my $account = $patron->account;
1189 # Update status of fine
1190 $lost_overdue->status('FOUND')->store();
1192 # Find related forgive credit
1193 my $refund = $lost_overdue->credits(
1195 credit_type_code => 'FORGIVEN',
1196 itemnumber => $self->itemnumber,
1197 status => [ { '!=' => 'VOID' }, undef ]
1199 { order_by => { '-desc' => 'date' }, rows => 1 }
1203 # Revert the forgive credit
1204 $refund->void({ interface => 'trigger' });
1205 $self->{_restored} = 1;
1208 # Reconcile balances if required
1209 if ( C4::Context->preference('AccountAutoReconcile') ) {
1210 $account->reconcile_balance;
1214 } elsif ( $lostreturn_policy eq 'charge' ) {
1215 $self->{_charge} = 1;
1222 =head3 public_read_list
1224 This method returns the list of publicly readable database fields for both API and UI output purposes
1228 sub public_read_list {
1230 'itemnumber', 'biblionumber', 'homebranch',
1231 'holdingbranch', 'location', 'collectioncode',
1232 'itemcallnumber', 'copynumber', 'enumchron',
1233 'barcode', 'dateaccessioned', 'itemnotes',
1234 'onloan', 'uri', 'itype',
1235 'notforloan', 'damaged', 'itemlost',
1236 'withdrawn', 'restricted'
1240 =head3 to_api_mapping
1242 This method returns the mapping for representing a Koha::Item object
1247 sub to_api_mapping {
1249 itemnumber => 'item_id',
1250 biblionumber => 'biblio_id',
1251 biblioitemnumber => undef,
1252 barcode => 'external_id',
1253 dateaccessioned => 'acquisition_date',
1254 booksellerid => 'acquisition_source',
1255 homebranch => 'home_library_id',
1256 price => 'purchase_price',
1257 replacementprice => 'replacement_price',
1258 replacementpricedate => 'replacement_price_date',
1259 datelastborrowed => 'last_checkout_date',
1260 datelastseen => 'last_seen_date',
1262 notforloan => 'not_for_loan_status',
1263 damaged => 'damaged_status',
1264 damaged_on => 'damaged_date',
1265 itemlost => 'lost_status',
1266 itemlost_on => 'lost_date',
1267 withdrawn => 'withdrawn',
1268 withdrawn_on => 'withdrawn_date',
1269 itemcallnumber => 'callnumber',
1270 coded_location_qualifier => 'coded_location_qualifier',
1271 issues => 'checkouts_count',
1272 renewals => 'renewals_count',
1273 reserves => 'holds_count',
1274 restricted => 'restricted_status',
1275 itemnotes => 'public_notes',
1276 itemnotes_nonpublic => 'internal_notes',
1277 holdingbranch => 'holding_library_id',
1278 timestamp => 'timestamp',
1279 location => 'location',
1280 permanent_location => 'permanent_location',
1281 onloan => 'checked_out_date',
1282 cn_source => 'call_number_source',
1283 cn_sort => 'call_number_sort',
1284 ccode => 'collection_code',
1285 materials => 'materials_notes',
1287 itype => 'item_type',
1288 more_subfields_xml => 'extended_subfields',
1289 enumchron => 'serial_issue_number',
1290 copynumber => 'copy_number',
1291 stocknumber => 'inventory_number',
1292 new_status => 'new_status'
1298 my $itemtype = $item->itemtype;
1300 Returns Koha object for effective itemtype
1306 return Koha::ItemTypes->find( $self->effective_itemtype );
1311 my $orders = $item->orders();
1313 Returns a Koha::Acquisition::Orders object
1320 my $orders = $self->_result->item_orders;
1321 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1324 =head3 tracked_links
1326 my $tracked_links = $item->tracked_links();
1328 Returns a Koha::TrackedLinks object
1335 my $tracked_links = $self->_result->linktrackers;
1336 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1339 =head3 move_to_biblio
1341 $item->move_to_biblio($to_biblio[, $params]);
1343 Move the item to another biblio and update any references in other tables.
1345 The final optional parameter, C<$params>, is expected to contain the
1346 'skip_record_index' key, which is relayed down to Koha::Item->store.
1347 There it prevents calling index_records, which takes most of the
1348 time in batch adds/deletes. The caller must take care of calling
1349 index_records separately.
1352 skip_record_index => 1|0
1354 Returns undef if the move failed or the biblionumber of the destination record otherwise
1358 sub move_to_biblio {
1359 my ( $self, $to_biblio, $params ) = @_;
1363 return if $self->biblionumber == $to_biblio->biblionumber;
1365 my $from_biblionumber = $self->biblionumber;
1366 my $to_biblionumber = $to_biblio->biblionumber;
1368 # Own biblionumber and biblioitemnumber
1370 biblionumber => $to_biblionumber,
1371 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1372 })->store({ skip_record_index => $params->{skip_record_index} });
1374 unless ($params->{skip_record_index}) {
1375 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1376 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1379 # Acquisition orders
1380 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1383 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1385 # hold_fill_target (there's no Koha object available yet)
1386 my $hold_fill_target = $self->_result->hold_fill_target;
1387 if ($hold_fill_target) {
1388 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1391 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1392 # and can't even fake one since the significant columns are nullable.
1393 my $storage = $self->_result->result_source->storage;
1396 my ($storage, $dbh, @cols) = @_;
1398 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1403 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1405 return $to_biblionumber;
1408 =head2 Internal methods
1410 =head3 _after_item_action_hooks
1412 Helper method that takes care of calling all plugin hooks
1416 sub _after_item_action_hooks {
1417 my ( $self, $params ) = @_;
1419 my $action = $params->{action};
1421 Koha::Plugins->call(
1422 'after_item_action',
1426 item_id => $self->itemnumber,
1441 Kyle M Hall <kyle@bywatersolutions.com>