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