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