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)
67 You should not turn it on if you do not understand what it is doing exactly.
73 my $params = @_ ? shift : {};
75 my $log_action = $params->{log_action} // 1;
77 # We do not want to oblige callers to pass this value
78 # Dev conveniences vs performance?
79 unless ( $self->biblioitemnumber ) {
80 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
83 # See related changes from C4::Items::AddItem
84 unless ( $self->itype ) {
85 $self->itype($self->biblio->biblioitem->itemtype);
88 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
90 my $today = dt_from_string;
91 my $action = 'create';
93 unless ( $self->in_storage ) { #AddItem
95 unless ( $self->permanent_location ) {
96 $self->permanent_location($self->location);
99 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
100 unless ( $self->location || !$default_location ) {
101 $self->permanent_location( $self->location || $default_location )
102 unless $self->permanent_location;
103 $self->location($default_location);
106 unless ( $self->replacementpricedate ) {
107 $self->replacementpricedate($today);
109 unless ( $self->datelastseen ) {
110 $self->datelastseen($today);
113 unless ( $self->dateaccessioned ) {
114 $self->dateaccessioned($today);
117 if ( $self->itemcallnumber
118 or $self->cn_source )
120 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
121 $self->cn_sort($cn_sort);
128 my %updated_columns = $self->_result->get_dirty_columns;
129 return $self->SUPER::store unless %updated_columns;
131 # Retrieve the item for comparison if we need to
133 exists $updated_columns{itemlost}
134 or exists $updated_columns{withdrawn}
135 or exists $updated_columns{damaged}
136 ) ? $self->get_from_storage : undef;
138 # Update *_on fields if needed
139 # FIXME: Why not for AddItem as well?
140 my @fields = qw( itemlost withdrawn damaged );
141 for my $field (@fields) {
143 # If the field is defined but empty or 0, we are
144 # removing/unsetting and thus need to clear out
146 if ( exists $updated_columns{$field}
147 && defined( $self->$field )
150 my $field_on = "${field}_on";
151 $self->$field_on(undef);
153 # If the field has changed otherwise, we much update
155 elsif (exists $updated_columns{$field}
156 && $updated_columns{$field}
157 && !$pre_mod_item->$field )
159 my $field_on = "${field}_on";
161 DateTime::Format::MySQL->format_datetime(
168 if ( exists $updated_columns{itemcallnumber}
169 or exists $updated_columns{cn_source} )
171 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
172 $self->cn_sort($cn_sort);
176 if ( exists $updated_columns{location}
177 and $self->location ne 'CART'
178 and $self->location ne 'PROC'
179 and not exists $updated_columns{permanent_location} )
181 $self->permanent_location( $self->location );
184 # If item was lost and has now been found,
185 # reverse any list item charges if necessary.
186 if ( exists $updated_columns{itemlost}
187 and $updated_columns{itemlost} <= 0
188 and $pre_mod_item->itemlost > 0 )
190 $self->_set_found_trigger($pre_mod_item);
195 my $result = $self->SUPER::store;
196 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
198 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
199 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
201 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
202 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
203 unless $params->{skip_record_index};
204 $self->get_from_storage->_after_item_action_hooks({ action => $action });
215 my $params = @_ ? shift : {};
217 # FIXME check the item has no current issues
218 # i.e. raise the appropriate exception
220 my $result = $self->SUPER::delete;
222 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
223 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
224 unless $params->{skip_record_index};
226 $self->_after_item_action_hooks({ action => 'delete' });
228 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
229 if C4::Context->preference("CataloguingLog");
240 my $params = @_ ? shift : {};
242 my $safe_to_delete = $self->safe_to_delete;
243 return $safe_to_delete unless $safe_to_delete eq '1';
245 $self->move_to_deleted;
247 return $self->delete($params);
250 =head3 safe_to_delete
252 returns 1 if the item is safe to delete,
254 "book_on_loan" if the item is checked out,
256 "not_same_branch" if the item is blocked by independent branches,
258 "book_reserved" if the there are holds aganst the item, or
260 "linked_analytics" if the item has linked analytic records.
262 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
269 return "book_on_loan" if $self->checkout;
271 return "not_same_branch"
272 if defined C4::Context->userenv
273 and !C4::Context->IsSuperLibrarian()
274 and C4::Context->preference("IndependentBranches")
275 and ( C4::Context->userenv->{branch} ne $self->homebranch );
277 # check it doesn't have a waiting reserve
278 return "book_reserved"
279 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
281 return "linked_analytics"
282 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
284 return "last_item_for_hold"
285 if $self->biblio->items->count == 1
286 && $self->biblio->holds->search(
295 =head3 move_to_deleted
297 my $is_moved = $item->move_to_deleted;
299 Move an item to the deleteditems table.
300 This can be done before deleting an item, to make sure the data are not completely deleted.
304 sub move_to_deleted {
306 my $item_infos = $self->unblessed;
307 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
308 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
312 =head3 effective_itemtype
314 Returns the itemtype for the item based on whether item level itemtypes are set or not.
318 sub effective_itemtype {
321 return $self->_result()->effective_itemtype();
331 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
333 return $self->{_home_branch};
336 =head3 holding_branch
343 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
345 return $self->{_holding_branch};
350 my $biblio = $item->biblio;
352 Return the bibliographic record of this item
358 my $biblio_rs = $self->_result->biblio;
359 return Koha::Biblio->_new_from_dbic( $biblio_rs );
364 my $biblioitem = $item->biblioitem;
366 Return the biblioitem record of this item
372 my $biblioitem_rs = $self->_result->biblioitem;
373 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
378 my $checkout = $item->checkout;
380 Return the checkout for this item
386 my $checkout_rs = $self->_result->issue;
387 return unless $checkout_rs;
388 return Koha::Checkout->_new_from_dbic( $checkout_rs );
393 my $holds = $item->holds();
394 my $holds = $item->holds($params);
395 my $holds = $item->holds({ found => 'W'});
397 Return holds attached to an item, optionally accept a hashref of params to pass to search
402 my ( $self,$params ) = @_;
403 my $holds_rs = $self->_result->reserves->search($params);
404 return Koha::Holds->_new_from_dbic( $holds_rs );
407 =head3 request_transfer
409 my $transfer = $item->request_transfer(
413 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
417 Add a transfer request for this item to the given branch for the given reason.
419 An exception will be thrown if the BranchTransferLimits would prevent the requested
420 transfer, unless 'ignore_limits' is passed to override the limits.
422 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
423 The caller should catch such cases and retry the transfer request as appropriate passing
424 an appropriate override.
427 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
428 * replace - Used to replace the existing transfer request with your own.
432 sub request_transfer {
433 my ( $self, $params ) = @_;
435 # check for mandatory params
436 my @mandatory = ( 'to', 'reason' );
437 for my $param (@mandatory) {
438 unless ( defined( $params->{$param} ) ) {
439 Koha::Exceptions::MissingParameter->throw(
440 error => "The $param parameter is mandatory" );
444 Koha::Exceptions::Item::Transfer::Limit->throw()
445 unless ( $params->{ignore_limits}
446 || $self->can_be_transferred( { to => $params->{to} } ) );
448 my $request = $self->get_transfer;
449 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
450 if ( $request && !$params->{enqueue} && !$params->{replace} );
452 $request->cancel( { reason => $params->{reason}, force => 1 } )
453 if ( defined($request) && $params->{replace} );
455 my $transfer = Koha::Item::Transfer->new(
457 itemnumber => $self->itemnumber,
458 daterequested => dt_from_string,
459 frombranch => $self->holdingbranch,
460 tobranch => $params->{to}->branchcode,
461 reason => $params->{reason},
462 comments => $params->{comment}
471 my $transfer = $item->get_transfer;
473 Return the active transfer request or undef
475 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
476 whereby the most recently sent, but not received, transfer will be returned
477 if it exists, otherwise the oldest unsatisfied transfer will be returned.
479 This allows for transfers to queue, which is the case for stock rotation and
480 rotating collections where a manual transfer may need to take precedence but
481 we still expect the item to end up at a final location eventually.
487 my $transfer_rs = $self->_result->branchtransfers->search(
489 datearrived => undef,
490 datecancelled => undef
494 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
498 return unless $transfer_rs;
499 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
504 my $transfer = $item->get_transfers;
506 Return the list of outstanding transfers (i.e requested but not yet cancelled
509 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
510 whereby the most recently sent, but not received, transfer will be returned
511 first if it exists, otherwise requests are in oldest to newest request order.
513 This allows for transfers to queue, which is the case for stock rotation and
514 rotating collections where a manual transfer may need to take precedence but
515 we still expect the item to end up at a final location eventually.
521 my $transfer_rs = $self->_result->branchtransfers->search(
523 datearrived => undef,
524 datecancelled => undef
528 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
531 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
534 =head3 last_returned_by
536 Gets and sets the last borrower to return an item.
538 Accepts and returns Koha::Patron objects
540 $item->last_returned_by( $borrowernumber );
542 $last_returned_by = $item->last_returned_by();
546 sub last_returned_by {
547 my ( $self, $borrower ) = @_;
549 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
552 return $items_last_returned_by_rs->update_or_create(
553 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
556 unless ( $self->{_last_returned_by} ) {
557 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
559 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
563 return $self->{_last_returned_by};
567 =head3 can_article_request
569 my $bool = $item->can_article_request( $borrower )
571 Returns true if item can be specifically requested
573 $borrower must be a Koha::Patron object
577 sub can_article_request {
578 my ( $self, $borrower ) = @_;
580 my $rule = $self->article_request_type($borrower);
582 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
586 =head3 hidden_in_opac
588 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
590 Returns true if item fields match the hidding criteria defined in $rules.
591 Returns false otherwise.
593 Takes HASHref that can have the following parameters:
595 $rules : { <field> => [ value_1, ... ], ... }
597 Note: $rules inherits its structure from the parsed YAML from reading
598 the I<OpacHiddenItems> system preference.
603 my ( $self, $params ) = @_;
605 my $rules = $params->{rules} // {};
608 if C4::Context->preference('hidelostitems') and
611 my $hidden_in_opac = 0;
613 foreach my $field ( keys %{$rules} ) {
615 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
621 return $hidden_in_opac;
624 =head3 can_be_transferred
626 $item->can_be_transferred({ to => $to_library, from => $from_library })
627 Checks if an item can be transferred to given library.
629 This feature is controlled by two system preferences:
630 UseBranchTransferLimits to enable / disable the feature
631 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
632 for setting the limitations
634 Takes HASHref that can have the following parameters:
635 MANDATORY PARAMETERS:
638 $from : Koha::Library # if not given, item holdingbranch
639 # will be used instead
641 Returns 1 if item can be transferred to $to_library, otherwise 0.
643 To find out whether at least one item of a Koha::Biblio can be transferred, please
644 see Koha::Biblio->can_be_transferred() instead of using this method for
645 multiple items of the same biblio.
649 sub can_be_transferred {
650 my ($self, $params) = @_;
652 my $to = $params->{to};
653 my $from = $params->{from};
655 $to = $to->branchcode;
656 $from = defined $from ? $from->branchcode : $self->holdingbranch;
658 return 1 if $from eq $to; # Transfer to current branch is allowed
659 return 1 unless C4::Context->preference('UseBranchTransferLimits');
661 my $limittype = C4::Context->preference('BranchTransferLimitsType');
662 return Koha::Item::Transfer::Limits->search({
665 $limittype => $limittype eq 'itemtype'
666 ? $self->effective_itemtype : $self->ccode
671 =head3 pickup_locations
673 $pickup_locations = $item->pickup_locations( {patron => $patron } )
675 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)
676 and if item can be transferred to each pickup location.
680 sub pickup_locations {
681 my ($self, $params) = @_;
683 my $patron = $params->{patron};
685 my $circ_control_branch =
686 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
688 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
690 if(defined $patron) {
691 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
692 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
695 my $pickup_libraries = Koha::Libraries->search();
696 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
697 $pickup_libraries = $self->home_branch->get_hold_libraries;
698 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
699 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
700 $pickup_libraries = $plib->get_hold_libraries;
701 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
702 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
703 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
704 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
707 return $pickup_libraries->search(
712 order_by => ['branchname']
714 ) unless C4::Context->preference('UseBranchTransferLimits');
716 my $limittype = C4::Context->preference('BranchTransferLimitsType');
717 my ($ccode, $itype) = (undef, undef);
718 if( $limittype eq 'ccode' ){
719 $ccode = $self->ccode;
721 $itype = $self->itype;
723 my $limits = Koha::Item::Transfer::Limits->search(
725 fromBranch => $self->holdingbranch,
729 { columns => ['toBranch'] }
732 return $pickup_libraries->search(
734 pickup_location => 1,
736 '-not_in' => $limits->_resultset->as_query
740 order_by => ['branchname']
745 =head3 article_request_type
747 my $type = $item->article_request_type( $borrower )
749 returns 'yes', 'no', 'bib_only', or 'item_only'
751 $borrower must be a Koha::Patron object
755 sub article_request_type {
756 my ( $self, $borrower ) = @_;
758 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
760 $branch_control eq 'homebranch' ? $self->homebranch
761 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
763 my $borrowertype = $borrower->categorycode;
764 my $itemtype = $self->effective_itemtype();
765 my $rule = Koha::CirculationRules->get_effective_rule(
767 rule_name => 'article_requests',
768 categorycode => $borrowertype,
769 itemtype => $itemtype,
770 branchcode => $branchcode
774 return q{} unless $rule;
775 return $rule->rule_value || q{}
784 my $attributes = { order_by => 'priority' };
785 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
787 itemnumber => $self->itemnumber,
790 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
791 waitingdate => { '!=' => undef },
794 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
795 return Koha::Holds->_new_from_dbic($hold_rs);
798 =head3 stockrotationitem
800 my $sritem = Koha::Item->stockrotationitem;
802 Returns the stock rotation item associated with the current item.
806 sub stockrotationitem {
808 my $rs = $self->_result->stockrotationitem;
810 return Koha::StockRotationItem->_new_from_dbic( $rs );
815 my $item = $item->add_to_rota($rota_id);
817 Add this item to the rota identified by $ROTA_ID, which means associating it
818 with the first stage of that rota. Should this item already be associated
819 with a rota, then we will move it to the new rota.
824 my ( $self, $rota_id ) = @_;
825 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
829 =head3 has_pending_hold
831 my $is_pending_hold = $item->has_pending_hold();
833 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
837 sub has_pending_hold {
839 my $pending_hold = $self->_result->tmp_holdsqueues;
840 return $pending_hold->count ? 1: 0;
845 my $field = $item->as_marc_field;
847 This method returns a MARC::Field object representing the Koha::Item object
848 with the current mappings configuration.
855 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
857 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
861 my $item_field = $tagslib->{$itemtag};
863 my $more_subfields = $self->additional_attributes->to_hashref;
864 foreach my $subfield (
866 $a->{display_order} <=> $b->{display_order}
867 || $a->{subfield} cmp $b->{subfield}
868 } grep { ref($_) && %$_ } values %$item_field
871 my $kohafield = $subfield->{kohafield};
872 my $tagsubfield = $subfield->{tagsubfield};
874 if ( defined $kohafield && $kohafield ne '' ) {
875 next if $kohafield !~ m{^items\.}; # That would be weird!
876 ( my $attribute = $kohafield ) =~ s|^items\.||;
877 $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
878 if defined $self->$attribute and $self->$attribute ne '';
880 $value = $more_subfields->{$tagsubfield}
883 next unless defined $value
886 if ( $subfield->{repeatable} ) {
887 my @values = split '\|', $value;
888 push @subfields, ( $tagsubfield => $_ ) for @values;
891 push @subfields, ( $tagsubfield => $value );
896 return unless @subfields;
898 return MARC::Field->new(
899 "$itemtag", ' ', ' ', @subfields
903 =head3 renewal_branchcode
905 Returns the branchcode to be recorded in statistics renewal of the item
909 sub renewal_branchcode {
911 my ($self, $params ) = @_;
913 my $interface = C4::Context->interface;
915 if ( $interface eq 'opac' ){
916 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
917 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
918 $branchcode = 'OPACRenew';
920 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
921 $branchcode = $self->homebranch;
923 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
924 $branchcode = $self->checkout->patron->branchcode;
926 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
927 $branchcode = $self->checkout->branchcode;
933 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
934 ? C4::Context->userenv->{branch} : $params->{branch};
941 Return the cover images associated with this item.
948 my $cover_image_rs = $self->_result->cover_images;
949 return unless $cover_image_rs;
950 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
953 =head3 columns_to_str
955 my $values = $items->columns_to_str;
957 Return a hashref with the string representation of the different attribute of the item.
959 This is meant to be used for display purpose only.
966 my $frameworkcode = $self->biblio->frameworkcode;
967 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
968 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
970 my $columns_info = $self->_result->result_source->columns_info;
972 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
974 for my $column ( keys %$columns_info ) {
976 next if $column eq 'more_subfields_xml';
978 my $value = $self->$column;
979 # 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
981 if ( not defined $value or $value eq "" ) {
982 $values->{$column} = $value;
987 exists $mss->{"items.$column"}
988 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
993 ? $subfield->{authorised_value}
994 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
995 $subfield->{tagsubfield}, $value, '', $tagslib )
1001 $self->more_subfields_xml
1002 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1007 my ( $field ) = $marc_more->fields;
1008 for my $sf ( $field->subfields ) {
1009 my $subfield_code = $sf->[0];
1010 my $value = $sf->[1];
1011 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1012 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1014 $subfield->{authorised_value}
1015 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1016 $subfield->{tagsubfield}, $value, '', $tagslib )
1019 push @{$more_values->{$subfield_code}}, $value;
1022 while ( my ( $k, $v ) = each %$more_values ) {
1023 $values->{$k} = join ' | ', @$v;
1030 =head3 additional_attributes
1032 my $attributes = $item->additional_attributes;
1033 $attributes->{k} = 'new k';
1034 $item->update({ more_subfields => $attributes->to_marcxml });
1036 Returns a Koha::Item::Attributes object that represents the non-mapped
1037 attributes for this item.
1041 sub additional_attributes {
1044 return Koha::Item::Attributes->new_from_marcxml(
1045 $self->more_subfields_xml,
1049 =head3 _set_found_trigger
1051 $self->_set_found_trigger
1053 Finds the most recent lost item charge for this item and refunds the patron
1054 appropriately, taking into account any payments or writeoffs already applied
1057 Internal function, not exported, called only by Koha::Item->store.
1061 sub _set_found_trigger {
1062 my ( $self, $pre_mod_item ) = @_;
1064 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1065 my $no_refund_after_days =
1066 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1067 if ($no_refund_after_days) {
1068 my $today = dt_from_string();
1069 my $lost_age_in_days =
1070 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1073 return $self unless $lost_age_in_days < $no_refund_after_days;
1076 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1079 return_branch => C4::Context->userenv
1080 ? C4::Context->userenv->{'branch'}
1085 if ( $lostreturn_policy ) {
1087 # refund charge made for lost book
1088 my $lost_charge = Koha::Account::Lines->search(
1090 itemnumber => $self->itemnumber,
1091 debit_type_code => 'LOST',
1092 status => [ undef, { '<>' => 'FOUND' } ]
1095 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1100 if ( $lost_charge ) {
1102 my $patron = $lost_charge->patron;
1105 my $account = $patron->account;
1106 my $total_to_refund = 0;
1109 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1111 # some amount has been cancelled. collect the offsets that are not writeoffs
1112 # this works because the only way to subtract from this kind of a debt is
1113 # using the UI buttons 'Pay' and 'Write off'
1114 my $credit_offsets = $lost_charge->debit_offsets(
1116 'credit_id' => { '!=' => undef },
1117 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1119 { join => 'credit' }
1122 $total_to_refund = ( $credit_offsets->count > 0 )
1123 ? $credit_offsets->total * -1 # credits are negative on the DB
1127 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1130 if ( $credit_total > 0 ) {
1132 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1133 $credit = $account->add_credit(
1135 amount => $credit_total,
1136 description => 'Item found ' . $self->itemnumber,
1137 type => 'LOST_FOUND',
1138 interface => C4::Context->interface,
1139 library_id => $branchcode,
1140 item_id => $self->itemnumber,
1141 issue_id => $lost_charge->issue_id
1145 $credit->apply( { debits => [$lost_charge] } );
1146 $self->{_refunded} = 1;
1149 # Update the account status
1150 $lost_charge->status('FOUND');
1151 $lost_charge->store();
1153 # Reconcile balances if required
1154 if ( C4::Context->preference('AccountAutoReconcile') ) {
1155 $account->reconcile_balance;
1160 # restore fine for lost book
1161 if ( $lostreturn_policy eq 'restore' ) {
1162 my $lost_overdue = Koha::Account::Lines->search(
1164 itemnumber => $self->itemnumber,
1165 debit_type_code => 'OVERDUE',
1169 order_by => { '-desc' => 'date' },
1174 if ( $lost_overdue ) {
1176 my $patron = $lost_overdue->patron;
1178 my $account = $patron->account;
1180 # Update status of fine
1181 $lost_overdue->status('FOUND')->store();
1183 # Find related forgive credit
1184 my $refund = $lost_overdue->credits(
1186 credit_type_code => 'FORGIVEN',
1187 itemnumber => $self->itemnumber,
1188 status => [ { '!=' => 'VOID' }, undef ]
1190 { order_by => { '-desc' => 'date' }, rows => 1 }
1194 # Revert the forgive credit
1195 $refund->void({ interface => 'trigger' });
1196 $self->{_restored} = 1;
1199 # Reconcile balances if required
1200 if ( C4::Context->preference('AccountAutoReconcile') ) {
1201 $account->reconcile_balance;
1205 } elsif ( $lostreturn_policy eq 'charge' ) {
1206 $self->{_charge} = 1;
1213 =head3 public_read_list
1215 This method returns the list of publicly readable database fields for both API and UI output purposes
1219 sub public_read_list {
1221 'itemnumber', 'biblionumber', 'homebranch',
1222 'holdingbranch', 'location', 'collectioncode',
1223 'itemcallnumber', 'copynumber', 'enumchron',
1224 'barcode', 'dateaccessioned', 'itemnotes',
1225 'onloan', 'uri', 'itype',
1226 'notforloan', 'damaged', 'itemlost',
1227 'withdrawn', 'restricted'
1233 Overloaded to_api method to ensure item-level itypes is adhered to.
1238 my ($self, $params) = @_;
1240 my $response = $self->SUPER::to_api($params);
1243 $overrides->{effective_item_type_id} = $self->effective_itemtype;
1245 return { %$response, %$overrides };
1248 =head3 to_api_mapping
1250 This method returns the mapping for representing a Koha::Item object
1255 sub to_api_mapping {
1257 itemnumber => 'item_id',
1258 biblionumber => 'biblio_id',
1259 biblioitemnumber => undef,
1260 barcode => 'external_id',
1261 dateaccessioned => 'acquisition_date',
1262 booksellerid => 'acquisition_source',
1263 homebranch => 'home_library_id',
1264 price => 'purchase_price',
1265 replacementprice => 'replacement_price',
1266 replacementpricedate => 'replacement_price_date',
1267 datelastborrowed => 'last_checkout_date',
1268 datelastseen => 'last_seen_date',
1270 notforloan => 'not_for_loan_status',
1271 damaged => 'damaged_status',
1272 damaged_on => 'damaged_date',
1273 itemlost => 'lost_status',
1274 itemlost_on => 'lost_date',
1275 withdrawn => 'withdrawn',
1276 withdrawn_on => 'withdrawn_date',
1277 itemcallnumber => 'callnumber',
1278 coded_location_qualifier => 'coded_location_qualifier',
1279 issues => 'checkouts_count',
1280 renewals => 'renewals_count',
1281 reserves => 'holds_count',
1282 restricted => 'restricted_status',
1283 itemnotes => 'public_notes',
1284 itemnotes_nonpublic => 'internal_notes',
1285 holdingbranch => 'holding_library_id',
1286 timestamp => 'timestamp',
1287 location => 'location',
1288 permanent_location => 'permanent_location',
1289 onloan => 'checked_out_date',
1290 cn_source => 'call_number_source',
1291 cn_sort => 'call_number_sort',
1292 ccode => 'collection_code',
1293 materials => 'materials_notes',
1295 itype => 'item_type_id',
1296 more_subfields_xml => 'extended_subfields',
1297 enumchron => 'serial_issue_number',
1298 copynumber => 'copy_number',
1299 stocknumber => 'inventory_number',
1300 new_status => 'new_status'
1306 my $itemtype = $item->itemtype;
1308 Returns Koha object for effective itemtype
1314 return Koha::ItemTypes->find( $self->effective_itemtype );
1319 my $orders = $item->orders();
1321 Returns a Koha::Acquisition::Orders object
1328 my $orders = $self->_result->item_orders;
1329 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1332 =head3 tracked_links
1334 my $tracked_links = $item->tracked_links();
1336 Returns a Koha::TrackedLinks object
1343 my $tracked_links = $self->_result->linktrackers;
1344 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1347 =head3 move_to_biblio
1349 $item->move_to_biblio($to_biblio[, $params]);
1351 Move the item to another biblio and update any references in other tables.
1353 The final optional parameter, C<$params>, is expected to contain the
1354 'skip_record_index' key, which is relayed down to Koha::Item->store.
1355 There it prevents calling index_records, which takes most of the
1356 time in batch adds/deletes. The caller must take care of calling
1357 index_records separately.
1360 skip_record_index => 1|0
1362 Returns undef if the move failed or the biblionumber of the destination record otherwise
1366 sub move_to_biblio {
1367 my ( $self, $to_biblio, $params ) = @_;
1371 return if $self->biblionumber == $to_biblio->biblionumber;
1373 my $from_biblionumber = $self->biblionumber;
1374 my $to_biblionumber = $to_biblio->biblionumber;
1376 # Own biblionumber and biblioitemnumber
1378 biblionumber => $to_biblionumber,
1379 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1380 })->store({ skip_record_index => $params->{skip_record_index} });
1382 unless ($params->{skip_record_index}) {
1383 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1384 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1387 # Acquisition orders
1388 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1391 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1393 # hold_fill_target (there's no Koha object available yet)
1394 my $hold_fill_target = $self->_result->hold_fill_target;
1395 if ($hold_fill_target) {
1396 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1399 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1400 # and can't even fake one since the significant columns are nullable.
1401 my $storage = $self->_result->result_source->storage;
1404 my ($storage, $dbh, @cols) = @_;
1406 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1411 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1413 return $to_biblionumber;
1416 =head2 Internal methods
1418 =head3 _after_item_action_hooks
1420 Helper method that takes care of calling all plugin hooks
1424 sub _after_item_action_hooks {
1425 my ( $self, $params ) = @_;
1427 my $action = $params->{action};
1429 Koha::Plugins->call(
1430 'after_item_action',
1434 item_id => $self->itemnumber,
1449 Kyle M Hall <kyle@bywatersolutions.com>