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