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