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