Bug 26963: (follow-up) Change subroutine name for QA tools
[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 $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 _can_pickup_locations
557
558 $item->_can_pickup_locations({ to => $to_libraries, from => $from_library })
559 Checks if an item can be transferred to given libraries.
560
561 This feature is controlled by two system preferences:
562 UseBranchTransferLimits to enable / disable the feature
563 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
564                          for setting the limitations
565
566 Takes HASHref that can have the following parameters:
567     MANDATORY PARAMETERS:
568     $to   : Array of Koha::Libraries
569     OPTIONAL PARAMETERS:
570     $from : Koha::Library  # if not given, item holdingbranch
571                            # will be used instead
572
573 Returns arry of Koha::Libraries that item can be transferred to $to_library and
574 are pickup_locations
575
576 If checking only one library please use $item->can_be_transferred.
577
578 =cut
579
580 sub _can_pickup_locations {
581     my ($self, $params ) = @_;
582
583     my $to   = $params->{to};
584     my $from = $params->{from};
585     $from = defined $from ? $from->branchcode : $self->holdingbranch;
586
587     my @pickup_locations;
588     my @destination_codes;
589     foreach my $lib (@$to){
590         next unless $lib->pickup_location;
591         push @destination_codes, $lib->branchcode;
592         push @pickup_locations, $lib;
593     }
594
595     return \@pickup_locations unless C4::Context->preference('UseBranchTransferLimits');
596
597     my $limittype = C4::Context->preference('BranchTransferLimitsType');
598     my $limiter = $limittype eq 'itemtype' ? $self->effective_itemtype : $self->ccode;
599
600     my $limits = Koha::Item::Transfer::Limits->search({
601                 fromBranch => $from,
602                 $limittype => $limiter
603             });
604     my @limits = $limits->get_column('toBranch');
605     return \@pickup_locations unless @limits;
606
607     my @can_transfer = Koha::Libraries->search({
608         pickup_location => 1,
609         branchcode => {
610             -in => \@destination_codes,
611             -not_in => \@limits,
612         }
613     });
614     return \@can_transfer;
615 }
616
617 =head3 pickup_locations
618
619 $pickup_locations = $item->pickup_locations( {patron => $patron } )
620
621 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)
622 and if item can be transferred to each pickup location.
623
624 =cut
625
626 sub pickup_locations {
627     my ($self, $params) = @_;
628
629     my $patron = $params->{patron};
630
631     my $circ_control_branch =
632       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
633     my $branchitemrule =
634       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
635
636     my @libs;
637     if(defined $patron) {
638         return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
639         return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
640     }
641
642     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
643         @libs  = $self->home_branch->get_hold_libraries;
644         push @libs, $self->home_branch unless scalar(@libs) > 0;
645     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
646         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
647         @libs  = $plib->get_hold_libraries;
648         push @libs, $self->home_branch unless scalar(@libs) > 0;
649     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
650         push @libs, $self->home_branch;
651     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
652         push @libs, $self->holding_branch;
653     } else {
654         @libs = Koha::Libraries->search({
655             pickup_location => 1
656         }, {
657             order_by => ['branchname']
658         })->as_list;
659     }
660
661     my $pickup_locations = $self->_can_pickup_locations({
662         to => \@libs
663     });
664
665     return $pickup_locations;
666 }
667
668 =head3 article_request_type
669
670 my $type = $item->article_request_type( $borrower )
671
672 returns 'yes', 'no', 'bib_only', or 'item_only'
673
674 $borrower must be a Koha::Patron object
675
676 =cut
677
678 sub article_request_type {
679     my ( $self, $borrower ) = @_;
680
681     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
682     my $branchcode =
683         $branch_control eq 'homebranch'    ? $self->homebranch
684       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
685       :                                      undef;
686     my $borrowertype = $borrower->categorycode;
687     my $itemtype = $self->effective_itemtype();
688     my $rule = Koha::CirculationRules->get_effective_rule(
689         {
690             rule_name    => 'article_requests',
691             categorycode => $borrowertype,
692             itemtype     => $itemtype,
693             branchcode   => $branchcode
694         }
695     );
696
697     return q{} unless $rule;
698     return $rule->rule_value || q{}
699 }
700
701 =head3 current_holds
702
703 =cut
704
705 sub current_holds {
706     my ( $self ) = @_;
707     my $attributes = { order_by => 'priority' };
708     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
709     my $params = {
710         itemnumber => $self->itemnumber,
711         suspend => 0,
712         -or => [
713             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
714             waitingdate => { '!=' => undef },
715         ],
716     };
717     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
718     return Koha::Holds->_new_from_dbic($hold_rs);
719 }
720
721 =head3 stockrotationitem
722
723   my $sritem = Koha::Item->stockrotationitem;
724
725 Returns the stock rotation item associated with the current item.
726
727 =cut
728
729 sub stockrotationitem {
730     my ( $self ) = @_;
731     my $rs = $self->_result->stockrotationitem;
732     return 0 if !$rs;
733     return Koha::StockRotationItem->_new_from_dbic( $rs );
734 }
735
736 =head3 add_to_rota
737
738   my $item = $item->add_to_rota($rota_id);
739
740 Add this item to the rota identified by $ROTA_ID, which means associating it
741 with the first stage of that rota.  Should this item already be associated
742 with a rota, then we will move it to the new rota.
743
744 =cut
745
746 sub add_to_rota {
747     my ( $self, $rota_id ) = @_;
748     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
749     return $self;
750 }
751
752 =head3 has_pending_hold
753
754   my $is_pending_hold = $item->has_pending_hold();
755
756 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
757
758 =cut
759
760 sub has_pending_hold {
761     my ( $self ) = @_;
762     my $pending_hold = $self->_result->tmp_holdsqueues;
763     return $pending_hold->count ? 1: 0;
764 }
765
766 =head3 as_marc_field
767
768     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
769     my $field = $item->as_marc_field({ [ mss => $mss ] });
770
771 This method returns a MARC::Field object representing the Koha::Item object
772 with the current mappings configuration.
773
774 =cut
775
776 sub as_marc_field {
777     my ( $self, $params ) = @_;
778
779     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
780     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
781
782     my @subfields;
783
784     my @columns = $self->_result->result_source->columns;
785
786     foreach my $item_field ( @columns ) {
787         my $mapping = $mss->{ "items.$item_field"}[0];
788         my $tagfield    = $mapping->{tagfield};
789         my $tagsubfield = $mapping->{tagsubfield};
790         next if !$tagfield; # TODO: Should we raise an exception instead?
791                             # Feels like safe fallback is better
792
793         push @subfields, $tagsubfield => $self->$item_field
794             if defined $self->$item_field and $item_field ne '';
795     }
796
797     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
798     push( @subfields, @{$unlinked_item_subfields} )
799         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
800
801     my $field;
802
803     $field = MARC::Field->new(
804         "$item_tag", ' ', ' ', @subfields
805     ) if @subfields;
806
807     return $field;
808 }
809
810 =head3 renewal_branchcode
811
812 Returns the branchcode to be recorded in statistics renewal of the item
813
814 =cut
815
816 sub renewal_branchcode {
817
818     my ($self, $params ) = @_;
819
820     my $interface = C4::Context->interface;
821     my $branchcode;
822     if ( $interface eq 'opac' ){
823         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
824         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
825             $branchcode = 'OPACRenew';
826         }
827         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
828             $branchcode = $self->homebranch;
829         }
830         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
831             $branchcode = $self->checkout->patron->branchcode;
832         }
833         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
834             $branchcode = $self->checkout->branchcode;
835         }
836         else {
837             $branchcode = "";
838         }
839     } else {
840         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
841             ? C4::Context->userenv->{branch} : $params->{branch};
842     }
843     return $branchcode;
844 }
845
846 =head3 cover_images
847
848 Return the cover images associated with this item.
849
850 =cut
851
852 sub cover_images {
853     my ( $self ) = @_;
854
855     my $cover_image_rs = $self->_result->cover_images;
856     return unless $cover_image_rs;
857     return Koha::CoverImages->_new_from_dbic($cover_image_rs);
858 }
859
860 =head3 _set_found_trigger
861
862     $self->_set_found_trigger
863
864 Finds the most recent lost item charge for this item and refunds the patron
865 appropriately, taking into account any payments or writeoffs already applied
866 against the charge.
867
868 Internal function, not exported, called only by Koha::Item->store.
869
870 =cut
871
872 sub _set_found_trigger {
873     my ( $self, $pre_mod_item ) = @_;
874
875     ## If item was lost, it has now been found, reverse any list item charges if necessary.
876     my $no_refund_after_days =
877       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
878     if ($no_refund_after_days) {
879         my $today = dt_from_string();
880         my $lost_age_in_days =
881           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
882           ->in_units('days');
883
884         return $self unless $lost_age_in_days < $no_refund_after_days;
885     }
886
887     my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
888         {
889             item          => $self,
890             return_branch => C4::Context->userenv
891             ? C4::Context->userenv->{'branch'}
892             : undef,
893         }
894       );
895
896     if ( $lostreturn_policy ) {
897
898         # refund charge made for lost book
899         my $lost_charge = Koha::Account::Lines->search(
900             {
901                 itemnumber      => $self->itemnumber,
902                 debit_type_code => 'LOST',
903                 status          => [ undef, { '<>' => 'FOUND' } ]
904             },
905             {
906                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
907                 rows     => 1
908             }
909         )->single;
910
911         if ( $lost_charge ) {
912
913             my $patron = $lost_charge->patron;
914             if ( $patron ) {
915
916                 my $account = $patron->account;
917                 my $total_to_refund = 0;
918
919                 # Use cases
920                 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
921
922                     # some amount has been cancelled. collect the offsets that are not writeoffs
923                     # this works because the only way to subtract from this kind of a debt is
924                     # using the UI buttons 'Pay' and 'Write off'
925                     my $credits_offsets = Koha::Account::Offsets->search(
926                         {
927                             debit_id  => $lost_charge->id,
928                             credit_id => { '!=' => undef },     # it is not the debit itself
929                             type      => { '!=' => 'Writeoff' },
930                             amount    => { '<' => 0 }    # credits are negative on the DB
931                         }
932                     );
933
934                     $total_to_refund = ( $credits_offsets->count > 0 )
935                       ? $credits_offsets->total * -1    # credits are negative on the DB
936                       : 0;
937                 }
938
939                 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
940
941                 my $credit;
942                 if ( $credit_total > 0 ) {
943                     my $branchcode =
944                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
945                     $credit = $account->add_credit(
946                         {
947                             amount      => $credit_total,
948                             description => 'Item found ' . $self->itemnumber,
949                             type        => 'LOST_FOUND',
950                             interface   => C4::Context->interface,
951                             library_id  => $branchcode,
952                             item_id     => $self->itemnumber,
953                             issue_id    => $lost_charge->issue_id
954                         }
955                     );
956
957                     $credit->apply( { debits => [$lost_charge] } );
958                     $self->{_refunded} = 1;
959                 }
960
961                 # Update the account status
962                 $lost_charge->status('FOUND');
963                 $lost_charge->store();
964
965                 # Reconcile balances if required
966                 if ( C4::Context->preference('AccountAutoReconcile') ) {
967                     $account->reconcile_balance;
968                 }
969             }
970         }
971
972         # restore fine for lost book
973         if ( $lostreturn_policy eq 'restore' ) {
974             my $lost_overdue = Koha::Account::Lines->search(
975                 {
976                     itemnumber      => $self->itemnumber,
977                     debit_type_code => 'OVERDUE',
978                     status          => 'LOST'
979                 },
980                 {
981                     order_by => { '-desc' => 'date' },
982                     rows     => 1
983                 }
984             )->single;
985
986             if ( $lost_overdue ) {
987
988                 my $patron = $lost_overdue->patron;
989                 if ($patron) {
990                     my $account = $patron->account;
991
992                     # Update status of fine
993                     $lost_overdue->status('FOUND')->store();
994
995                     # Find related forgive credit
996                     my $refund = $lost_overdue->credits(
997                         {
998                             credit_type_code => 'FORGIVEN',
999                             itemnumber       => $self->itemnumber,
1000                             status           => [ { '!=' => 'VOID' }, undef ]
1001                         },
1002                         { order_by => { '-desc' => 'date' }, rows => 1 }
1003                     )->single;
1004
1005                     if ( $refund ) {
1006                         # Revert the forgive credit
1007                         $refund->void();
1008                         $self->{_restored} = 1;
1009                     }
1010
1011                     # Reconcile balances if required
1012                     if ( C4::Context->preference('AccountAutoReconcile') ) {
1013                         $account->reconcile_balance;
1014                     }
1015                 }
1016             }
1017         } elsif ( $lostreturn_policy eq 'charge' ) {
1018             $self->{_charge} = 1;
1019         }
1020     }
1021
1022     return $self;
1023 }
1024
1025 =head3 to_api_mapping
1026
1027 This method returns the mapping for representing a Koha::Item object
1028 on the API.
1029
1030 =cut
1031
1032 sub to_api_mapping {
1033     return {
1034         itemnumber               => 'item_id',
1035         biblionumber             => 'biblio_id',
1036         biblioitemnumber         => undef,
1037         barcode                  => 'external_id',
1038         dateaccessioned          => 'acquisition_date',
1039         booksellerid             => 'acquisition_source',
1040         homebranch               => 'home_library_id',
1041         price                    => 'purchase_price',
1042         replacementprice         => 'replacement_price',
1043         replacementpricedate     => 'replacement_price_date',
1044         datelastborrowed         => 'last_checkout_date',
1045         datelastseen             => 'last_seen_date',
1046         stack                    => undef,
1047         notforloan               => 'not_for_loan_status',
1048         damaged                  => 'damaged_status',
1049         damaged_on               => 'damaged_date',
1050         itemlost                 => 'lost_status',
1051         itemlost_on              => 'lost_date',
1052         withdrawn                => 'withdrawn',
1053         withdrawn_on             => 'withdrawn_date',
1054         itemcallnumber           => 'callnumber',
1055         coded_location_qualifier => 'coded_location_qualifier',
1056         issues                   => 'checkouts_count',
1057         renewals                 => 'renewals_count',
1058         reserves                 => 'holds_count',
1059         restricted               => 'restricted_status',
1060         itemnotes                => 'public_notes',
1061         itemnotes_nonpublic      => 'internal_notes',
1062         holdingbranch            => 'holding_library_id',
1063         timestamp                => 'timestamp',
1064         location                 => 'location',
1065         permanent_location       => 'permanent_location',
1066         onloan                   => 'checked_out_date',
1067         cn_source                => 'call_number_source',
1068         cn_sort                  => 'call_number_sort',
1069         ccode                    => 'collection_code',
1070         materials                => 'materials_notes',
1071         uri                      => 'uri',
1072         itype                    => 'item_type',
1073         more_subfields_xml       => 'extended_subfields',
1074         enumchron                => 'serial_issue_number',
1075         copynumber               => 'copy_number',
1076         stocknumber              => 'inventory_number',
1077         new_status               => 'new_status'
1078     };
1079 }
1080
1081 =head3 itemtype
1082
1083     my $itemtype = $item->itemtype;
1084
1085     Returns Koha object for effective itemtype
1086
1087 =cut
1088
1089 sub itemtype {
1090     my ( $self ) = @_;
1091     return Koha::ItemTypes->find( $self->effective_itemtype );
1092 }
1093
1094 =head2 Internal methods
1095
1096 =head3 _after_item_action_hooks
1097
1098 Helper method that takes care of calling all plugin hooks
1099
1100 =cut
1101
1102 sub _after_item_action_hooks {
1103     my ( $self, $params ) = @_;
1104
1105     my $action = $params->{action};
1106
1107     Koha::Plugins->call(
1108         'after_item_action',
1109         {
1110             action  => $action,
1111             item    => $self,
1112             item_id => $self->itemnumber,
1113         }
1114     );
1115 }
1116
1117 =head3 _type
1118
1119 =cut
1120
1121 sub _type {
1122     return 'Item';
1123 }
1124
1125 =head1 AUTHOR
1126
1127 Kyle M Hall <kyle@bywatersolutions.com>
1128
1129 =cut
1130
1131 1;