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