3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
23 use Data::Dumper qw( Dumper );
26 use Koha::DateUtils qw( dt_from_string output_pref );
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
35 use Koha::CirculationRules;
36 use Koha::CoverImages;
37 use Koha::SearchEngine::Indexer;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
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, "item " . Dumper( $self->unblessed ) );
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 $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
854 my $field = $item->as_marc_field({ [ mss => $mss ] });
856 This method returns a MARC::Field object representing the Koha::Item object
857 with the current mappings configuration.
862 my ( $self, $params ) = @_;
864 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
865 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
869 my @columns = $self->_result->result_source->columns;
871 foreach my $item_field ( @columns ) {
872 my $mapping = $mss->{ "items.$item_field"}[0];
873 my $tagfield = $mapping->{tagfield};
874 my $tagsubfield = $mapping->{tagsubfield};
875 next if !$tagfield; # TODO: Should we raise an exception instead?
876 # Feels like safe fallback is better
878 push @subfields, $tagsubfield => $self->$item_field
879 if defined $self->$item_field and $item_field ne '';
882 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
883 push( @subfields, @{$unlinked_item_subfields} )
884 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
888 $field = MARC::Field->new(
889 "$item_tag", ' ', ' ', @subfields
895 =head3 renewal_branchcode
897 Returns the branchcode to be recorded in statistics renewal of the item
901 sub renewal_branchcode {
903 my ($self, $params ) = @_;
905 my $interface = C4::Context->interface;
907 if ( $interface eq 'opac' ){
908 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
909 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
910 $branchcode = 'OPACRenew';
912 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
913 $branchcode = $self->homebranch;
915 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
916 $branchcode = $self->checkout->patron->branchcode;
918 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
919 $branchcode = $self->checkout->branchcode;
925 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
926 ? C4::Context->userenv->{branch} : $params->{branch};
933 Return the cover images associated with this item.
940 my $cover_image_rs = $self->_result->cover_images;
941 return unless $cover_image_rs;
942 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
945 =head3 columns_to_str
947 my $values = $items->columns_to_str;
949 Return a hashref with the string representation of the different attribute of the item.
951 This is meant to be used for display purpose only.
958 my $frameworkcode = $self->biblio->frameworkcode;
959 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
960 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
962 my $columns_info = $self->_result->result_source->columns_info;
964 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
966 for my $column ( keys %$columns_info ) {
968 next if $column eq 'more_subfields_xml';
970 my $value = $self->$column;
971 # 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
973 if ( not defined $value or $value eq "" ) {
974 $values->{$column} = $value;
979 exists $mss->{"items.$column"}
980 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
985 ? $subfield->{authorised_value}
986 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
987 $subfield->{tagsubfield}, $value, '', $tagslib )
993 $self->more_subfields_xml
994 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
999 my ( $field ) = $marc_more->fields;
1000 for my $sf ( $field->subfields ) {
1001 my $subfield_code = $sf->[0];
1002 my $value = $sf->[1];
1003 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1004 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1006 $subfield->{authorised_value}
1007 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1008 $subfield->{tagsubfield}, $value, '', $tagslib )
1011 push @{$more_values->{$subfield_code}}, $value;
1014 while ( my ( $k, $v ) = each %$more_values ) {
1015 $values->{$k} = join ' | ', @$v;
1022 =head3 _set_found_trigger
1024 $self->_set_found_trigger
1026 Finds the most recent lost item charge for this item and refunds the patron
1027 appropriately, taking into account any payments or writeoffs already applied
1030 Internal function, not exported, called only by Koha::Item->store.
1034 sub _set_found_trigger {
1035 my ( $self, $pre_mod_item ) = @_;
1037 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1038 my $no_refund_after_days =
1039 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1040 if ($no_refund_after_days) {
1041 my $today = dt_from_string();
1042 my $lost_age_in_days =
1043 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1046 return $self unless $lost_age_in_days < $no_refund_after_days;
1049 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1052 return_branch => C4::Context->userenv
1053 ? C4::Context->userenv->{'branch'}
1058 if ( $lostreturn_policy ) {
1060 # refund charge made for lost book
1061 my $lost_charge = Koha::Account::Lines->search(
1063 itemnumber => $self->itemnumber,
1064 debit_type_code => 'LOST',
1065 status => [ undef, { '<>' => 'FOUND' } ]
1068 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1073 if ( $lost_charge ) {
1075 my $patron = $lost_charge->patron;
1078 my $account = $patron->account;
1079 my $total_to_refund = 0;
1082 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1084 # some amount has been cancelled. collect the offsets that are not writeoffs
1085 # this works because the only way to subtract from this kind of a debt is
1086 # using the UI buttons 'Pay' and 'Write off'
1087 my $credit_offsets = $lost_charge->debit_offsets(
1089 'credit_id' => { '!=' => undef },
1090 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1092 { join => 'credit' }
1095 $total_to_refund = ( $credit_offsets->count > 0 )
1096 ? $credit_offsets->total * -1 # credits are negative on the DB
1100 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1103 if ( $credit_total > 0 ) {
1105 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1106 $credit = $account->add_credit(
1108 amount => $credit_total,
1109 description => 'Item found ' . $self->itemnumber,
1110 type => 'LOST_FOUND',
1111 interface => C4::Context->interface,
1112 library_id => $branchcode,
1113 item_id => $self->itemnumber,
1114 issue_id => $lost_charge->issue_id
1118 $credit->apply( { debits => [$lost_charge] } );
1119 $self->{_refunded} = 1;
1122 # Update the account status
1123 $lost_charge->status('FOUND');
1124 $lost_charge->store();
1126 # Reconcile balances if required
1127 if ( C4::Context->preference('AccountAutoReconcile') ) {
1128 $account->reconcile_balance;
1133 # restore fine for lost book
1134 if ( $lostreturn_policy eq 'restore' ) {
1135 my $lost_overdue = Koha::Account::Lines->search(
1137 itemnumber => $self->itemnumber,
1138 debit_type_code => 'OVERDUE',
1142 order_by => { '-desc' => 'date' },
1147 if ( $lost_overdue ) {
1149 my $patron = $lost_overdue->patron;
1151 my $account = $patron->account;
1153 # Update status of fine
1154 $lost_overdue->status('FOUND')->store();
1156 # Find related forgive credit
1157 my $refund = $lost_overdue->credits(
1159 credit_type_code => 'FORGIVEN',
1160 itemnumber => $self->itemnumber,
1161 status => [ { '!=' => 'VOID' }, undef ]
1163 { order_by => { '-desc' => 'date' }, rows => 1 }
1167 # Revert the forgive credit
1168 $refund->void({ interface => 'trigger' });
1169 $self->{_restored} = 1;
1172 # Reconcile balances if required
1173 if ( C4::Context->preference('AccountAutoReconcile') ) {
1174 $account->reconcile_balance;
1178 } elsif ( $lostreturn_policy eq 'charge' ) {
1179 $self->{_charge} = 1;
1186 =head3 to_api_mapping
1188 This method returns the mapping for representing a Koha::Item object
1193 sub to_api_mapping {
1195 itemnumber => 'item_id',
1196 biblionumber => 'biblio_id',
1197 biblioitemnumber => undef,
1198 barcode => 'external_id',
1199 dateaccessioned => 'acquisition_date',
1200 booksellerid => 'acquisition_source',
1201 homebranch => 'home_library_id',
1202 price => 'purchase_price',
1203 replacementprice => 'replacement_price',
1204 replacementpricedate => 'replacement_price_date',
1205 datelastborrowed => 'last_checkout_date',
1206 datelastseen => 'last_seen_date',
1208 notforloan => 'not_for_loan_status',
1209 damaged => 'damaged_status',
1210 damaged_on => 'damaged_date',
1211 itemlost => 'lost_status',
1212 itemlost_on => 'lost_date',
1213 withdrawn => 'withdrawn',
1214 withdrawn_on => 'withdrawn_date',
1215 itemcallnumber => 'callnumber',
1216 coded_location_qualifier => 'coded_location_qualifier',
1217 issues => 'checkouts_count',
1218 renewals => 'renewals_count',
1219 reserves => 'holds_count',
1220 restricted => 'restricted_status',
1221 itemnotes => 'public_notes',
1222 itemnotes_nonpublic => 'internal_notes',
1223 holdingbranch => 'holding_library_id',
1224 timestamp => 'timestamp',
1225 location => 'location',
1226 permanent_location => 'permanent_location',
1227 onloan => 'checked_out_date',
1228 cn_source => 'call_number_source',
1229 cn_sort => 'call_number_sort',
1230 ccode => 'collection_code',
1231 materials => 'materials_notes',
1233 itype => 'item_type',
1234 more_subfields_xml => 'extended_subfields',
1235 enumchron => 'serial_issue_number',
1236 copynumber => 'copy_number',
1237 stocknumber => 'inventory_number',
1238 new_status => 'new_status'
1244 my $itemtype = $item->itemtype;
1246 Returns Koha object for effective itemtype
1252 return Koha::ItemTypes->find( $self->effective_itemtype );
1257 my $orders = $item->orders();
1259 Returns a Koha::Acquisition::Orders object
1266 my $orders = $self->_result->item_orders;
1267 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1270 =head3 tracked_links
1272 my $tracked_links = $item->tracked_links();
1274 Returns a Koha::TrackedLinks object
1281 my $tracked_links = $self->_result->linktrackers;
1282 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1285 =head3 move_to_biblio
1287 $item->move_to_biblio($to_biblio[, $params]);
1289 Move the item to another biblio and update any references in other tables.
1291 The final optional parameter, C<$params>, is expected to contain the
1292 'skip_record_index' key, which is relayed down to Koha::Item->store.
1293 There it prevents calling index_records, which takes most of the
1294 time in batch adds/deletes. The caller must take care of calling
1295 index_records separately.
1298 skip_record_index => 1|0
1300 Returns undef if the move failed or the biblionumber of the destination record otherwise
1304 sub move_to_biblio {
1305 my ( $self, $to_biblio, $params ) = @_;
1309 return if $self->biblionumber == $to_biblio->biblionumber;
1311 my $from_biblionumber = $self->biblionumber;
1312 my $to_biblionumber = $to_biblio->biblionumber;
1314 # Own biblionumber and biblioitemnumber
1316 biblionumber => $to_biblionumber,
1317 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1318 })->store({ skip_record_index => $params->{skip_record_index} });
1320 unless ($params->{skip_record_index}) {
1321 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1322 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1325 # Acquisition orders
1326 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1329 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1331 # hold_fill_target (there's no Koha object available yet)
1332 my $hold_fill_target = $self->_result->hold_fill_target;
1333 if ($hold_fill_target) {
1334 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1337 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1338 # and can't even fake one since the significant columns are nullable.
1339 my $storage = $self->_result->result_source->storage;
1342 my ($storage, $dbh, @cols) = @_;
1344 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1349 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1351 return $to_biblionumber;
1354 =head2 Internal methods
1356 =head3 _after_item_action_hooks
1358 Helper method that takes care of calling all plugin hooks
1362 sub _after_item_action_hooks {
1363 my ( $self, $params ) = @_;
1365 my $action = $params->{action};
1367 Koha::Plugins->call(
1368 'after_item_action',
1372 item_id => $self->itemnumber,
1387 Kyle M Hall <kyle@bywatersolutions.com>