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 );
34 use Koha::CirculationRules;
35 use Koha::CoverImages;
36 use Koha::SearchEngine::Indexer;
37 use Koha::Exceptions::Item::Transfer;
38 use Koha::Item::Transfer::Limits;
39 use Koha::Item::Transfers;
40 use Koha::Item::Attributes;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
47 use Koha::TrackedLinks;
48 use Koha::Result::Boolean;
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, $self );
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;
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
280 $error = "book_on_loan" if $self->checkout;
282 $error = "not_same_branch"
283 if defined C4::Context->userenv
284 and !C4::Context->IsSuperLibrarian()
285 and C4::Context->preference("IndependentBranches")
286 and ( C4::Context->userenv->{branch} ne $self->homebranch );
288 # check it doesn't have a waiting reserve
289 $error = "book_reserved"
290 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
292 $error = "linked_analytics"
293 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
295 $error = "last_item_for_hold"
296 if $self->biblio->items->count == 1
297 && $self->biblio->holds->search(
304 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
307 return Koha::Result::Boolean->new(1);
310 =head3 move_to_deleted
312 my $is_moved = $item->move_to_deleted;
314 Move an item to the deleteditems table.
315 This can be done before deleting an item, to make sure the data are not completely deleted.
319 sub move_to_deleted {
321 my $item_infos = $self->unblessed;
322 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
323 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
327 =head3 effective_itemtype
329 Returns the itemtype for the item based on whether item level itemtypes are set or not.
333 sub effective_itemtype {
336 return $self->_result()->effective_itemtype();
346 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
348 return $self->{_home_branch};
351 =head3 holding_branch
358 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
360 return $self->{_holding_branch};
365 my $biblio = $item->biblio;
367 Return the bibliographic record of this item
373 my $biblio_rs = $self->_result->biblio;
374 return Koha::Biblio->_new_from_dbic( $biblio_rs );
379 my $biblioitem = $item->biblioitem;
381 Return the biblioitem record of this item
387 my $biblioitem_rs = $self->_result->biblioitem;
388 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
393 my $checkout = $item->checkout;
395 Return the checkout for this item
401 my $checkout_rs = $self->_result->issue;
402 return unless $checkout_rs;
403 return Koha::Checkout->_new_from_dbic( $checkout_rs );
408 my $holds = $item->holds();
409 my $holds = $item->holds($params);
410 my $holds = $item->holds({ found => 'W'});
412 Return holds attached to an item, optionally accept a hashref of params to pass to search
417 my ( $self,$params ) = @_;
418 my $holds_rs = $self->_result->reserves->search($params);
419 return Koha::Holds->_new_from_dbic( $holds_rs );
422 =head3 request_transfer
424 my $transfer = $item->request_transfer(
428 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
432 Add a transfer request for this item to the given branch for the given reason.
434 An exception will be thrown if the BranchTransferLimits would prevent the requested
435 transfer, unless 'ignore_limits' is passed to override the limits.
437 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
438 The caller should catch such cases and retry the transfer request as appropriate passing
439 an appropriate override.
442 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
443 * replace - Used to replace the existing transfer request with your own.
447 sub request_transfer {
448 my ( $self, $params ) = @_;
450 # check for mandatory params
451 my @mandatory = ( 'to', 'reason' );
452 for my $param (@mandatory) {
453 unless ( defined( $params->{$param} ) ) {
454 Koha::Exceptions::MissingParameter->throw(
455 error => "The $param parameter is mandatory" );
459 Koha::Exceptions::Item::Transfer::Limit->throw()
460 unless ( $params->{ignore_limits}
461 || $self->can_be_transferred( { to => $params->{to} } ) );
463 my $request = $self->get_transfer;
464 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
465 if ( $request && !$params->{enqueue} && !$params->{replace} );
467 $request->cancel( { reason => $params->{reason}, force => 1 } )
468 if ( defined($request) && $params->{replace} );
470 my $transfer = Koha::Item::Transfer->new(
472 itemnumber => $self->itemnumber,
473 daterequested => dt_from_string,
474 frombranch => $self->holdingbranch,
475 tobranch => $params->{to}->branchcode,
476 reason => $params->{reason},
477 comments => $params->{comment}
486 my $transfer = $item->get_transfer;
488 Return the active transfer request or undef
490 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
491 whereby the most recently sent, but not received, transfer will be returned
492 if it exists, otherwise the oldest unsatisfied transfer will be returned.
494 This allows for transfers to queue, which is the case for stock rotation and
495 rotating collections where a manual transfer may need to take precedence but
496 we still expect the item to end up at a final location eventually.
502 my $transfer_rs = $self->_result->branchtransfers->search(
504 datearrived => undef,
505 datecancelled => undef
509 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
513 return unless $transfer_rs;
514 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
519 my $transfer = $item->get_transfers;
521 Return the list of outstanding transfers (i.e requested but not yet cancelled
524 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
525 whereby the most recently sent, but not received, transfer will be returned
526 first if it exists, otherwise requests are in oldest to newest request order.
528 This allows for transfers to queue, which is the case for stock rotation and
529 rotating collections where a manual transfer may need to take precedence but
530 we still expect the item to end up at a final location eventually.
536 my $transfer_rs = $self->_result->branchtransfers->search(
538 datearrived => undef,
539 datecancelled => undef
543 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
546 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
549 =head3 last_returned_by
551 Gets and sets the last borrower to return an item.
553 Accepts and returns Koha::Patron objects
555 $item->last_returned_by( $borrowernumber );
557 $last_returned_by = $item->last_returned_by();
561 sub last_returned_by {
562 my ( $self, $borrower ) = @_;
564 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
567 return $items_last_returned_by_rs->update_or_create(
568 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
571 unless ( $self->{_last_returned_by} ) {
572 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
574 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
578 return $self->{_last_returned_by};
582 =head3 can_article_request
584 my $bool = $item->can_article_request( $borrower )
586 Returns true if item can be specifically requested
588 $borrower must be a Koha::Patron object
592 sub can_article_request {
593 my ( $self, $borrower ) = @_;
595 my $rule = $self->article_request_type($borrower);
597 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
601 =head3 hidden_in_opac
603 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
605 Returns true if item fields match the hidding criteria defined in $rules.
606 Returns false otherwise.
608 Takes HASHref that can have the following parameters:
610 $rules : { <field> => [ value_1, ... ], ... }
612 Note: $rules inherits its structure from the parsed YAML from reading
613 the I<OpacHiddenItems> system preference.
618 my ( $self, $params ) = @_;
620 my $rules = $params->{rules} // {};
623 if C4::Context->preference('hidelostitems') and
626 my $hidden_in_opac = 0;
628 foreach my $field ( keys %{$rules} ) {
630 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
636 return $hidden_in_opac;
639 =head3 can_be_transferred
641 $item->can_be_transferred({ to => $to_library, from => $from_library })
642 Checks if an item can be transferred to given library.
644 This feature is controlled by two system preferences:
645 UseBranchTransferLimits to enable / disable the feature
646 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
647 for setting the limitations
649 Takes HASHref that can have the following parameters:
650 MANDATORY PARAMETERS:
653 $from : Koha::Library # if not given, item holdingbranch
654 # will be used instead
656 Returns 1 if item can be transferred to $to_library, otherwise 0.
658 To find out whether at least one item of a Koha::Biblio can be transferred, please
659 see Koha::Biblio->can_be_transferred() instead of using this method for
660 multiple items of the same biblio.
664 sub can_be_transferred {
665 my ($self, $params) = @_;
667 my $to = $params->{to};
668 my $from = $params->{from};
670 $to = $to->branchcode;
671 $from = defined $from ? $from->branchcode : $self->holdingbranch;
673 return 1 if $from eq $to; # Transfer to current branch is allowed
674 return 1 unless C4::Context->preference('UseBranchTransferLimits');
676 my $limittype = C4::Context->preference('BranchTransferLimitsType');
677 return Koha::Item::Transfer::Limits->search({
680 $limittype => $limittype eq 'itemtype'
681 ? $self->effective_itemtype : $self->ccode
686 =head3 pickup_locations
688 $pickup_locations = $item->pickup_locations( {patron => $patron } )
690 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)
691 and if item can be transferred to each pickup location.
695 sub pickup_locations {
696 my ($self, $params) = @_;
698 my $patron = $params->{patron};
700 my $circ_control_branch =
701 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
703 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
705 if(defined $patron) {
706 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
707 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
710 my $pickup_libraries = Koha::Libraries->search();
711 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
712 $pickup_libraries = $self->home_branch->get_hold_libraries;
713 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
714 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
715 $pickup_libraries = $plib->get_hold_libraries;
716 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
717 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
718 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
719 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
722 return $pickup_libraries->search(
727 order_by => ['branchname']
729 ) unless C4::Context->preference('UseBranchTransferLimits');
731 my $limittype = C4::Context->preference('BranchTransferLimitsType');
732 my ($ccode, $itype) = (undef, undef);
733 if( $limittype eq 'ccode' ){
734 $ccode = $self->ccode;
736 $itype = $self->itype;
738 my $limits = Koha::Item::Transfer::Limits->search(
740 fromBranch => $self->holdingbranch,
744 { columns => ['toBranch'] }
747 return $pickup_libraries->search(
749 pickup_location => 1,
751 '-not_in' => $limits->_resultset->as_query
755 order_by => ['branchname']
760 =head3 article_request_type
762 my $type = $item->article_request_type( $borrower )
764 returns 'yes', 'no', 'bib_only', or 'item_only'
766 $borrower must be a Koha::Patron object
770 sub article_request_type {
771 my ( $self, $borrower ) = @_;
773 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
775 $branch_control eq 'homebranch' ? $self->homebranch
776 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
778 my $borrowertype = $borrower->categorycode;
779 my $itemtype = $self->effective_itemtype();
780 my $rule = Koha::CirculationRules->get_effective_rule(
782 rule_name => 'article_requests',
783 categorycode => $borrowertype,
784 itemtype => $itemtype,
785 branchcode => $branchcode
789 return q{} unless $rule;
790 return $rule->rule_value || q{}
799 my $attributes = { order_by => 'priority' };
800 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
802 itemnumber => $self->itemnumber,
805 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
806 waitingdate => { '!=' => undef },
809 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
810 return Koha::Holds->_new_from_dbic($hold_rs);
813 =head3 stockrotationitem
815 my $sritem = Koha::Item->stockrotationitem;
817 Returns the stock rotation item associated with the current item.
821 sub stockrotationitem {
823 my $rs = $self->_result->stockrotationitem;
825 return Koha::StockRotationItem->_new_from_dbic( $rs );
830 my $item = $item->add_to_rota($rota_id);
832 Add this item to the rota identified by $ROTA_ID, which means associating it
833 with the first stage of that rota. Should this item already be associated
834 with a rota, then we will move it to the new rota.
839 my ( $self, $rota_id ) = @_;
840 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
844 =head3 has_pending_hold
846 my $is_pending_hold = $item->has_pending_hold();
848 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
852 sub has_pending_hold {
854 my $pending_hold = $self->_result->tmp_holdsqueues;
855 return $pending_hold->count ? 1: 0;
860 my $field = $item->as_marc_field;
862 This method returns a MARC::Field object representing the Koha::Item object
863 with the current mappings configuration.
870 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
872 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
876 my $item_field = $tagslib->{$itemtag};
878 my $more_subfields = $self->additional_attributes->to_hashref;
879 foreach my $subfield (
881 $a->{display_order} <=> $b->{display_order}
882 || $a->{subfield} cmp $b->{subfield}
883 } grep { ref($_) && %$_ } values %$item_field
886 my $kohafield = $subfield->{kohafield};
887 my $tagsubfield = $subfield->{tagsubfield};
889 if ( defined $kohafield ) {
890 next if $kohafield !~ m{^items\.}; # That would be weird!
891 ( my $attribute = $kohafield ) =~ s|^items\.||;
892 $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
893 if defined $self->$attribute and $self->$attribute ne '';
895 $value = $more_subfields->{$tagsubfield}
898 next unless defined $value
901 if ( $subfield->{repeatable} ) {
902 my @values = split '\|', $value;
903 push @subfields, ( $tagsubfield => $_ ) for @values;
906 push @subfields, ( $tagsubfield => $value );
911 return unless @subfields;
913 return MARC::Field->new(
914 "$itemtag", ' ', ' ', @subfields
918 =head3 renewal_branchcode
920 Returns the branchcode to be recorded in statistics renewal of the item
924 sub renewal_branchcode {
926 my ($self, $params ) = @_;
928 my $interface = C4::Context->interface;
930 if ( $interface eq 'opac' ){
931 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
932 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
933 $branchcode = 'OPACRenew';
935 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
936 $branchcode = $self->homebranch;
938 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
939 $branchcode = $self->checkout->patron->branchcode;
941 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
942 $branchcode = $self->checkout->branchcode;
948 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
949 ? C4::Context->userenv->{branch} : $params->{branch};
956 Return the cover images associated with this item.
963 my $cover_image_rs = $self->_result->cover_images;
964 return unless $cover_image_rs;
965 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
968 =head3 columns_to_str
970 my $values = $items->columns_to_str;
972 Return a hashref with the string representation of the different attribute of the item.
974 This is meant to be used for display purpose only.
981 my $frameworkcode = $self->biblio->frameworkcode;
982 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
983 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
985 my $columns_info = $self->_result->result_source->columns_info;
987 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
989 for my $column ( keys %$columns_info ) {
991 next if $column eq 'more_subfields_xml';
993 my $value = $self->$column;
994 # 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
996 if ( not defined $value or $value eq "" ) {
997 $values->{$column} = $value;
1002 exists $mss->{"items.$column"}
1003 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1006 $values->{$column} =
1008 ? $subfield->{authorised_value}
1009 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1010 $subfield->{tagsubfield}, $value, '', $tagslib )
1016 $self->more_subfields_xml
1017 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1022 my ( $field ) = $marc_more->fields;
1023 for my $sf ( $field->subfields ) {
1024 my $subfield_code = $sf->[0];
1025 my $value = $sf->[1];
1026 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1027 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1029 $subfield->{authorised_value}
1030 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1031 $subfield->{tagsubfield}, $value, '', $tagslib )
1034 push @{$more_values->{$subfield_code}}, $value;
1037 while ( my ( $k, $v ) = each %$more_values ) {
1038 $values->{$k} = join ' | ', @$v;
1045 =head3 additional_attributes
1047 my $attributes = $item->additional_attributes;
1048 $attributes->{k} = 'new k';
1049 $item->update({ more_subfields => $attributes->to_marcxml });
1051 Returns a Koha::Item::Attributes object that represents the non-mapped
1052 attributes for this item.
1056 sub additional_attributes {
1059 return Koha::Item::Attributes->new_from_marcxml(
1060 $self->more_subfields_xml,
1064 =head3 _set_found_trigger
1066 $self->_set_found_trigger
1068 Finds the most recent lost item charge for this item and refunds the patron
1069 appropriately, taking into account any payments or writeoffs already applied
1072 Internal function, not exported, called only by Koha::Item->store.
1076 sub _set_found_trigger {
1077 my ( $self, $pre_mod_item ) = @_;
1079 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1080 my $no_refund_after_days =
1081 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1082 if ($no_refund_after_days) {
1083 my $today = dt_from_string();
1084 my $lost_age_in_days =
1085 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1088 return $self unless $lost_age_in_days < $no_refund_after_days;
1091 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1094 return_branch => C4::Context->userenv
1095 ? C4::Context->userenv->{'branch'}
1100 if ( $lostreturn_policy ) {
1102 # refund charge made for lost book
1103 my $lost_charge = Koha::Account::Lines->search(
1105 itemnumber => $self->itemnumber,
1106 debit_type_code => 'LOST',
1107 status => [ undef, { '<>' => 'FOUND' } ]
1110 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1115 if ( $lost_charge ) {
1117 my $patron = $lost_charge->patron;
1120 my $account = $patron->account;
1121 my $total_to_refund = 0;
1124 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1126 # some amount has been cancelled. collect the offsets that are not writeoffs
1127 # this works because the only way to subtract from this kind of a debt is
1128 # using the UI buttons 'Pay' and 'Write off'
1129 my $credit_offsets = $lost_charge->debit_offsets(
1131 'credit_id' => { '!=' => undef },
1132 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1134 { join => 'credit' }
1137 $total_to_refund = ( $credit_offsets->count > 0 )
1138 ? $credit_offsets->total * -1 # credits are negative on the DB
1142 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1145 if ( $credit_total > 0 ) {
1147 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1148 $credit = $account->add_credit(
1150 amount => $credit_total,
1151 description => 'Item found ' . $self->itemnumber,
1152 type => 'LOST_FOUND',
1153 interface => C4::Context->interface,
1154 library_id => $branchcode,
1155 item_id => $self->itemnumber,
1156 issue_id => $lost_charge->issue_id
1160 $credit->apply( { debits => [$lost_charge] } );
1164 message => 'lost_refunded',
1165 payload => { credit_id => $credit->id }
1170 # Update the account status
1171 $lost_charge->status('FOUND');
1172 $lost_charge->store();
1174 # Reconcile balances if required
1175 if ( C4::Context->preference('AccountAutoReconcile') ) {
1176 $account->reconcile_balance;
1181 # restore fine for lost book
1182 if ( $lostreturn_policy eq 'restore' ) {
1183 my $lost_overdue = Koha::Account::Lines->search(
1185 itemnumber => $self->itemnumber,
1186 debit_type_code => 'OVERDUE',
1190 order_by => { '-desc' => 'date' },
1195 if ( $lost_overdue ) {
1197 my $patron = $lost_overdue->patron;
1199 my $account = $patron->account;
1201 # Update status of fine
1202 $lost_overdue->status('FOUND')->store();
1204 # Find related forgive credit
1205 my $refund = $lost_overdue->credits(
1207 credit_type_code => 'FORGIVEN',
1208 itemnumber => $self->itemnumber,
1209 status => [ { '!=' => 'VOID' }, undef ]
1211 { order_by => { '-desc' => 'date' }, rows => 1 }
1215 # Revert the forgive credit
1216 $refund->void({ interface => 'trigger' });
1220 message => 'lost_restored',
1221 payload => { refund_id => $refund->id }
1226 # Reconcile balances if required
1227 if ( C4::Context->preference('AccountAutoReconcile') ) {
1228 $account->reconcile_balance;
1232 } elsif ( $lostreturn_policy eq 'charge' ) {
1236 message => 'lost_charge',
1245 =head3 public_read_list
1247 This method returns the list of publicly readable database fields for both API and UI output purposes
1251 sub public_read_list {
1253 'itemnumber', 'biblionumber', 'homebranch',
1254 'holdingbranch', 'location', 'collectioncode',
1255 'itemcallnumber', 'copynumber', 'enumchron',
1256 'barcode', 'dateaccessioned', 'itemnotes',
1257 'onloan', 'uri', 'itype',
1258 'notforloan', 'damaged', 'itemlost',
1259 'withdrawn', 'restricted'
1263 =head3 to_api_mapping
1265 This method returns the mapping for representing a Koha::Item object
1270 sub to_api_mapping {
1272 itemnumber => 'item_id',
1273 biblionumber => 'biblio_id',
1274 biblioitemnumber => undef,
1275 barcode => 'external_id',
1276 dateaccessioned => 'acquisition_date',
1277 booksellerid => 'acquisition_source',
1278 homebranch => 'home_library_id',
1279 price => 'purchase_price',
1280 replacementprice => 'replacement_price',
1281 replacementpricedate => 'replacement_price_date',
1282 datelastborrowed => 'last_checkout_date',
1283 datelastseen => 'last_seen_date',
1285 notforloan => 'not_for_loan_status',
1286 damaged => 'damaged_status',
1287 damaged_on => 'damaged_date',
1288 itemlost => 'lost_status',
1289 itemlost_on => 'lost_date',
1290 withdrawn => 'withdrawn',
1291 withdrawn_on => 'withdrawn_date',
1292 itemcallnumber => 'callnumber',
1293 coded_location_qualifier => 'coded_location_qualifier',
1294 issues => 'checkouts_count',
1295 renewals => 'renewals_count',
1296 reserves => 'holds_count',
1297 restricted => 'restricted_status',
1298 itemnotes => 'public_notes',
1299 itemnotes_nonpublic => 'internal_notes',
1300 holdingbranch => 'holding_library_id',
1301 timestamp => 'timestamp',
1302 location => 'location',
1303 permanent_location => 'permanent_location',
1304 onloan => 'checked_out_date',
1305 cn_source => 'call_number_source',
1306 cn_sort => 'call_number_sort',
1307 ccode => 'collection_code',
1308 materials => 'materials_notes',
1310 itype => 'item_type_id',
1311 more_subfields_xml => 'extended_subfields',
1312 enumchron => 'serial_issue_number',
1313 copynumber => 'copy_number',
1314 stocknumber => 'inventory_number',
1315 new_status => 'new_status'
1321 my $itemtype = $item->itemtype;
1323 Returns Koha object for effective itemtype
1329 return Koha::ItemTypes->find( $self->effective_itemtype );
1334 my $orders = $item->orders();
1336 Returns a Koha::Acquisition::Orders object
1343 my $orders = $self->_result->item_orders;
1344 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1347 =head3 tracked_links
1349 my $tracked_links = $item->tracked_links();
1351 Returns a Koha::TrackedLinks object
1358 my $tracked_links = $self->_result->linktrackers;
1359 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1362 =head3 move_to_biblio
1364 $item->move_to_biblio($to_biblio[, $params]);
1366 Move the item to another biblio and update any references in other tables.
1368 The final optional parameter, C<$params>, is expected to contain the
1369 'skip_record_index' key, which is relayed down to Koha::Item->store.
1370 There it prevents calling index_records, which takes most of the
1371 time in batch adds/deletes. The caller must take care of calling
1372 index_records separately.
1375 skip_record_index => 1|0
1377 Returns undef if the move failed or the biblionumber of the destination record otherwise
1381 sub move_to_biblio {
1382 my ( $self, $to_biblio, $params ) = @_;
1386 return if $self->biblionumber == $to_biblio->biblionumber;
1388 my $from_biblionumber = $self->biblionumber;
1389 my $to_biblionumber = $to_biblio->biblionumber;
1391 # Own biblionumber and biblioitemnumber
1393 biblionumber => $to_biblionumber,
1394 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1395 })->store({ skip_record_index => $params->{skip_record_index} });
1397 unless ($params->{skip_record_index}) {
1398 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1399 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1402 # Acquisition orders
1403 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1406 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1408 # hold_fill_target (there's no Koha object available yet)
1409 my $hold_fill_target = $self->_result->hold_fill_target;
1410 if ($hold_fill_target) {
1411 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1414 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1415 # and can't even fake one since the significant columns are nullable.
1416 my $storage = $self->_result->result_source->storage;
1419 my ($storage, $dbh, @cols) = @_;
1421 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1426 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1428 return $to_biblionumber;
1431 =head2 Internal methods
1433 =head3 _after_item_action_hooks
1435 Helper method that takes care of calling all plugin hooks
1439 sub _after_item_action_hooks {
1440 my ( $self, $params ) = @_;
1442 my $action = $params->{action};
1444 Koha::Plugins->call(
1445 'after_item_action',
1449 item_id => $self->itemnumber,
1464 Kyle M Hall <kyle@bywatersolutions.com>