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