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