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