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