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