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