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