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