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