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