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