Bug 25755: (QA follow-up) Clarify POD and parameters
[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 Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
26
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
29
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
35
36 use Koha::Checkouts;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::SearchEngine::Indexer;
40 use Koha::Exceptions::Item::Transfer;
41 use Koha::Item::Transfer::Limits;
42 use Koha::Item::Transfers;
43 use Koha::ItemTypes;
44 use Koha::Patrons;
45 use Koha::Plugins;
46 use Koha::Libraries;
47 use Koha::StockRotationItem;
48 use Koha::StockRotationRotas;
49
50 use base qw(Koha::Object);
51
52 =head1 NAME
53
54 Koha::Item - Koha Item object class
55
56 =head1 API
57
58 =head2 Class methods
59
60 =cut
61
62 =head3 store
63
64     $item->store;
65
66 $params can take an optional 'skip_record_index' parameter.
67 If set, the reindexation process will not happen (index_records not called)
68
69 NOTE: This is a temporary fix to answer a performance issue when lot of items
70 are added (or modified) at the same time.
71 The correct way to fix this is to make the ES reindexation process async.
72 You should not turn it on if you do not understand what it is doing exactly.
73
74 =cut
75
76 sub store {
77     my $self = shift;
78     my $params = @_ ? shift : {};
79
80     my $log_action = $params->{log_action} // 1;
81
82     # We do not want to oblige callers to pass this value
83     # Dev conveniences vs performance?
84     unless ( $self->biblioitemnumber ) {
85         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
86     }
87
88     # See related changes from C4::Items::AddItem
89     unless ( $self->itype ) {
90         $self->itype($self->biblio->biblioitem->itemtype);
91     }
92
93     my $today  = dt_from_string;
94     my $action = 'create';
95
96     unless ( $self->in_storage ) { #AddItem
97         unless ( $self->permanent_location ) {
98             $self->permanent_location($self->location);
99         }
100         unless ( $self->replacementpricedate ) {
101             $self->replacementpricedate($today);
102         }
103         unless ( $self->datelastseen ) {
104             $self->datelastseen($today);
105         }
106
107         unless ( $self->dateaccessioned ) {
108             $self->dateaccessioned($today);
109         }
110
111         if (   $self->itemcallnumber
112             or $self->cn_source )
113         {
114             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
115             $self->cn_sort($cn_sort);
116         }
117
118     } else { # ModItem
119
120         $action = 'modify';
121
122         my %updated_columns = $self->_result->get_dirty_columns;
123         return $self->SUPER::store unless %updated_columns;
124
125         # Retrieve the item for comparison if we need to
126         my $pre_mod_item = (
127                  exists $updated_columns{itemlost}
128               or exists $updated_columns{withdrawn}
129               or exists $updated_columns{damaged}
130         ) ? $self->get_from_storage : undef;
131
132         # Update *_on  fields if needed
133         # FIXME: Why not for AddItem as well?
134         my @fields = qw( itemlost withdrawn damaged );
135         for my $field (@fields) {
136
137             # If the field is defined but empty or 0, we are
138             # removing/unsetting and thus need to clear out
139             # the 'on' field
140             if (   exists $updated_columns{$field}
141                 && defined( $self->$field )
142                 && !$self->$field )
143             {
144                 my $field_on = "${field}_on";
145                 $self->$field_on(undef);
146             }
147             # If the field has changed otherwise, we much update
148             # the 'on' field
149             elsif (exists $updated_columns{$field}
150                 && $updated_columns{$field}
151                 && !$pre_mod_item->$field )
152             {
153                 my $field_on = "${field}_on";
154                 $self->$field_on(
155                     DateTime::Format::MySQL->format_datetime(
156                         dt_from_string()
157                     )
158                 );
159             }
160         }
161
162         if (   exists $updated_columns{itemcallnumber}
163             or exists $updated_columns{cn_source} )
164         {
165             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
166             $self->cn_sort($cn_sort);
167         }
168
169
170         if (    exists $updated_columns{location}
171             and $self->location ne 'CART'
172             and $self->location ne 'PROC'
173             and not exists $updated_columns{permanent_location} )
174         {
175             $self->permanent_location( $self->location );
176         }
177
178         # If item was lost and has now been found,
179         # reverse any list item charges if necessary.
180         if (    exists $updated_columns{itemlost}
181             and $updated_columns{itemlost} <= 0
182             and $pre_mod_item->itemlost > 0 )
183         {
184             $self->_set_found_trigger($pre_mod_item);
185         }
186
187     }
188
189     unless ( $self->dateaccessioned ) {
190         $self->dateaccessioned($today);
191     }
192
193     my $result = $self->SUPER::store;
194     if ( $log_action && C4::Context->preference("CataloguingLog") ) {
195         $action eq 'create'
196           ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
197           : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
198     }
199     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
200     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
201         unless $params->{skip_record_index};
202     $self->get_from_storage->_after_item_action_hooks({ action => $action });
203
204     return $result;
205 }
206
207 =head3 delete
208
209 =cut
210
211 sub delete {
212     my $self = shift;
213     my $params = @_ ? shift : {};
214
215     # FIXME check the item has no current issues
216     # i.e. raise the appropriate exception
217
218     my $result = $self->SUPER::delete;
219
220     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
221     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
222         unless $params->{skip_record_index};
223
224     $self->_after_item_action_hooks({ action => 'delete' });
225
226     logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
227       if C4::Context->preference("CataloguingLog");
228
229     return $result;
230 }
231
232 =head3 safe_delete
233
234 =cut
235
236 sub safe_delete {
237     my $self = shift;
238     my $params = @_ ? shift : {};
239
240     my $safe_to_delete = $self->safe_to_delete;
241     return $safe_to_delete unless $safe_to_delete eq '1';
242
243     $self->move_to_deleted;
244
245     return $self->delete($params);
246 }
247
248 =head3 safe_to_delete
249
250 returns 1 if the item is safe to delete,
251
252 "book_on_loan" if the item is checked out,
253
254 "not_same_branch" if the item is blocked by independent branches,
255
256 "book_reserved" if the there are holds aganst the item, or
257
258 "linked_analytics" if the item has linked analytic records.
259
260 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
261
262 =cut
263
264 sub safe_to_delete {
265     my ($self) = @_;
266
267     return "book_on_loan" if $self->checkout;
268
269     return "not_same_branch"
270       if defined C4::Context->userenv
271       and !C4::Context->IsSuperLibrarian()
272       and C4::Context->preference("IndependentBranches")
273       and ( C4::Context->userenv->{branch} ne $self->homebranch );
274
275     # check it doesn't have a waiting reserve
276     return "book_reserved"
277       if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
278
279     return "linked_analytics"
280       if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
281
282     return "last_item_for_hold"
283       if $self->biblio->items->count == 1
284       && $self->biblio->holds->search(
285           {
286               itemnumber => undef,
287           }
288         )->count;
289
290     return 1;
291 }
292
293 =head3 move_to_deleted
294
295 my $is_moved = $item->move_to_deleted;
296
297 Move an item to the deleteditems table.
298 This can be done before deleting an item, to make sure the data are not completely deleted.
299
300 =cut
301
302 sub move_to_deleted {
303     my ($self) = @_;
304     my $item_infos = $self->unblessed;
305     delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
306     return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
307 }
308
309
310 =head3 effective_itemtype
311
312 Returns the itemtype for the item based on whether item level itemtypes are set or not.
313
314 =cut
315
316 sub effective_itemtype {
317     my ( $self ) = @_;
318
319     return $self->_result()->effective_itemtype();
320 }
321
322 =head3 home_branch
323
324 =cut
325
326 sub home_branch {
327     my ($self) = @_;
328
329     $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
330
331     return $self->{_home_branch};
332 }
333
334 =head3 holding_branch
335
336 =cut
337
338 sub holding_branch {
339     my ($self) = @_;
340
341     $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
342
343     return $self->{_holding_branch};
344 }
345
346 =head3 biblio
347
348 my $biblio = $item->biblio;
349
350 Return the bibliographic record of this item
351
352 =cut
353
354 sub biblio {
355     my ( $self ) = @_;
356     my $biblio_rs = $self->_result->biblio;
357     return Koha::Biblio->_new_from_dbic( $biblio_rs );
358 }
359
360 =head3 biblioitem
361
362 my $biblioitem = $item->biblioitem;
363
364 Return the biblioitem record of this item
365
366 =cut
367
368 sub biblioitem {
369     my ( $self ) = @_;
370     my $biblioitem_rs = $self->_result->biblioitem;
371     return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
372 }
373
374 =head3 checkout
375
376 my $checkout = $item->checkout;
377
378 Return the checkout for this item
379
380 =cut
381
382 sub checkout {
383     my ( $self ) = @_;
384     my $checkout_rs = $self->_result->issue;
385     return unless $checkout_rs;
386     return Koha::Checkout->_new_from_dbic( $checkout_rs );
387 }
388
389 =head3 holds
390
391 my $holds = $item->holds();
392 my $holds = $item->holds($params);
393 my $holds = $item->holds({ found => 'W'});
394
395 Return holds attached to an item, optionally accept a hashref of params to pass to search
396
397 =cut
398
399 sub holds {
400     my ( $self,$params ) = @_;
401     my $holds_rs = $self->_result->reserves->search($params);
402     return Koha::Holds->_new_from_dbic( $holds_rs );
403 }
404
405 =head3 request_transfer
406
407   my $transfer = $item->request_transfer(
408       { to => $to_library, reason => $reason, ignore_limits => 0 } );
409
410 Add a transfer request for this item to the given branch for the given reason.
411
412 An exception will be thrown if the BranchTransferLimits would prevent the requested
413 transfer, unless 'ignore_limits' is passed to override the limits.
414
415 Note: At this time, only one active transfer (i.e pending arrival date) may exist
416 at a time for any given item. An exception will be thrown should you attempt to
417 add a request when a transfer has already been queued, whether it is in transit
418 or just at the request stage.
419
420 =cut
421
422 sub request_transfer {
423     my ( $self, $params ) = @_;
424
425     # check for mandatory params
426     my @mandatory = ( 'to', 'reason' );
427     for my $param (@mandatory) {
428         unless ( defined( $params->{$param} ) ) {
429             Koha::Exceptions::MissingParameter->throw(
430                 error => "The $param parameter is mandatory" );
431         }
432     }
433
434     my $request;
435     Koha::Exceptions::Item::Transfer::Found->throw( transfer => $request )
436       if ( $request = $self->get_transfer );
437
438     Koha::Exceptions::Item::Transfer::Limit->throw()
439       unless ( $params->{ignore_limits}
440         || $self->can_be_transferred( { to => $params->{to} } ) );
441
442     my $transfer = Koha::Item::Transfer->new(
443         {
444             itemnumber    => $self->itemnumber,
445             daterequested => dt_from_string,
446             frombranch    => $self->holdingbranch,
447             tobranch      => $params->{to}->branchcode,
448             reason        => $params->{reason},
449             comments      => $params->{comment}
450         }
451     )->store();
452     return $transfer;
453 }
454
455 =head3 get_transfer
456
457   my $transfer = $item->get_transfer;
458
459 Return the active transfer request or undef
460
461 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
462 whereby the most recently sent, but not recieved, transfer will be returned
463 if it exists, otherwise the oldest unsatisfied transfer will be returned.
464
465 FIXME: Add Tests for FIFO functionality
466
467 =cut
468
469 sub get_transfer {
470     my ($self) = @_;
471     my $transfer_rs = $self->_result->branchtransfers->search(
472         { datearrived => undef },
473         {
474             order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
475             rows     => 1
476         }
477     )->first;
478     return unless $transfer_rs;
479     return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
480 }
481
482 =head3 last_returned_by
483
484 Gets and sets the last borrower to return an item.
485
486 Accepts and returns Koha::Patron objects
487
488 $item->last_returned_by( $borrowernumber );
489
490 $last_returned_by = $item->last_returned_by();
491
492 =cut
493
494 sub last_returned_by {
495     my ( $self, $borrower ) = @_;
496
497     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
498
499     if ($borrower) {
500         return $items_last_returned_by_rs->update_or_create(
501             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
502     }
503     else {
504         unless ( $self->{_last_returned_by} ) {
505             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
506             if ($result) {
507                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
508             }
509         }
510
511         return $self->{_last_returned_by};
512     }
513 }
514
515 =head3 can_article_request
516
517 my $bool = $item->can_article_request( $borrower )
518
519 Returns true if item can be specifically requested
520
521 $borrower must be a Koha::Patron object
522
523 =cut
524
525 sub can_article_request {
526     my ( $self, $borrower ) = @_;
527
528     my $rule = $self->article_request_type($borrower);
529
530     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
531     return q{};
532 }
533
534 =head3 hidden_in_opac
535
536 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
537
538 Returns true if item fields match the hidding criteria defined in $rules.
539 Returns false otherwise.
540
541 Takes HASHref that can have the following parameters:
542     OPTIONAL PARAMETERS:
543     $rules : { <field> => [ value_1, ... ], ... }
544
545 Note: $rules inherits its structure from the parsed YAML from reading
546 the I<OpacHiddenItems> system preference.
547
548 =cut
549
550 sub hidden_in_opac {
551     my ( $self, $params ) = @_;
552
553     my $rules = $params->{rules} // {};
554
555     return 1
556         if C4::Context->preference('hidelostitems') and
557            $self->itemlost > 0;
558
559     my $hidden_in_opac = 0;
560
561     foreach my $field ( keys %{$rules} ) {
562
563         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
564             $hidden_in_opac = 1;
565             last;
566         }
567     }
568
569     return $hidden_in_opac;
570 }
571
572 =head3 can_be_transferred
573
574 $item->can_be_transferred({ to => $to_library, from => $from_library })
575 Checks if an item can be transferred to given library.
576
577 This feature is controlled by two system preferences:
578 UseBranchTransferLimits to enable / disable the feature
579 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
580                          for setting the limitations
581
582 Takes HASHref that can have the following parameters:
583     MANDATORY PARAMETERS:
584     $to   : Koha::Library
585     OPTIONAL PARAMETERS:
586     $from : Koha::Library  # if not given, item holdingbranch
587                            # will be used instead
588
589 Returns 1 if item can be transferred to $to_library, otherwise 0.
590
591 To find out whether at least one item of a Koha::Biblio can be transferred, please
592 see Koha::Biblio->can_be_transferred() instead of using this method for
593 multiple items of the same biblio.
594
595 =cut
596
597 sub can_be_transferred {
598     my ($self, $params) = @_;
599
600     my $to   = $params->{to};
601     my $from = $params->{from};
602
603     $to   = $to->branchcode;
604     $from = defined $from ? $from->branchcode : $self->holdingbranch;
605
606     return 1 if $from eq $to; # Transfer to current branch is allowed
607     return 1 unless C4::Context->preference('UseBranchTransferLimits');
608
609     my $limittype = C4::Context->preference('BranchTransferLimitsType');
610     return Koha::Item::Transfer::Limits->search({
611         toBranch => $to,
612         fromBranch => $from,
613         $limittype => $limittype eq 'itemtype'
614                         ? $self->effective_itemtype : $self->ccode
615     })->count ? 0 : 1;
616
617 }
618
619 =head3 pickup_locations
620
621 $pickup_locations = $item->pickup_locations( {patron => $patron } )
622
623 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
624 and if item can be transferred to each pickup location.
625
626 =cut
627
628 sub pickup_locations {
629     my ($self, $params) = @_;
630
631     my $patron = $params->{patron};
632
633     my $circ_control_branch =
634       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
635     my $branchitemrule =
636       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
637
638     if(defined $patron) {
639         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
640         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
641     }
642
643     my $pickup_libraries = Koha::Libraries->search();
644     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
645         $pickup_libraries = $self->home_branch->get_hold_libraries;
646     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
647         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
648         $pickup_libraries = $plib->get_hold_libraries;
649     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
650         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
651     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
652         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
653     };
654
655     return $pickup_libraries->search(
656         {
657             pickup_location => 1
658         },
659         {
660             order_by => ['branchname']
661         }
662     ) unless C4::Context->preference('UseBranchTransferLimits');
663
664     my $limittype = C4::Context->preference('BranchTransferLimitsType');
665     my ($ccode, $itype) = (undef, undef);
666     if( $limittype eq 'ccode' ){
667         $ccode = $self->ccode;
668     } else {
669         $itype = $self->itype;
670     }
671     my $limits = Koha::Item::Transfer::Limits->search(
672         {
673             fromBranch => $self->holdingbranch,
674             ccode      => $ccode,
675             itemtype   => $itype,
676         },
677         { columns => ['toBranch'] }
678     );
679
680     return $pickup_libraries->search(
681         {
682             pickup_location => 1,
683             branchcode      => {
684                 '-not_in' => $limits->_resultset->as_query
685             }
686         },
687         {
688             order_by => ['branchname']
689         }
690     );
691 }
692
693 =head3 article_request_type
694
695 my $type = $item->article_request_type( $borrower )
696
697 returns 'yes', 'no', 'bib_only', or 'item_only'
698
699 $borrower must be a Koha::Patron object
700
701 =cut
702
703 sub article_request_type {
704     my ( $self, $borrower ) = @_;
705
706     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
707     my $branchcode =
708         $branch_control eq 'homebranch'    ? $self->homebranch
709       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
710       :                                      undef;
711     my $borrowertype = $borrower->categorycode;
712     my $itemtype = $self->effective_itemtype();
713     my $rule = Koha::CirculationRules->get_effective_rule(
714         {
715             rule_name    => 'article_requests',
716             categorycode => $borrowertype,
717             itemtype     => $itemtype,
718             branchcode   => $branchcode
719         }
720     );
721
722     return q{} unless $rule;
723     return $rule->rule_value || q{}
724 }
725
726 =head3 current_holds
727
728 =cut
729
730 sub current_holds {
731     my ( $self ) = @_;
732     my $attributes = { order_by => 'priority' };
733     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
734     my $params = {
735         itemnumber => $self->itemnumber,
736         suspend => 0,
737         -or => [
738             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
739             waitingdate => { '!=' => undef },
740         ],
741     };
742     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
743     return Koha::Holds->_new_from_dbic($hold_rs);
744 }
745
746 =head3 stockrotationitem
747
748   my $sritem = Koha::Item->stockrotationitem;
749
750 Returns the stock rotation item associated with the current item.
751
752 =cut
753
754 sub stockrotationitem {
755     my ( $self ) = @_;
756     my $rs = $self->_result->stockrotationitem;
757     return 0 if !$rs;
758     return Koha::StockRotationItem->_new_from_dbic( $rs );
759 }
760
761 =head3 add_to_rota
762
763   my $item = $item->add_to_rota($rota_id);
764
765 Add this item to the rota identified by $ROTA_ID, which means associating it
766 with the first stage of that rota.  Should this item already be associated
767 with a rota, then we will move it to the new rota.
768
769 =cut
770
771 sub add_to_rota {
772     my ( $self, $rota_id ) = @_;
773     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
774     return $self;
775 }
776
777 =head3 has_pending_hold
778
779   my $is_pending_hold = $item->has_pending_hold();
780
781 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
782
783 =cut
784
785 sub has_pending_hold {
786     my ( $self ) = @_;
787     my $pending_hold = $self->_result->tmp_holdsqueues;
788     return $pending_hold->count ? 1: 0;
789 }
790
791 =head3 as_marc_field
792
793     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
794     my $field = $item->as_marc_field({ [ mss => $mss ] });
795
796 This method returns a MARC::Field object representing the Koha::Item object
797 with the current mappings configuration.
798
799 =cut
800
801 sub as_marc_field {
802     my ( $self, $params ) = @_;
803
804     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
805     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
806
807     my @subfields;
808
809     my @columns = $self->_result->result_source->columns;
810
811     foreach my $item_field ( @columns ) {
812         my $mapping = $mss->{ "items.$item_field"}[0];
813         my $tagfield    = $mapping->{tagfield};
814         my $tagsubfield = $mapping->{tagsubfield};
815         next if !$tagfield; # TODO: Should we raise an exception instead?
816                             # Feels like safe fallback is better
817
818         push @subfields, $tagsubfield => $self->$item_field
819             if defined $self->$item_field and $item_field ne '';
820     }
821
822     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
823     push( @subfields, @{$unlinked_item_subfields} )
824         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
825
826     my $field;
827
828     $field = MARC::Field->new(
829         "$item_tag", ' ', ' ', @subfields
830     ) if @subfields;
831
832     return $field;
833 }
834
835 =head3 renewal_branchcode
836
837 Returns the branchcode to be recorded in statistics renewal of the item
838
839 =cut
840
841 sub renewal_branchcode {
842
843     my ($self, $params ) = @_;
844
845     my $interface = C4::Context->interface;
846     my $branchcode;
847     if ( $interface eq 'opac' ){
848         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
849         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
850             $branchcode = 'OPACRenew';
851         }
852         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
853             $branchcode = $self->homebranch;
854         }
855         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
856             $branchcode = $self->checkout->patron->branchcode;
857         }
858         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
859             $branchcode = $self->checkout->branchcode;
860         }
861         else {
862             $branchcode = "";
863         }
864     } else {
865         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
866             ? C4::Context->userenv->{branch} : $params->{branch};
867     }
868     return $branchcode;
869 }
870
871 =head3 cover_images
872
873 Return the cover images associated with this item.
874
875 =cut
876
877 sub cover_images {
878     my ( $self ) = @_;
879
880     my $cover_image_rs = $self->_result->cover_images;
881     return unless $cover_image_rs;
882     return Koha::CoverImages->_new_from_dbic($cover_image_rs);
883 }
884
885 =head3 _set_found_trigger
886
887     $self->_set_found_trigger
888
889 Finds the most recent lost item charge for this item and refunds the patron
890 appropriately, taking into account any payments or writeoffs already applied
891 against the charge.
892
893 Internal function, not exported, called only by Koha::Item->store.
894
895 =cut
896
897 sub _set_found_trigger {
898     my ( $self, $pre_mod_item ) = @_;
899
900     ## If item was lost, it has now been found, reverse any list item charges if necessary.
901     my $no_refund_after_days =
902       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
903     if ($no_refund_after_days) {
904         my $today = dt_from_string();
905         my $lost_age_in_days =
906           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
907           ->in_units('days');
908
909         return $self unless $lost_age_in_days < $no_refund_after_days;
910     }
911
912     my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
913         {
914             item          => $self,
915             return_branch => C4::Context->userenv
916             ? C4::Context->userenv->{'branch'}
917             : undef,
918         }
919       );
920
921     if ( $lostreturn_policy ) {
922
923         # refund charge made for lost book
924         my $lost_charge = Koha::Account::Lines->search(
925             {
926                 itemnumber      => $self->itemnumber,
927                 debit_type_code => 'LOST',
928                 status          => [ undef, { '<>' => 'FOUND' } ]
929             },
930             {
931                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
932                 rows     => 1
933             }
934         )->single;
935
936         if ( $lost_charge ) {
937
938             my $patron = $lost_charge->patron;
939             if ( $patron ) {
940
941                 my $account = $patron->account;
942                 my $total_to_refund = 0;
943
944                 # Use cases
945                 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
946
947                     # some amount has been cancelled. collect the offsets that are not writeoffs
948                     # this works because the only way to subtract from this kind of a debt is
949                     # using the UI buttons 'Pay' and 'Write off'
950                     my $credits_offsets = Koha::Account::Offsets->search(
951                         {
952                             debit_id  => $lost_charge->id,
953                             credit_id => { '!=' => undef },     # it is not the debit itself
954                             type      => { '!=' => 'Writeoff' },
955                             amount    => { '<' => 0 }    # credits are negative on the DB
956                         }
957                     );
958
959                     $total_to_refund = ( $credits_offsets->count > 0 )
960                       ? $credits_offsets->total * -1    # credits are negative on the DB
961                       : 0;
962                 }
963
964                 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
965
966                 my $credit;
967                 if ( $credit_total > 0 ) {
968                     my $branchcode =
969                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
970                     $credit = $account->add_credit(
971                         {
972                             amount      => $credit_total,
973                             description => 'Item found ' . $self->itemnumber,
974                             type        => 'LOST_FOUND',
975                             interface   => C4::Context->interface,
976                             library_id  => $branchcode,
977                             item_id     => $self->itemnumber,
978                             issue_id    => $lost_charge->issue_id
979                         }
980                     );
981
982                     $credit->apply( { debits => [$lost_charge] } );
983                     $self->{_refunded} = 1;
984                 }
985
986                 # Update the account status
987                 $lost_charge->status('FOUND');
988                 $lost_charge->store();
989
990                 # Reconcile balances if required
991                 if ( C4::Context->preference('AccountAutoReconcile') ) {
992                     $account->reconcile_balance;
993                 }
994             }
995         }
996
997         # restore fine for lost book
998         if ( $lostreturn_policy eq 'restore' ) {
999             my $lost_overdue = Koha::Account::Lines->search(
1000                 {
1001                     itemnumber      => $self->itemnumber,
1002                     debit_type_code => 'OVERDUE',
1003                     status          => 'LOST'
1004                 },
1005                 {
1006                     order_by => { '-desc' => 'date' },
1007                     rows     => 1
1008                 }
1009             )->single;
1010
1011             if ( $lost_overdue ) {
1012
1013                 my $patron = $lost_overdue->patron;
1014                 if ($patron) {
1015                     my $account = $patron->account;
1016
1017                     # Update status of fine
1018                     $lost_overdue->status('FOUND')->store();
1019
1020                     # Find related forgive credit
1021                     my $refund = $lost_overdue->credits(
1022                         {
1023                             credit_type_code => 'FORGIVEN',
1024                             itemnumber       => $self->itemnumber,
1025                             status           => [ { '!=' => 'VOID' }, undef ]
1026                         },
1027                         { order_by => { '-desc' => 'date' }, rows => 1 }
1028                     )->single;
1029
1030                     if ( $refund ) {
1031                         # Revert the forgive credit
1032                         $refund->void();
1033                         $self->{_restored} = 1;
1034                     }
1035
1036                     # Reconcile balances if required
1037                     if ( C4::Context->preference('AccountAutoReconcile') ) {
1038                         $account->reconcile_balance;
1039                     }
1040                 }
1041             }
1042         } elsif ( $lostreturn_policy eq 'charge' ) {
1043             $self->{_charge} = 1;
1044         }
1045     }
1046
1047     return $self;
1048 }
1049
1050 =head3 to_api_mapping
1051
1052 This method returns the mapping for representing a Koha::Item object
1053 on the API.
1054
1055 =cut
1056
1057 sub to_api_mapping {
1058     return {
1059         itemnumber               => 'item_id',
1060         biblionumber             => 'biblio_id',
1061         biblioitemnumber         => undef,
1062         barcode                  => 'external_id',
1063         dateaccessioned          => 'acquisition_date',
1064         booksellerid             => 'acquisition_source',
1065         homebranch               => 'home_library_id',
1066         price                    => 'purchase_price',
1067         replacementprice         => 'replacement_price',
1068         replacementpricedate     => 'replacement_price_date',
1069         datelastborrowed         => 'last_checkout_date',
1070         datelastseen             => 'last_seen_date',
1071         stack                    => undef,
1072         notforloan               => 'not_for_loan_status',
1073         damaged                  => 'damaged_status',
1074         damaged_on               => 'damaged_date',
1075         itemlost                 => 'lost_status',
1076         itemlost_on              => 'lost_date',
1077         withdrawn                => 'withdrawn',
1078         withdrawn_on             => 'withdrawn_date',
1079         itemcallnumber           => 'callnumber',
1080         coded_location_qualifier => 'coded_location_qualifier',
1081         issues                   => 'checkouts_count',
1082         renewals                 => 'renewals_count',
1083         reserves                 => 'holds_count',
1084         restricted               => 'restricted_status',
1085         itemnotes                => 'public_notes',
1086         itemnotes_nonpublic      => 'internal_notes',
1087         holdingbranch            => 'holding_library_id',
1088         timestamp                => 'timestamp',
1089         location                 => 'location',
1090         permanent_location       => 'permanent_location',
1091         onloan                   => 'checked_out_date',
1092         cn_source                => 'call_number_source',
1093         cn_sort                  => 'call_number_sort',
1094         ccode                    => 'collection_code',
1095         materials                => 'materials_notes',
1096         uri                      => 'uri',
1097         itype                    => 'item_type',
1098         more_subfields_xml       => 'extended_subfields',
1099         enumchron                => 'serial_issue_number',
1100         copynumber               => 'copy_number',
1101         stocknumber              => 'inventory_number',
1102         new_status               => 'new_status'
1103     };
1104 }
1105
1106 =head3 itemtype
1107
1108     my $itemtype = $item->itemtype;
1109
1110     Returns Koha object for effective itemtype
1111
1112 =cut
1113
1114 sub itemtype {
1115     my ( $self ) = @_;
1116     return Koha::ItemTypes->find( $self->effective_itemtype );
1117 }
1118
1119 =head2 Internal methods
1120
1121 =head3 _after_item_action_hooks
1122
1123 Helper method that takes care of calling all plugin hooks
1124
1125 =cut
1126
1127 sub _after_item_action_hooks {
1128     my ( $self, $params ) = @_;
1129
1130     my $action = $params->{action};
1131
1132     Koha::Plugins->call(
1133         'after_item_action',
1134         {
1135             action  => $action,
1136             item    => $self,
1137             item_id => $self->itemnumber,
1138         }
1139     );
1140 }
1141
1142 =head3 _type
1143
1144 =cut
1145
1146 sub _type {
1147     return 'Item';
1148 }
1149
1150 =head1 AUTHOR
1151
1152 Kyle M Hall <kyle@bywatersolutions.com>
1153
1154 =cut
1155
1156 1;