Bug 24446: (QA follow-up) Correction to datecancelled for ModItemTransfer
[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     {
409         to     => $to_library,
410         reason => $reason,
411         [ ignore_limits => 0, enqueue => 1, replace => 1 ]
412     }
413   );
414
415 Add a transfer request for this item to the given branch for the given reason.
416
417 An exception will be thrown if the BranchTransferLimits would prevent the requested
418 transfer, unless 'ignore_limits' is passed to override the limits.
419
420 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
421 The caller should catch such cases and retry the transfer request as appropriate passing
422 an appropriate override.
423
424 Overrides
425 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
426 * replace - Used to replace the existing transfer request with your own.
427
428 =cut
429
430 sub request_transfer {
431     my ( $self, $params ) = @_;
432
433     # check for mandatory params
434     my @mandatory = ( 'to', 'reason' );
435     for my $param (@mandatory) {
436         unless ( defined( $params->{$param} ) ) {
437             Koha::Exceptions::MissingParameter->throw(
438                 error => "The $param parameter is mandatory" );
439         }
440     }
441
442     Koha::Exceptions::Item::Transfer::Limit->throw()
443       unless ( $params->{ignore_limits}
444         || $self->can_be_transferred( { to => $params->{to} } ) );
445
446     my $request = $self->get_transfer;
447     Koha::Exceptions::Item::Transfer::Found->throw( transfer => $request )
448       if ( $request && !$params->{enqueue} && !$params->{replace} );
449
450     $request->cancel( { reason => $params->{reason}, force => 1 } )
451       if ( defined($request) && $params->{replace} );
452
453     my $transfer = Koha::Item::Transfer->new(
454         {
455             itemnumber    => $self->itemnumber,
456             daterequested => dt_from_string,
457             frombranch    => $self->holdingbranch,
458             tobranch      => $params->{to}->branchcode,
459             reason        => $params->{reason},
460             comments      => $params->{comment}
461         }
462     )->store();
463
464     return $transfer;
465 }
466
467 =head3 get_transfer
468
469   my $transfer = $item->get_transfer;
470
471 Return the active transfer request or undef
472
473 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
474 whereby the most recently sent, but not received, transfer will be returned
475 if it exists, otherwise the oldest unsatisfied transfer will be returned.
476
477 This allows for transfers to queue, which is the case for stock rotation and
478 rotating collections where a manual transfer may need to take precedence but
479 we still expect the item to end up at a final location eventually.
480
481 =cut
482
483 sub get_transfer {
484     my ($self) = @_;
485     my $transfer_rs = $self->_result->branchtransfers->search(
486         {
487             datearrived   => undef,
488             datecancelled => undef
489         },
490         {
491             order_by =>
492               [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
493             rows => 1
494         }
495     )->first;
496     return unless $transfer_rs;
497     return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
498 }
499
500 =head3 last_returned_by
501
502 Gets and sets the last borrower to return an item.
503
504 Accepts and returns Koha::Patron objects
505
506 $item->last_returned_by( $borrowernumber );
507
508 $last_returned_by = $item->last_returned_by();
509
510 =cut
511
512 sub last_returned_by {
513     my ( $self, $borrower ) = @_;
514
515     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
516
517     if ($borrower) {
518         return $items_last_returned_by_rs->update_or_create(
519             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
520     }
521     else {
522         unless ( $self->{_last_returned_by} ) {
523             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
524             if ($result) {
525                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
526             }
527         }
528
529         return $self->{_last_returned_by};
530     }
531 }
532
533 =head3 can_article_request
534
535 my $bool = $item->can_article_request( $borrower )
536
537 Returns true if item can be specifically requested
538
539 $borrower must be a Koha::Patron object
540
541 =cut
542
543 sub can_article_request {
544     my ( $self, $borrower ) = @_;
545
546     my $rule = $self->article_request_type($borrower);
547
548     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
549     return q{};
550 }
551
552 =head3 hidden_in_opac
553
554 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
555
556 Returns true if item fields match the hidding criteria defined in $rules.
557 Returns false otherwise.
558
559 Takes HASHref that can have the following parameters:
560     OPTIONAL PARAMETERS:
561     $rules : { <field> => [ value_1, ... ], ... }
562
563 Note: $rules inherits its structure from the parsed YAML from reading
564 the I<OpacHiddenItems> system preference.
565
566 =cut
567
568 sub hidden_in_opac {
569     my ( $self, $params ) = @_;
570
571     my $rules = $params->{rules} // {};
572
573     return 1
574         if C4::Context->preference('hidelostitems') and
575            $self->itemlost > 0;
576
577     my $hidden_in_opac = 0;
578
579     foreach my $field ( keys %{$rules} ) {
580
581         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
582             $hidden_in_opac = 1;
583             last;
584         }
585     }
586
587     return $hidden_in_opac;
588 }
589
590 =head3 can_be_transferred
591
592 $item->can_be_transferred({ to => $to_library, from => $from_library })
593 Checks if an item can be transferred to given library.
594
595 This feature is controlled by two system preferences:
596 UseBranchTransferLimits to enable / disable the feature
597 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
598                          for setting the limitations
599
600 Takes HASHref that can have the following parameters:
601     MANDATORY PARAMETERS:
602     $to   : Koha::Library
603     OPTIONAL PARAMETERS:
604     $from : Koha::Library  # if not given, item holdingbranch
605                            # will be used instead
606
607 Returns 1 if item can be transferred to $to_library, otherwise 0.
608
609 To find out whether at least one item of a Koha::Biblio can be transferred, please
610 see Koha::Biblio->can_be_transferred() instead of using this method for
611 multiple items of the same biblio.
612
613 =cut
614
615 sub can_be_transferred {
616     my ($self, $params) = @_;
617
618     my $to   = $params->{to};
619     my $from = $params->{from};
620
621     $to   = $to->branchcode;
622     $from = defined $from ? $from->branchcode : $self->holdingbranch;
623
624     return 1 if $from eq $to; # Transfer to current branch is allowed
625     return 1 unless C4::Context->preference('UseBranchTransferLimits');
626
627     my $limittype = C4::Context->preference('BranchTransferLimitsType');
628     return Koha::Item::Transfer::Limits->search({
629         toBranch => $to,
630         fromBranch => $from,
631         $limittype => $limittype eq 'itemtype'
632                         ? $self->effective_itemtype : $self->ccode
633     })->count ? 0 : 1;
634
635 }
636
637 =head3 pickup_locations
638
639 $pickup_locations = $item->pickup_locations( {patron => $patron } )
640
641 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)
642 and if item can be transferred to each pickup location.
643
644 =cut
645
646 sub pickup_locations {
647     my ($self, $params) = @_;
648
649     my $patron = $params->{patron};
650
651     my $circ_control_branch =
652       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
653     my $branchitemrule =
654       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
655
656     if(defined $patron) {
657         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
658         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
659     }
660
661     my $pickup_libraries = Koha::Libraries->search();
662     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
663         $pickup_libraries = $self->home_branch->get_hold_libraries;
664     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
665         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
666         $pickup_libraries = $plib->get_hold_libraries;
667     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
668         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
669     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
670         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
671     };
672
673     return $pickup_libraries->search(
674         {
675             pickup_location => 1
676         },
677         {
678             order_by => ['branchname']
679         }
680     ) unless C4::Context->preference('UseBranchTransferLimits');
681
682     my $limittype = C4::Context->preference('BranchTransferLimitsType');
683     my ($ccode, $itype) = (undef, undef);
684     if( $limittype eq 'ccode' ){
685         $ccode = $self->ccode;
686     } else {
687         $itype = $self->itype;
688     }
689     my $limits = Koha::Item::Transfer::Limits->search(
690         {
691             fromBranch => $self->holdingbranch,
692             ccode      => $ccode,
693             itemtype   => $itype,
694         },
695         { columns => ['toBranch'] }
696     );
697
698     return $pickup_libraries->search(
699         {
700             pickup_location => 1,
701             branchcode      => {
702                 '-not_in' => $limits->_resultset->as_query
703             }
704         },
705         {
706             order_by => ['branchname']
707         }
708     );
709 }
710
711 =head3 article_request_type
712
713 my $type = $item->article_request_type( $borrower )
714
715 returns 'yes', 'no', 'bib_only', or 'item_only'
716
717 $borrower must be a Koha::Patron object
718
719 =cut
720
721 sub article_request_type {
722     my ( $self, $borrower ) = @_;
723
724     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
725     my $branchcode =
726         $branch_control eq 'homebranch'    ? $self->homebranch
727       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
728       :                                      undef;
729     my $borrowertype = $borrower->categorycode;
730     my $itemtype = $self->effective_itemtype();
731     my $rule = Koha::CirculationRules->get_effective_rule(
732         {
733             rule_name    => 'article_requests',
734             categorycode => $borrowertype,
735             itemtype     => $itemtype,
736             branchcode   => $branchcode
737         }
738     );
739
740     return q{} unless $rule;
741     return $rule->rule_value || q{}
742 }
743
744 =head3 current_holds
745
746 =cut
747
748 sub current_holds {
749     my ( $self ) = @_;
750     my $attributes = { order_by => 'priority' };
751     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
752     my $params = {
753         itemnumber => $self->itemnumber,
754         suspend => 0,
755         -or => [
756             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
757             waitingdate => { '!=' => undef },
758         ],
759     };
760     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
761     return Koha::Holds->_new_from_dbic($hold_rs);
762 }
763
764 =head3 stockrotationitem
765
766   my $sritem = Koha::Item->stockrotationitem;
767
768 Returns the stock rotation item associated with the current item.
769
770 =cut
771
772 sub stockrotationitem {
773     my ( $self ) = @_;
774     my $rs = $self->_result->stockrotationitem;
775     return 0 if !$rs;
776     return Koha::StockRotationItem->_new_from_dbic( $rs );
777 }
778
779 =head3 add_to_rota
780
781   my $item = $item->add_to_rota($rota_id);
782
783 Add this item to the rota identified by $ROTA_ID, which means associating it
784 with the first stage of that rota.  Should this item already be associated
785 with a rota, then we will move it to the new rota.
786
787 =cut
788
789 sub add_to_rota {
790     my ( $self, $rota_id ) = @_;
791     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
792     return $self;
793 }
794
795 =head3 has_pending_hold
796
797   my $is_pending_hold = $item->has_pending_hold();
798
799 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
800
801 =cut
802
803 sub has_pending_hold {
804     my ( $self ) = @_;
805     my $pending_hold = $self->_result->tmp_holdsqueues;
806     return $pending_hold->count ? 1: 0;
807 }
808
809 =head3 as_marc_field
810
811     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
812     my $field = $item->as_marc_field({ [ mss => $mss ] });
813
814 This method returns a MARC::Field object representing the Koha::Item object
815 with the current mappings configuration.
816
817 =cut
818
819 sub as_marc_field {
820     my ( $self, $params ) = @_;
821
822     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
823     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
824
825     my @subfields;
826
827     my @columns = $self->_result->result_source->columns;
828
829     foreach my $item_field ( @columns ) {
830         my $mapping = $mss->{ "items.$item_field"}[0];
831         my $tagfield    = $mapping->{tagfield};
832         my $tagsubfield = $mapping->{tagsubfield};
833         next if !$tagfield; # TODO: Should we raise an exception instead?
834                             # Feels like safe fallback is better
835
836         push @subfields, $tagsubfield => $self->$item_field
837             if defined $self->$item_field and $item_field ne '';
838     }
839
840     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
841     push( @subfields, @{$unlinked_item_subfields} )
842         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
843
844     my $field;
845
846     $field = MARC::Field->new(
847         "$item_tag", ' ', ' ', @subfields
848     ) if @subfields;
849
850     return $field;
851 }
852
853 =head3 renewal_branchcode
854
855 Returns the branchcode to be recorded in statistics renewal of the item
856
857 =cut
858
859 sub renewal_branchcode {
860
861     my ($self, $params ) = @_;
862
863     my $interface = C4::Context->interface;
864     my $branchcode;
865     if ( $interface eq 'opac' ){
866         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
867         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
868             $branchcode = 'OPACRenew';
869         }
870         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
871             $branchcode = $self->homebranch;
872         }
873         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
874             $branchcode = $self->checkout->patron->branchcode;
875         }
876         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
877             $branchcode = $self->checkout->branchcode;
878         }
879         else {
880             $branchcode = "";
881         }
882     } else {
883         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
884             ? C4::Context->userenv->{branch} : $params->{branch};
885     }
886     return $branchcode;
887 }
888
889 =head3 cover_images
890
891 Return the cover images associated with this item.
892
893 =cut
894
895 sub cover_images {
896     my ( $self ) = @_;
897
898     my $cover_image_rs = $self->_result->cover_images;
899     return unless $cover_image_rs;
900     return Koha::CoverImages->_new_from_dbic($cover_image_rs);
901 }
902
903 =head3 _set_found_trigger
904
905     $self->_set_found_trigger
906
907 Finds the most recent lost item charge for this item and refunds the patron
908 appropriately, taking into account any payments or writeoffs already applied
909 against the charge.
910
911 Internal function, not exported, called only by Koha::Item->store.
912
913 =cut
914
915 sub _set_found_trigger {
916     my ( $self, $pre_mod_item ) = @_;
917
918     ## If item was lost, it has now been found, reverse any list item charges if necessary.
919     my $no_refund_after_days =
920       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
921     if ($no_refund_after_days) {
922         my $today = dt_from_string();
923         my $lost_age_in_days =
924           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
925           ->in_units('days');
926
927         return $self unless $lost_age_in_days < $no_refund_after_days;
928     }
929
930     my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
931         {
932             item          => $self,
933             return_branch => C4::Context->userenv
934             ? C4::Context->userenv->{'branch'}
935             : undef,
936         }
937       );
938
939     if ( $lostreturn_policy ) {
940
941         # refund charge made for lost book
942         my $lost_charge = Koha::Account::Lines->search(
943             {
944                 itemnumber      => $self->itemnumber,
945                 debit_type_code => 'LOST',
946                 status          => [ undef, { '<>' => 'FOUND' } ]
947             },
948             {
949                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
950                 rows     => 1
951             }
952         )->single;
953
954         if ( $lost_charge ) {
955
956             my $patron = $lost_charge->patron;
957             if ( $patron ) {
958
959                 my $account = $patron->account;
960                 my $total_to_refund = 0;
961
962                 # Use cases
963                 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
964
965                     # some amount has been cancelled. collect the offsets that are not writeoffs
966                     # this works because the only way to subtract from this kind of a debt is
967                     # using the UI buttons 'Pay' and 'Write off'
968                     my $credits_offsets = Koha::Account::Offsets->search(
969                         {
970                             debit_id  => $lost_charge->id,
971                             credit_id => { '!=' => undef },     # it is not the debit itself
972                             type      => { '!=' => 'Writeoff' },
973                             amount    => { '<' => 0 }    # credits are negative on the DB
974                         }
975                     );
976
977                     $total_to_refund = ( $credits_offsets->count > 0 )
978                       ? $credits_offsets->total * -1    # credits are negative on the DB
979                       : 0;
980                 }
981
982                 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
983
984                 my $credit;
985                 if ( $credit_total > 0 ) {
986                     my $branchcode =
987                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
988                     $credit = $account->add_credit(
989                         {
990                             amount      => $credit_total,
991                             description => 'Item found ' . $self->itemnumber,
992                             type        => 'LOST_FOUND',
993                             interface   => C4::Context->interface,
994                             library_id  => $branchcode,
995                             item_id     => $self->itemnumber,
996                             issue_id    => $lost_charge->issue_id
997                         }
998                     );
999
1000                     $credit->apply( { debits => [$lost_charge] } );
1001                     $self->{_refunded} = 1;
1002                 }
1003
1004                 # Update the account status
1005                 $lost_charge->status('FOUND');
1006                 $lost_charge->store();
1007
1008                 # Reconcile balances if required
1009                 if ( C4::Context->preference('AccountAutoReconcile') ) {
1010                     $account->reconcile_balance;
1011                 }
1012             }
1013         }
1014
1015         # restore fine for lost book
1016         if ( $lostreturn_policy eq 'restore' ) {
1017             my $lost_overdue = Koha::Account::Lines->search(
1018                 {
1019                     itemnumber      => $self->itemnumber,
1020                     debit_type_code => 'OVERDUE',
1021                     status          => 'LOST'
1022                 },
1023                 {
1024                     order_by => { '-desc' => 'date' },
1025                     rows     => 1
1026                 }
1027             )->single;
1028
1029             if ( $lost_overdue ) {
1030
1031                 my $patron = $lost_overdue->patron;
1032                 if ($patron) {
1033                     my $account = $patron->account;
1034
1035                     # Update status of fine
1036                     $lost_overdue->status('FOUND')->store();
1037
1038                     # Find related forgive credit
1039                     my $refund = $lost_overdue->credits(
1040                         {
1041                             credit_type_code => 'FORGIVEN',
1042                             itemnumber       => $self->itemnumber,
1043                             status           => [ { '!=' => 'VOID' }, undef ]
1044                         },
1045                         { order_by => { '-desc' => 'date' }, rows => 1 }
1046                     )->single;
1047
1048                     if ( $refund ) {
1049                         # Revert the forgive credit
1050                         $refund->void();
1051                         $self->{_restored} = 1;
1052                     }
1053
1054                     # Reconcile balances if required
1055                     if ( C4::Context->preference('AccountAutoReconcile') ) {
1056                         $account->reconcile_balance;
1057                     }
1058                 }
1059             }
1060         } elsif ( $lostreturn_policy eq 'charge' ) {
1061             $self->{_charge} = 1;
1062         }
1063     }
1064
1065     return $self;
1066 }
1067
1068 =head3 to_api_mapping
1069
1070 This method returns the mapping for representing a Koha::Item object
1071 on the API.
1072
1073 =cut
1074
1075 sub to_api_mapping {
1076     return {
1077         itemnumber               => 'item_id',
1078         biblionumber             => 'biblio_id',
1079         biblioitemnumber         => undef,
1080         barcode                  => 'external_id',
1081         dateaccessioned          => 'acquisition_date',
1082         booksellerid             => 'acquisition_source',
1083         homebranch               => 'home_library_id',
1084         price                    => 'purchase_price',
1085         replacementprice         => 'replacement_price',
1086         replacementpricedate     => 'replacement_price_date',
1087         datelastborrowed         => 'last_checkout_date',
1088         datelastseen             => 'last_seen_date',
1089         stack                    => undef,
1090         notforloan               => 'not_for_loan_status',
1091         damaged                  => 'damaged_status',
1092         damaged_on               => 'damaged_date',
1093         itemlost                 => 'lost_status',
1094         itemlost_on              => 'lost_date',
1095         withdrawn                => 'withdrawn',
1096         withdrawn_on             => 'withdrawn_date',
1097         itemcallnumber           => 'callnumber',
1098         coded_location_qualifier => 'coded_location_qualifier',
1099         issues                   => 'checkouts_count',
1100         renewals                 => 'renewals_count',
1101         reserves                 => 'holds_count',
1102         restricted               => 'restricted_status',
1103         itemnotes                => 'public_notes',
1104         itemnotes_nonpublic      => 'internal_notes',
1105         holdingbranch            => 'holding_library_id',
1106         timestamp                => 'timestamp',
1107         location                 => 'location',
1108         permanent_location       => 'permanent_location',
1109         onloan                   => 'checked_out_date',
1110         cn_source                => 'call_number_source',
1111         cn_sort                  => 'call_number_sort',
1112         ccode                    => 'collection_code',
1113         materials                => 'materials_notes',
1114         uri                      => 'uri',
1115         itype                    => 'item_type',
1116         more_subfields_xml       => 'extended_subfields',
1117         enumchron                => 'serial_issue_number',
1118         copynumber               => 'copy_number',
1119         stocknumber              => 'inventory_number',
1120         new_status               => 'new_status'
1121     };
1122 }
1123
1124 =head3 itemtype
1125
1126     my $itemtype = $item->itemtype;
1127
1128     Returns Koha object for effective itemtype
1129
1130 =cut
1131
1132 sub itemtype {
1133     my ( $self ) = @_;
1134     return Koha::ItemTypes->find( $self->effective_itemtype );
1135 }
1136
1137 =head2 Internal methods
1138
1139 =head3 _after_item_action_hooks
1140
1141 Helper method that takes care of calling all plugin hooks
1142
1143 =cut
1144
1145 sub _after_item_action_hooks {
1146     my ( $self, $params ) = @_;
1147
1148     my $action = $params->{action};
1149
1150     Koha::Plugins->call(
1151         'after_item_action',
1152         {
1153             action  => $action,
1154             item    => $self,
1155             item_id => $self->itemnumber,
1156         }
1157     );
1158 }
1159
1160 =head3 _type
1161
1162 =cut
1163
1164 sub _type {
1165     return 'Item';
1166 }
1167
1168 =head1 AUTHOR
1169
1170 Kyle M Hall <kyle@bywatersolutions.com>
1171
1172 =cut
1173
1174 1;