Bug 27526: Adjust code to use Koha::Items
[koha-ffzg.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 sub columns_to_str {
944     my ( $self ) = @_;
945
946     my $frameworkcode = $self->biblio->frameworkcode;
947     my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
948     my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
949
950     my $columns_info = $self->_result->result_source->columns_info;
951
952     my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
953     my $values = {};
954     for my $column ( keys %$columns_info ) {
955
956         next if $column eq 'more_subfields_xml';
957
958         my $value;
959         if ( Koha::Object::_datetime_column_type( $columns_info->{$column}->{data_type} ) ) {
960             $value = output_pref({ dateformat => 'rfc3339', dt => dt_from_string($value, 'sql')});
961         } else {
962             $value = $self->$column;
963         }
964
965         if ( not defined $value or $value eq "" ) {
966             $values->{$column} = $value;
967             next;
968         }
969
970         my $subfield =
971           exists $mss->{"items.$column"}
972           ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
973           : undef;
974
975         $values->{$column} =
976             $subfield
977           ? $subfield->{authorised_value}
978               ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
979                   $subfield->{tagsubfield}, $value, '', $tagslib )
980               : $value
981           : $value;
982     }
983
984     my $marc_more=
985       $self->more_subfields_xml
986       ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
987       : undef;
988
989     my $more_values;
990     if ( $marc_more ) {
991         my ( $field ) = $marc_more->fields;
992         for my $sf ( $field->subfields ) {
993             my $subfield_code = $sf->[0];
994             my $value = $sf->[1];
995             my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
996             next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
997             $value =
998               $subfield->{authorised_value}
999               ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1000                 $subfield->{tagsubfield}, $value, '', $tagslib )
1001               : $value;
1002
1003             push @{$more_values->{$subfield_code}}, $value;
1004         }
1005
1006         while ( my ( $k, $v ) = each %$more_values ) {
1007             $values->{$k} = join ' | ', @$v;
1008         }
1009     }
1010
1011     return $values;
1012 }
1013
1014 =head3 _set_found_trigger
1015
1016     $self->_set_found_trigger
1017
1018 Finds the most recent lost item charge for this item and refunds the patron
1019 appropriately, taking into account any payments or writeoffs already applied
1020 against the charge.
1021
1022 Internal function, not exported, called only by Koha::Item->store.
1023
1024 =cut
1025
1026 sub _set_found_trigger {
1027     my ( $self, $pre_mod_item ) = @_;
1028
1029     ## If item was lost, it has now been found, reverse any list item charges if necessary.
1030     my $no_refund_after_days =
1031       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1032     if ($no_refund_after_days) {
1033         my $today = dt_from_string();
1034         my $lost_age_in_days =
1035           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1036           ->in_units('days');
1037
1038         return $self unless $lost_age_in_days < $no_refund_after_days;
1039     }
1040
1041     my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1042         {
1043             item          => $self,
1044             return_branch => C4::Context->userenv
1045             ? C4::Context->userenv->{'branch'}
1046             : undef,
1047         }
1048       );
1049
1050     if ( $lostreturn_policy ) {
1051
1052         # refund charge made for lost book
1053         my $lost_charge = Koha::Account::Lines->search(
1054             {
1055                 itemnumber      => $self->itemnumber,
1056                 debit_type_code => 'LOST',
1057                 status          => [ undef, { '<>' => 'FOUND' } ]
1058             },
1059             {
1060                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1061                 rows     => 1
1062             }
1063         )->single;
1064
1065         if ( $lost_charge ) {
1066
1067             my $patron = $lost_charge->patron;
1068             if ( $patron ) {
1069
1070                 my $account = $patron->account;
1071                 my $total_to_refund = 0;
1072
1073                 # Use cases
1074                 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1075
1076                     # some amount has been cancelled. collect the offsets that are not writeoffs
1077                     # this works because the only way to subtract from this kind of a debt is
1078                     # using the UI buttons 'Pay' and 'Write off'
1079                     my $credit_offsets = $lost_charge->debit_offsets(
1080                         {
1081                             'credit_id'               => { '!=' => undef },
1082                             'credit.credit_type_code' => { '!=' => 'Writeoff' }
1083                         },
1084                         { join => 'credit' }
1085                     );
1086
1087                     $total_to_refund = ( $credit_offsets->count > 0 )
1088                       ? $credit_offsets->total * -1    # credits are negative on the DB
1089                       : 0;
1090                 }
1091
1092                 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1093
1094                 my $credit;
1095                 if ( $credit_total > 0 ) {
1096                     my $branchcode =
1097                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1098                     $credit = $account->add_credit(
1099                         {
1100                             amount      => $credit_total,
1101                             description => 'Item found ' . $self->itemnumber,
1102                             type        => 'LOST_FOUND',
1103                             interface   => C4::Context->interface,
1104                             library_id  => $branchcode,
1105                             item_id     => $self->itemnumber,
1106                             issue_id    => $lost_charge->issue_id
1107                         }
1108                     );
1109
1110                     $credit->apply( { debits => [$lost_charge] } );
1111                     $self->{_refunded} = 1;
1112                 }
1113
1114                 # Update the account status
1115                 $lost_charge->status('FOUND');
1116                 $lost_charge->store();
1117
1118                 # Reconcile balances if required
1119                 if ( C4::Context->preference('AccountAutoReconcile') ) {
1120                     $account->reconcile_balance;
1121                 }
1122             }
1123         }
1124
1125         # restore fine for lost book
1126         if ( $lostreturn_policy eq 'restore' ) {
1127             my $lost_overdue = Koha::Account::Lines->search(
1128                 {
1129                     itemnumber      => $self->itemnumber,
1130                     debit_type_code => 'OVERDUE',
1131                     status          => 'LOST'
1132                 },
1133                 {
1134                     order_by => { '-desc' => 'date' },
1135                     rows     => 1
1136                 }
1137             )->single;
1138
1139             if ( $lost_overdue ) {
1140
1141                 my $patron = $lost_overdue->patron;
1142                 if ($patron) {
1143                     my $account = $patron->account;
1144
1145                     # Update status of fine
1146                     $lost_overdue->status('FOUND')->store();
1147
1148                     # Find related forgive credit
1149                     my $refund = $lost_overdue->credits(
1150                         {
1151                             credit_type_code => 'FORGIVEN',
1152                             itemnumber       => $self->itemnumber,
1153                             status           => [ { '!=' => 'VOID' }, undef ]
1154                         },
1155                         { order_by => { '-desc' => 'date' }, rows => 1 }
1156                     )->single;
1157
1158                     if ( $refund ) {
1159                         # Revert the forgive credit
1160                         $refund->void({ interface => 'trigger' });
1161                         $self->{_restored} = 1;
1162                     }
1163
1164                     # Reconcile balances if required
1165                     if ( C4::Context->preference('AccountAutoReconcile') ) {
1166                         $account->reconcile_balance;
1167                     }
1168                 }
1169             }
1170         } elsif ( $lostreturn_policy eq 'charge' ) {
1171             $self->{_charge} = 1;
1172         }
1173     }
1174
1175     return $self;
1176 }
1177
1178 =head3 to_api_mapping
1179
1180 This method returns the mapping for representing a Koha::Item object
1181 on the API.
1182
1183 =cut
1184
1185 sub to_api_mapping {
1186     return {
1187         itemnumber               => 'item_id',
1188         biblionumber             => 'biblio_id',
1189         biblioitemnumber         => undef,
1190         barcode                  => 'external_id',
1191         dateaccessioned          => 'acquisition_date',
1192         booksellerid             => 'acquisition_source',
1193         homebranch               => 'home_library_id',
1194         price                    => 'purchase_price',
1195         replacementprice         => 'replacement_price',
1196         replacementpricedate     => 'replacement_price_date',
1197         datelastborrowed         => 'last_checkout_date',
1198         datelastseen             => 'last_seen_date',
1199         stack                    => undef,
1200         notforloan               => 'not_for_loan_status',
1201         damaged                  => 'damaged_status',
1202         damaged_on               => 'damaged_date',
1203         itemlost                 => 'lost_status',
1204         itemlost_on              => 'lost_date',
1205         withdrawn                => 'withdrawn',
1206         withdrawn_on             => 'withdrawn_date',
1207         itemcallnumber           => 'callnumber',
1208         coded_location_qualifier => 'coded_location_qualifier',
1209         issues                   => 'checkouts_count',
1210         renewals                 => 'renewals_count',
1211         reserves                 => 'holds_count',
1212         restricted               => 'restricted_status',
1213         itemnotes                => 'public_notes',
1214         itemnotes_nonpublic      => 'internal_notes',
1215         holdingbranch            => 'holding_library_id',
1216         timestamp                => 'timestamp',
1217         location                 => 'location',
1218         permanent_location       => 'permanent_location',
1219         onloan                   => 'checked_out_date',
1220         cn_source                => 'call_number_source',
1221         cn_sort                  => 'call_number_sort',
1222         ccode                    => 'collection_code',
1223         materials                => 'materials_notes',
1224         uri                      => 'uri',
1225         itype                    => 'item_type',
1226         more_subfields_xml       => 'extended_subfields',
1227         enumchron                => 'serial_issue_number',
1228         copynumber               => 'copy_number',
1229         stocknumber              => 'inventory_number',
1230         new_status               => 'new_status'
1231     };
1232 }
1233
1234 =head3 itemtype
1235
1236     my $itemtype = $item->itemtype;
1237
1238     Returns Koha object for effective itemtype
1239
1240 =cut
1241
1242 sub itemtype {
1243     my ( $self ) = @_;
1244     return Koha::ItemTypes->find( $self->effective_itemtype );
1245 }
1246
1247 =head3 orders
1248
1249   my $orders = $item->orders();
1250
1251 Returns a Koha::Acquisition::Orders object
1252
1253 =cut
1254
1255 sub orders {
1256     my ( $self ) = @_;
1257
1258     my $orders = $self->_result->item_orders;
1259     return Koha::Acquisition::Orders->_new_from_dbic($orders);
1260 }
1261
1262 =head3 tracked_links
1263
1264   my $tracked_links = $item->tracked_links();
1265
1266 Returns a Koha::TrackedLinks object
1267
1268 =cut
1269
1270 sub tracked_links {
1271     my ( $self ) = @_;
1272
1273     my $tracked_links = $self->_result->linktrackers;
1274     return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1275 }
1276
1277 =head3 move_to_biblio
1278
1279   $item->move_to_biblio($to_biblio[, $params]);
1280
1281 Move the item to another biblio and update any references in other tables.
1282
1283 The final optional parameter, C<$params>, is expected to contain the
1284 'skip_record_index' key, which is relayed down to Koha::Item->store.
1285 There it prevents calling index_records, which takes most of the
1286 time in batch adds/deletes. The caller must take care of calling
1287 index_records separately.
1288
1289 $params:
1290     skip_record_index => 1|0
1291
1292 Returns undef if the move failed or the biblionumber of the destination record otherwise
1293
1294 =cut
1295
1296 sub move_to_biblio {
1297     my ( $self, $to_biblio, $params ) = @_;
1298
1299     $params //= {};
1300
1301     return if $self->biblionumber == $to_biblio->biblionumber;
1302
1303     my $from_biblionumber = $self->biblionumber;
1304     my $to_biblionumber = $to_biblio->biblionumber;
1305
1306     # Own biblionumber and biblioitemnumber
1307     $self->set({
1308         biblionumber => $to_biblionumber,
1309         biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1310     })->store({ skip_record_index => $params->{skip_record_index} });
1311
1312     unless ($params->{skip_record_index}) {
1313         my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1314         $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1315     }
1316
1317     # Acquisition orders
1318     $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1319
1320     # Holds
1321     $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1322
1323     # hold_fill_target (there's no Koha object available yet)
1324     my $hold_fill_target = $self->_result->hold_fill_target;
1325     if ($hold_fill_target) {
1326         $hold_fill_target->update({ biblionumber => $to_biblionumber });
1327     }
1328
1329     # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1330     # and can't even fake one since the significant columns are nullable.
1331     my $storage = $self->_result->result_source->storage;
1332     $storage->dbh_do(
1333         sub {
1334             my ($storage, $dbh, @cols) = @_;
1335
1336             $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1337         }
1338     );
1339
1340     # tracked_links
1341     $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1342
1343     return $to_biblionumber;
1344 }
1345
1346 =head2 Internal methods
1347
1348 =head3 _after_item_action_hooks
1349
1350 Helper method that takes care of calling all plugin hooks
1351
1352 =cut
1353
1354 sub _after_item_action_hooks {
1355     my ( $self, $params ) = @_;
1356
1357     my $action = $params->{action};
1358
1359     Koha::Plugins->call(
1360         'after_item_action',
1361         {
1362             action  => $action,
1363             item    => $self,
1364             item_id => $self->itemnumber,
1365         }
1366     );
1367 }
1368
1369 =head3 _type
1370
1371 =cut
1372
1373 sub _type {
1374     return 'Item';
1375 }
1376
1377 =head1 AUTHOR
1378
1379 Kyle M Hall <kyle@bywatersolutions.com>
1380
1381 =cut
1382
1383 1;