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