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