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