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