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 my $result = $self->SUPER::store;
200 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
202 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
205 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207 unless $params->{skip_record_index};
208 $self->get_from_storage->_after_item_action_hooks({ action => $action });
219 my $params = @_ ? shift : {};
221 # FIXME check the item has no current issues
222 # i.e. raise the appropriate exception
224 my $result = $self->SUPER::delete;
226 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
227 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
228 unless $params->{skip_record_index};
230 $self->_after_item_action_hooks({ action => 'delete' });
232 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
233 if C4::Context->preference("CataloguingLog");
244 my $params = @_ ? shift : {};
246 my $safe_to_delete = $self->safe_to_delete;
247 return $safe_to_delete unless $safe_to_delete eq '1';
249 $self->move_to_deleted;
251 return $self->delete($params);
254 =head3 safe_to_delete
256 returns 1 if the item is safe to delete,
258 "book_on_loan" if the item is checked out,
260 "not_same_branch" if the item is blocked by independent branches,
262 "book_reserved" if the there are holds aganst the item, or
264 "linked_analytics" if the item has linked analytic records.
266 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
273 return "book_on_loan" if $self->checkout;
275 return "not_same_branch"
276 if defined C4::Context->userenv
277 and !C4::Context->IsSuperLibrarian()
278 and C4::Context->preference("IndependentBranches")
279 and ( C4::Context->userenv->{branch} ne $self->homebranch );
281 # check it doesn't have a waiting reserve
282 return "book_reserved"
283 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
285 return "linked_analytics"
286 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
288 return "last_item_for_hold"
289 if $self->biblio->items->count == 1
290 && $self->biblio->holds->search(
299 =head3 move_to_deleted
301 my $is_moved = $item->move_to_deleted;
303 Move an item to the deleteditems table.
304 This can be done before deleting an item, to make sure the data are not completely deleted.
308 sub move_to_deleted {
310 my $item_infos = $self->unblessed;
311 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
312 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
316 =head3 effective_itemtype
318 Returns the itemtype for the item based on whether item level itemtypes are set or not.
322 sub effective_itemtype {
325 return $self->_result()->effective_itemtype();
335 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
337 return $self->{_home_branch};
340 =head3 holding_branch
347 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
349 return $self->{_holding_branch};
354 my $biblio = $item->biblio;
356 Return the bibliographic record of this item
362 my $biblio_rs = $self->_result->biblio;
363 return Koha::Biblio->_new_from_dbic( $biblio_rs );
368 my $biblioitem = $item->biblioitem;
370 Return the biblioitem record of this item
376 my $biblioitem_rs = $self->_result->biblioitem;
377 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
382 my $checkout = $item->checkout;
384 Return the checkout for this item
390 my $checkout_rs = $self->_result->issue;
391 return unless $checkout_rs;
392 return Koha::Checkout->_new_from_dbic( $checkout_rs );
397 my $holds = $item->holds();
398 my $holds = $item->holds($params);
399 my $holds = $item->holds({ found => 'W'});
401 Return holds attached to an item, optionally accept a hashref of params to pass to search
406 my ( $self,$params ) = @_;
407 my $holds_rs = $self->_result->reserves->search($params);
408 return Koha::Holds->_new_from_dbic( $holds_rs );
411 =head3 request_transfer
413 my $transfer = $item->request_transfer(
417 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
421 Add a transfer request for this item to the given branch for the given reason.
423 An exception will be thrown if the BranchTransferLimits would prevent the requested
424 transfer, unless 'ignore_limits' is passed to override the limits.
426 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
427 The caller should catch such cases and retry the transfer request as appropriate passing
428 an appropriate override.
431 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
432 * replace - Used to replace the existing transfer request with your own.
436 sub request_transfer {
437 my ( $self, $params ) = @_;
439 # check for mandatory params
440 my @mandatory = ( 'to', 'reason' );
441 for my $param (@mandatory) {
442 unless ( defined( $params->{$param} ) ) {
443 Koha::Exceptions::MissingParameter->throw(
444 error => "The $param parameter is mandatory" );
448 Koha::Exceptions::Item::Transfer::Limit->throw()
449 unless ( $params->{ignore_limits}
450 || $self->can_be_transferred( { to => $params->{to} } ) );
452 my $request = $self->get_transfer;
453 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
454 if ( $request && !$params->{enqueue} && !$params->{replace} );
456 $request->cancel( { reason => $params->{reason}, force => 1 } )
457 if ( defined($request) && $params->{replace} );
459 my $transfer = Koha::Item::Transfer->new(
461 itemnumber => $self->itemnumber,
462 daterequested => dt_from_string,
463 frombranch => $self->holdingbranch,
464 tobranch => $params->{to}->branchcode,
465 reason => $params->{reason},
466 comments => $params->{comment}
475 my $transfer = $item->get_transfer;
477 Return the active transfer request or undef
479 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
480 whereby the most recently sent, but not received, transfer will be returned
481 if it exists, otherwise the oldest unsatisfied transfer will be returned.
483 This allows for transfers to queue, which is the case for stock rotation and
484 rotating collections where a manual transfer may need to take precedence but
485 we still expect the item to end up at a final location eventually.
491 my $transfer_rs = $self->_result->branchtransfers->search(
493 datearrived => undef,
494 datecancelled => undef
498 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
502 return unless $transfer_rs;
503 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
508 my $transfer = $item->get_transfers;
510 Return the list of outstanding transfers (i.e requested but not yet cancelled
513 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
514 whereby the most recently sent, but not received, transfer will be returned
515 first if it exists, otherwise requests are in oldest to newest request order.
517 This allows for transfers to queue, which is the case for stock rotation and
518 rotating collections where a manual transfer may need to take precedence but
519 we still expect the item to end up at a final location eventually.
525 my $transfer_rs = $self->_result->branchtransfers->search(
527 datearrived => undef,
528 datecancelled => undef
532 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
535 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
538 =head3 last_returned_by
540 Gets and sets the last borrower to return an item.
542 Accepts and returns Koha::Patron objects
544 $item->last_returned_by( $borrowernumber );
546 $last_returned_by = $item->last_returned_by();
550 sub last_returned_by {
551 my ( $self, $borrower ) = @_;
553 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
556 return $items_last_returned_by_rs->update_or_create(
557 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
560 unless ( $self->{_last_returned_by} ) {
561 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
563 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
567 return $self->{_last_returned_by};
571 =head3 can_article_request
573 my $bool = $item->can_article_request( $borrower )
575 Returns true if item can be specifically requested
577 $borrower must be a Koha::Patron object
581 sub can_article_request {
582 my ( $self, $borrower ) = @_;
584 my $rule = $self->article_request_type($borrower);
586 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
590 =head3 hidden_in_opac
592 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
594 Returns true if item fields match the hidding criteria defined in $rules.
595 Returns false otherwise.
597 Takes HASHref that can have the following parameters:
599 $rules : { <field> => [ value_1, ... ], ... }
601 Note: $rules inherits its structure from the parsed YAML from reading
602 the I<OpacHiddenItems> system preference.
607 my ( $self, $params ) = @_;
609 my $rules = $params->{rules} // {};
612 if C4::Context->preference('hidelostitems') and
615 my $hidden_in_opac = 0;
617 foreach my $field ( keys %{$rules} ) {
619 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
625 return $hidden_in_opac;
628 =head3 can_be_transferred
630 $item->can_be_transferred({ to => $to_library, from => $from_library })
631 Checks if an item can be transferred to given library.
633 This feature is controlled by two system preferences:
634 UseBranchTransferLimits to enable / disable the feature
635 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
636 for setting the limitations
638 Takes HASHref that can have the following parameters:
639 MANDATORY PARAMETERS:
642 $from : Koha::Library # if not given, item holdingbranch
643 # will be used instead
645 Returns 1 if item can be transferred to $to_library, otherwise 0.
647 To find out whether at least one item of a Koha::Biblio can be transferred, please
648 see Koha::Biblio->can_be_transferred() instead of using this method for
649 multiple items of the same biblio.
653 sub can_be_transferred {
654 my ($self, $params) = @_;
656 my $to = $params->{to};
657 my $from = $params->{from};
659 $to = $to->branchcode;
660 $from = defined $from ? $from->branchcode : $self->holdingbranch;
662 return 1 if $from eq $to; # Transfer to current branch is allowed
663 return 1 unless C4::Context->preference('UseBranchTransferLimits');
665 my $limittype = C4::Context->preference('BranchTransferLimitsType');
666 return Koha::Item::Transfer::Limits->search({
669 $limittype => $limittype eq 'itemtype'
670 ? $self->effective_itemtype : $self->ccode
675 =head3 pickup_locations
677 $pickup_locations = $item->pickup_locations( {patron => $patron } )
679 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)
680 and if item can be transferred to each pickup location.
684 sub pickup_locations {
685 my ($self, $params) = @_;
687 my $patron = $params->{patron};
689 my $circ_control_branch =
690 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
692 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
694 if(defined $patron) {
695 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
696 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
699 my $pickup_libraries = Koha::Libraries->search();
700 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
701 $pickup_libraries = $self->home_branch->get_hold_libraries;
702 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
703 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
704 $pickup_libraries = $plib->get_hold_libraries;
705 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
706 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
707 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
708 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
711 return $pickup_libraries->search(
716 order_by => ['branchname']
718 ) unless C4::Context->preference('UseBranchTransferLimits');
720 my $limittype = C4::Context->preference('BranchTransferLimitsType');
721 my ($ccode, $itype) = (undef, undef);
722 if( $limittype eq 'ccode' ){
723 $ccode = $self->ccode;
725 $itype = $self->itype;
727 my $limits = Koha::Item::Transfer::Limits->search(
729 fromBranch => $self->holdingbranch,
733 { columns => ['toBranch'] }
736 return $pickup_libraries->search(
738 pickup_location => 1,
740 '-not_in' => $limits->_resultset->as_query
744 order_by => ['branchname']
749 =head3 article_request_type
751 my $type = $item->article_request_type( $borrower )
753 returns 'yes', 'no', 'bib_only', or 'item_only'
755 $borrower must be a Koha::Patron object
759 sub article_request_type {
760 my ( $self, $borrower ) = @_;
762 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
764 $branch_control eq 'homebranch' ? $self->homebranch
765 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
767 my $borrowertype = $borrower->categorycode;
768 my $itemtype = $self->effective_itemtype();
769 my $rule = Koha::CirculationRules->get_effective_rule(
771 rule_name => 'article_requests',
772 categorycode => $borrowertype,
773 itemtype => $itemtype,
774 branchcode => $branchcode
778 return q{} unless $rule;
779 return $rule->rule_value || q{}
788 my $attributes = { order_by => 'priority' };
789 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
791 itemnumber => $self->itemnumber,
794 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
795 waitingdate => { '!=' => undef },
798 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
799 return Koha::Holds->_new_from_dbic($hold_rs);
802 =head3 stockrotationitem
804 my $sritem = Koha::Item->stockrotationitem;
806 Returns the stock rotation item associated with the current item.
810 sub stockrotationitem {
812 my $rs = $self->_result->stockrotationitem;
814 return Koha::StockRotationItem->_new_from_dbic( $rs );
819 my $item = $item->add_to_rota($rota_id);
821 Add this item to the rota identified by $ROTA_ID, which means associating it
822 with the first stage of that rota. Should this item already be associated
823 with a rota, then we will move it to the new rota.
828 my ( $self, $rota_id ) = @_;
829 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
833 =head3 has_pending_hold
835 my $is_pending_hold = $item->has_pending_hold();
837 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
841 sub has_pending_hold {
843 my $pending_hold = $self->_result->tmp_holdsqueues;
844 return $pending_hold->count ? 1: 0;
849 my $field = $item->as_marc_field;
851 This method returns a MARC::Field object representing the Koha::Item object
852 with the current mappings configuration.
859 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
861 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
865 my $item_field = $tagslib->{$itemtag};
867 my $more_subfields = $self->additional_attributes->to_hashref;
868 foreach my $subfield (
870 $a->{display_order} <=> $b->{display_order}
871 || $a->{subfield} cmp $b->{subfield}
872 } grep { ref($_) && %$_ } values %$item_field
875 my $kohafield = $subfield->{kohafield};
876 my $tagsubfield = $subfield->{tagsubfield};
878 if ( defined $kohafield ) {
879 next if $kohafield !~ m{^items\.}; # That would be weird!
880 ( my $attribute = $kohafield ) =~ s|^items\.||;
881 $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
882 if defined $self->$attribute and $self->$attribute ne '';
884 $value = $more_subfields->{$tagsubfield}
887 next unless defined $value
890 if ( $subfield->{repeatable} ) {
891 my @values = split '\|', $value;
892 push @subfields, ( $tagsubfield => $_ ) for @values;
895 push @subfields, ( $tagsubfield => $value );
900 return unless @subfields;
902 return MARC::Field->new(
903 "$itemtag", ' ', ' ', @subfields
907 =head3 renewal_branchcode
909 Returns the branchcode to be recorded in statistics renewal of the item
913 sub renewal_branchcode {
915 my ($self, $params ) = @_;
917 my $interface = C4::Context->interface;
919 if ( $interface eq 'opac' ){
920 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
921 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
922 $branchcode = 'OPACRenew';
924 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
925 $branchcode = $self->homebranch;
927 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
928 $branchcode = $self->checkout->patron->branchcode;
930 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
931 $branchcode = $self->checkout->branchcode;
937 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
938 ? C4::Context->userenv->{branch} : $params->{branch};
945 Return the cover images associated with this item.
952 my $cover_image_rs = $self->_result->cover_images;
953 return unless $cover_image_rs;
954 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
957 =head3 columns_to_str
959 my $values = $items->columns_to_str;
961 Return a hashref with the string representation of the different attribute of the item.
963 This is meant to be used for display purpose only.
970 my $frameworkcode = $self->biblio->frameworkcode;
971 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
972 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
974 my $columns_info = $self->_result->result_source->columns_info;
976 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
978 for my $column ( keys %$columns_info ) {
980 next if $column eq 'more_subfields_xml';
982 my $value = $self->$column;
983 # 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
985 if ( not defined $value or $value eq "" ) {
986 $values->{$column} = $value;
991 exists $mss->{"items.$column"}
992 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
997 ? $subfield->{authorised_value}
998 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
999 $subfield->{tagsubfield}, $value, '', $tagslib )
1005 $self->more_subfields_xml
1006 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1011 my ( $field ) = $marc_more->fields;
1012 for my $sf ( $field->subfields ) {
1013 my $subfield_code = $sf->[0];
1014 my $value = $sf->[1];
1015 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1016 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1018 $subfield->{authorised_value}
1019 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1020 $subfield->{tagsubfield}, $value, '', $tagslib )
1023 push @{$more_values->{$subfield_code}}, $value;
1026 while ( my ( $k, $v ) = each %$more_values ) {
1027 $values->{$k} = join ' | ', @$v;
1034 =head3 additional_attributes
1036 my $attributes = $item->additional_attributes;
1037 $attributes->{k} = 'new k';
1038 $item->update({ more_subfields => $attributes->to_marcxml });
1040 Returns a Koha::Item::Attributes object that represents the non-mapped
1041 attributes for this item.
1045 sub additional_attributes {
1048 return Koha::Item::Attributes->new_from_marcxml(
1049 $self->more_subfields_xml,
1053 =head3 _set_found_trigger
1055 $self->_set_found_trigger
1057 Finds the most recent lost item charge for this item and refunds the patron
1058 appropriately, taking into account any payments or writeoffs already applied
1061 Internal function, not exported, called only by Koha::Item->store.
1065 sub _set_found_trigger {
1066 my ( $self, $pre_mod_item ) = @_;
1068 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1069 my $no_refund_after_days =
1070 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1071 if ($no_refund_after_days) {
1072 my $today = dt_from_string();
1073 my $lost_age_in_days =
1074 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1077 return $self unless $lost_age_in_days < $no_refund_after_days;
1080 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1083 return_branch => C4::Context->userenv
1084 ? C4::Context->userenv->{'branch'}
1089 if ( $lostreturn_policy ) {
1091 # refund charge made for lost book
1092 my $lost_charge = Koha::Account::Lines->search(
1094 itemnumber => $self->itemnumber,
1095 debit_type_code => 'LOST',
1096 status => [ undef, { '<>' => 'FOUND' } ]
1099 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1104 if ( $lost_charge ) {
1106 my $patron = $lost_charge->patron;
1109 my $account = $patron->account;
1110 my $total_to_refund = 0;
1113 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1115 # some amount has been cancelled. collect the offsets that are not writeoffs
1116 # this works because the only way to subtract from this kind of a debt is
1117 # using the UI buttons 'Pay' and 'Write off'
1118 my $credit_offsets = $lost_charge->debit_offsets(
1120 'credit_id' => { '!=' => undef },
1121 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1123 { join => 'credit' }
1126 $total_to_refund = ( $credit_offsets->count > 0 )
1127 ? $credit_offsets->total * -1 # credits are negative on the DB
1131 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1134 if ( $credit_total > 0 ) {
1136 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1137 $credit = $account->add_credit(
1139 amount => $credit_total,
1140 description => 'Item found ' . $self->itemnumber,
1141 type => 'LOST_FOUND',
1142 interface => C4::Context->interface,
1143 library_id => $branchcode,
1144 item_id => $self->itemnumber,
1145 issue_id => $lost_charge->issue_id
1149 $credit->apply( { debits => [$lost_charge] } );
1150 $self->{_refunded} = 1;
1153 # Update the account status
1154 $lost_charge->status('FOUND');
1155 $lost_charge->store();
1157 # Reconcile balances if required
1158 if ( C4::Context->preference('AccountAutoReconcile') ) {
1159 $account->reconcile_balance;
1164 # restore fine for lost book
1165 if ( $lostreturn_policy eq 'restore' ) {
1166 my $lost_overdue = Koha::Account::Lines->search(
1168 itemnumber => $self->itemnumber,
1169 debit_type_code => 'OVERDUE',
1173 order_by => { '-desc' => 'date' },
1178 if ( $lost_overdue ) {
1180 my $patron = $lost_overdue->patron;
1182 my $account = $patron->account;
1184 # Update status of fine
1185 $lost_overdue->status('FOUND')->store();
1187 # Find related forgive credit
1188 my $refund = $lost_overdue->credits(
1190 credit_type_code => 'FORGIVEN',
1191 itemnumber => $self->itemnumber,
1192 status => [ { '!=' => 'VOID' }, undef ]
1194 { order_by => { '-desc' => 'date' }, rows => 1 }
1198 # Revert the forgive credit
1199 $refund->void({ interface => 'trigger' });
1200 $self->{_restored} = 1;
1203 # Reconcile balances if required
1204 if ( C4::Context->preference('AccountAutoReconcile') ) {
1205 $account->reconcile_balance;
1209 } elsif ( $lostreturn_policy eq 'charge' ) {
1210 $self->{_charge} = 1;
1217 =head3 public_read_list
1219 This method returns the list of publicly readable database fields for both API and UI output purposes
1223 sub public_read_list {
1225 'itemnumber', 'biblionumber', 'homebranch',
1226 'holdingbranch', 'location', 'collectioncode',
1227 'itemcallnumber', 'copynumber', 'enumchron',
1228 'barcode', 'dateaccessioned', 'itemnotes',
1229 'onloan', 'uri', 'itype',
1230 'notforloan', 'damaged', 'itemlost',
1231 'withdrawn', 'restricted'
1237 Overloaded to_api method to ensure item-level itypes is adhered to.
1242 my ($self, $params) = @_;
1244 my $response = $self->SUPER::to_api($params);
1247 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1249 return { %$response, %$overrides };
1252 =head3 to_api_mapping
1254 This method returns the mapping for representing a Koha::Item object
1259 sub to_api_mapping {
1261 itemnumber => 'item_id',
1262 biblionumber => 'biblio_id',
1263 biblioitemnumber => undef,
1264 barcode => 'external_id',
1265 dateaccessioned => 'acquisition_date',
1266 booksellerid => 'acquisition_source',
1267 homebranch => 'home_library_id',
1268 price => 'purchase_price',
1269 replacementprice => 'replacement_price',
1270 replacementpricedate => 'replacement_price_date',
1271 datelastborrowed => 'last_checkout_date',
1272 datelastseen => 'last_seen_date',
1274 notforloan => 'not_for_loan_status',
1275 damaged => 'damaged_status',
1276 damaged_on => 'damaged_date',
1277 itemlost => 'lost_status',
1278 itemlost_on => 'lost_date',
1279 withdrawn => 'withdrawn',
1280 withdrawn_on => 'withdrawn_date',
1281 itemcallnumber => 'callnumber',
1282 coded_location_qualifier => 'coded_location_qualifier',
1283 issues => 'checkouts_count',
1284 renewals => 'renewals_count',
1285 reserves => 'holds_count',
1286 restricted => 'restricted_status',
1287 itemnotes => 'public_notes',
1288 itemnotes_nonpublic => 'internal_notes',
1289 holdingbranch => 'holding_library_id',
1290 timestamp => 'timestamp',
1291 location => 'location',
1292 permanent_location => 'permanent_location',
1293 onloan => 'checked_out_date',
1294 cn_source => 'call_number_source',
1295 cn_sort => 'call_number_sort',
1296 ccode => 'collection_code',
1297 materials => 'materials_notes',
1299 itype => 'item_type_id',
1300 more_subfields_xml => 'extended_subfields',
1301 enumchron => 'serial_issue_number',
1302 copynumber => 'copy_number',
1303 stocknumber => 'inventory_number',
1304 new_status => 'new_status'
1310 my $itemtype = $item->itemtype;
1312 Returns Koha object for effective itemtype
1318 return Koha::ItemTypes->find( $self->effective_itemtype );
1323 my $orders = $item->orders();
1325 Returns a Koha::Acquisition::Orders object
1332 my $orders = $self->_result->item_orders;
1333 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1336 =head3 tracked_links
1338 my $tracked_links = $item->tracked_links();
1340 Returns a Koha::TrackedLinks object
1347 my $tracked_links = $self->_result->linktrackers;
1348 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1351 =head3 move_to_biblio
1353 $item->move_to_biblio($to_biblio[, $params]);
1355 Move the item to another biblio and update any references in other tables.
1357 The final optional parameter, C<$params>, is expected to contain the
1358 'skip_record_index' key, which is relayed down to Koha::Item->store.
1359 There it prevents calling index_records, which takes most of the
1360 time in batch adds/deletes. The caller must take care of calling
1361 index_records separately.
1364 skip_record_index => 1|0
1366 Returns undef if the move failed or the biblionumber of the destination record otherwise
1370 sub move_to_biblio {
1371 my ( $self, $to_biblio, $params ) = @_;
1375 return if $self->biblionumber == $to_biblio->biblionumber;
1377 my $from_biblionumber = $self->biblionumber;
1378 my $to_biblionumber = $to_biblio->biblionumber;
1380 # Own biblionumber and biblioitemnumber
1382 biblionumber => $to_biblionumber,
1383 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1384 })->store({ skip_record_index => $params->{skip_record_index} });
1386 unless ($params->{skip_record_index}) {
1387 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1388 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1391 # Acquisition orders
1392 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1395 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1397 # hold_fill_target (there's no Koha object available yet)
1398 my $hold_fill_target = $self->_result->hold_fill_target;
1399 if ($hold_fill_target) {
1400 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1403 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1404 # and can't even fake one since the significant columns are nullable.
1405 my $storage = $self->_result->result_source->storage;
1408 my ($storage, $dbh, @cols) = @_;
1410 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1415 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1417 return $to_biblionumber;
1420 =head2 Internal methods
1422 =head3 _after_item_action_hooks
1424 Helper method that takes care of calling all plugin hooks
1428 sub _after_item_action_hooks {
1429 my ( $self, $params ) = @_;
1431 my $action = $params->{action};
1433 Koha::Plugins->call(
1434 'after_item_action',
1438 item_id => $self->itemnumber,
1453 Kyle M Hall <kyle@bywatersolutions.com>