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