Bug 25548: Remove Apache rewrite directives that trigger redirects
[koha.git] / Koha / Item.pm
blob10d4eb2c4816fdf54e02e1b347b31b02a72bb9c2
1 package Koha::Item;
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>.
20 use Modern::Perl;
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
36 use Koha::Checkouts;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::SearchEngine::Indexer;
40 use Koha::Item::Transfer::Limits;
41 use Koha::Item::Transfers;
42 use Koha::ItemTypes;
43 use Koha::Patrons;
44 use Koha::Plugins;
45 use Koha::Libraries;
46 use Koha::StockRotationItem;
47 use Koha::StockRotationRotas;
49 use base qw(Koha::Object);
51 =head1 NAME
53 Koha::Item - Koha Item object class
55 =head1 API
57 =head2 Class methods
59 =cut
61 =head3 store
63 $item->store;
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.
73 =cut
75 sub store {
76 my $self = shift;
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 my $today = dt_from_string;
93 my $plugin_action = 'create';
95 unless ( $self->in_storage ) { #AddItem
96 unless ( $self->permanent_location ) {
97 $self->permanent_location($self->location);
99 unless ( $self->replacementpricedate ) {
100 $self->replacementpricedate($today);
102 unless ( $self->datelastseen ) {
103 $self->datelastseen($today);
106 unless ( $self->dateaccessioned ) {
107 $self->dateaccessioned($today);
110 if ( $self->itemcallnumber
111 or $self->cn_source )
113 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
114 $self->cn_sort($cn_sort);
117 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
118 if $log_action && C4::Context->preference("CataloguingLog");
120 } else { # ModItem
122 $plugin_action = 'modify';
124 my %updated_columns = $self->_result->get_dirty_columns;
125 return $self->SUPER::store unless %updated_columns;
127 # Retrieve the item for comparison if we need to
128 my $pre_mod_item = (
129 exists $updated_columns{itemlost}
130 or exists $updated_columns{withdrawn}
131 or exists $updated_columns{damaged}
132 ) ? $self->get_from_storage : undef;
134 # Update *_on fields if needed
135 # FIXME: Why not for AddItem as well?
136 my @fields = qw( itemlost withdrawn damaged );
137 for my $field (@fields) {
139 # If the field is defined but empty or 0, we are
140 # removing/unsetting and thus need to clear out
141 # the 'on' field
142 if ( exists $updated_columns{$field}
143 && defined( $self->$field )
144 && !$self->$field )
146 my $field_on = "${field}_on";
147 $self->$field_on(undef);
149 # If the field has changed otherwise, we much update
150 # the 'on' field
151 elsif (exists $updated_columns{$field}
152 && $updated_columns{$field}
153 && !$pre_mod_item->$field )
155 my $field_on = "${field}_on";
156 $self->$field_on(
157 DateTime::Format::MySQL->format_datetime(
158 dt_from_string()
164 if ( exists $updated_columns{itemcallnumber}
165 or exists $updated_columns{cn_source} )
167 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
168 $self->cn_sort($cn_sort);
172 if ( exists $updated_columns{location}
173 and $self->location ne 'CART'
174 and $self->location ne 'PROC'
175 and not exists $updated_columns{permanent_location} )
177 $self->permanent_location( $self->location );
180 # If item was lost and has now been found,
181 # reverse any list item charges if necessary.
182 if ( exists $updated_columns{itemlost}
183 and $updated_columns{itemlost} <= 0
184 and $pre_mod_item->itemlost > 0 )
186 $self->_set_found_trigger($pre_mod_item);
189 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
190 if $log_action && C4::Context->preference("CataloguingLog");
193 unless ( $self->dateaccessioned ) {
194 $self->dateaccessioned($today);
197 my $result = $self->SUPER::store;
198 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
199 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
200 unless $params->{skip_record_index};
201 $self->get_from_storage->_after_item_action_hooks({ action => $plugin_action });
203 return $result;
206 =head3 delete
208 =cut
210 sub delete {
211 my $self = shift;
212 my $params = @_ ? shift : {};
214 # FIXME check the item has no current issues
215 # i.e. raise the appropriate exception
217 my $result = $self->SUPER::delete;
219 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
220 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
221 unless $params->{skip_record_index};
223 $self->_after_item_action_hooks({ action => 'delete' });
225 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
226 if C4::Context->preference("CataloguingLog");
228 return $result;
231 =head3 safe_delete
233 =cut
235 sub safe_delete {
236 my $self = shift;
237 my $params = @_ ? shift : {};
239 my $safe_to_delete = $self->safe_to_delete;
240 return $safe_to_delete unless $safe_to_delete eq '1';
242 $self->move_to_deleted;
244 return $self->delete($params);
247 =head3 safe_to_delete
249 returns 1 if the item is safe to delete,
251 "book_on_loan" if the item is checked out,
253 "not_same_branch" if the item is blocked by independent branches,
255 "book_reserved" if the there are holds aganst the item, or
257 "linked_analytics" if the item has linked analytic records.
259 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
261 =cut
263 sub safe_to_delete {
264 my ($self) = @_;
266 return "book_on_loan" if $self->checkout;
268 return "not_same_branch"
269 if defined C4::Context->userenv
270 and !C4::Context->IsSuperLibrarian()
271 and C4::Context->preference("IndependentBranches")
272 and ( C4::Context->userenv->{branch} ne $self->homebranch );
274 # check it doesn't have a waiting reserve
275 return "book_reserved"
276 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
278 return "linked_analytics"
279 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
281 return "last_item_for_hold"
282 if $self->biblio->items->count == 1
283 && $self->biblio->holds->search(
285 itemnumber => undef,
287 )->count;
289 return 1;
292 =head3 move_to_deleted
294 my $is_moved = $item->move_to_deleted;
296 Move an item to the deleteditems table.
297 This can be done before deleting an item, to make sure the data are not completely deleted.
299 =cut
301 sub move_to_deleted {
302 my ($self) = @_;
303 my $item_infos = $self->unblessed;
304 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
305 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
309 =head3 effective_itemtype
311 Returns the itemtype for the item based on whether item level itemtypes are set or not.
313 =cut
315 sub effective_itemtype {
316 my ( $self ) = @_;
318 return $self->_result()->effective_itemtype();
321 =head3 home_branch
323 =cut
325 sub home_branch {
326 my ($self) = @_;
328 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
330 return $self->{_home_branch};
333 =head3 holding_branch
335 =cut
337 sub holding_branch {
338 my ($self) = @_;
340 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
342 return $self->{_holding_branch};
345 =head3 biblio
347 my $biblio = $item->biblio;
349 Return the bibliographic record of this item
351 =cut
353 sub biblio {
354 my ( $self ) = @_;
355 my $biblio_rs = $self->_result->biblio;
356 return Koha::Biblio->_new_from_dbic( $biblio_rs );
359 =head3 biblioitem
361 my $biblioitem = $item->biblioitem;
363 Return the biblioitem record of this item
365 =cut
367 sub biblioitem {
368 my ( $self ) = @_;
369 my $biblioitem_rs = $self->_result->biblioitem;
370 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
373 =head3 checkout
375 my $checkout = $item->checkout;
377 Return the checkout for this item
379 =cut
381 sub checkout {
382 my ( $self ) = @_;
383 my $checkout_rs = $self->_result->issue;
384 return unless $checkout_rs;
385 return Koha::Checkout->_new_from_dbic( $checkout_rs );
388 =head3 holds
390 my $holds = $item->holds();
391 my $holds = $item->holds($params);
392 my $holds = $item->holds({ found => 'W'});
394 Return holds attached to an item, optionally accept a hashref of params to pass to search
396 =cut
398 sub holds {
399 my ( $self,$params ) = @_;
400 my $holds_rs = $self->_result->reserves->search($params);
401 return Koha::Holds->_new_from_dbic( $holds_rs );
404 =head3 get_transfer
406 my $transfer = $item->get_transfer;
408 Return the transfer if the item is in transit or undef
410 =cut
412 sub get_transfer {
413 my ( $self ) = @_;
414 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
415 return unless $transfer_rs;
416 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
419 =head3 last_returned_by
421 Gets and sets the last borrower to return an item.
423 Accepts and returns Koha::Patron objects
425 $item->last_returned_by( $borrowernumber );
427 $last_returned_by = $item->last_returned_by();
429 =cut
431 sub last_returned_by {
432 my ( $self, $borrower ) = @_;
434 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
436 if ($borrower) {
437 return $items_last_returned_by_rs->update_or_create(
438 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
440 else {
441 unless ( $self->{_last_returned_by} ) {
442 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
443 if ($result) {
444 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
448 return $self->{_last_returned_by};
452 =head3 can_article_request
454 my $bool = $item->can_article_request( $borrower )
456 Returns true if item can be specifically requested
458 $borrower must be a Koha::Patron object
460 =cut
462 sub can_article_request {
463 my ( $self, $borrower ) = @_;
465 my $rule = $self->article_request_type($borrower);
467 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
468 return q{};
471 =head3 hidden_in_opac
473 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
475 Returns true if item fields match the hidding criteria defined in $rules.
476 Returns false otherwise.
478 Takes HASHref that can have the following parameters:
479 OPTIONAL PARAMETERS:
480 $rules : { <field> => [ value_1, ... ], ... }
482 Note: $rules inherits its structure from the parsed YAML from reading
483 the I<OpacHiddenItems> system preference.
485 =cut
487 sub hidden_in_opac {
488 my ( $self, $params ) = @_;
490 my $rules = $params->{rules} // {};
492 return 1
493 if C4::Context->preference('hidelostitems') and
494 $self->itemlost > 0;
496 my $hidden_in_opac = 0;
498 foreach my $field ( keys %{$rules} ) {
500 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
501 $hidden_in_opac = 1;
502 last;
506 return $hidden_in_opac;
509 =head3 can_be_transferred
511 $item->can_be_transferred({ to => $to_library, from => $from_library })
512 Checks if an item can be transferred to given library.
514 This feature is controlled by two system preferences:
515 UseBranchTransferLimits to enable / disable the feature
516 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
517 for setting the limitations
519 Takes HASHref that can have the following parameters:
520 MANDATORY PARAMETERS:
521 $to : Koha::Library
522 OPTIONAL PARAMETERS:
523 $from : Koha::Library # if not given, item holdingbranch
524 # will be used instead
526 Returns 1 if item can be transferred to $to_library, otherwise 0.
528 To find out whether at least one item of a Koha::Biblio can be transferred, please
529 see Koha::Biblio->can_be_transferred() instead of using this method for
530 multiple items of the same biblio.
532 =cut
534 sub can_be_transferred {
535 my ($self, $params) = @_;
537 my $to = $params->{to};
538 my $from = $params->{from};
540 $to = $to->branchcode;
541 $from = defined $from ? $from->branchcode : $self->holdingbranch;
543 return 1 if $from eq $to; # Transfer to current branch is allowed
544 return 1 unless C4::Context->preference('UseBranchTransferLimits');
546 my $limittype = C4::Context->preference('BranchTransferLimitsType');
547 return Koha::Item::Transfer::Limits->search({
548 toBranch => $to,
549 fromBranch => $from,
550 $limittype => $limittype eq 'itemtype'
551 ? $self->effective_itemtype : $self->ccode
552 })->count ? 0 : 1;
556 =head3 pickup_locations
558 $pickup_locations = $item->pickup_locations( {patron => $patron } )
560 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)
561 and if item can be transferred to each pickup location.
563 =cut
565 sub pickup_locations {
566 my ($self, $params) = @_;
568 my $patron = $params->{patron};
570 my $circ_control_branch =
571 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
572 my $branchitemrule =
573 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
575 if(defined $patron) {
576 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
577 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
580 my $pickup_libraries = Koha::Libraries->search();
581 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
582 $pickup_libraries = $self->home_branch->get_hold_libraries;
583 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
584 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
585 $pickup_libraries = $plib->get_hold_libraries;
586 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
587 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
588 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
589 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
592 return $pickup_libraries->search(
594 pickup_location => 1
597 order_by => ['branchname']
599 ) unless C4::Context->preference('UseBranchTransferLimits');
601 my $limittype = C4::Context->preference('BranchTransferLimitsType');
602 my ($ccode, $itype) = (undef, undef);
603 if( $limittype eq 'ccode' ){
604 $ccode = $self->ccode;
605 } else {
606 $itype = $self->itype;
608 my $limits = Koha::Item::Transfer::Limits->search(
610 fromBranch => $self->holdingbranch,
611 ccode => $ccode,
612 itemtype => $itype,
614 { columns => ['toBranch'] }
617 return $pickup_libraries->search(
619 pickup_location => 1,
620 branchcode => {
621 '-not_in' => $limits->_resultset->as_query
625 order_by => ['branchname']
630 =head3 article_request_type
632 my $type = $item->article_request_type( $borrower )
634 returns 'yes', 'no', 'bib_only', or 'item_only'
636 $borrower must be a Koha::Patron object
638 =cut
640 sub article_request_type {
641 my ( $self, $borrower ) = @_;
643 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
644 my $branchcode =
645 $branch_control eq 'homebranch' ? $self->homebranch
646 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
647 : undef;
648 my $borrowertype = $borrower->categorycode;
649 my $itemtype = $self->effective_itemtype();
650 my $rule = Koha::CirculationRules->get_effective_rule(
652 rule_name => 'article_requests',
653 categorycode => $borrowertype,
654 itemtype => $itemtype,
655 branchcode => $branchcode
659 return q{} unless $rule;
660 return $rule->rule_value || q{}
663 =head3 current_holds
665 =cut
667 sub current_holds {
668 my ( $self ) = @_;
669 my $attributes = { order_by => 'priority' };
670 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
671 my $params = {
672 itemnumber => $self->itemnumber,
673 suspend => 0,
674 -or => [
675 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
676 waitingdate => { '!=' => undef },
679 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
680 return Koha::Holds->_new_from_dbic($hold_rs);
683 =head3 stockrotationitem
685 my $sritem = Koha::Item->stockrotationitem;
687 Returns the stock rotation item associated with the current item.
689 =cut
691 sub stockrotationitem {
692 my ( $self ) = @_;
693 my $rs = $self->_result->stockrotationitem;
694 return 0 if !$rs;
695 return Koha::StockRotationItem->_new_from_dbic( $rs );
698 =head3 add_to_rota
700 my $item = $item->add_to_rota($rota_id);
702 Add this item to the rota identified by $ROTA_ID, which means associating it
703 with the first stage of that rota. Should this item already be associated
704 with a rota, then we will move it to the new rota.
706 =cut
708 sub add_to_rota {
709 my ( $self, $rota_id ) = @_;
710 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
711 return $self;
714 =head3 has_pending_hold
716 my $is_pending_hold = $item->has_pending_hold();
718 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
720 =cut
722 sub has_pending_hold {
723 my ( $self ) = @_;
724 my $pending_hold = $self->_result->tmp_holdsqueues;
725 return $pending_hold->count ? 1: 0;
728 =head3 as_marc_field
730 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
731 my $field = $item->as_marc_field({ [ mss => $mss ] });
733 This method returns a MARC::Field object representing the Koha::Item object
734 with the current mappings configuration.
736 =cut
738 sub as_marc_field {
739 my ( $self, $params ) = @_;
741 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
742 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
744 my @subfields;
746 my @columns = $self->_result->result_source->columns;
748 foreach my $item_field ( @columns ) {
749 my $mapping = $mss->{ "items.$item_field"}[0];
750 my $tagfield = $mapping->{tagfield};
751 my $tagsubfield = $mapping->{tagsubfield};
752 next if !$tagfield; # TODO: Should we raise an exception instead?
753 # Feels like safe fallback is better
755 push @subfields, $tagsubfield => $self->$item_field
756 if defined $self->$item_field and $item_field ne '';
759 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
760 push( @subfields, @{$unlinked_item_subfields} )
761 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
763 my $field;
765 $field = MARC::Field->new(
766 "$item_tag", ' ', ' ', @subfields
767 ) if @subfields;
769 return $field;
772 =head3 renewal_branchcode
774 Returns the branchcode to be recorded in statistics renewal of the item
776 =cut
778 sub renewal_branchcode {
780 my ($self, $params ) = @_;
782 my $interface = C4::Context->interface;
783 my $branchcode;
784 if ( $interface eq 'opac' ){
785 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
786 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
787 $branchcode = 'OPACRenew';
789 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
790 $branchcode = $self->homebranch;
792 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
793 $branchcode = $self->checkout->patron->branchcode;
795 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
796 $branchcode = $self->checkout->branchcode;
798 else {
799 $branchcode = "";
801 } else {
802 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
803 ? C4::Context->userenv->{branch} : $params->{branch};
805 return $branchcode;
808 =head3 cover_images
810 Return the cover images associated with this item.
812 =cut
814 sub cover_images {
815 my ( $self ) = @_;
817 my $cover_image_rs = $self->_result->cover_images;
818 return unless $cover_image_rs;
819 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
822 =head3 _set_found_trigger
824 $self->_set_found_trigger
826 Finds the most recent lost item charge for this item and refunds the patron
827 appropriately, taking into account any payments or writeoffs already applied
828 against the charge.
830 Internal function, not exported, called only by Koha::Item->store.
832 =cut
834 sub _set_found_trigger {
835 my ( $self, $pre_mod_item ) = @_;
837 ## If item was lost, it has now been found, reverse any list item charges if necessary.
838 my $no_refund_after_days =
839 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
840 if ($no_refund_after_days) {
841 my $today = dt_from_string();
842 my $lost_age_in_days =
843 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
844 ->in_units('days');
846 return $self unless $lost_age_in_days < $no_refund_after_days;
849 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
851 item => $self,
852 return_branch => C4::Context->userenv
853 ? C4::Context->userenv->{'branch'}
854 : undef,
858 if ( $lostreturn_policy ) {
860 # refund charge made for lost book
861 my $lost_charge = Koha::Account::Lines->search(
863 itemnumber => $self->itemnumber,
864 debit_type_code => 'LOST',
865 status => [ undef, { '<>' => 'FOUND' } ]
868 order_by => { -desc => [ 'date', 'accountlines_id' ] },
869 rows => 1
871 )->single;
873 if ( $lost_charge ) {
875 my $patron = $lost_charge->patron;
876 if ( $patron ) {
878 my $account = $patron->account;
879 my $total_to_refund = 0;
881 # Use cases
882 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
884 # some amount has been cancelled. collect the offsets that are not writeoffs
885 # this works because the only way to subtract from this kind of a debt is
886 # using the UI buttons 'Pay' and 'Write off'
887 my $credits_offsets = Koha::Account::Offsets->search(
889 debit_id => $lost_charge->id,
890 credit_id => { '!=' => undef }, # it is not the debit itself
891 type => { '!=' => 'Writeoff' },
892 amount => { '<' => 0 } # credits are negative on the DB
896 $total_to_refund = ( $credits_offsets->count > 0 )
897 ? $credits_offsets->total * -1 # credits are negative on the DB
898 : 0;
901 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
903 my $credit;
904 if ( $credit_total > 0 ) {
905 my $branchcode =
906 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
907 $credit = $account->add_credit(
909 amount => $credit_total,
910 description => 'Item found ' . $self->itemnumber,
911 type => 'LOST_FOUND',
912 interface => C4::Context->interface,
913 library_id => $branchcode,
914 item_id => $self->itemnumber,
915 issue_id => $lost_charge->issue_id
919 $credit->apply( { debits => [$lost_charge] } );
920 $self->{_refunded} = 1;
923 # Update the account status
924 $lost_charge->status('FOUND');
925 $lost_charge->store();
927 # Reconcile balances if required
928 if ( C4::Context->preference('AccountAutoReconcile') ) {
929 $account->reconcile_balance;
934 # restore fine for lost book
935 if ( $lostreturn_policy eq 'restore' ) {
936 my $lost_overdue = Koha::Account::Lines->search(
938 itemnumber => $self->itemnumber,
939 debit_type_code => 'OVERDUE',
940 status => 'LOST'
943 order_by => { '-desc' => 'date' },
944 rows => 1
946 )->single;
948 if ( $lost_overdue ) {
950 my $patron = $lost_overdue->patron;
951 if ($patron) {
952 my $account = $patron->account;
954 # Update status of fine
955 $lost_overdue->status('FOUND')->store();
957 # Find related forgive credit
958 my $refund = $lost_overdue->credits(
960 credit_type_code => 'FORGIVEN',
961 itemnumber => $self->itemnumber,
962 status => [ { '!=' => 'VOID' }, undef ]
964 { order_by => { '-desc' => 'date' }, rows => 1 }
965 )->single;
967 if ( $refund ) {
968 # Revert the forgive credit
969 $refund->void();
970 $self->{_restored} = 1;
973 # Reconcile balances if required
974 if ( C4::Context->preference('AccountAutoReconcile') ) {
975 $account->reconcile_balance;
979 } elsif ( $lostreturn_policy eq 'charge' ) {
980 $self->{_charge} = 1;
984 return $self;
987 =head3 to_api_mapping
989 This method returns the mapping for representing a Koha::Item object
990 on the API.
992 =cut
994 sub to_api_mapping {
995 return {
996 itemnumber => 'item_id',
997 biblionumber => 'biblio_id',
998 biblioitemnumber => undef,
999 barcode => 'external_id',
1000 dateaccessioned => 'acquisition_date',
1001 booksellerid => 'acquisition_source',
1002 homebranch => 'home_library_id',
1003 price => 'purchase_price',
1004 replacementprice => 'replacement_price',
1005 replacementpricedate => 'replacement_price_date',
1006 datelastborrowed => 'last_checkout_date',
1007 datelastseen => 'last_seen_date',
1008 stack => undef,
1009 notforloan => 'not_for_loan_status',
1010 damaged => 'damaged_status',
1011 damaged_on => 'damaged_date',
1012 itemlost => 'lost_status',
1013 itemlost_on => 'lost_date',
1014 withdrawn => 'withdrawn',
1015 withdrawn_on => 'withdrawn_date',
1016 itemcallnumber => 'callnumber',
1017 coded_location_qualifier => 'coded_location_qualifier',
1018 issues => 'checkouts_count',
1019 renewals => 'renewals_count',
1020 reserves => 'holds_count',
1021 restricted => 'restricted_status',
1022 itemnotes => 'public_notes',
1023 itemnotes_nonpublic => 'internal_notes',
1024 holdingbranch => 'holding_library_id',
1025 timestamp => 'timestamp',
1026 location => 'location',
1027 permanent_location => 'permanent_location',
1028 onloan => 'checked_out_date',
1029 cn_source => 'call_number_source',
1030 cn_sort => 'call_number_sort',
1031 ccode => 'collection_code',
1032 materials => 'materials_notes',
1033 uri => 'uri',
1034 itype => 'item_type',
1035 more_subfields_xml => 'extended_subfields',
1036 enumchron => 'serial_issue_number',
1037 copynumber => 'copy_number',
1038 stocknumber => 'inventory_number',
1039 new_status => 'new_status'
1043 =head3 itemtype
1045 my $itemtype = $item->itemtype;
1047 Returns Koha object for effective itemtype
1049 =cut
1051 sub itemtype {
1052 my ( $self ) = @_;
1053 return Koha::ItemTypes->find( $self->effective_itemtype );
1056 =head2 Internal methods
1058 =head3 _after_item_action_hooks
1060 Helper method that takes care of calling all plugin hooks
1062 =cut
1064 sub _after_item_action_hooks {
1065 my ( $self, $params ) = @_;
1067 my $action = $params->{action};
1069 Koha::Plugins->call(
1070 'after_item_action',
1072 action => $action,
1073 item => $self,
1074 item_id => $self->itemnumber,
1079 =head3 _type
1081 =cut
1083 sub _type {
1084 return 'Item';
1087 =head1 AUTHOR
1089 Kyle M Hall <kyle@bywatersolutions.com>
1091 =cut