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;
49 use base qw(Koha::Object);
53 Koha::Item - Koha Item object class
65 $params can take an optional 'skip_record_index' parameter.
66 If set, the reindexation process will not happen (index_records not called)
68 NOTE: This is a temporary fix to answer a performance issue when lot of items
69 are added (or modified) at the same time.
70 The correct way to fix this is to make the ES reindexation process async.
71 You should not turn it on if you do not understand what it is doing exactly.
77 my $params = @_ ? shift : {};
79 my $log_action = $params->{log_action} // 1;
81 # We do not want to oblige callers to pass this value
82 # Dev conveniences vs performance?
83 unless ( $self->biblioitemnumber ) {
84 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
87 # See related changes from C4::Items::AddItem
88 unless ( $self->itype ) {
89 $self->itype($self->biblio->biblioitem->itemtype);
92 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
94 my $today = dt_from_string;
95 my $action = 'create';
97 unless ( $self->in_storage ) { #AddItem
99 unless ( $self->permanent_location ) {
100 $self->permanent_location($self->location);
103 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
104 unless ( $self->location || !$default_location ) {
105 $self->permanent_location( $self->location || $default_location )
106 unless $self->permanent_location;
107 $self->location($default_location);
110 unless ( $self->replacementpricedate ) {
111 $self->replacementpricedate($today);
113 unless ( $self->datelastseen ) {
114 $self->datelastseen($today);
117 unless ( $self->dateaccessioned ) {
118 $self->dateaccessioned($today);
121 if ( $self->itemcallnumber
122 or $self->cn_source )
124 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
125 $self->cn_sort($cn_sort);
132 my %updated_columns = $self->_result->get_dirty_columns;
133 return $self->SUPER::store unless %updated_columns;
135 # Retrieve the item for comparison if we need to
137 exists $updated_columns{itemlost}
138 or exists $updated_columns{withdrawn}
139 or exists $updated_columns{damaged}
140 ) ? $self->get_from_storage : undef;
142 # Update *_on fields if needed
143 # FIXME: Why not for AddItem as well?
144 my @fields = qw( itemlost withdrawn damaged );
145 for my $field (@fields) {
147 # If the field is defined but empty or 0, we are
148 # removing/unsetting and thus need to clear out
150 if ( exists $updated_columns{$field}
151 && defined( $self->$field )
154 my $field_on = "${field}_on";
155 $self->$field_on(undef);
157 # If the field has changed otherwise, we much update
159 elsif (exists $updated_columns{$field}
160 && $updated_columns{$field}
161 && !$pre_mod_item->$field )
163 my $field_on = "${field}_on";
165 DateTime::Format::MySQL->format_datetime(
172 if ( exists $updated_columns{itemcallnumber}
173 or exists $updated_columns{cn_source} )
175 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
176 $self->cn_sort($cn_sort);
180 if ( exists $updated_columns{location}
181 and $self->location ne 'CART'
182 and $self->location ne 'PROC'
183 and not exists $updated_columns{permanent_location} )
185 $self->permanent_location( $self->location );
188 # If item was lost and has now been found,
189 # reverse any list item charges if necessary.
190 if ( exists $updated_columns{itemlost}
191 and $updated_columns{itemlost} <= 0
192 and $pre_mod_item->itemlost > 0 )
194 $self->_set_found_trigger($pre_mod_item);
199 unless ( $self->dateaccessioned ) {
200 $self->dateaccessioned($today);
203 my $result = $self->SUPER::store;
204 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
206 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
207 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
209 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
210 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
211 unless $params->{skip_record_index};
212 $self->get_from_storage->_after_item_action_hooks({ action => $action });
223 my $params = @_ ? shift : {};
225 # FIXME check the item has no current issues
226 # i.e. raise the appropriate exception
228 my $result = $self->SUPER::delete;
230 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
231 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
232 unless $params->{skip_record_index};
234 $self->_after_item_action_hooks({ action => 'delete' });
236 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
237 if C4::Context->preference("CataloguingLog");
248 my $params = @_ ? shift : {};
250 my $safe_to_delete = $self->safe_to_delete;
251 return $safe_to_delete unless $safe_to_delete eq '1';
253 $self->move_to_deleted;
255 return $self->delete($params);
258 =head3 safe_to_delete
260 returns 1 if the item is safe to delete,
262 "book_on_loan" if the item is checked out,
264 "not_same_branch" if the item is blocked by independent branches,
266 "book_reserved" if the there are holds aganst the item, or
268 "linked_analytics" if the item has linked analytic records.
270 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
277 return "book_on_loan" if $self->checkout;
279 return "not_same_branch"
280 if defined C4::Context->userenv
281 and !C4::Context->IsSuperLibrarian()
282 and C4::Context->preference("IndependentBranches")
283 and ( C4::Context->userenv->{branch} ne $self->homebranch );
285 # check it doesn't have a waiting reserve
286 return "book_reserved"
287 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
289 return "linked_analytics"
290 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
292 return "last_item_for_hold"
293 if $self->biblio->items->count == 1
294 && $self->biblio->holds->search(
303 =head3 move_to_deleted
305 my $is_moved = $item->move_to_deleted;
307 Move an item to the deleteditems table.
308 This can be done before deleting an item, to make sure the data are not completely deleted.
312 sub move_to_deleted {
314 my $item_infos = $self->unblessed;
315 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
316 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
320 =head3 effective_itemtype
322 Returns the itemtype for the item based on whether item level itemtypes are set or not.
326 sub effective_itemtype {
329 return $self->_result()->effective_itemtype();
339 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
341 return $self->{_home_branch};
344 =head3 holding_branch
351 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
353 return $self->{_holding_branch};
358 my $biblio = $item->biblio;
360 Return the bibliographic record of this item
366 my $biblio_rs = $self->_result->biblio;
367 return Koha::Biblio->_new_from_dbic( $biblio_rs );
372 my $biblioitem = $item->biblioitem;
374 Return the biblioitem record of this item
380 my $biblioitem_rs = $self->_result->biblioitem;
381 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
386 my $checkout = $item->checkout;
388 Return the checkout for this item
394 my $checkout_rs = $self->_result->issue;
395 return unless $checkout_rs;
396 return Koha::Checkout->_new_from_dbic( $checkout_rs );
401 my $holds = $item->holds();
402 my $holds = $item->holds($params);
403 my $holds = $item->holds({ found => 'W'});
405 Return holds attached to an item, optionally accept a hashref of params to pass to search
410 my ( $self,$params ) = @_;
411 my $holds_rs = $self->_result->reserves->search($params);
412 return Koha::Holds->_new_from_dbic( $holds_rs );
415 =head3 request_transfer
417 my $transfer = $item->request_transfer(
421 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
425 Add a transfer request for this item to the given branch for the given reason.
427 An exception will be thrown if the BranchTransferLimits would prevent the requested
428 transfer, unless 'ignore_limits' is passed to override the limits.
430 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
431 The caller should catch such cases and retry the transfer request as appropriate passing
432 an appropriate override.
435 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
436 * replace - Used to replace the existing transfer request with your own.
440 sub request_transfer {
441 my ( $self, $params ) = @_;
443 # check for mandatory params
444 my @mandatory = ( 'to', 'reason' );
445 for my $param (@mandatory) {
446 unless ( defined( $params->{$param} ) ) {
447 Koha::Exceptions::MissingParameter->throw(
448 error => "The $param parameter is mandatory" );
452 Koha::Exceptions::Item::Transfer::Limit->throw()
453 unless ( $params->{ignore_limits}
454 || $self->can_be_transferred( { to => $params->{to} } ) );
456 my $request = $self->get_transfer;
457 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
458 if ( $request && !$params->{enqueue} && !$params->{replace} );
460 $request->cancel( { reason => $params->{reason}, force => 1 } )
461 if ( defined($request) && $params->{replace} );
463 my $transfer = Koha::Item::Transfer->new(
465 itemnumber => $self->itemnumber,
466 daterequested => dt_from_string,
467 frombranch => $self->holdingbranch,
468 tobranch => $params->{to}->branchcode,
469 reason => $params->{reason},
470 comments => $params->{comment}
479 my $transfer = $item->get_transfer;
481 Return the active transfer request or undef
483 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
484 whereby the most recently sent, but not received, transfer will be returned
485 if it exists, otherwise the oldest unsatisfied transfer will be returned.
487 This allows for transfers to queue, which is the case for stock rotation and
488 rotating collections where a manual transfer may need to take precedence but
489 we still expect the item to end up at a final location eventually.
495 my $transfer_rs = $self->_result->branchtransfers->search(
497 datearrived => undef,
498 datecancelled => undef
502 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
506 return unless $transfer_rs;
507 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
512 my $transfer = $item->get_transfers;
514 Return the list of outstanding transfers (i.e requested but not yet cancelled
517 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
518 whereby the most recently sent, but not received, transfer will be returned
519 first if it exists, otherwise requests are in oldest to newest request order.
521 This allows for transfers to queue, which is the case for stock rotation and
522 rotating collections where a manual transfer may need to take precedence but
523 we still expect the item to end up at a final location eventually.
529 my $transfer_rs = $self->_result->branchtransfers->search(
531 datearrived => undef,
532 datecancelled => undef
536 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
539 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
542 =head3 last_returned_by
544 Gets and sets the last borrower to return an item.
546 Accepts and returns Koha::Patron objects
548 $item->last_returned_by( $borrowernumber );
550 $last_returned_by = $item->last_returned_by();
554 sub last_returned_by {
555 my ( $self, $borrower ) = @_;
557 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
560 return $items_last_returned_by_rs->update_or_create(
561 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
564 unless ( $self->{_last_returned_by} ) {
565 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
567 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
571 return $self->{_last_returned_by};
575 =head3 can_article_request
577 my $bool = $item->can_article_request( $borrower )
579 Returns true if item can be specifically requested
581 $borrower must be a Koha::Patron object
585 sub can_article_request {
586 my ( $self, $borrower ) = @_;
588 my $rule = $self->article_request_type($borrower);
590 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
594 =head3 hidden_in_opac
596 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
598 Returns true if item fields match the hidding criteria defined in $rules.
599 Returns false otherwise.
601 Takes HASHref that can have the following parameters:
603 $rules : { <field> => [ value_1, ... ], ... }
605 Note: $rules inherits its structure from the parsed YAML from reading
606 the I<OpacHiddenItems> system preference.
611 my ( $self, $params ) = @_;
613 my $rules = $params->{rules} // {};
616 if C4::Context->preference('hidelostitems') and
619 my $hidden_in_opac = 0;
621 foreach my $field ( keys %{$rules} ) {
623 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
629 return $hidden_in_opac;
632 =head3 can_be_transferred
634 $item->can_be_transferred({ to => $to_library, from => $from_library })
635 Checks if an item can be transferred to given library.
637 This feature is controlled by two system preferences:
638 UseBranchTransferLimits to enable / disable the feature
639 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
640 for setting the limitations
642 Takes HASHref that can have the following parameters:
643 MANDATORY PARAMETERS:
646 $from : Koha::Library # if not given, item holdingbranch
647 # will be used instead
649 Returns 1 if item can be transferred to $to_library, otherwise 0.
651 To find out whether at least one item of a Koha::Biblio can be transferred, please
652 see Koha::Biblio->can_be_transferred() instead of using this method for
653 multiple items of the same biblio.
657 sub can_be_transferred {
658 my ($self, $params) = @_;
660 my $to = $params->{to};
661 my $from = $params->{from};
663 $to = $to->branchcode;
664 $from = defined $from ? $from->branchcode : $self->holdingbranch;
666 return 1 if $from eq $to; # Transfer to current branch is allowed
667 return 1 unless C4::Context->preference('UseBranchTransferLimits');
669 my $limittype = C4::Context->preference('BranchTransferLimitsType');
670 return Koha::Item::Transfer::Limits->search({
673 $limittype => $limittype eq 'itemtype'
674 ? $self->effective_itemtype : $self->ccode
679 =head3 pickup_locations
681 $pickup_locations = $item->pickup_locations( {patron => $patron } )
683 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)
684 and if item can be transferred to each pickup location.
688 sub pickup_locations {
689 my ($self, $params) = @_;
691 my $patron = $params->{patron};
693 my $circ_control_branch =
694 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
696 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
698 if(defined $patron) {
699 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
700 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
703 my $pickup_libraries = Koha::Libraries->search();
704 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
705 $pickup_libraries = $self->home_branch->get_hold_libraries;
706 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
707 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
708 $pickup_libraries = $plib->get_hold_libraries;
709 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
710 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
711 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
712 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
715 return $pickup_libraries->search(
720 order_by => ['branchname']
722 ) unless C4::Context->preference('UseBranchTransferLimits');
724 my $limittype = C4::Context->preference('BranchTransferLimitsType');
725 my ($ccode, $itype) = (undef, undef);
726 if( $limittype eq 'ccode' ){
727 $ccode = $self->ccode;
729 $itype = $self->itype;
731 my $limits = Koha::Item::Transfer::Limits->search(
733 fromBranch => $self->holdingbranch,
737 { columns => ['toBranch'] }
740 return $pickup_libraries->search(
742 pickup_location => 1,
744 '-not_in' => $limits->_resultset->as_query
748 order_by => ['branchname']
753 =head3 article_request_type
755 my $type = $item->article_request_type( $borrower )
757 returns 'yes', 'no', 'bib_only', or 'item_only'
759 $borrower must be a Koha::Patron object
763 sub article_request_type {
764 my ( $self, $borrower ) = @_;
766 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
768 $branch_control eq 'homebranch' ? $self->homebranch
769 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
771 my $borrowertype = $borrower->categorycode;
772 my $itemtype = $self->effective_itemtype();
773 my $rule = Koha::CirculationRules->get_effective_rule(
775 rule_name => 'article_requests',
776 categorycode => $borrowertype,
777 itemtype => $itemtype,
778 branchcode => $branchcode
782 return q{} unless $rule;
783 return $rule->rule_value || q{}
792 my $attributes = { order_by => 'priority' };
793 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
795 itemnumber => $self->itemnumber,
798 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
799 waitingdate => { '!=' => undef },
802 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
803 return Koha::Holds->_new_from_dbic($hold_rs);
806 =head3 stockrotationitem
808 my $sritem = Koha::Item->stockrotationitem;
810 Returns the stock rotation item associated with the current item.
814 sub stockrotationitem {
816 my $rs = $self->_result->stockrotationitem;
818 return Koha::StockRotationItem->_new_from_dbic( $rs );
823 my $item = $item->add_to_rota($rota_id);
825 Add this item to the rota identified by $ROTA_ID, which means associating it
826 with the first stage of that rota. Should this item already be associated
827 with a rota, then we will move it to the new rota.
832 my ( $self, $rota_id ) = @_;
833 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
837 =head3 has_pending_hold
839 my $is_pending_hold = $item->has_pending_hold();
841 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
845 sub has_pending_hold {
847 my $pending_hold = $self->_result->tmp_holdsqueues;
848 return $pending_hold->count ? 1: 0;
853 my $field = $item->as_marc_field;
855 This method returns a MARC::Field object representing the Koha::Item object
856 with the current mappings configuration.
863 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
865 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
869 my $item_field = $tagslib->{$itemtag};
871 my $more_subfields = $self->additional_attributes->to_hashref;
872 foreach my $subfield (
874 $a->{display_order} <=> $b->{display_order}
875 || $a->{subfield} cmp $b->{subfield}
876 } grep { ref($_) && %$_ } values %$item_field
879 my $kohafield = $subfield->{kohafield};
880 my $tagsubfield = $subfield->{tagsubfield};
882 if ( defined $kohafield ) {
883 next if $kohafield !~ m{^items\.}; # That would be weird!
884 ( my $attribute = $kohafield ) =~ s|^items\.||;
885 $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
886 if defined $self->$attribute and $self->$attribute ne '';
888 $value = $more_subfields->{$tagsubfield}
891 next unless defined $value
894 if ( $subfield->{repeatable} ) {
895 my @values = split '\|', $value;
896 push @subfields, ( $tagsubfield => $_ ) for @values;
899 push @subfields, ( $tagsubfield => $value );
904 return unless @subfields;
906 return MARC::Field->new(
907 "$itemtag", ' ', ' ', @subfields
911 =head3 renewal_branchcode
913 Returns the branchcode to be recorded in statistics renewal of the item
917 sub renewal_branchcode {
919 my ($self, $params ) = @_;
921 my $interface = C4::Context->interface;
923 if ( $interface eq 'opac' ){
924 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
925 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
926 $branchcode = 'OPACRenew';
928 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
929 $branchcode = $self->homebranch;
931 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
932 $branchcode = $self->checkout->patron->branchcode;
934 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
935 $branchcode = $self->checkout->branchcode;
941 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
942 ? C4::Context->userenv->{branch} : $params->{branch};
949 Return the cover images associated with this item.
956 my $cover_image_rs = $self->_result->cover_images;
957 return unless $cover_image_rs;
958 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
961 =head3 columns_to_str
963 my $values = $items->columns_to_str;
965 Return a hashref with the string representation of the different attribute of the item.
967 This is meant to be used for display purpose only.
974 my $frameworkcode = $self->biblio->frameworkcode;
975 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
976 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
978 my $columns_info = $self->_result->result_source->columns_info;
980 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
982 for my $column ( keys %$columns_info ) {
984 next if $column eq 'more_subfields_xml';
986 my $value = $self->$column;
987 # 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
989 if ( not defined $value or $value eq "" ) {
990 $values->{$column} = $value;
995 exists $mss->{"items.$column"}
996 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1001 ? $subfield->{authorised_value}
1002 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1003 $subfield->{tagsubfield}, $value, '', $tagslib )
1009 $self->more_subfields_xml
1010 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1015 my ( $field ) = $marc_more->fields;
1016 for my $sf ( $field->subfields ) {
1017 my $subfield_code = $sf->[0];
1018 my $value = $sf->[1];
1019 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1020 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1022 $subfield->{authorised_value}
1023 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1024 $subfield->{tagsubfield}, $value, '', $tagslib )
1027 push @{$more_values->{$subfield_code}}, $value;
1030 while ( my ( $k, $v ) = each %$more_values ) {
1031 $values->{$k} = join ' | ', @$v;
1038 =head3 additional_attributes
1040 my $attributes = $item->additional_attributes;
1041 $attributes->{k} = 'new k';
1042 $item->update({ more_subfields => $attributes->to_marcxml });
1044 Returns a Koha::Item::Attributes object that represents the non-mapped
1045 attributes for this item.
1049 sub additional_attributes {
1052 return Koha::Item::Attributes->new_from_marcxml(
1053 $self->more_subfields_xml,
1057 =head3 _set_found_trigger
1059 $self->_set_found_trigger
1061 Finds the most recent lost item charge for this item and refunds the patron
1062 appropriately, taking into account any payments or writeoffs already applied
1065 Internal function, not exported, called only by Koha::Item->store.
1069 sub _set_found_trigger {
1070 my ( $self, $pre_mod_item ) = @_;
1072 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1073 my $no_refund_after_days =
1074 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1075 if ($no_refund_after_days) {
1076 my $today = dt_from_string();
1077 my $lost_age_in_days =
1078 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1081 return $self unless $lost_age_in_days < $no_refund_after_days;
1084 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1087 return_branch => C4::Context->userenv
1088 ? C4::Context->userenv->{'branch'}
1093 if ( $lostreturn_policy ) {
1095 # refund charge made for lost book
1096 my $lost_charge = Koha::Account::Lines->search(
1098 itemnumber => $self->itemnumber,
1099 debit_type_code => 'LOST',
1100 status => [ undef, { '<>' => 'FOUND' } ]
1103 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1108 if ( $lost_charge ) {
1110 my $patron = $lost_charge->patron;
1113 my $account = $patron->account;
1114 my $total_to_refund = 0;
1117 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1119 # some amount has been cancelled. collect the offsets that are not writeoffs
1120 # this works because the only way to subtract from this kind of a debt is
1121 # using the UI buttons 'Pay' and 'Write off'
1122 my $credit_offsets = $lost_charge->debit_offsets(
1124 'credit_id' => { '!=' => undef },
1125 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1127 { join => 'credit' }
1130 $total_to_refund = ( $credit_offsets->count > 0 )
1131 ? $credit_offsets->total * -1 # credits are negative on the DB
1135 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1138 if ( $credit_total > 0 ) {
1140 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1141 $credit = $account->add_credit(
1143 amount => $credit_total,
1144 description => 'Item found ' . $self->itemnumber,
1145 type => 'LOST_FOUND',
1146 interface => C4::Context->interface,
1147 library_id => $branchcode,
1148 item_id => $self->itemnumber,
1149 issue_id => $lost_charge->issue_id
1153 $credit->apply( { debits => [$lost_charge] } );
1154 $self->{_refunded} = 1;
1157 # Update the account status
1158 $lost_charge->status('FOUND');
1159 $lost_charge->store();
1161 # Reconcile balances if required
1162 if ( C4::Context->preference('AccountAutoReconcile') ) {
1163 $account->reconcile_balance;
1168 # restore fine for lost book
1169 if ( $lostreturn_policy eq 'restore' ) {
1170 my $lost_overdue = Koha::Account::Lines->search(
1172 itemnumber => $self->itemnumber,
1173 debit_type_code => 'OVERDUE',
1177 order_by => { '-desc' => 'date' },
1182 if ( $lost_overdue ) {
1184 my $patron = $lost_overdue->patron;
1186 my $account = $patron->account;
1188 # Update status of fine
1189 $lost_overdue->status('FOUND')->store();
1191 # Find related forgive credit
1192 my $refund = $lost_overdue->credits(
1194 credit_type_code => 'FORGIVEN',
1195 itemnumber => $self->itemnumber,
1196 status => [ { '!=' => 'VOID' }, undef ]
1198 { order_by => { '-desc' => 'date' }, rows => 1 }
1202 # Revert the forgive credit
1203 $refund->void({ interface => 'trigger' });
1204 $self->{_restored} = 1;
1207 # Reconcile balances if required
1208 if ( C4::Context->preference('AccountAutoReconcile') ) {
1209 $account->reconcile_balance;
1213 } elsif ( $lostreturn_policy eq 'charge' ) {
1214 $self->{_charge} = 1;
1221 =head3 public_read_list
1223 This method returns the list of publicly readable database fields for both API and UI output purposes
1227 sub public_read_list {
1229 'itemnumber', 'biblionumber', 'homebranch',
1230 'holdingbranch', 'location', 'collectioncode',
1231 'itemcallnumber', 'copynumber', 'enumchron',
1232 'barcode', 'dateaccessioned', 'itemnotes',
1233 'onloan', 'uri', 'itype',
1234 'notforloan', 'damaged', 'itemlost',
1235 'withdrawn', 'restricted'
1239 =head3 to_api_mapping
1241 This method returns the mapping for representing a Koha::Item object
1246 sub to_api_mapping {
1248 itemnumber => 'item_id',
1249 biblionumber => 'biblio_id',
1250 biblioitemnumber => undef,
1251 barcode => 'external_id',
1252 dateaccessioned => 'acquisition_date',
1253 booksellerid => 'acquisition_source',
1254 homebranch => 'home_library_id',
1255 price => 'purchase_price',
1256 replacementprice => 'replacement_price',
1257 replacementpricedate => 'replacement_price_date',
1258 datelastborrowed => 'last_checkout_date',
1259 datelastseen => 'last_seen_date',
1261 notforloan => 'not_for_loan_status',
1262 damaged => 'damaged_status',
1263 damaged_on => 'damaged_date',
1264 itemlost => 'lost_status',
1265 itemlost_on => 'lost_date',
1266 withdrawn => 'withdrawn',
1267 withdrawn_on => 'withdrawn_date',
1268 itemcallnumber => 'callnumber',
1269 coded_location_qualifier => 'coded_location_qualifier',
1270 issues => 'checkouts_count',
1271 renewals => 'renewals_count',
1272 reserves => 'holds_count',
1273 restricted => 'restricted_status',
1274 itemnotes => 'public_notes',
1275 itemnotes_nonpublic => 'internal_notes',
1276 holdingbranch => 'holding_library_id',
1277 timestamp => 'timestamp',
1278 location => 'location',
1279 permanent_location => 'permanent_location',
1280 onloan => 'checked_out_date',
1281 cn_source => 'call_number_source',
1282 cn_sort => 'call_number_sort',
1283 ccode => 'collection_code',
1284 materials => 'materials_notes',
1286 itype => 'item_type_id',
1287 more_subfields_xml => 'extended_subfields',
1288 enumchron => 'serial_issue_number',
1289 copynumber => 'copy_number',
1290 stocknumber => 'inventory_number',
1291 new_status => 'new_status'
1297 my $itemtype = $item->itemtype;
1299 Returns Koha object for effective itemtype
1305 return Koha::ItemTypes->find( $self->effective_itemtype );
1310 my $orders = $item->orders();
1312 Returns a Koha::Acquisition::Orders object
1319 my $orders = $self->_result->item_orders;
1320 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1323 =head3 tracked_links
1325 my $tracked_links = $item->tracked_links();
1327 Returns a Koha::TrackedLinks object
1334 my $tracked_links = $self->_result->linktrackers;
1335 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1338 =head3 move_to_biblio
1340 $item->move_to_biblio($to_biblio[, $params]);
1342 Move the item to another biblio and update any references in other tables.
1344 The final optional parameter, C<$params>, is expected to contain the
1345 'skip_record_index' key, which is relayed down to Koha::Item->store.
1346 There it prevents calling index_records, which takes most of the
1347 time in batch adds/deletes. The caller must take care of calling
1348 index_records separately.
1351 skip_record_index => 1|0
1353 Returns undef if the move failed or the biblionumber of the destination record otherwise
1357 sub move_to_biblio {
1358 my ( $self, $to_biblio, $params ) = @_;
1362 return if $self->biblionumber == $to_biblio->biblionumber;
1364 my $from_biblionumber = $self->biblionumber;
1365 my $to_biblionumber = $to_biblio->biblionumber;
1367 # Own biblionumber and biblioitemnumber
1369 biblionumber => $to_biblionumber,
1370 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1371 })->store({ skip_record_index => $params->{skip_record_index} });
1373 unless ($params->{skip_record_index}) {
1374 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1375 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1378 # Acquisition orders
1379 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1382 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1384 # hold_fill_target (there's no Koha object available yet)
1385 my $hold_fill_target = $self->_result->hold_fill_target;
1386 if ($hold_fill_target) {
1387 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1390 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1391 # and can't even fake one since the significant columns are nullable.
1392 my $storage = $self->_result->result_source->storage;
1395 my ($storage, $dbh, @cols) = @_;
1397 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1402 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1404 return $to_biblionumber;
1407 =head2 Internal methods
1409 =head3 _after_item_action_hooks
1411 Helper method that takes care of calling all plugin hooks
1415 sub _after_item_action_hooks {
1416 my ( $self, $params ) = @_;
1418 my $action = $params->{action};
1420 Koha::Plugins->call(
1421 'after_item_action',
1425 item_id => $self->itemnumber,
1440 Kyle M Hall <kyle@bywatersolutions.com>