Bug 18501: Adjust condition for flagging an item found
[koha.git] / Koha / Item.pm
1 package Koha::Item;
2
3 # Copyright ByWater Solutions 2014
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
26
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
29
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
34 use C4::ClassSource; # FIXME We would like to avoid that
35 use C4::Log qw( logaction );
36
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::ItemTypes;
42 use Koha::Patrons;
43 use Koha::Plugins;
44 use Koha::Libraries;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
47
48 use base qw(Koha::Object);
49
50 =head1 NAME
51
52 Koha::Item - Koha Item object class
53
54 =head1 API
55
56 =head2 Class methods
57
58 =cut
59
60 =head3 store
61
62     $item->store;
63
64 $params can take an optional 'skip_modzebra_update' parameter.
65 If set, the reindexation process will not happen (ModZebra not called)
66
67 NOTE: This is a temporary fix to answer a performance issue when lot of items
68 are added (or modified) at the same time.
69 The correct way to fix this is to make the ES reindexation process async.
70 You should not turn it on if you do not understand what it is doing exactly.
71
72 =cut
73
74 sub store {
75     my $self = shift;
76     my $params = @_ ? shift : {};
77
78     my $log_action = $params->{log_action} // 1;
79
80     # We do not want to oblige callers to pass this value
81     # Dev conveniences vs performance?
82     unless ( $self->biblioitemnumber ) {
83         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
84     }
85
86     # See related changes from C4::Items::AddItem
87     unless ( $self->itype ) {
88         $self->itype($self->biblio->biblioitem->itemtype);
89     }
90
91     my $today = dt_from_string;
92     unless ( $self->in_storage ) { #AddItem
93         unless ( $self->permanent_location ) {
94             $self->permanent_location($self->location);
95         }
96         unless ( $self->replacementpricedate ) {
97             $self->replacementpricedate($today);
98         }
99         unless ( $self->datelastseen ) {
100             $self->datelastseen($today);
101         }
102
103         unless ( $self->dateaccessioned ) {
104             $self->dateaccessioned($today);
105         }
106
107         if (   $self->itemcallnumber
108             or $self->cn_source )
109         {
110             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
111             $self->cn_sort($cn_sort);
112         }
113
114         C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
115             unless $params->{skip_modzebra_update};
116
117         logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
118           if $log_action && C4::Context->preference("CataloguingLog");
119
120         $self->_after_item_action_hooks({ action => 'create' });
121
122     } else { # ModItem
123
124         { # Update *_on  fields if needed
125           # Why not for AddItem as well?
126             my @fields = qw( itemlost withdrawn damaged );
127
128             # Only retrieve the item if we need to set an "on" date field
129             if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
130                 my $pre_mod_item = $self->get_from_storage;
131                 for my $field (@fields) {
132                     if (    $self->$field
133                         and not $pre_mod_item->$field )
134                     {
135                         my $field_on = "${field}_on";
136                         $self->$field_on(
137                           DateTime::Format::MySQL->format_datetime( dt_from_string() )
138                         );
139                     }
140                 }
141             }
142
143             # If the field is defined but empty, we are removing and,
144             # and thus need to clear out the 'on' field as well
145             for my $field (@fields) {
146                 if ( defined( $self->$field ) && !$self->$field ) {
147                     my $field_on = "${field}_on";
148                     $self->$field_on(undef);
149                 }
150             }
151         }
152
153         my %updated_columns = $self->_result->get_dirty_columns;
154         return $self->SUPER::store unless %updated_columns;
155
156         if (   exists $updated_columns{itemcallnumber}
157             or exists $updated_columns{cn_source} )
158         {
159             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
160             $self->cn_sort($cn_sort);
161         }
162
163
164         if (    exists $updated_columns{location}
165             and $self->location ne 'CART'
166             and $self->location ne 'PROC'
167             and not exists $updated_columns{permanent_location} )
168         {
169             $self->permanent_location( $self->location );
170         }
171
172         # If item was lost, it has now been found, reverse any list item charges if necessary.
173         if ( exists $updated_columns{itemlost}
174                 and $self->itemlost != $updated_columns{itemlost}
175                 and $self->itemlost >= 1
176                 and $updated_columns{itemlost} <= 0
177         ) {
178             $self->_set_found_trigger;
179             $self->paidfor('');
180         }
181
182         C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
183             unless $params->{skip_modzebra_update};
184
185         $self->_after_item_action_hooks({ action => 'modify' });
186
187         logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
188           if $log_action && C4::Context->preference("CataloguingLog");
189     }
190
191     unless ( $self->dateaccessioned ) {
192         $self->dateaccessioned($today);
193     }
194
195     return $self->SUPER::store;
196 }
197
198 =head3 delete
199
200 =cut
201
202 sub delete {
203     my $self = shift;
204     my $params = @_ ? shift : {};
205
206     # FIXME check the item has no current issues
207     # i.e. raise the appropriate exception
208
209     C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
210         unless $params->{skip_modzebra_update};
211
212     $self->_after_item_action_hooks({ action => 'delete' });
213
214     logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
215       if C4::Context->preference("CataloguingLog");
216
217     return $self->SUPER::delete;
218 }
219
220 =head3 safe_delete
221
222 =cut
223
224 sub safe_delete {
225     my $self = shift;
226     my $params = @_ ? shift : {};
227
228     my $safe_to_delete = $self->safe_to_delete;
229     return $safe_to_delete unless $safe_to_delete eq '1';
230
231     $self->move_to_deleted;
232
233     return $self->delete($params);
234 }
235
236 =head3 safe_to_delete
237
238 returns 1 if the item is safe to delete,
239
240 "book_on_loan" if the item is checked out,
241
242 "not_same_branch" if the item is blocked by independent branches,
243
244 "book_reserved" if the there are holds aganst the item, or
245
246 "linked_analytics" if the item has linked analytic records.
247
248 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
249
250 =cut
251
252 sub safe_to_delete {
253     my ($self) = @_;
254
255     return "book_on_loan" if $self->checkout;
256
257     return "not_same_branch"
258       if defined C4::Context->userenv
259       and !C4::Context->IsSuperLibrarian()
260       and C4::Context->preference("IndependentBranches")
261       and ( C4::Context->userenv->{branch} ne $self->homebranch );
262
263     # check it doesn't have a waiting reserve
264     return "book_reserved"
265       if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
266
267     return "linked_analytics"
268       if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
269
270     return "last_item_for_hold"
271       if $self->biblio->items->count == 1
272       && $self->biblio->holds->search(
273           {
274               itemnumber => undef,
275           }
276         )->count;
277
278     return 1;
279 }
280
281 =head3 move_to_deleted
282
283 my $is_moved = $item->move_to_deleted;
284
285 Move an item to the deleteditems table.
286 This can be done before deleting an item, to make sure the data are not completely deleted.
287
288 =cut
289
290 sub move_to_deleted {
291     my ($self) = @_;
292     my $item_infos = $self->unblessed;
293     delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
294     return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
295 }
296
297
298 =head3 effective_itemtype
299
300 Returns the itemtype for the item based on whether item level itemtypes are set or not.
301
302 =cut
303
304 sub effective_itemtype {
305     my ( $self ) = @_;
306
307     return $self->_result()->effective_itemtype();
308 }
309
310 =head3 home_branch
311
312 =cut
313
314 sub home_branch {
315     my ($self) = @_;
316
317     $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
318
319     return $self->{_home_branch};
320 }
321
322 =head3 holding_branch
323
324 =cut
325
326 sub holding_branch {
327     my ($self) = @_;
328
329     $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
330
331     return $self->{_holding_branch};
332 }
333
334 =head3 biblio
335
336 my $biblio = $item->biblio;
337
338 Return the bibliographic record of this item
339
340 =cut
341
342 sub biblio {
343     my ( $self ) = @_;
344     my $biblio_rs = $self->_result->biblio;
345     return Koha::Biblio->_new_from_dbic( $biblio_rs );
346 }
347
348 =head3 biblioitem
349
350 my $biblioitem = $item->biblioitem;
351
352 Return the biblioitem record of this item
353
354 =cut
355
356 sub biblioitem {
357     my ( $self ) = @_;
358     my $biblioitem_rs = $self->_result->biblioitem;
359     return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
360 }
361
362 =head3 checkout
363
364 my $checkout = $item->checkout;
365
366 Return the checkout for this item
367
368 =cut
369
370 sub checkout {
371     my ( $self ) = @_;
372     my $checkout_rs = $self->_result->issue;
373     return unless $checkout_rs;
374     return Koha::Checkout->_new_from_dbic( $checkout_rs );
375 }
376
377 =head3 holds
378
379 my $holds = $item->holds();
380 my $holds = $item->holds($params);
381 my $holds = $item->holds({ found => 'W'});
382
383 Return holds attached to an item, optionally accept a hashref of params to pass to search
384
385 =cut
386
387 sub holds {
388     my ( $self,$params ) = @_;
389     my $holds_rs = $self->_result->reserves->search($params);
390     return Koha::Holds->_new_from_dbic( $holds_rs );
391 }
392
393 =head3 get_transfer
394
395 my $transfer = $item->get_transfer;
396
397 Return the transfer if the item is in transit or undef
398
399 =cut
400
401 sub get_transfer {
402     my ( $self ) = @_;
403     my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
404     return unless $transfer_rs;
405     return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
406 }
407
408 =head3 last_returned_by
409
410 Gets and sets the last borrower to return an item.
411
412 Accepts and returns Koha::Patron objects
413
414 $item->last_returned_by( $borrowernumber );
415
416 $last_returned_by = $item->last_returned_by();
417
418 =cut
419
420 sub last_returned_by {
421     my ( $self, $borrower ) = @_;
422
423     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
424
425     if ($borrower) {
426         return $items_last_returned_by_rs->update_or_create(
427             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
428     }
429     else {
430         unless ( $self->{_last_returned_by} ) {
431             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
432             if ($result) {
433                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
434             }
435         }
436
437         return $self->{_last_returned_by};
438     }
439 }
440
441 =head3 can_article_request
442
443 my $bool = $item->can_article_request( $borrower )
444
445 Returns true if item can be specifically requested
446
447 $borrower must be a Koha::Patron object
448
449 =cut
450
451 sub can_article_request {
452     my ( $self, $borrower ) = @_;
453
454     my $rule = $self->article_request_type($borrower);
455
456     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
457     return q{};
458 }
459
460 =head3 hidden_in_opac
461
462 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
463
464 Returns true if item fields match the hidding criteria defined in $rules.
465 Returns false otherwise.
466
467 Takes HASHref that can have the following parameters:
468     OPTIONAL PARAMETERS:
469     $rules : { <field> => [ value_1, ... ], ... }
470
471 Note: $rules inherits its structure from the parsed YAML from reading
472 the I<OpacHiddenItems> system preference.
473
474 =cut
475
476 sub hidden_in_opac {
477     my ( $self, $params ) = @_;
478
479     my $rules = $params->{rules} // {};
480
481     return 1
482         if C4::Context->preference('hidelostitems') and
483            $self->itemlost > 0;
484
485     my $hidden_in_opac = 0;
486
487     foreach my $field ( keys %{$rules} ) {
488
489         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
490             $hidden_in_opac = 1;
491             last;
492         }
493     }
494
495     return $hidden_in_opac;
496 }
497
498 =head3 can_be_transferred
499
500 $item->can_be_transferred({ to => $to_library, from => $from_library })
501 Checks if an item can be transferred to given library.
502
503 This feature is controlled by two system preferences:
504 UseBranchTransferLimits to enable / disable the feature
505 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
506                          for setting the limitations
507
508 Takes HASHref that can have the following parameters:
509     MANDATORY PARAMETERS:
510     $to   : Koha::Library
511     OPTIONAL PARAMETERS:
512     $from : Koha::Library  # if not given, item holdingbranch
513                            # will be used instead
514
515 Returns 1 if item can be transferred to $to_library, otherwise 0.
516
517 To find out whether at least one item of a Koha::Biblio can be transferred, please
518 see Koha::Biblio->can_be_transferred() instead of using this method for
519 multiple items of the same biblio.
520
521 =cut
522
523 sub can_be_transferred {
524     my ($self, $params) = @_;
525
526     my $to   = $params->{to};
527     my $from = $params->{from};
528
529     $to   = $to->branchcode;
530     $from = defined $from ? $from->branchcode : $self->holdingbranch;
531
532     return 1 if $from eq $to; # Transfer to current branch is allowed
533     return 1 unless C4::Context->preference('UseBranchTransferLimits');
534
535     my $limittype = C4::Context->preference('BranchTransferLimitsType');
536     return Koha::Item::Transfer::Limits->search({
537         toBranch => $to,
538         fromBranch => $from,
539         $limittype => $limittype eq 'itemtype'
540                         ? $self->effective_itemtype : $self->ccode
541     })->count ? 0 : 1;
542 }
543
544 =head3 pickup_locations
545
546 $pickup_locations = $item->pickup_locations( {patron => $patron } )
547
548 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)
549 and if item can be transferred to each pickup location.
550
551 =cut
552
553 sub pickup_locations {
554     my ($self, $params) = @_;
555
556     my $patron = $params->{patron};
557
558     my $circ_control_branch =
559       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
560     my $branchitemrule =
561       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
562
563     my @libs;
564     if(defined $patron) {
565         return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
566         return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
567     }
568
569     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
570         @libs  = $self->home_branch->get_hold_libraries;
571         push @libs, $self->home_branch unless scalar(@libs) > 0;
572     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
573         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
574         @libs  = $plib->get_hold_libraries;
575         push @libs, $self->home_branch unless scalar(@libs) > 0;
576     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
577         push @libs, $self->home_branch;
578     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
579         push @libs, $self->holding_branch;
580     } else {
581         @libs = Koha::Libraries->search({
582             pickup_location => 1
583         }, {
584             order_by => ['branchname']
585         })->as_list;
586     }
587
588     my @pickup_locations;
589     foreach my $library (@libs) {
590         if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
591             push @pickup_locations, $library;
592         }
593     }
594
595     return \@pickup_locations;
596 }
597
598 =head3 article_request_type
599
600 my $type = $item->article_request_type( $borrower )
601
602 returns 'yes', 'no', 'bib_only', or 'item_only'
603
604 $borrower must be a Koha::Patron object
605
606 =cut
607
608 sub article_request_type {
609     my ( $self, $borrower ) = @_;
610
611     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
612     my $branchcode =
613         $branch_control eq 'homebranch'    ? $self->homebranch
614       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
615       :                                      undef;
616     my $borrowertype = $borrower->categorycode;
617     my $itemtype = $self->effective_itemtype();
618     my $rule = Koha::CirculationRules->get_effective_rule(
619         {
620             rule_name    => 'article_requests',
621             categorycode => $borrowertype,
622             itemtype     => $itemtype,
623             branchcode   => $branchcode
624         }
625     );
626
627     return q{} unless $rule;
628     return $rule->rule_value || q{}
629 }
630
631 =head3 current_holds
632
633 =cut
634
635 sub current_holds {
636     my ( $self ) = @_;
637     my $attributes = { order_by => 'priority' };
638     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
639     my $params = {
640         itemnumber => $self->itemnumber,
641         suspend => 0,
642         -or => [
643             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
644             waitingdate => { '!=' => undef },
645         ],
646     };
647     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
648     return Koha::Holds->_new_from_dbic($hold_rs);
649 }
650
651 =head3 stockrotationitem
652
653   my $sritem = Koha::Item->stockrotationitem;
654
655 Returns the stock rotation item associated with the current item.
656
657 =cut
658
659 sub stockrotationitem {
660     my ( $self ) = @_;
661     my $rs = $self->_result->stockrotationitem;
662     return 0 if !$rs;
663     return Koha::StockRotationItem->_new_from_dbic( $rs );
664 }
665
666 =head3 add_to_rota
667
668   my $item = $item->add_to_rota($rota_id);
669
670 Add this item to the rota identified by $ROTA_ID, which means associating it
671 with the first stage of that rota.  Should this item already be associated
672 with a rota, then we will move it to the new rota.
673
674 =cut
675
676 sub add_to_rota {
677     my ( $self, $rota_id ) = @_;
678     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
679     return $self;
680 }
681
682 =head3 has_pending_hold
683
684   my $is_pending_hold = $item->has_pending_hold();
685
686 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
687
688 =cut
689
690 sub has_pending_hold {
691     my ( $self ) = @_;
692     my $pending_hold = $self->_result->tmp_holdsqueues;
693     return $pending_hold->count ? 1: 0;
694 }
695
696 =head3 as_marc_field
697
698     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
699     my $field = $item->as_marc_field({ [ mss => $mss ] });
700
701 This method returns a MARC::Field object representing the Koha::Item object
702 with the current mappings configuration.
703
704 =cut
705
706 sub as_marc_field {
707     my ( $self, $params ) = @_;
708
709     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
710     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
711
712     my @subfields;
713
714     my @columns = $self->_result->result_source->columns;
715
716     foreach my $item_field ( @columns ) {
717         my $mapping = $mss->{ "items.$item_field"}[0];
718         my $tagfield    = $mapping->{tagfield};
719         my $tagsubfield = $mapping->{tagsubfield};
720         next if !$tagfield; # TODO: Should we raise an exception instead?
721                             # Feels like safe fallback is better
722
723         push @subfields, $tagsubfield => $self->$item_field
724             if defined $self->$item_field and $item_field ne '';
725     }
726
727     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
728     push( @subfields, @{$unlinked_item_subfields} )
729         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
730
731     my $field;
732
733     $field = MARC::Field->new(
734         "$item_tag", ' ', ' ', @subfields
735     ) if @subfields;
736
737     return $field;
738 }
739
740 =head3 renewal_branchcode
741
742 Returns the branchcode to be recorded in statistics renewal of the item
743
744 =cut
745
746 sub renewal_branchcode {
747
748     my ($self, $params ) = @_;
749
750     my $interface = C4::Context->interface;
751     my $branchcode;
752     if ( $interface eq 'opac' ){
753         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
754         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
755             $branchcode = 'OPACRenew';
756         }
757         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
758             $branchcode = $self->homebranch;
759         }
760         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
761             $branchcode = $self->checkout->patron->branchcode;
762         }
763         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
764             $branchcode = $self->checkout->branchcode;
765         }
766         else {
767             $branchcode = "";
768         }
769     } else {
770         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
771             ? C4::Context->userenv->{branch} : $params->{branch};
772     }
773     return $branchcode;
774 }
775
776 =head3 _set_found_trigger
777
778     $self->_set_found_trigger
779
780 Finds the most recent lost item charge for this item and refunds the patron
781 appropriatly, taking into account any payments or writeoffs already applied
782 against the charge.
783
784 Internal function, not exported, called only by Koha::Item->store.
785
786 =cut
787
788 sub _set_found_trigger {
789     my ( $self, $params ) = @_;
790
791     ## If item was lost, it has now been found, reverse any list item charges if necessary.
792     my $refund = 1;
793     my $no_refund_after_days =
794       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
795     if ($no_refund_after_days) {
796         my $today = dt_from_string();
797         my $lost_age_in_days =
798           dt_from_string( $self->itemlost_on )->delta_days($today)
799           ->in_units('days');
800
801         return $self unless $lost_age_in_days < $no_refund_after_days;
802     }
803
804     return $self
805       unless Koha::CirculationRules->get_lostreturn_policy(
806         {
807             current_branch => C4::Context->userenv->{branch},
808             item           => $self,
809         }
810       );
811
812     # check for charge made for lost book
813     my $accountlines = Koha::Account::Lines->search(
814         {
815             itemnumber      => $self->itemnumber,
816             debit_type_code => 'LOST',
817             status          => [ undef, { '<>' => 'FOUND' } ]
818         },
819         {
820             order_by => { -desc => [ 'date', 'accountlines_id' ] }
821         }
822     );
823
824     return $self unless $accountlines->count > 0;
825
826     my $accountline     = $accountlines->next;
827     my $total_to_refund = 0;
828
829     return $self unless $accountline->borrowernumber;
830
831     my $patron = Koha::Patrons->find( $accountline->borrowernumber );
832     return $self
833       unless $patron;  # Patron has been deleted, nobody to credit the return to
834                        # FIXME Should not we notify this somehwere
835
836     my $account = $patron->account;
837
838     # Use cases
839     if ( $accountline->amount > $accountline->amountoutstanding ) {
840
841     # some amount has been cancelled. collect the offsets that are not writeoffs
842     # this works because the only way to subtract from this kind of a debt is
843     # using the UI buttons 'Pay' and 'Write off'
844         my $credits_offsets = Koha::Account::Offsets->search(
845             {
846                 debit_id  => $accountline->id,
847                 credit_id => { '!=' => undef },     # it is not the debit itself
848                 type      => { '!=' => 'Writeoff' },
849                 amount => { '<' => 0 }    # credits are negative on the DB
850             }
851         );
852
853         $total_to_refund = ( $credits_offsets->count > 0 )
854           ? $credits_offsets->total * -1    # credits are negative on the DB
855           : 0;
856     }
857
858     my $credit_total = $accountline->amountoutstanding + $total_to_refund;
859
860     my $credit;
861     if ( $credit_total > 0 ) {
862         my $branchcode =
863           C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
864         $credit = $account->add_credit(
865             {
866                 amount      => $credit_total,
867                 description => 'Item found ' . $item_id,
868                 type        => 'LOST_FOUND',
869                 interface   => C4::Context->interface,
870                 library_id  => $branchcode,
871                 item_id     => $itemnumber
872             }
873         );
874
875         $credit->apply( { debits => [$accountline] } );
876         $self->{_refunded} = 1;
877     }
878
879     # Update the account status
880     $accountline->discard_changes->status('FOUND')
881       ; # FIXME JD Why discard_changes? $accountline has not been modified since last fetch
882     $accountline->store;
883
884     if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
885         $account->reconcile_balance;
886     }
887
888     return $self;
889 }
890
891 =head3 to_api_mapping
892
893 This method returns the mapping for representing a Koha::Item object
894 on the API.
895
896 =cut
897
898 sub to_api_mapping {
899     return {
900         itemnumber               => 'item_id',
901         biblionumber             => 'biblio_id',
902         biblioitemnumber         => undef,
903         barcode                  => 'external_id',
904         dateaccessioned          => 'acquisition_date',
905         booksellerid             => 'acquisition_source',
906         homebranch               => 'home_library_id',
907         price                    => 'purchase_price',
908         replacementprice         => 'replacement_price',
909         replacementpricedate     => 'replacement_price_date',
910         datelastborrowed         => 'last_checkout_date',
911         datelastseen             => 'last_seen_date',
912         stack                    => undef,
913         notforloan               => 'not_for_loan_status',
914         damaged                  => 'damaged_status',
915         damaged_on               => 'damaged_date',
916         itemlost                 => 'lost_status',
917         itemlost_on              => 'lost_date',
918         withdrawn                => 'withdrawn',
919         withdrawn_on             => 'withdrawn_date',
920         itemcallnumber           => 'callnumber',
921         coded_location_qualifier => 'coded_location_qualifier',
922         issues                   => 'checkouts_count',
923         renewals                 => 'renewals_count',
924         reserves                 => 'holds_count',
925         restricted               => 'restricted_status',
926         itemnotes                => 'public_notes',
927         itemnotes_nonpublic      => 'internal_notes',
928         holdingbranch            => 'holding_library_id',
929         paidfor                  => undef,
930         timestamp                => 'timestamp',
931         location                 => 'location',
932         permanent_location       => 'permanent_location',
933         onloan                   => 'checked_out_date',
934         cn_source                => 'call_number_source',
935         cn_sort                  => 'call_number_sort',
936         ccode                    => 'collection_code',
937         materials                => 'materials_notes',
938         uri                      => 'uri',
939         itype                    => 'item_type',
940         more_subfields_xml       => 'extended_subfields',
941         enumchron                => 'serial_issue_number',
942         copynumber               => 'copy_number',
943         stocknumber              => 'inventory_number',
944         new_status               => 'new_status'
945     };
946 }
947
948 =head3 itemtype
949
950     my $itemtype = $item->itemtype;
951
952     Returns Koha object for effective itemtype
953
954 =cut
955
956 sub itemtype {
957     my ( $self ) = @_;
958     return Koha::ItemTypes->find( $self->effective_itemtype );
959 }
960
961 =head2 Internal methods
962
963 =head3 _after_item_action_hooks
964
965 Helper method that takes care of calling all plugin hooks
966
967 =cut
968
969 sub _after_item_action_hooks {
970     my ( $self, $params ) = @_;
971
972     my $action = $params->{action};
973
974     Koha::Plugins->call(
975         'after_item_action',
976         {
977             action  => $action,
978             item    => $self,
979             item_id => $self->itemnumber,
980         }
981     );
982 }
983
984 =head3 _type
985
986 =cut
987
988 sub _type {
989     return 'Item';
990 }
991
992 =head1 AUTHOR
993
994 Kyle M Hall <kyle@bywatersolutions.com>
995
996 =cut
997
998 1;