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