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