Bug 33176: Enforce bad values
[koha.git] / Koha / Item.pm
1 package Koha::Item;
2
3 # Copyright ByWater Solutions 2014
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use List::MoreUtils qw( any );
23 use Try::Tiny qw( catch try );
24
25 use Koha::Database;
26 use Koha::DateUtils qw( dt_from_string output_pref );
27
28 use C4::Context;
29 use C4::Circulation qw( barcodedecode GetBranchItemRule );
30 use C4::Reserves;
31 use C4::ClassSource qw( GetClassSort );
32 use C4::Log qw( logaction );
33
34 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::Biblio::ItemGroups;
36 use Koha::Checkouts;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::Exceptions;
40 use Koha::Exceptions::Checkin;
41 use Koha::Exceptions::Item::Bundle;
42 use Koha::Exceptions::Item::Transfer;
43 use Koha::Item::Attributes;
44 use Koha::Exceptions::Item::Bundle;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Item::Transfers;
47 use Koha::ItemTypes;
48 use Koha::Libraries;
49 use Koha::Patrons;
50 use Koha::Plugins;
51 use Koha::Recalls;
52 use Koha::Result::Boolean;
53 use Koha::SearchEngine::Indexer;
54 use Koha::StockRotationItem;
55 use Koha::StockRotationRotas;
56 use Koha::TrackedLinks;
57
58 use base qw(Koha::Object);
59
60 =head1 NAME
61
62 Koha::Item - Koha Item object class
63
64 =head1 API
65
66 =head2 Class methods
67
68 =cut
69
70 =head3 store
71
72     $item->store;
73
74 $params can take an optional 'skip_record_index' parameter.
75 If set, the reindexation process will not happen (index_records not called)
76 You should not turn it on if you do not understand what it is doing exactly.
77
78 =cut
79
80 sub store {
81     my $self = shift;
82     my $params = @_ ? shift : {};
83
84     my $log_action = $params->{log_action} // 1;
85
86     # We do not want to oblige callers to pass this value
87     # Dev conveniences vs performance?
88     unless ( $self->biblioitemnumber ) {
89         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
90     }
91
92     # See related changes from C4::Items::AddItem
93     unless ( $self->itype ) {
94         $self->itype($self->biblio->biblioitem->itemtype);
95     }
96
97     $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
98
99     my $today  = dt_from_string;
100     my $action = 'create';
101
102     unless ( $self->in_storage ) { #AddItem
103
104         unless ( $self->permanent_location ) {
105             $self->permanent_location($self->location);
106         }
107
108         my $default_location = C4::Context->preference('NewItemsDefaultLocation');
109         unless ( $self->location || !$default_location ) {
110             $self->permanent_location( $self->location || $default_location )
111               unless $self->permanent_location;
112             $self->location($default_location);
113         }
114
115         unless ( $self->replacementpricedate ) {
116             $self->replacementpricedate($today);
117         }
118         unless ( $self->datelastseen ) {
119             $self->datelastseen($today);
120         }
121
122         unless ( $self->dateaccessioned ) {
123             $self->dateaccessioned($today);
124         }
125
126         if (   $self->itemcallnumber
127             or $self->cn_source )
128         {
129             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
130             $self->cn_sort($cn_sort);
131         }
132
133     } else { # ModItem
134
135         $action = 'modify';
136
137         my %updated_columns = $self->_result->get_dirty_columns;
138         return $self->SUPER::store unless %updated_columns;
139
140         # Retrieve the item for comparison if we need to
141         my $pre_mod_item = (
142                  exists $updated_columns{itemlost}
143               or exists $updated_columns{withdrawn}
144               or exists $updated_columns{damaged}
145         ) ? $self->get_from_storage : undef;
146
147         # Update *_on  fields if needed
148         # FIXME: Why not for AddItem as well?
149         my @fields = qw( itemlost withdrawn damaged );
150         for my $field (@fields) {
151
152             # If the field is defined but empty or 0, we are
153             # removing/unsetting and thus need to clear out
154             # the 'on' field
155             if (   exists $updated_columns{$field}
156                 && defined( $self->$field )
157                 && !$self->$field )
158             {
159                 my $field_on = "${field}_on";
160                 $self->$field_on(undef);
161             }
162             # If the field has changed otherwise, we much update
163             # the 'on' field
164             elsif (exists $updated_columns{$field}
165                 && $updated_columns{$field}
166                 && !$pre_mod_item->$field )
167             {
168                 my $field_on = "${field}_on";
169                 $self->$field_on(dt_from_string);
170             }
171         }
172
173         if (   exists $updated_columns{itemcallnumber}
174             or exists $updated_columns{cn_source} )
175         {
176             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
177             $self->cn_sort($cn_sort);
178         }
179
180
181         if (    exists $updated_columns{location}
182             and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
183             and not exists $updated_columns{permanent_location} )
184         {
185             $self->permanent_location( $self->location );
186         }
187
188         # If item was lost and has now been found,
189         # reverse any list item charges if necessary.
190         if (    exists $updated_columns{itemlost}
191             and $updated_columns{itemlost} <= 0
192             and $pre_mod_item->itemlost > 0 )
193         {
194             $self->_set_found_trigger($pre_mod_item);
195         }
196
197     }
198
199     my $result = $self->SUPER::store;
200     if ( $log_action && C4::Context->preference("CataloguingLog") ) {
201         $action eq 'create'
202           ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
203           : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
204     }
205     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
206     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
207         unless $params->{skip_record_index};
208     $self->get_from_storage->_after_item_action_hooks({ action => $action });
209
210     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
211         {
212             biblio_ids => [ $self->biblionumber ]
213         }
214     ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
215
216     return $result;
217 }
218
219 =head3 delete
220
221 =cut
222
223 sub delete {
224     my $self = shift;
225     my $params = @_ ? shift : {};
226
227     # FIXME check the item has no current issues
228     # i.e. raise the appropriate exception
229
230     # Get the item group so we can delete it later if it has no items left
231     my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
232
233     my $result = $self->SUPER::delete;
234
235     # Delete the item gorup if it has no items left
236     $item_group->delete if ( $item_group && $item_group->items->count == 0 );
237
238     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240         unless $params->{skip_record_index};
241
242     $self->_after_item_action_hooks({ action => 'delete' });
243
244     logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245       if C4::Context->preference("CataloguingLog");
246
247     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
248         {
249             biblio_ids => [ $self->biblionumber ]
250         }
251     ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
252
253     return $result;
254 }
255
256 =head3 safe_delete
257
258 =cut
259
260 sub safe_delete {
261     my $self = shift;
262     my $params = @_ ? shift : {};
263
264     my $safe_to_delete = $self->safe_to_delete;
265     return $safe_to_delete unless $safe_to_delete;
266
267     $self->move_to_deleted;
268
269     return $self->delete($params);
270 }
271
272 =head3 safe_to_delete
273
274 returns 1 if the item is safe to delete,
275
276 "book_on_loan" if the item is checked out,
277
278 "not_same_branch" if the item is blocked by independent branches,
279
280 "book_reserved" if the there are holds aganst the item, or
281
282 "linked_analytics" if the item has linked analytic records.
283
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
285
286 =cut
287
288 sub safe_to_delete {
289     my ($self) = @_;
290
291     my $error;
292
293     $error = "book_on_loan" if $self->checkout;
294
295     $error //= "not_same_branch"
296       if defined C4::Context->userenv
297       && !C4::Context->IsSuperLibrarian()
298       && C4::Context->preference("IndependentBranches")
299       && ( 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->filter_by_found->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 patron to return an item.
603
604 Accepts a patron's id (borrowernumber) and returns Koha::Patron objects
605
606 $item->last_returned_by( $borrowernumber );
607
608 my $patron = $item->last_returned_by();
609
610 =cut
611
612 sub last_returned_by {
613     my ( $self, $borrowernumber ) = @_;
614     if ( $borrowernumber ) {
615         $self->_result->update_or_create_related('last_returned_by',
616             { borrowernumber => $borrowernumber, itemnumber => $self->itemnumber } );
617     }
618     my $rs = $self->_result->last_returned_by;
619     return unless $rs;
620     return Koha::Patron->_new_from_dbic($rs->borrowernumber);
621 }
622
623 =head3 can_article_request
624
625 my $bool = $item->can_article_request( $borrower )
626
627 Returns true if item can be specifically requested
628
629 $borrower must be a Koha::Patron object
630
631 =cut
632
633 sub can_article_request {
634     my ( $self, $borrower ) = @_;
635
636     my $rule = $self->article_request_type($borrower);
637
638     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
639     return q{};
640 }
641
642 =head3 hidden_in_opac
643
644 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
645
646 Returns true if item fields match the hidding criteria defined in $rules.
647 Returns false otherwise.
648
649 Takes HASHref that can have the following parameters:
650     OPTIONAL PARAMETERS:
651     $rules : { <field> => [ value_1, ... ], ... }
652
653 Note: $rules inherits its structure from the parsed YAML from reading
654 the I<OpacHiddenItems> system preference.
655
656 =cut
657
658 sub hidden_in_opac {
659     my ( $self, $params ) = @_;
660
661     my $rules = $params->{rules} // {};
662
663     return 1
664         if C4::Context->preference('hidelostitems') and
665            $self->itemlost > 0;
666
667     my $hidden_in_opac = 0;
668
669     foreach my $field ( keys %{$rules} ) {
670
671         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
672             $hidden_in_opac = 1;
673             last;
674         }
675     }
676
677     return $hidden_in_opac;
678 }
679
680 =head3 can_be_transferred
681
682 $item->can_be_transferred({ to => $to_library, from => $from_library })
683 Checks if an item can be transferred to given library.
684
685 This feature is controlled by two system preferences:
686 UseBranchTransferLimits to enable / disable the feature
687 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
688                          for setting the limitations
689
690 Takes HASHref that can have the following parameters:
691     MANDATORY PARAMETERS:
692     $to   : Koha::Library
693     OPTIONAL PARAMETERS:
694     $from : Koha::Library  # if not given, item holdingbranch
695                            # will be used instead
696
697 Returns 1 if item can be transferred to $to_library, otherwise 0.
698
699 To find out whether at least one item of a Koha::Biblio can be transferred, please
700 see Koha::Biblio->can_be_transferred() instead of using this method for
701 multiple items of the same biblio.
702
703 =cut
704
705 sub can_be_transferred {
706     my ($self, $params) = @_;
707
708     my $to   = $params->{to};
709     my $from = $params->{from};
710
711     $to   = $to->branchcode;
712     $from = defined $from ? $from->branchcode : $self->holdingbranch;
713
714     return 1 if $from eq $to; # Transfer to current branch is allowed
715     return 1 unless C4::Context->preference('UseBranchTransferLimits');
716
717     my $limittype = C4::Context->preference('BranchTransferLimitsType');
718     return Koha::Item::Transfer::Limits->search({
719         toBranch => $to,
720         fromBranch => $from,
721         $limittype => $limittype eq 'itemtype'
722                         ? $self->effective_itemtype : $self->ccode
723     })->count ? 0 : 1;
724
725 }
726
727 =head3 pickup_locations
728
729     my $pickup_locations = $item->pickup_locations({ patron => $patron })
730
731 Returns possible pickup locations for this item, according to patron's home library
732 and if item can be transferred to each pickup location.
733
734 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
735 is not passed.
736
737 =cut
738
739 sub pickup_locations {
740     my ($self, $params) = @_;
741
742     Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
743       unless exists $params->{patron};
744
745     my $patron = $params->{patron};
746
747     my $circ_control_branch =
748       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
749     my $branchitemrule =
750       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
751
752     return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
753     return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
754
755     my $pickup_libraries = Koha::Libraries->search();
756     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
757         $pickup_libraries = $self->home_branch->get_hold_libraries;
758     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
759         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
760         $pickup_libraries = $plib->get_hold_libraries;
761     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
762         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
763     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
764         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
765     };
766
767     return $pickup_libraries->search(
768         {
769             pickup_location => 1
770         },
771         {
772             order_by => ['branchname']
773         }
774     ) unless C4::Context->preference('UseBranchTransferLimits');
775
776     my $limittype = C4::Context->preference('BranchTransferLimitsType');
777     my ($ccode, $itype) = (undef, undef);
778     if( $limittype eq 'ccode' ){
779         $ccode = $self->ccode;
780     } else {
781         $itype = $self->itype;
782     }
783     my $limits = Koha::Item::Transfer::Limits->search(
784         {
785             fromBranch => $self->holdingbranch,
786             ccode      => $ccode,
787             itemtype   => $itype,
788         },
789         { columns => ['toBranch'] }
790     );
791
792     return $pickup_libraries->search(
793         {
794             pickup_location => 1,
795             branchcode      => {
796                 '-not_in' => $limits->_resultset->as_query
797             }
798         },
799         {
800             order_by => ['branchname']
801         }
802     );
803 }
804
805 =head3 article_request_type
806
807 my $type = $item->article_request_type( $borrower )
808
809 returns 'yes', 'no', 'bib_only', or 'item_only'
810
811 $borrower must be a Koha::Patron object
812
813 =cut
814
815 sub article_request_type {
816     my ( $self, $borrower ) = @_;
817
818     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
819     my $branchcode =
820         $branch_control eq 'homebranch'    ? $self->homebranch
821       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
822       :                                      undef;
823     my $borrowertype = $borrower->categorycode;
824     my $itemtype = $self->effective_itemtype();
825     my $rule = Koha::CirculationRules->get_effective_rule(
826         {
827             rule_name    => 'article_requests',
828             categorycode => $borrowertype,
829             itemtype     => $itemtype,
830             branchcode   => $branchcode
831         }
832     );
833
834     return q{} unless $rule;
835     return $rule->rule_value || q{}
836 }
837
838 =head3 current_holds
839
840 =cut
841
842 sub current_holds {
843     my ( $self ) = @_;
844     my $attributes = { order_by => 'priority' };
845     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
846     my $params = {
847         itemnumber => $self->itemnumber,
848         suspend => 0,
849         -or => [
850             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
851             waitingdate => { '!=' => undef },
852         ],
853     };
854     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
855     return Koha::Holds->_new_from_dbic($hold_rs);
856 }
857
858 =head3 stockrotationitem
859
860   my $sritem = Koha::Item->stockrotationitem;
861
862 Returns the stock rotation item associated with the current item.
863
864 =cut
865
866 sub stockrotationitem {
867     my ( $self ) = @_;
868     my $rs = $self->_result->stockrotationitem;
869     return 0 if !$rs;
870     return Koha::StockRotationItem->_new_from_dbic( $rs );
871 }
872
873 =head3 add_to_rota
874
875   my $item = $item->add_to_rota($rota_id);
876
877 Add this item to the rota identified by $ROTA_ID, which means associating it
878 with the first stage of that rota.  Should this item already be associated
879 with a rota, then we will move it to the new rota.
880
881 =cut
882
883 sub add_to_rota {
884     my ( $self, $rota_id ) = @_;
885     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
886     return $self;
887 }
888
889 =head3 has_pending_hold
890
891   my $is_pending_hold = $item->has_pending_hold();
892
893 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
894
895 =cut
896
897 sub has_pending_hold {
898     my ( $self ) = @_;
899     my $pending_hold = $self->_result->tmp_holdsqueues;
900     return $pending_hold->count ? 1: 0;
901 }
902
903 =head3 has_pending_recall {
904
905   my $has_pending_recall
906
907 Return if whether has pending recall of not.
908
909 =cut
910
911 sub has_pending_recall {
912     my ( $self ) = @_;
913
914     # FIXME Must be moved to $self->recalls
915     return Koha::Recalls->search(
916         {
917             item_id   => $self->itemnumber,
918             status    => 'waiting',
919         }
920     )->count;
921 }
922
923 =head3 as_marc_field
924
925     my $field = $item->as_marc_field;
926
927 This method returns a MARC::Field object representing the Koha::Item object
928 with the current mappings configuration.
929
930 =cut
931
932 sub as_marc_field {
933     my ( $self ) = @_;
934
935     my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
936
937     my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
938
939     my @subfields;
940
941     my $item_field = $tagslib->{$itemtag};
942
943     my $more_subfields = $self->additional_attributes->to_hashref;
944     foreach my $subfield (
945         sort {
946                $a->{display_order} <=> $b->{display_order}
947             || $a->{subfield} cmp $b->{subfield}
948         } grep { ref($_) && %$_ } values %$item_field
949     ){
950
951         my $kohafield = $subfield->{kohafield};
952         my $tagsubfield = $subfield->{tagsubfield};
953         my $value;
954         if ( defined $kohafield && $kohafield ne '' ) {
955             next if $kohafield !~ m{^items\.}; # That would be weird!
956             ( my $attribute = $kohafield ) =~ s|^items\.||;
957             $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
958                 if defined $self->$attribute and $self->$attribute ne '';
959         } else {
960             $value = $more_subfields->{$tagsubfield}
961         }
962
963         next unless defined $value
964             and $value ne q{};
965
966         if ( $subfield->{repeatable} ) {
967             my @values = split '\|', $value;
968             push @subfields, ( $tagsubfield => $_ ) for @values;
969         }
970         else {
971             push @subfields, ( $tagsubfield => $value );
972         }
973
974     }
975
976     return unless @subfields;
977
978     return MARC::Field->new(
979         "$itemtag", ' ', ' ', @subfields
980     );
981 }
982
983 =head3 renewal_branchcode
984
985 Returns the branchcode to be recorded in statistics renewal of the item
986
987 =cut
988
989 sub renewal_branchcode {
990
991     my ($self, $params ) = @_;
992
993     my $interface = C4::Context->interface;
994     my $branchcode;
995     if ( $interface eq 'opac' ){
996         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
997         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
998             $branchcode = 'OPACRenew';
999         }
1000         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
1001             $branchcode = $self->homebranch;
1002         }
1003         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
1004             $branchcode = $self->checkout->patron->branchcode;
1005         }
1006         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
1007             $branchcode = $self->checkout->branchcode;
1008         }
1009         else {
1010             $branchcode = "";
1011         }
1012     } else {
1013         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
1014             ? C4::Context->userenv->{branch} : $params->{branch};
1015     }
1016     return $branchcode;
1017 }
1018
1019 =head3 cover_images
1020
1021 Return the cover images associated with this item.
1022
1023 =cut
1024
1025 sub cover_images {
1026     my ( $self ) = @_;
1027
1028     my $cover_image_rs = $self->_result->cover_images;
1029     return unless $cover_image_rs;
1030     return Koha::CoverImages->_new_from_dbic($cover_image_rs);
1031 }
1032
1033 =head3 columns_to_str
1034
1035     my $values = $items->columns_to_str;
1036
1037 Return a hashref with the string representation of the different attribute of the item.
1038
1039 This is meant to be used for display purpose only.
1040
1041 =cut
1042
1043 sub columns_to_str {
1044     my ( $self ) = @_;
1045     my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
1046     my $tagslib       = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1047     my $mss           = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1048
1049     my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1050
1051     my $values = {};
1052     for my $column ( @{$self->_columns}) {
1053
1054         next if $column eq 'more_subfields_xml';
1055
1056         my $value = $self->$column;
1057         # 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
1058
1059         if ( not defined $value or $value eq "" ) {
1060             $values->{$column} = $value;
1061             next;
1062         }
1063
1064         my $subfield =
1065           exists $mss->{"items.$column"}
1066           ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1067           : undef;
1068
1069         $values->{$column} =
1070             $subfield
1071           ? $subfield->{authorised_value}
1072               ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1073                   $subfield->{tagsubfield}, $value, '', $tagslib )
1074               : $value
1075           : $value;
1076     }
1077
1078     my $marc_more=
1079       $self->more_subfields_xml
1080       ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1081       : undef;
1082
1083     my $more_values;
1084     if ( $marc_more ) {
1085         my ( $field ) = $marc_more->fields;
1086         for my $sf ( $field->subfields ) {
1087             my $subfield_code = $sf->[0];
1088             my $value = $sf->[1];
1089             my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1090             next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1091             $value =
1092               $subfield->{authorised_value}
1093               ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1094                 $subfield->{tagsubfield}, $value, '', $tagslib )
1095               : $value;
1096
1097             push @{$more_values->{$subfield_code}}, $value;
1098         }
1099
1100         while ( my ( $k, $v ) = each %$more_values ) {
1101             $values->{$k} = join ' | ', @$v;
1102         }
1103     }
1104
1105     return $values;
1106 }
1107
1108 =head3 additional_attributes
1109
1110     my $attributes = $item->additional_attributes;
1111     $attributes->{k} = 'new k';
1112     $item->update({ more_subfields => $attributes->to_marcxml });
1113
1114 Returns a Koha::Item::Attributes object that represents the non-mapped
1115 attributes for this item.
1116
1117 =cut
1118
1119 sub additional_attributes {
1120     my ($self) = @_;
1121
1122     return Koha::Item::Attributes->new_from_marcxml(
1123         $self->more_subfields_xml,
1124     );
1125 }
1126
1127 =head3 _set_found_trigger
1128
1129     $self->_set_found_trigger
1130
1131 Finds the most recent lost item charge for this item and refunds the patron
1132 appropriately, taking into account any payments or writeoffs already applied
1133 against the charge.
1134
1135 Internal function, not exported, called only by Koha::Item->store.
1136
1137 =cut
1138
1139 sub _set_found_trigger {
1140     my ( $self, $pre_mod_item ) = @_;
1141
1142     # Reverse any lost item charges if necessary.
1143     my $no_refund_after_days =
1144       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1145     if ($no_refund_after_days) {
1146         my $today = dt_from_string();
1147         my $lost_age_in_days =
1148           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1149           ->in_units('days');
1150
1151         return $self unless $lost_age_in_days < $no_refund_after_days;
1152     }
1153
1154     my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
1155         {
1156             item          => $self,
1157             return_branch => C4::Context->userenv
1158             ? C4::Context->userenv->{'branch'}
1159             : undef,
1160         }
1161       );
1162     my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
1163
1164     if ( $lostreturn_policy ) {
1165
1166         # refund charge made for lost book
1167         my $lost_charge = Koha::Account::Lines->search(
1168             {
1169                 itemnumber      => $self->itemnumber,
1170                 debit_type_code => 'LOST',
1171                 status          => [ undef, { '<>' => 'FOUND' } ]
1172             },
1173             {
1174                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1175                 rows     => 1
1176             }
1177         )->single;
1178
1179         if ( $lost_charge ) {
1180
1181             my $patron = $lost_charge->patron;
1182             if ( $patron ) {
1183
1184                 my $account = $patron->account;
1185
1186                 # Credit outstanding amount
1187                 my $credit_total = $lost_charge->amountoutstanding;
1188
1189                 # Use cases
1190                 if (
1191                     $lost_charge->amount > $lost_charge->amountoutstanding &&
1192                     $lostreturn_policy ne "refund_unpaid"
1193                 ) {
1194                     # some amount has been cancelled. collect the offsets that are not writeoffs
1195                     # this works because the only way to subtract from this kind of a debt is
1196                     # using the UI buttons 'Pay' and 'Write off'
1197
1198                     # We don't credit any payments if return policy is
1199                     # "refund_unpaid"
1200                     #
1201                     # In that case only unpaid/outstanding amount
1202                     # will be credited which settles the debt without
1203                     # creating extra credits
1204
1205                     my $credit_offsets = $lost_charge->debit_offsets(
1206                         {
1207                             'credit_id'               => { '!=' => undef },
1208                             'credit.credit_type_code' => { '!=' => 'Writeoff' }
1209                         },
1210                         { join => 'credit' }
1211                     );
1212
1213                     my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1214                         # credits are negative on the DB
1215                         $credit_offsets->total * -1 :
1216                         0;
1217                     # Credit the outstanding amount, then add what has been
1218                     # paid to create a net credit for this amount
1219                     $credit_total += $total_to_refund;
1220                 }
1221
1222                 my $credit;
1223                 if ( $credit_total > 0 ) {
1224                     my $branchcode =
1225                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1226                     $credit = $account->add_credit(
1227                         {
1228                             amount      => $credit_total,
1229                             description => 'Item found ' . $self->itemnumber,
1230                             type        => 'LOST_FOUND',
1231                             interface   => C4::Context->interface,
1232                             library_id  => $branchcode,
1233                             item_id     => $self->itemnumber,
1234                             issue_id    => $lost_charge->issue_id
1235                         }
1236                     );
1237
1238                     $credit->apply( { debits => [$lost_charge] } );
1239                     $self->add_message(
1240                         {
1241                             type    => 'info',
1242                             message => 'lost_refunded',
1243                             payload => { credit_id => $credit->id }
1244                         }
1245                     );
1246                 }
1247
1248                 # Update the account status
1249                 $lost_charge->status('FOUND');
1250                 $lost_charge->store();
1251
1252                 # Reconcile balances if required
1253                 if ( C4::Context->preference('AccountAutoReconcile') ) {
1254                     $account->reconcile_balance;
1255                 }
1256             }
1257         }
1258
1259         # possibly restore fine for lost book
1260         my $lost_overdue = Koha::Account::Lines->search(
1261             {
1262                 itemnumber      => $self->itemnumber,
1263                 debit_type_code => 'OVERDUE',
1264                 status          => 'LOST'
1265             },
1266             {
1267                 order_by => { '-desc' => 'date' },
1268                 rows     => 1
1269             }
1270         )->single;
1271         if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
1272
1273             my $patron = $lost_overdue->patron;
1274             if ($patron) {
1275                 my $account = $patron->account;
1276
1277                 # Update status of fine
1278                 $lost_overdue->status('FOUND')->store();
1279
1280                 # Find related forgive credit
1281                 my $refund = $lost_overdue->credits(
1282                     {
1283                         credit_type_code => 'FORGIVEN',
1284                         itemnumber       => $self->itemnumber,
1285                         status           => [ { '!=' => 'VOID' }, undef ]
1286                     },
1287                     { order_by => { '-desc' => 'date' }, rows => 1 }
1288                 )->single;
1289
1290                 if ( $refund ) {
1291                     # Revert the forgive credit
1292                     $refund->void({ interface => 'trigger' });
1293                     $self->add_message(
1294                         {
1295                             type    => 'info',
1296                             message => 'lost_restored',
1297                             payload => { refund_id => $refund->id }
1298                         }
1299                     );
1300                 }
1301
1302                 # Reconcile balances if required
1303                 if ( C4::Context->preference('AccountAutoReconcile') ) {
1304                     $account->reconcile_balance;
1305                 }
1306             }
1307
1308         } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
1309             $self->add_message(
1310                 {
1311                     type    => 'info',
1312                     message => 'lost_charge',
1313                 }
1314             );
1315         }
1316     }
1317
1318     my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
1319
1320     if ( $processingreturn_policy ) {
1321
1322         # refund processing charge made for lost book
1323         my $processing_charge = Koha::Account::Lines->search(
1324             {
1325                 itemnumber      => $self->itemnumber,
1326                 debit_type_code => 'PROCESSING',
1327                 status          => [ undef, { '<>' => 'FOUND' } ]
1328             },
1329             {
1330                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1331                 rows     => 1
1332             }
1333         )->single;
1334
1335         if ( $processing_charge ) {
1336
1337             my $patron = $processing_charge->patron;
1338             if ( $patron ) {
1339
1340                 my $account = $patron->account;
1341
1342                 # Credit outstanding amount
1343                 my $credit_total = $processing_charge->amountoutstanding;
1344
1345                 # Use cases
1346                 if (
1347                     $processing_charge->amount > $processing_charge->amountoutstanding &&
1348                     $processingreturn_policy ne "refund_unpaid"
1349                 ) {
1350                     # some amount has been cancelled. collect the offsets that are not writeoffs
1351                     # this works because the only way to subtract from this kind of a debt is
1352                     # using the UI buttons 'Pay' and 'Write off'
1353
1354                     # We don't credit any payments if return policy is
1355                     # "refund_unpaid"
1356                     #
1357                     # In that case only unpaid/outstanding amount
1358                     # will be credited which settles the debt without
1359                     # creating extra credits
1360
1361                     my $credit_offsets = $processing_charge->debit_offsets(
1362                         {
1363                             'credit_id'               => { '!=' => undef },
1364                             'credit.credit_type_code' => { '!=' => 'Writeoff' }
1365                         },
1366                         { join => 'credit' }
1367                     );
1368
1369                     my $total_to_refund = ( $credit_offsets->count > 0 ) ?
1370                         # credits are negative on the DB
1371                         $credit_offsets->total * -1 :
1372                         0;
1373                     # Credit the outstanding amount, then add what has been
1374                     # paid to create a net credit for this amount
1375                     $credit_total += $total_to_refund;
1376                 }
1377
1378                 my $credit;
1379                 if ( $credit_total > 0 ) {
1380                     my $branchcode =
1381                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1382                     $credit = $account->add_credit(
1383                         {
1384                             amount      => $credit_total,
1385                             description => 'Item found ' . $self->itemnumber,
1386                             type        => 'PROCESSING_FOUND',
1387                             interface   => C4::Context->interface,
1388                             library_id  => $branchcode,
1389                             item_id     => $self->itemnumber,
1390                             issue_id    => $processing_charge->issue_id
1391                         }
1392                     );
1393
1394                     $credit->apply( { debits => [$processing_charge] } );
1395                     $self->add_message(
1396                         {
1397                             type    => 'info',
1398                             message => 'processing_refunded',
1399                             payload => { credit_id => $credit->id }
1400                         }
1401                     );
1402                 }
1403
1404                 # Update the account status
1405                 $processing_charge->status('FOUND');
1406                 $processing_charge->store();
1407
1408                 # Reconcile balances if required
1409                 if ( C4::Context->preference('AccountAutoReconcile') ) {
1410                     $account->reconcile_balance;
1411                 }
1412             }
1413         }
1414     }
1415
1416     return $self;
1417 }
1418
1419 =head3 public_read_list
1420
1421 This method returns the list of publicly readable database fields for both API and UI output purposes
1422
1423 =cut
1424
1425 sub public_read_list {
1426     return [
1427         'itemnumber',     'biblionumber',    'homebranch',
1428         'holdingbranch',  'location',        'collectioncode',
1429         'itemcallnumber', 'copynumber',      'enumchron',
1430         'barcode',        'dateaccessioned', 'itemnotes',
1431         'onloan',         'uri',             'itype',
1432         'notforloan',     'damaged',         'itemlost',
1433         'withdrawn',      'restricted'
1434     ];
1435 }
1436
1437 =head3 to_api
1438
1439 Overloaded to_api method to ensure item-level itypes is adhered to.
1440
1441 =cut
1442
1443 sub to_api {
1444     my ($self, $params) = @_;
1445
1446     my $response = $self->SUPER::to_api($params);
1447     my $overrides = {};
1448
1449     $overrides->{effective_item_type_id} = $self->effective_itemtype;
1450     $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->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                 my $checkout = $bundle_item->checkout;
1708                 if ($checkout) {
1709                     unless ($options->{force_checkin}) {
1710                         Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
1711                     }
1712
1713                     my $branchcode = C4::Context->userenv->{'branch'};
1714                     my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
1715                     unless ($success) {
1716                         Koha::Exceptions::Checkin::FailedCheckin->throw();
1717                     }
1718                 }
1719
1720                 $self->_result->add_to_item_bundles_hosts(
1721                     { item => $bundle_item->itemnumber } );
1722
1723                 $bundle_item->notforloan($BundleNotLoanValue)->store();
1724             }
1725         );
1726     }
1727     catch {
1728
1729         # 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
1730         if ( ref($_) eq 'DBIx::Class::Exception' ) {
1731             if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1732                 # FK constraints
1733                 # FIXME: MySQL error, if we support more DB engines we should implement this for each
1734                 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1735                     Koha::Exceptions::Object::FKConstraint->throw(
1736                         error     => 'Broken FK constraint',
1737                         broken_fk => $+{column}
1738                     );
1739                 }
1740             }
1741             elsif (
1742                 $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1743             {
1744                 Koha::Exceptions::Object::DuplicateID->throw(
1745                     error        => 'Duplicate ID',
1746                     duplicate_id => $+{key}
1747                 );
1748             }
1749             elsif ( $_->{msg} =~
1750 /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1751               )
1752             {    # The optional \W in the regex might be a quote or backtick
1753                 my $type     = $+{type};
1754                 my $value    = $+{value};
1755                 my $property = $+{property};
1756                 $property =~ s/['`]//g;
1757                 Koha::Exceptions::Object::BadValue->throw(
1758                     type     => $type,
1759                     value    => $value,
1760                     property => $property =~ /(\w+\.\w+)$/
1761                     ? $1
1762                     : $property
1763                     ,    # results in table.column without quotes or backtics
1764                 );
1765             }
1766
1767             # Catch-all for foreign key breakages. It will help find other use cases
1768             $_->rethrow();
1769         }
1770         else {
1771             $_->rethrow();
1772         }
1773     };
1774 }
1775
1776 =head3 remove_from_bundle
1777
1778 Remove this item from any bundle it may have been attached to.
1779
1780 =cut
1781
1782 sub remove_from_bundle {
1783     my ($self) = @_;
1784
1785     my $bundle_item_rs = $self->_result->item_bundles_item;
1786     if ( $bundle_item_rs ) {
1787         $bundle_item_rs->delete;
1788         $self->notforloan(0)->store();
1789         return 1;
1790     }
1791     return 0;
1792 }
1793
1794 =head2 Internal methods
1795
1796 =head3 _after_item_action_hooks
1797
1798 Helper method that takes care of calling all plugin hooks
1799
1800 =cut
1801
1802 sub _after_item_action_hooks {
1803     my ( $self, $params ) = @_;
1804
1805     my $action = $params->{action};
1806
1807     Koha::Plugins->call(
1808         'after_item_action',
1809         {
1810             action  => $action,
1811             item    => $self,
1812             item_id => $self->itemnumber,
1813         }
1814     );
1815 }
1816
1817 =head3 recall
1818
1819     my $recall = $item->recall;
1820
1821 Return the relevant recall for this item
1822
1823 =cut
1824
1825 sub recall {
1826     my ( $self ) = @_;
1827     my @recalls = Koha::Recalls->search(
1828         {
1829             biblio_id => $self->biblionumber,
1830             completed => 0,
1831         },
1832         { order_by => { -asc => 'created_date' } }
1833     )->as_list;
1834     foreach my $recall (@recalls) {
1835         if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1836             return $recall;
1837         }
1838     }
1839     # no item-level recall to return, so return earliest biblio-level
1840     # FIXME: eventually this will be based on priority
1841     return $recalls[0];
1842 }
1843
1844 =head3 can_be_recalled
1845
1846     if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1847
1848 Does item-level checks and returns if items can be recalled by this borrower
1849
1850 =cut
1851
1852 sub can_be_recalled {
1853     my ( $self, $params ) = @_;
1854
1855     return 0 if !( C4::Context->preference('UseRecalls') );
1856
1857     # check if this item is not for loan, withdrawn or lost
1858     return 0 if ( $self->notforloan != 0 );
1859     return 0 if ( $self->itemlost != 0 );
1860     return 0 if ( $self->withdrawn != 0 );
1861
1862     # check if this item is not checked out - if not checked out, can't be recalled
1863     return 0 if ( !defined( $self->checkout ) );
1864
1865     my $patron = $params->{patron};
1866
1867     my $branchcode = C4::Context->userenv->{'branch'};
1868     if ( $patron ) {
1869         $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1870     }
1871
1872     # Check the circulation rule for each relevant itemtype for this item
1873     my $rule = Koha::CirculationRules->get_effective_rules({
1874         branchcode => $branchcode,
1875         categorycode => $patron ? $patron->categorycode : undef,
1876         itemtype => $self->effective_itemtype,
1877         rules => [
1878             'recalls_allowed',
1879             'recalls_per_record',
1880             'on_shelf_recalls',
1881         ],
1882     });
1883
1884     # check recalls allowed has been set and is not zero
1885     return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1886
1887     if ( $patron ) {
1888         # check borrower has not reached open recalls allowed limit
1889         return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1890
1891         # check borrower has not reach open recalls allowed per record limit
1892         return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1893
1894         # check if this patron has already recalled this item
1895         return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1896
1897         # check if this patron has already checked out this item
1898         return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1899
1900         # check if this patron has already reserved this item
1901         return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1902     }
1903
1904     # check item availability
1905     # items are unavailable for recall if they are lost, withdrawn or notforloan
1906     my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1907
1908     # if there are no available items at all, no recall can be placed
1909     return 0 if ( scalar @items == 0 );
1910
1911     my $checked_out_count = 0;
1912     foreach (@items) {
1913         if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1914     }
1915
1916     # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1917     return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1918
1919     # can't recall if no items have been checked out
1920     return 0 if ( $checked_out_count == 0 );
1921
1922     # can recall
1923     return 1;
1924 }
1925
1926 =head3 can_be_waiting_recall
1927
1928     if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1929
1930 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1931 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1932
1933 =cut
1934
1935 sub can_be_waiting_recall {
1936     my ( $self ) = @_;
1937
1938     return 0 if !( C4::Context->preference('UseRecalls') );
1939
1940     # check if this item is not for loan, withdrawn or lost
1941     return 0 if ( $self->notforloan != 0 );
1942     return 0 if ( $self->itemlost != 0 );
1943     return 0 if ( $self->withdrawn != 0 );
1944
1945     my $branchcode = $self->holdingbranch;
1946     if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1947         $branchcode = C4::Context->userenv->{'branch'};
1948     } else {
1949         $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1950     }
1951
1952     # Check the circulation rule for each relevant itemtype for this item
1953     my $most_relevant_recall = $self->check_recalls;
1954     my $rule = Koha::CirculationRules->get_effective_rules(
1955         {
1956             branchcode   => $branchcode,
1957             categorycode => $most_relevant_recall ? $most_relevant_recall->patron->categorycode : undef,
1958             itemtype     => $self->effective_itemtype,
1959             rules        => [ 'recalls_allowed', ],
1960         }
1961     );
1962
1963     # check recalls allowed has been set and is not zero
1964     return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1965
1966     # can recall
1967     return 1;
1968 }
1969
1970 =head3 check_recalls
1971
1972     my $recall = $item->check_recalls;
1973
1974 Get the most relevant recall for this item.
1975
1976 =cut
1977
1978 sub check_recalls {
1979     my ( $self ) = @_;
1980
1981     my @recalls = Koha::Recalls->search(
1982         {   biblio_id => $self->biblionumber,
1983             item_id   => [ $self->itemnumber, undef ]
1984         },
1985         { order_by => { -asc => 'created_date' } }
1986     )->filter_by_current->as_list;
1987
1988     my $recall;
1989     # iterate through relevant recalls to find the best one.
1990     # if we come across a waiting recall, use this one.
1991     # 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.
1992     foreach my $r ( @recalls ) {
1993         if ( $r->waiting ) {
1994             $recall = $r;
1995             last;
1996         }
1997     }
1998     unless ( defined $recall ) {
1999         $recall = $recalls[0];
2000     }
2001
2002     return $recall;
2003 }
2004
2005 =head3 is_notforloan
2006
2007     my $is_notforloan = $item->is_notforloan;
2008
2009 Determine whether or not this item is "notforloan" based on
2010 the item's notforloan status or its item type
2011
2012 =cut
2013
2014 sub is_notforloan {
2015     my ( $self ) = @_;
2016     my $is_notforloan = 0;
2017
2018     if ( $self->notforloan ){
2019         $is_notforloan = 1;
2020     }
2021     else {
2022         my $itemtype = $self->itemtype;
2023         if ($itemtype){
2024             if ( $itemtype->notforloan ){
2025                 $is_notforloan = 1;
2026             }
2027         }
2028     }
2029
2030     return $is_notforloan;
2031 }
2032
2033 =head3 is_denied_renewal
2034
2035     my $is_denied_renewal = $item->is_denied_renewal;
2036
2037 Determine whether or not this item can be renewed based on the
2038 rules set in the ItemsDeniedRenewal system preference.
2039
2040 =cut
2041
2042 sub is_denied_renewal {
2043     my ( $self ) = @_;
2044     my $denyingrules = C4::Context->yaml_preference('ItemsDeniedRenewal');
2045     return 0 unless $denyingrules;
2046     foreach my $field (keys %$denyingrules) {
2047         # Silently ignore bad column names; TODO we should validate elsewhere
2048         next if !$self->_result->result_source->has_column($field);
2049         my $val = $self->$field;
2050         if( !defined $val) {
2051             if ( any { !defined $_ }  @{$denyingrules->{$field}} ){
2052                 return 1;
2053             }
2054         } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
2055            # If the results matches the values in the syspref
2056            # We return true if match found
2057             return 1;
2058         }
2059     }
2060     return 0;
2061 }
2062
2063 =head3 strings_map
2064
2065 Returns a map of column name to string representations including the string,
2066 the mapping type and the mapping category where appropriate.
2067
2068 Currently handles authorised value mappings, library, callnumber and itemtype
2069 expansions.
2070
2071 Accepts a param hashref where the 'public' key denotes whether we want the public
2072 or staff client strings.
2073
2074 =cut
2075
2076 sub strings_map {
2077     my ( $self, $params ) = @_;
2078     my $frameworkcode = C4::Biblio::GetFrameworkCode($self->biblionumber);
2079     my $tagslib       = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2080     my $mss           = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
2081
2082     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
2083
2084     # Hardcoded known 'authorised_value' values mapped to API codes
2085     my $code_to_type = {
2086         branches  => 'library',
2087         cn_source => 'call_number_source',
2088         itemtypes => 'item_type',
2089     };
2090
2091     # Handle not null and default values for integers and dates
2092     my $strings = {};
2093
2094     foreach my $col ( @{$self->_columns} ) {
2095
2096         # By now, we are done with known columns, now check the framework for mappings
2097         my $field = $self->_result->result_source->name . '.' . $col;
2098
2099         # Check there's an entry in the MARC subfield structure for the field
2100         if (   exists $mss->{$field}
2101             && scalar @{ $mss->{$field} } > 0
2102             && $mss->{$field}[0]->{authorised_value} )
2103         {
2104             my $subfield = $mss->{$field}[0];
2105             my $code     = $subfield->{authorised_value};
2106
2107             my $str  = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
2108             my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
2109             $strings->{$col} = {
2110                 str  => $str,
2111                 type => $type,
2112                 ( $type eq 'av' ? ( category => $code ) : () ),
2113             };
2114         }
2115     }
2116
2117     return $strings;
2118 }
2119
2120 =head3 _type
2121
2122 =cut
2123
2124 sub _type {
2125     return 'Item';
2126 }
2127
2128 =head1 AUTHOR
2129
2130 Kyle M Hall <kyle@bywatersolutions.com>
2131
2132 =cut
2133
2134 1;