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