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