Bug 28854: (QA follow-up) Fix QA tool complaints
[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_mapping
1333
1334 This method returns the mapping for representing a Koha::Item object
1335 on the API.
1336
1337 =cut
1338
1339 sub to_api_mapping {
1340     return {
1341         itemnumber               => 'item_id',
1342         biblionumber             => 'biblio_id',
1343         biblioitemnumber         => undef,
1344         barcode                  => 'external_id',
1345         dateaccessioned          => 'acquisition_date',
1346         booksellerid             => 'acquisition_source',
1347         homebranch               => 'home_library_id',
1348         price                    => 'purchase_price',
1349         replacementprice         => 'replacement_price',
1350         replacementpricedate     => 'replacement_price_date',
1351         datelastborrowed         => 'last_checkout_date',
1352         datelastseen             => 'last_seen_date',
1353         stack                    => undef,
1354         notforloan               => 'not_for_loan_status',
1355         damaged                  => 'damaged_status',
1356         damaged_on               => 'damaged_date',
1357         itemlost                 => 'lost_status',
1358         itemlost_on              => 'lost_date',
1359         withdrawn                => 'withdrawn',
1360         withdrawn_on             => 'withdrawn_date',
1361         itemcallnumber           => 'callnumber',
1362         coded_location_qualifier => 'coded_location_qualifier',
1363         issues                   => 'checkouts_count',
1364         renewals                 => 'renewals_count',
1365         reserves                 => 'holds_count',
1366         restricted               => 'restricted_status',
1367         itemnotes                => 'public_notes',
1368         itemnotes_nonpublic      => 'internal_notes',
1369         holdingbranch            => 'holding_library_id',
1370         timestamp                => 'timestamp',
1371         location                 => 'location',
1372         permanent_location       => 'permanent_location',
1373         onloan                   => 'checked_out_date',
1374         cn_source                => 'call_number_source',
1375         cn_sort                  => 'call_number_sort',
1376         ccode                    => 'collection_code',
1377         materials                => 'materials_notes',
1378         uri                      => 'uri',
1379         itype                    => 'item_type_id',
1380         more_subfields_xml       => 'extended_subfields',
1381         enumchron                => 'serial_issue_number',
1382         copynumber               => 'copy_number',
1383         stocknumber              => 'inventory_number',
1384         new_status               => 'new_status'
1385     };
1386 }
1387
1388 =head3 itemtype
1389
1390     my $itemtype = $item->itemtype;
1391
1392     Returns Koha object for effective itemtype
1393
1394 =cut
1395
1396 sub itemtype {
1397     my ( $self ) = @_;
1398     return Koha::ItemTypes->find( $self->effective_itemtype );
1399 }
1400
1401 =head3 orders
1402
1403   my $orders = $item->orders();
1404
1405 Returns a Koha::Acquisition::Orders object
1406
1407 =cut
1408
1409 sub orders {
1410     my ( $self ) = @_;
1411
1412     my $orders = $self->_result->item_orders;
1413     return Koha::Acquisition::Orders->_new_from_dbic($orders);
1414 }
1415
1416 =head3 tracked_links
1417
1418   my $tracked_links = $item->tracked_links();
1419
1420 Returns a Koha::TrackedLinks object
1421
1422 =cut
1423
1424 sub tracked_links {
1425     my ( $self ) = @_;
1426
1427     my $tracked_links = $self->_result->linktrackers;
1428     return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1429 }
1430
1431 =head3 move_to_biblio
1432
1433   $item->move_to_biblio($to_biblio[, $params]);
1434
1435 Move the item to another biblio and update any references in other tables.
1436
1437 The final optional parameter, C<$params>, is expected to contain the
1438 'skip_record_index' key, which is relayed down to Koha::Item->store.
1439 There it prevents calling index_records, which takes most of the
1440 time in batch adds/deletes. The caller must take care of calling
1441 index_records separately.
1442
1443 $params:
1444     skip_record_index => 1|0
1445
1446 Returns undef if the move failed or the biblionumber of the destination record otherwise
1447
1448 =cut
1449
1450 sub move_to_biblio {
1451     my ( $self, $to_biblio, $params ) = @_;
1452
1453     $params //= {};
1454
1455     return if $self->biblionumber == $to_biblio->biblionumber;
1456
1457     my $from_biblionumber = $self->biblionumber;
1458     my $to_biblionumber = $to_biblio->biblionumber;
1459
1460     # Own biblionumber and biblioitemnumber
1461     $self->set({
1462         biblionumber => $to_biblionumber,
1463         biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1464     })->store({ skip_record_index => $params->{skip_record_index} });
1465
1466     unless ($params->{skip_record_index}) {
1467         my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1468         $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1469     }
1470
1471     # Acquisition orders
1472     $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1473
1474     # Holds
1475     $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1476
1477     # hold_fill_target (there's no Koha object available yet)
1478     my $hold_fill_target = $self->_result->hold_fill_target;
1479     if ($hold_fill_target) {
1480         $hold_fill_target->update({ biblionumber => $to_biblionumber });
1481     }
1482
1483     # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1484     # and can't even fake one since the significant columns are nullable.
1485     my $storage = $self->_result->result_source->storage;
1486     $storage->dbh_do(
1487         sub {
1488             my ($storage, $dbh, @cols) = @_;
1489
1490             $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1491         }
1492     );
1493
1494     # tracked_links
1495     $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1496
1497     return $to_biblionumber;
1498 }
1499
1500 =head3 bundle_items
1501
1502   my $bundle_items = $item->bundle_items;
1503
1504 Returns the items associated with this bundle
1505
1506 =cut
1507
1508 sub bundle_items {
1509     my ($self) = @_;
1510
1511     if ( !$self->{_bundle_items_cached} ) {
1512         my $bundle_items = Koha::Items->search(
1513             { 'item_bundles_item.host' => $self->itemnumber },
1514             { join                     => 'item_bundles_item' } );
1515         $self->{_bundle_items}        = $bundle_items;
1516         $self->{_bundle_items_cached} = 1;
1517     }
1518
1519     return $self->{_bundle_items};
1520 }
1521
1522 =head3 is_bundle
1523
1524   my $is_bundle = $item->is_bundle;
1525
1526 Returns whether the item is a bundle or not
1527
1528 =cut
1529
1530 sub is_bundle {
1531     my ($self) = @_;
1532     return $self->bundle_items->count ? 1 : 0;
1533 }
1534
1535 =head3 bundle_host
1536
1537   my $bundle = $item->bundle_host;
1538
1539 Returns the bundle item this item is attached to
1540
1541 =cut
1542
1543 sub bundle_host {
1544     my ($self) = @_;
1545
1546     my $bundle_items_rs = $self->_result->item_bundles_item;
1547     return unless $bundle_items_rs;
1548     return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1549 }
1550
1551 =head3 in_bundle
1552
1553   my $in_bundle = $item->in_bundle;
1554
1555 Returns whether this item is currently in a bundle
1556
1557 =cut
1558
1559 sub in_bundle {
1560     my ($self) = @_;
1561     return $self->bundle_host ? 1 : 0;
1562 }
1563
1564 =head3 add_to_bundle
1565
1566   my $link = $item->add_to_bundle($bundle_item);
1567
1568 Adds the bundle_item passed to this item
1569
1570 =cut
1571
1572 sub add_to_bundle {
1573     my ( $self, $bundle_item ) = @_;
1574
1575     my $schema = Koha::Database->new->schema;
1576
1577     my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1578
1579     try {
1580         $schema->txn_do(
1581             sub {
1582                 $self->_result->add_to_item_bundles_hosts(
1583                     { item => $bundle_item->itemnumber } );
1584
1585                 $bundle_item->notforloan($BundleNotLoanValue)->store();
1586             }
1587         );
1588     }
1589     catch {
1590
1591         # 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
1592         if ( ref($_) eq 'DBIx::Class::Exception' ) {
1593             warn $_->{msg};
1594             if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1595                 # FK constraints
1596                 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1597                 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1598                     Koha::Exceptions::Object::FKConstraint->throw(
1599                         error     => 'Broken FK constraint',
1600                         broken_fk => $+{column}
1601                     );
1602                 }
1603             }
1604             elsif (
1605                 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1606             {
1607                 Koha::Exceptions::Object::DuplicateID->throw(
1608                     error        => 'Duplicate ID',
1609                     duplicate_id => $+{key}
1610                 );
1611             }
1612             elsif ( $_->{msg} =~
1613 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1614               )
1615             {    # The optional \W in the regex might be a quote or backtick
1616                 my $type     = $+{type};
1617                 my $value    = $+{value};
1618                 my $property = $+{property};
1619                 $property =~ s/['`]//g;
1620                 Koha::Exceptions::Object::BadValue->throw(
1621                     type     => $type,
1622                     value    => $value,
1623                     property => $property =~ /(\w+\.\w+)$/
1624                     ? $1
1625                     : $property
1626                     ,    # results in table.column without quotes or backtics
1627                 );
1628             }
1629
1630             # Catch-all for foreign key breakages. It will help find other use cases
1631             $_->rethrow();
1632         }
1633         else {
1634             $_;
1635         }
1636     };
1637 }
1638
1639 =head3 remove_from_bundle
1640
1641 Remove this item from any bundle it may have been attached to.
1642
1643 =cut
1644
1645 sub remove_from_bundle {
1646     my ($self) = @_;
1647
1648     my $bundle_item_rs = $self->_result->item_bundles_item;
1649     if ( $bundle_item_rs ) {
1650         $bundle_item_rs->delete;
1651         $self->notforloan(0)->store();
1652         return 1;
1653     }
1654     return 0;
1655 }
1656
1657 =head2 Internal methods
1658
1659 =head3 _after_item_action_hooks
1660
1661 Helper method that takes care of calling all plugin hooks
1662
1663 =cut
1664
1665 sub _after_item_action_hooks {
1666     my ( $self, $params ) = @_;
1667
1668     my $action = $params->{action};
1669
1670     Koha::Plugins->call(
1671         'after_item_action',
1672         {
1673             action  => $action,
1674             item    => $self,
1675             item_id => $self->itemnumber,
1676         }
1677     );
1678 }
1679
1680 =head3 recall
1681
1682     my $recall = $item->recall;
1683
1684 Return the relevant recall for this item
1685
1686 =cut
1687
1688 sub recall {
1689     my ( $self ) = @_;
1690     my @recalls = Koha::Recalls->search(
1691         {
1692             biblio_id => $self->biblionumber,
1693             completed => 0,
1694         },
1695         { order_by => { -asc => 'created_date' } }
1696     )->as_list;
1697     foreach my $recall (@recalls) {
1698         if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1699             return $recall;
1700         }
1701     }
1702     # no item-level recall to return, so return earliest biblio-level
1703     # FIXME: eventually this will be based on priority
1704     return $recalls[0];
1705 }
1706
1707 =head3 can_be_recalled
1708
1709     if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1710
1711 Does item-level checks and returns if items can be recalled by this borrower
1712
1713 =cut
1714
1715 sub can_be_recalled {
1716     my ( $self, $params ) = @_;
1717
1718     return 0 if !( C4::Context->preference('UseRecalls') );
1719
1720     # check if this item is not for loan, withdrawn or lost
1721     return 0 if ( $self->notforloan != 0 );
1722     return 0 if ( $self->itemlost != 0 );
1723     return 0 if ( $self->withdrawn != 0 );
1724
1725     # check if this item is not checked out - if not checked out, can't be recalled
1726     return 0 if ( !defined( $self->checkout ) );
1727
1728     my $patron = $params->{patron};
1729
1730     my $branchcode = C4::Context->userenv->{'branch'};
1731     if ( $patron ) {
1732         $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1733     }
1734
1735     # Check the circulation rule for each relevant itemtype for this item
1736     my $rule = Koha::CirculationRules->get_effective_rules({
1737         branchcode => $branchcode,
1738         categorycode => $patron ? $patron->categorycode : undef,
1739         itemtype => $self->effective_itemtype,
1740         rules => [
1741             'recalls_allowed',
1742             'recalls_per_record',
1743             'on_shelf_recalls',
1744         ],
1745     });
1746
1747     # check recalls allowed has been set and is not zero
1748     return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1749
1750     if ( $patron ) {
1751         # check borrower has not reached open recalls allowed limit
1752         return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1753
1754         # check borrower has not reach open recalls allowed per record limit
1755         return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1756
1757         # check if this patron has already recalled this item
1758         return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1759
1760         # check if this patron has already checked out this item
1761         return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1762
1763         # check if this patron has already reserved this item
1764         return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1765     }
1766
1767     # check item availability
1768     # items are unavailable for recall if they are lost, withdrawn or notforloan
1769     my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1770
1771     # if there are no available items at all, no recall can be placed
1772     return 0 if ( scalar @items == 0 );
1773
1774     my $checked_out_count = 0;
1775     foreach (@items) {
1776         if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1777     }
1778
1779     # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1780     return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1781
1782     # can't recall if no items have been checked out
1783     return 0 if ( $checked_out_count == 0 );
1784
1785     # can recall
1786     return 1;
1787 }
1788
1789 =head3 can_be_waiting_recall
1790
1791     if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1792
1793 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1794 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1795
1796 =cut
1797
1798 sub can_be_waiting_recall {
1799     my ( $self ) = @_;
1800
1801     return 0 if !( C4::Context->preference('UseRecalls') );
1802
1803     # check if this item is not for loan, withdrawn or lost
1804     return 0 if ( $self->notforloan != 0 );
1805     return 0 if ( $self->itemlost != 0 );
1806     return 0 if ( $self->withdrawn != 0 );
1807
1808     my $branchcode = $self->holdingbranch;
1809     if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1810         $branchcode = C4::Context->userenv->{'branch'};
1811     } else {
1812         $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1813     }
1814
1815     # Check the circulation rule for each relevant itemtype for this item
1816     my $rule = Koha::CirculationRules->get_effective_rules({
1817         branchcode => $branchcode,
1818         categorycode => undef,
1819         itemtype => $self->effective_itemtype,
1820         rules => [
1821             'recalls_allowed',
1822         ],
1823     });
1824
1825     # check recalls allowed has been set and is not zero
1826     return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1827
1828     # can recall
1829     return 1;
1830 }
1831
1832 =head3 check_recalls
1833
1834     my $recall = $item->check_recalls;
1835
1836 Get the most relevant recall for this item.
1837
1838 =cut
1839
1840 sub check_recalls {
1841     my ( $self ) = @_;
1842
1843     my @recalls = Koha::Recalls->search(
1844         {   biblio_id => $self->biblionumber,
1845             item_id   => [ $self->itemnumber, undef ]
1846         },
1847         { order_by => { -asc => 'created_date' } }
1848     )->filter_by_current->as_list;
1849
1850     my $recall;
1851     # iterate through relevant recalls to find the best one.
1852     # if we come across a waiting recall, use this one.
1853     # 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.
1854     foreach my $r ( @recalls ) {
1855         if ( $r->waiting ) {
1856             $recall = $r;
1857             last;
1858         }
1859     }
1860     unless ( defined $recall ) {
1861         $recall = $recalls[0];
1862     }
1863
1864     return $recall;
1865 }
1866
1867 =head3 is_notforloan
1868
1869     my $is_notforloan = $item->is_notforloan;
1870
1871 Determine whether or not this item is "notforloan" based on
1872 the item's notforloan status or its item type
1873
1874 =cut
1875
1876 sub is_notforloan {
1877     my ( $self ) = @_;
1878     my $is_notforloan = 0;
1879
1880     if ( $self->notforloan ){
1881         $is_notforloan = 1;
1882     }
1883     else {
1884         my $itemtype = $self->itemtype;
1885         if ($itemtype){
1886             if ( $itemtype->notforloan ){
1887                 $is_notforloan = 1;
1888             }
1889         }
1890     }
1891
1892     return $is_notforloan;
1893 }
1894
1895 =head3 _type
1896
1897 =cut
1898
1899 sub _type {
1900     return 'Item';
1901 }
1902
1903 =head1 AUTHOR
1904
1905 Kyle M Hall <kyle@bywatersolutions.com>
1906
1907 =cut
1908
1909 1;