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