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