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