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