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