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