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