Bug 25755: Fix typo in POD
[srvgit] / 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 received, transfer will be returned
463 if it exists, otherwise the oldest unsatisfied transfer will be returned.
464
465 =cut
466
467 sub get_transfer {
468     my ($self) = @_;
469     my $transfer_rs = $self->_result->branchtransfers->search(
470         { datearrived => undef },
471         {
472             order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
473             rows     => 1
474         }
475     )->first;
476     return unless $transfer_rs;
477     return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
478 }
479
480 =head3 last_returned_by
481
482 Gets and sets the last borrower to return an item.
483
484 Accepts and returns Koha::Patron objects
485
486 $item->last_returned_by( $borrowernumber );
487
488 $last_returned_by = $item->last_returned_by();
489
490 =cut
491
492 sub last_returned_by {
493     my ( $self, $borrower ) = @_;
494
495     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
496
497     if ($borrower) {
498         return $items_last_returned_by_rs->update_or_create(
499             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
500     }
501     else {
502         unless ( $self->{_last_returned_by} ) {
503             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
504             if ($result) {
505                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
506             }
507         }
508
509         return $self->{_last_returned_by};
510     }
511 }
512
513 =head3 can_article_request
514
515 my $bool = $item->can_article_request( $borrower )
516
517 Returns true if item can be specifically requested
518
519 $borrower must be a Koha::Patron object
520
521 =cut
522
523 sub can_article_request {
524     my ( $self, $borrower ) = @_;
525
526     my $rule = $self->article_request_type($borrower);
527
528     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
529     return q{};
530 }
531
532 =head3 hidden_in_opac
533
534 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
535
536 Returns true if item fields match the hidding criteria defined in $rules.
537 Returns false otherwise.
538
539 Takes HASHref that can have the following parameters:
540     OPTIONAL PARAMETERS:
541     $rules : { <field> => [ value_1, ... ], ... }
542
543 Note: $rules inherits its structure from the parsed YAML from reading
544 the I<OpacHiddenItems> system preference.
545
546 =cut
547
548 sub hidden_in_opac {
549     my ( $self, $params ) = @_;
550
551     my $rules = $params->{rules} // {};
552
553     return 1
554         if C4::Context->preference('hidelostitems') and
555            $self->itemlost > 0;
556
557     my $hidden_in_opac = 0;
558
559     foreach my $field ( keys %{$rules} ) {
560
561         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
562             $hidden_in_opac = 1;
563             last;
564         }
565     }
566
567     return $hidden_in_opac;
568 }
569
570 =head3 can_be_transferred
571
572 $item->can_be_transferred({ to => $to_library, from => $from_library })
573 Checks if an item can be transferred to given library.
574
575 This feature is controlled by two system preferences:
576 UseBranchTransferLimits to enable / disable the feature
577 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
578                          for setting the limitations
579
580 Takes HASHref that can have the following parameters:
581     MANDATORY PARAMETERS:
582     $to   : Koha::Library
583     OPTIONAL PARAMETERS:
584     $from : Koha::Library  # if not given, item holdingbranch
585                            # will be used instead
586
587 Returns 1 if item can be transferred to $to_library, otherwise 0.
588
589 To find out whether at least one item of a Koha::Biblio can be transferred, please
590 see Koha::Biblio->can_be_transferred() instead of using this method for
591 multiple items of the same biblio.
592
593 =cut
594
595 sub can_be_transferred {
596     my ($self, $params) = @_;
597
598     my $to   = $params->{to};
599     my $from = $params->{from};
600
601     $to   = $to->branchcode;
602     $from = defined $from ? $from->branchcode : $self->holdingbranch;
603
604     return 1 if $from eq $to; # Transfer to current branch is allowed
605     return 1 unless C4::Context->preference('UseBranchTransferLimits');
606
607     my $limittype = C4::Context->preference('BranchTransferLimitsType');
608     return Koha::Item::Transfer::Limits->search({
609         toBranch => $to,
610         fromBranch => $from,
611         $limittype => $limittype eq 'itemtype'
612                         ? $self->effective_itemtype : $self->ccode
613     })->count ? 0 : 1;
614
615 }
616
617 =head3 pickup_locations
618
619 $pickup_locations = $item->pickup_locations( {patron => $patron } )
620
621 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)
622 and if item can be transferred to each pickup location.
623
624 =cut
625
626 sub pickup_locations {
627     my ($self, $params) = @_;
628
629     my $patron = $params->{patron};
630
631     my $circ_control_branch =
632       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
633     my $branchitemrule =
634       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
635
636     if(defined $patron) {
637         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
638         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
639     }
640
641     my $pickup_libraries = Koha::Libraries->search();
642     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
643         $pickup_libraries = $self->home_branch->get_hold_libraries;
644     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
645         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
646         $pickup_libraries = $plib->get_hold_libraries;
647     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
648         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
649     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
650         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
651     };
652
653     return $pickup_libraries->search(
654         {
655             pickup_location => 1
656         },
657         {
658             order_by => ['branchname']
659         }
660     ) unless C4::Context->preference('UseBranchTransferLimits');
661
662     my $limittype = C4::Context->preference('BranchTransferLimitsType');
663     my ($ccode, $itype) = (undef, undef);
664     if( $limittype eq 'ccode' ){
665         $ccode = $self->ccode;
666     } else {
667         $itype = $self->itype;
668     }
669     my $limits = Koha::Item::Transfer::Limits->search(
670         {
671             fromBranch => $self->holdingbranch,
672             ccode      => $ccode,
673             itemtype   => $itype,
674         },
675         { columns => ['toBranch'] }
676     );
677
678     return $pickup_libraries->search(
679         {
680             pickup_location => 1,
681             branchcode      => {
682                 '-not_in' => $limits->_resultset->as_query
683             }
684         },
685         {
686             order_by => ['branchname']
687         }
688     );
689 }
690
691 =head3 article_request_type
692
693 my $type = $item->article_request_type( $borrower )
694
695 returns 'yes', 'no', 'bib_only', or 'item_only'
696
697 $borrower must be a Koha::Patron object
698
699 =cut
700
701 sub article_request_type {
702     my ( $self, $borrower ) = @_;
703
704     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
705     my $branchcode =
706         $branch_control eq 'homebranch'    ? $self->homebranch
707       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
708       :                                      undef;
709     my $borrowertype = $borrower->categorycode;
710     my $itemtype = $self->effective_itemtype();
711     my $rule = Koha::CirculationRules->get_effective_rule(
712         {
713             rule_name    => 'article_requests',
714             categorycode => $borrowertype,
715             itemtype     => $itemtype,
716             branchcode   => $branchcode
717         }
718     );
719
720     return q{} unless $rule;
721     return $rule->rule_value || q{}
722 }
723
724 =head3 current_holds
725
726 =cut
727
728 sub current_holds {
729     my ( $self ) = @_;
730     my $attributes = { order_by => 'priority' };
731     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
732     my $params = {
733         itemnumber => $self->itemnumber,
734         suspend => 0,
735         -or => [
736             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
737             waitingdate => { '!=' => undef },
738         ],
739     };
740     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
741     return Koha::Holds->_new_from_dbic($hold_rs);
742 }
743
744 =head3 stockrotationitem
745
746   my $sritem = Koha::Item->stockrotationitem;
747
748 Returns the stock rotation item associated with the current item.
749
750 =cut
751
752 sub stockrotationitem {
753     my ( $self ) = @_;
754     my $rs = $self->_result->stockrotationitem;
755     return 0 if !$rs;
756     return Koha::StockRotationItem->_new_from_dbic( $rs );
757 }
758
759 =head3 add_to_rota
760
761   my $item = $item->add_to_rota($rota_id);
762
763 Add this item to the rota identified by $ROTA_ID, which means associating it
764 with the first stage of that rota.  Should this item already be associated
765 with a rota, then we will move it to the new rota.
766
767 =cut
768
769 sub add_to_rota {
770     my ( $self, $rota_id ) = @_;
771     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
772     return $self;
773 }
774
775 =head3 has_pending_hold
776
777   my $is_pending_hold = $item->has_pending_hold();
778
779 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
780
781 =cut
782
783 sub has_pending_hold {
784     my ( $self ) = @_;
785     my $pending_hold = $self->_result->tmp_holdsqueues;
786     return $pending_hold->count ? 1: 0;
787 }
788
789 =head3 as_marc_field
790
791     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
792     my $field = $item->as_marc_field({ [ mss => $mss ] });
793
794 This method returns a MARC::Field object representing the Koha::Item object
795 with the current mappings configuration.
796
797 =cut
798
799 sub as_marc_field {
800     my ( $self, $params ) = @_;
801
802     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
803     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
804
805     my @subfields;
806
807     my @columns = $self->_result->result_source->columns;
808
809     foreach my $item_field ( @columns ) {
810         my $mapping = $mss->{ "items.$item_field"}[0];
811         my $tagfield    = $mapping->{tagfield};
812         my $tagsubfield = $mapping->{tagsubfield};
813         next if !$tagfield; # TODO: Should we raise an exception instead?
814                             # Feels like safe fallback is better
815
816         push @subfields, $tagsubfield => $self->$item_field
817             if defined $self->$item_field and $item_field ne '';
818     }
819
820     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
821     push( @subfields, @{$unlinked_item_subfields} )
822         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
823
824     my $field;
825
826     $field = MARC::Field->new(
827         "$item_tag", ' ', ' ', @subfields
828     ) if @subfields;
829
830     return $field;
831 }
832
833 =head3 renewal_branchcode
834
835 Returns the branchcode to be recorded in statistics renewal of the item
836
837 =cut
838
839 sub renewal_branchcode {
840
841     my ($self, $params ) = @_;
842
843     my $interface = C4::Context->interface;
844     my $branchcode;
845     if ( $interface eq 'opac' ){
846         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
847         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
848             $branchcode = 'OPACRenew';
849         }
850         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
851             $branchcode = $self->homebranch;
852         }
853         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
854             $branchcode = $self->checkout->patron->branchcode;
855         }
856         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
857             $branchcode = $self->checkout->branchcode;
858         }
859         else {
860             $branchcode = "";
861         }
862     } else {
863         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
864             ? C4::Context->userenv->{branch} : $params->{branch};
865     }
866     return $branchcode;
867 }
868
869 =head3 cover_images
870
871 Return the cover images associated with this item.
872
873 =cut
874
875 sub cover_images {
876     my ( $self ) = @_;
877
878     my $cover_image_rs = $self->_result->cover_images;
879     return unless $cover_image_rs;
880     return Koha::CoverImages->_new_from_dbic($cover_image_rs);
881 }
882
883 =head3 _set_found_trigger
884
885     $self->_set_found_trigger
886
887 Finds the most recent lost item charge for this item and refunds the patron
888 appropriately, taking into account any payments or writeoffs already applied
889 against the charge.
890
891 Internal function, not exported, called only by Koha::Item->store.
892
893 =cut
894
895 sub _set_found_trigger {
896     my ( $self, $pre_mod_item ) = @_;
897
898     ## If item was lost, it has now been found, reverse any list item charges if necessary.
899     my $no_refund_after_days =
900       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
901     if ($no_refund_after_days) {
902         my $today = dt_from_string();
903         my $lost_age_in_days =
904           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
905           ->in_units('days');
906
907         return $self unless $lost_age_in_days < $no_refund_after_days;
908     }
909
910     my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
911         {
912             item          => $self,
913             return_branch => C4::Context->userenv
914             ? C4::Context->userenv->{'branch'}
915             : undef,
916         }
917       );
918
919     if ( $lostreturn_policy ) {
920
921         # refund charge made for lost book
922         my $lost_charge = Koha::Account::Lines->search(
923             {
924                 itemnumber      => $self->itemnumber,
925                 debit_type_code => 'LOST',
926                 status          => [ undef, { '<>' => 'FOUND' } ]
927             },
928             {
929                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
930                 rows     => 1
931             }
932         )->single;
933
934         if ( $lost_charge ) {
935
936             my $patron = $lost_charge->patron;
937             if ( $patron ) {
938
939                 my $account = $patron->account;
940                 my $total_to_refund = 0;
941
942                 # Use cases
943                 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
944
945                     # some amount has been cancelled. collect the offsets that are not writeoffs
946                     # this works because the only way to subtract from this kind of a debt is
947                     # using the UI buttons 'Pay' and 'Write off'
948                     my $credits_offsets = Koha::Account::Offsets->search(
949                         {
950                             debit_id  => $lost_charge->id,
951                             credit_id => { '!=' => undef },     # it is not the debit itself
952                             type      => { '!=' => 'Writeoff' },
953                             amount    => { '<' => 0 }    # credits are negative on the DB
954                         }
955                     );
956
957                     $total_to_refund = ( $credits_offsets->count > 0 )
958                       ? $credits_offsets->total * -1    # credits are negative on the DB
959                       : 0;
960                 }
961
962                 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
963
964                 my $credit;
965                 if ( $credit_total > 0 ) {
966                     my $branchcode =
967                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
968                     $credit = $account->add_credit(
969                         {
970                             amount      => $credit_total,
971                             description => 'Item found ' . $self->itemnumber,
972                             type        => 'LOST_FOUND',
973                             interface   => C4::Context->interface,
974                             library_id  => $branchcode,
975                             item_id     => $self->itemnumber,
976                             issue_id    => $lost_charge->issue_id
977                         }
978                     );
979
980                     $credit->apply( { debits => [$lost_charge] } );
981                     $self->{_refunded} = 1;
982                 }
983
984                 # Update the account status
985                 $lost_charge->status('FOUND');
986                 $lost_charge->store();
987
988                 # Reconcile balances if required
989                 if ( C4::Context->preference('AccountAutoReconcile') ) {
990                     $account->reconcile_balance;
991                 }
992             }
993         }
994
995         # restore fine for lost book
996         if ( $lostreturn_policy eq 'restore' ) {
997             my $lost_overdue = Koha::Account::Lines->search(
998                 {
999                     itemnumber      => $self->itemnumber,
1000                     debit_type_code => 'OVERDUE',
1001                     status          => 'LOST'
1002                 },
1003                 {
1004                     order_by => { '-desc' => 'date' },
1005                     rows     => 1
1006                 }
1007             )->single;
1008
1009             if ( $lost_overdue ) {
1010
1011                 my $patron = $lost_overdue->patron;
1012                 if ($patron) {
1013                     my $account = $patron->account;
1014
1015                     # Update status of fine
1016                     $lost_overdue->status('FOUND')->store();
1017
1018                     # Find related forgive credit
1019                     my $refund = $lost_overdue->credits(
1020                         {
1021                             credit_type_code => 'FORGIVEN',
1022                             itemnumber       => $self->itemnumber,
1023                             status           => [ { '!=' => 'VOID' }, undef ]
1024                         },
1025                         { order_by => { '-desc' => 'date' }, rows => 1 }
1026                     )->single;
1027
1028                     if ( $refund ) {
1029                         # Revert the forgive credit
1030                         $refund->void();
1031                         $self->{_restored} = 1;
1032                     }
1033
1034                     # Reconcile balances if required
1035                     if ( C4::Context->preference('AccountAutoReconcile') ) {
1036                         $account->reconcile_balance;
1037                     }
1038                 }
1039             }
1040         } elsif ( $lostreturn_policy eq 'charge' ) {
1041             $self->{_charge} = 1;
1042         }
1043     }
1044
1045     return $self;
1046 }
1047
1048 =head3 to_api_mapping
1049
1050 This method returns the mapping for representing a Koha::Item object
1051 on the API.
1052
1053 =cut
1054
1055 sub to_api_mapping {
1056     return {
1057         itemnumber               => 'item_id',
1058         biblionumber             => 'biblio_id',
1059         biblioitemnumber         => undef,
1060         barcode                  => 'external_id',
1061         dateaccessioned          => 'acquisition_date',
1062         booksellerid             => 'acquisition_source',
1063         homebranch               => 'home_library_id',
1064         price                    => 'purchase_price',
1065         replacementprice         => 'replacement_price',
1066         replacementpricedate     => 'replacement_price_date',
1067         datelastborrowed         => 'last_checkout_date',
1068         datelastseen             => 'last_seen_date',
1069         stack                    => undef,
1070         notforloan               => 'not_for_loan_status',
1071         damaged                  => 'damaged_status',
1072         damaged_on               => 'damaged_date',
1073         itemlost                 => 'lost_status',
1074         itemlost_on              => 'lost_date',
1075         withdrawn                => 'withdrawn',
1076         withdrawn_on             => 'withdrawn_date',
1077         itemcallnumber           => 'callnumber',
1078         coded_location_qualifier => 'coded_location_qualifier',
1079         issues                   => 'checkouts_count',
1080         renewals                 => 'renewals_count',
1081         reserves                 => 'holds_count',
1082         restricted               => 'restricted_status',
1083         itemnotes                => 'public_notes',
1084         itemnotes_nonpublic      => 'internal_notes',
1085         holdingbranch            => 'holding_library_id',
1086         timestamp                => 'timestamp',
1087         location                 => 'location',
1088         permanent_location       => 'permanent_location',
1089         onloan                   => 'checked_out_date',
1090         cn_source                => 'call_number_source',
1091         cn_sort                  => 'call_number_sort',
1092         ccode                    => 'collection_code',
1093         materials                => 'materials_notes',
1094         uri                      => 'uri',
1095         itype                    => 'item_type',
1096         more_subfields_xml       => 'extended_subfields',
1097         enumchron                => 'serial_issue_number',
1098         copynumber               => 'copy_number',
1099         stocknumber              => 'inventory_number',
1100         new_status               => 'new_status'
1101     };
1102 }
1103
1104 =head3 itemtype
1105
1106     my $itemtype = $item->itemtype;
1107
1108     Returns Koha object for effective itemtype
1109
1110 =cut
1111
1112 sub itemtype {
1113     my ( $self ) = @_;
1114     return Koha::ItemTypes->find( $self->effective_itemtype );
1115 }
1116
1117 =head2 Internal methods
1118
1119 =head3 _after_item_action_hooks
1120
1121 Helper method that takes care of calling all plugin hooks
1122
1123 =cut
1124
1125 sub _after_item_action_hooks {
1126     my ( $self, $params ) = @_;
1127
1128     my $action = $params->{action};
1129
1130     Koha::Plugins->call(
1131         'after_item_action',
1132         {
1133             action  => $action,
1134             item    => $self,
1135             item_id => $self->itemnumber,
1136         }
1137     );
1138 }
1139
1140 =head3 _type
1141
1142 =cut
1143
1144 sub _type {
1145     return 'Item';
1146 }
1147
1148 =head1 AUTHOR
1149
1150 Kyle M Hall <kyle@bywatersolutions.com>
1151
1152 =cut
1153
1154 1;