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