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