]> git.koha-community.org Git - koha.git/blob - Koha/Item.pm
Bug 33309: Index items after renewal transaction completes
[koha.git] / Koha / Item.pm
1 package Koha::Item;
2
3 # Copyright ByWater Solutions 2014
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use List::MoreUtils qw( any );
23
24 use Koha::Database;
25 use Koha::DateUtils qw( dt_from_string output_pref );
26
27 use C4::Context;
28 use C4::Circulation qw( barcodedecode GetBranchItemRule );
29 use C4::Reserves;
30 use C4::ClassSource qw( GetClassSort );
31 use C4::Log qw( logaction );
32
33 use Koha::Checkouts;
34 use Koha::CirculationRules;
35 use Koha::CoverImages;
36 use Koha::SearchEngine::Indexer;
37 use Koha::Exceptions::Item::Transfer;
38 use Koha::Item::Transfer::Limits;
39 use Koha::Item::Transfers;
40 use Koha::Item::Attributes;
41 use Koha::ItemTypes;
42 use Koha::Patrons;
43 use Koha::Plugins;
44 use Koha::Libraries;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
47 use Koha::TrackedLinks;
48
49 use base qw(Koha::Object);
50
51 =head1 NAME
52
53 Koha::Item - Koha Item object class
54
55 =head1 API
56
57 =head2 Class methods
58
59 =cut
60
61 =head3 store
62
63     $item->store;
64
65 $params can take an optional 'skip_record_index' parameter.
66 If set, the reindexation process will not happen (index_records not called)
67 You should not turn it on if you do not understand what it is doing exactly.
68
69 =cut
70
71 sub store {
72     my $self = shift;
73     my $params = @_ ? shift : {};
74
75     my $log_action = $params->{log_action} // 1;
76
77     # We do not want to oblige callers to pass this value
78     # Dev conveniences vs performance?
79     unless ( $self->biblioitemnumber ) {
80         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
81     }
82
83     # See related changes from C4::Items::AddItem
84     unless ( $self->itype ) {
85         $self->itype($self->biblio->biblioitem->itemtype);
86     }
87
88     $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
89
90     my $today  = dt_from_string;
91     my $action = 'create';
92
93     unless ( $self->in_storage ) { #AddItem
94
95         unless ( $self->permanent_location ) {
96             $self->permanent_location($self->location);
97         }
98
99         my $default_location = C4::Context->preference('NewItemsDefaultLocation');
100         unless ( $self->location || !$default_location ) {
101             $self->permanent_location( $self->location || $default_location )
102               unless $self->permanent_location;
103             $self->location($default_location);
104         }
105
106         unless ( $self->replacementpricedate ) {
107             $self->replacementpricedate($today);
108         }
109         unless ( $self->datelastseen ) {
110             $self->datelastseen($today);
111         }
112
113         unless ( $self->dateaccessioned ) {
114             $self->dateaccessioned($today);
115         }
116
117         if (   $self->itemcallnumber
118             or $self->cn_source )
119         {
120             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
121             $self->cn_sort($cn_sort);
122         }
123
124     } else { # ModItem
125
126         $action = 'modify';
127
128         my %updated_columns = $self->_result->get_dirty_columns;
129         return $self->SUPER::store unless %updated_columns;
130
131         # Retrieve the item for comparison if we need to
132         my $pre_mod_item = (
133                  exists $updated_columns{itemlost}
134               or exists $updated_columns{withdrawn}
135               or exists $updated_columns{damaged}
136         ) ? $self->get_from_storage : undef;
137
138         # Update *_on  fields if needed
139         # FIXME: Why not for AddItem as well?
140         my @fields = qw( itemlost withdrawn damaged );
141         for my $field (@fields) {
142
143             # If the field is defined but empty or 0, we are
144             # removing/unsetting and thus need to clear out
145             # the 'on' field
146             if (   exists $updated_columns{$field}
147                 && defined( $self->$field )
148                 && !$self->$field )
149             {
150                 my $field_on = "${field}_on";
151                 $self->$field_on(undef);
152             }
153             # If the field has changed otherwise, we much update
154             # the 'on' field
155             elsif (exists $updated_columns{$field}
156                 && $updated_columns{$field}
157                 && !$pre_mod_item->$field )
158             {
159                 my $field_on = "${field}_on";
160                 $self->$field_on(
161                     DateTime::Format::MySQL->format_datetime(
162                         dt_from_string()
163                     )
164                 );
165             }
166         }
167
168         if (   exists $updated_columns{itemcallnumber}
169             or exists $updated_columns{cn_source} )
170         {
171             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
172             $self->cn_sort($cn_sort);
173         }
174
175
176         if (    exists $updated_columns{location}
177             and $self->location ne 'CART'
178             and $self->location ne 'PROC'
179             and not exists $updated_columns{permanent_location} )
180         {
181             $self->permanent_location( $self->location );
182         }
183
184         # If item was lost and has now been found,
185         # reverse any list item charges if necessary.
186         if (    exists $updated_columns{itemlost}
187             and $updated_columns{itemlost} <= 0
188             and $pre_mod_item->itemlost > 0 )
189         {
190             $self->_set_found_trigger($pre_mod_item);
191         }
192
193     }
194
195     my $result = $self->SUPER::store;
196     if ( $log_action && C4::Context->preference("CataloguingLog") ) {
197         $action eq 'create'
198           ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
199           : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
200     }
201     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
202     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
203         unless $params->{skip_record_index};
204     $self->get_from_storage->_after_item_action_hooks({ action => $action });
205
206     return $result;
207 }
208
209 =head3 delete
210
211 =cut
212
213 sub delete {
214     my $self = shift;
215     my $params = @_ ? shift : {};
216
217     # FIXME check the item has no current issues
218     # i.e. raise the appropriate exception
219
220     my $result = $self->SUPER::delete;
221
222     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
223     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
224         unless $params->{skip_record_index};
225
226     $self->_after_item_action_hooks({ action => 'delete' });
227
228     logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
229       if C4::Context->preference("CataloguingLog");
230
231     return $result;
232 }
233
234 =head3 safe_delete
235
236 =cut
237
238 sub safe_delete {
239     my $self = shift;
240     my $params = @_ ? shift : {};
241
242     my $safe_to_delete = $self->safe_to_delete;
243     return $safe_to_delete unless $safe_to_delete eq '1';
244
245     $self->move_to_deleted;
246
247     return $self->delete($params);
248 }
249
250 =head3 safe_to_delete
251
252 returns 1 if the item is safe to delete,
253
254 "book_on_loan" if the item is checked out,
255
256 "not_same_branch" if the item is blocked by independent branches,
257
258 "book_reserved" if the there are holds aganst the item, or
259
260 "linked_analytics" if the item has linked analytic records.
261
262 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
263
264 =cut
265
266 sub safe_to_delete {
267     my ($self) = @_;
268
269     return "book_on_loan" if $self->checkout;
270
271     return "not_same_branch"
272       if defined C4::Context->userenv
273       and !C4::Context->IsSuperLibrarian()
274       and C4::Context->preference("IndependentBranches")
275       and ( C4::Context->userenv->{branch} ne $self->homebranch );
276
277     # check it doesn't have a waiting reserve
278     return "book_reserved"
279       if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
280
281     return "linked_analytics"
282       if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
283
284     return "last_item_for_hold"
285       if $self->biblio->items->count == 1
286       && $self->biblio->holds->search(
287           {
288               itemnumber => undef,
289           }
290         )->count;
291
292     return 1;
293 }
294
295 =head3 move_to_deleted
296
297 my $is_moved = $item->move_to_deleted;
298
299 Move an item to the deleteditems table.
300 This can be done before deleting an item, to make sure the data are not completely deleted.
301
302 =cut
303
304 sub move_to_deleted {
305     my ($self) = @_;
306     my $item_infos = $self->unblessed;
307     delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
308     return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
309 }
310
311
312 =head3 effective_itemtype
313
314 Returns the itemtype for the item based on whether item level itemtypes are set or not.
315
316 =cut
317
318 sub effective_itemtype {
319     my ( $self ) = @_;
320
321     return $self->_result()->effective_itemtype();
322 }
323
324 =head3 home_branch
325
326 =cut
327
328 sub home_branch {
329     my ($self) = @_;
330
331     $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
332
333     return $self->{_home_branch};
334 }
335
336 =head3 holding_branch
337
338 =cut
339
340 sub holding_branch {
341     my ($self) = @_;
342
343     $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
344
345     return $self->{_holding_branch};
346 }
347
348 =head3 biblio
349
350 my $biblio = $item->biblio;
351
352 Return the bibliographic record of this item
353
354 =cut
355
356 sub biblio {
357     my ( $self ) = @_;
358     my $biblio_rs = $self->_result->biblio;
359     return Koha::Biblio->_new_from_dbic( $biblio_rs );
360 }
361
362 =head3 biblioitem
363
364 my $biblioitem = $item->biblioitem;
365
366 Return the biblioitem record of this item
367
368 =cut
369
370 sub biblioitem {
371     my ( $self ) = @_;
372     my $biblioitem_rs = $self->_result->biblioitem;
373     return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
374 }
375
376 =head3 checkout
377
378 my $checkout = $item->checkout;
379
380 Return the checkout for this item
381
382 =cut
383
384 sub checkout {
385     my ( $self ) = @_;
386     my $checkout_rs = $self->_result->issue;
387     return unless $checkout_rs;
388     return Koha::Checkout->_new_from_dbic( $checkout_rs );
389 }
390
391 =head3 holds
392
393 my $holds = $item->holds();
394 my $holds = $item->holds($params);
395 my $holds = $item->holds({ found => 'W'});
396
397 Return holds attached to an item, optionally accept a hashref of params to pass to search
398
399 =cut
400
401 sub holds {
402     my ( $self,$params ) = @_;
403     my $holds_rs = $self->_result->reserves->search($params);
404     return Koha::Holds->_new_from_dbic( $holds_rs );
405 }
406
407 =head3 request_transfer
408
409   my $transfer = $item->request_transfer(
410     {
411         to     => $to_library,
412         reason => $reason,
413         [ ignore_limits => 0, enqueue => 1, replace => 1 ]
414     }
415   );
416
417 Add a transfer request for this item to the given branch for the given reason.
418
419 An exception will be thrown if the BranchTransferLimits would prevent the requested
420 transfer, unless 'ignore_limits' is passed to override the limits.
421
422 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
423 The caller should catch such cases and retry the transfer request as appropriate passing
424 an appropriate override.
425
426 Overrides
427 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
428 * replace - Used to replace the existing transfer request with your own.
429
430 =cut
431
432 sub request_transfer {
433     my ( $self, $params ) = @_;
434
435     # check for mandatory params
436     my @mandatory = ( 'to', 'reason' );
437     for my $param (@mandatory) {
438         unless ( defined( $params->{$param} ) ) {
439             Koha::Exceptions::MissingParameter->throw(
440                 error => "The $param parameter is mandatory" );
441         }
442     }
443
444     Koha::Exceptions::Item::Transfer::Limit->throw()
445       unless ( $params->{ignore_limits}
446         || $self->can_be_transferred( { to => $params->{to} } ) );
447
448     my $request = $self->get_transfer;
449     Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
450       if ( $request && !$params->{enqueue} && !$params->{replace} );
451
452     $request->cancel( { reason => $params->{reason}, force => 1 } )
453       if ( defined($request) && $params->{replace} );
454
455     my $transfer = Koha::Item::Transfer->new(
456         {
457             itemnumber    => $self->itemnumber,
458             daterequested => dt_from_string,
459             frombranch    => $self->holdingbranch,
460             tobranch      => $params->{to}->branchcode,
461             reason        => $params->{reason},
462             comments      => $params->{comment}
463         }
464     )->store();
465
466     return $transfer;
467 }
468
469 =head3 get_transfer
470
471   my $transfer = $item->get_transfer;
472
473 Return the active transfer request or undef
474
475 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
476 whereby the most recently sent, but not received, transfer will be returned
477 if it exists, otherwise the oldest unsatisfied transfer will be returned.
478
479 This allows for transfers to queue, which is the case for stock rotation and
480 rotating collections where a manual transfer may need to take precedence but
481 we still expect the item to end up at a final location eventually.
482
483 =cut
484
485 sub get_transfer {
486     my ($self) = @_;
487     my $transfer_rs = $self->_result->branchtransfers->search(
488         {
489             datearrived   => undef,
490             datecancelled => undef
491         },
492         {
493             order_by =>
494               [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
495             rows => 1
496         }
497     )->first;
498     return unless $transfer_rs;
499     return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
500 }
501
502 =head3 get_transfers
503
504   my $transfer = $item->get_transfers;
505
506 Return the list of outstanding transfers (i.e requested but not yet cancelled
507 or received).
508
509 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
510 whereby the most recently sent, but not received, transfer will be returned
511 first if it exists, otherwise requests are in oldest to newest request order.
512
513 This allows for transfers to queue, which is the case for stock rotation and
514 rotating collections where a manual transfer may need to take precedence but
515 we still expect the item to end up at a final location eventually.
516
517 =cut
518
519 sub get_transfers {
520     my ($self) = @_;
521     my $transfer_rs = $self->_result->branchtransfers->search(
522         {
523             datearrived   => undef,
524             datecancelled => undef
525         },
526         {
527             order_by =>
528               [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
529         }
530     );
531     return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
532 }
533
534 =head3 last_returned_by
535
536 Gets and sets the last borrower to return an item.
537
538 Accepts and returns Koha::Patron objects
539
540 $item->last_returned_by( $borrowernumber );
541
542 $last_returned_by = $item->last_returned_by();
543
544 =cut
545
546 sub last_returned_by {
547     my ( $self, $borrower ) = @_;
548
549     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
550
551     if ($borrower) {
552         return $items_last_returned_by_rs->update_or_create(
553             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
554     }
555     else {
556         unless ( $self->{_last_returned_by} ) {
557             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
558             if ($result) {
559                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
560             }
561         }
562
563         return $self->{_last_returned_by};
564     }
565 }
566
567 =head3 can_article_request
568
569 my $bool = $item->can_article_request( $borrower )
570
571 Returns true if item can be specifically requested
572
573 $borrower must be a Koha::Patron object
574
575 =cut
576
577 sub can_article_request {
578     my ( $self, $borrower ) = @_;
579
580     my $rule = $self->article_request_type($borrower);
581
582     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
583     return q{};
584 }
585
586 =head3 hidden_in_opac
587
588 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
589
590 Returns true if item fields match the hidding criteria defined in $rules.
591 Returns false otherwise.
592
593 Takes HASHref that can have the following parameters:
594     OPTIONAL PARAMETERS:
595     $rules : { <field> => [ value_1, ... ], ... }
596
597 Note: $rules inherits its structure from the parsed YAML from reading
598 the I<OpacHiddenItems> system preference.
599
600 =cut
601
602 sub hidden_in_opac {
603     my ( $self, $params ) = @_;
604
605     my $rules = $params->{rules} // {};
606
607     return 1
608         if C4::Context->preference('hidelostitems') and
609            $self->itemlost > 0;
610
611     my $hidden_in_opac = 0;
612
613     foreach my $field ( keys %{$rules} ) {
614
615         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
616             $hidden_in_opac = 1;
617             last;
618         }
619     }
620
621     return $hidden_in_opac;
622 }
623
624 =head3 can_be_transferred
625
626 $item->can_be_transferred({ to => $to_library, from => $from_library })
627 Checks if an item can be transferred to given library.
628
629 This feature is controlled by two system preferences:
630 UseBranchTransferLimits to enable / disable the feature
631 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
632                          for setting the limitations
633
634 Takes HASHref that can have the following parameters:
635     MANDATORY PARAMETERS:
636     $to   : Koha::Library
637     OPTIONAL PARAMETERS:
638     $from : Koha::Library  # if not given, item holdingbranch
639                            # will be used instead
640
641 Returns 1 if item can be transferred to $to_library, otherwise 0.
642
643 To find out whether at least one item of a Koha::Biblio can be transferred, please
644 see Koha::Biblio->can_be_transferred() instead of using this method for
645 multiple items of the same biblio.
646
647 =cut
648
649 sub can_be_transferred {
650     my ($self, $params) = @_;
651
652     my $to   = $params->{to};
653     my $from = $params->{from};
654
655     $to   = $to->branchcode;
656     $from = defined $from ? $from->branchcode : $self->holdingbranch;
657
658     return 1 if $from eq $to; # Transfer to current branch is allowed
659     return 1 unless C4::Context->preference('UseBranchTransferLimits');
660
661     my $limittype = C4::Context->preference('BranchTransferLimitsType');
662     return Koha::Item::Transfer::Limits->search({
663         toBranch => $to,
664         fromBranch => $from,
665         $limittype => $limittype eq 'itemtype'
666                         ? $self->effective_itemtype : $self->ccode
667     })->count ? 0 : 1;
668
669 }
670
671 =head3 pickup_locations
672
673 $pickup_locations = $item->pickup_locations( {patron => $patron } )
674
675 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)
676 and if item can be transferred to each pickup location.
677
678 =cut
679
680 sub pickup_locations {
681     my ($self, $params) = @_;
682
683     my $patron = $params->{patron};
684
685     my $circ_control_branch =
686       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
687     my $branchitemrule =
688       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
689
690     if(defined $patron) {
691         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
692         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
693     }
694
695     my $pickup_libraries = Koha::Libraries->search();
696     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
697         $pickup_libraries = $self->home_branch->get_hold_libraries;
698     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
699         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
700         $pickup_libraries = $plib->get_hold_libraries;
701     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
702         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
703     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
704         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
705     };
706
707     return $pickup_libraries->search(
708         {
709             pickup_location => 1
710         },
711         {
712             order_by => ['branchname']
713         }
714     ) unless C4::Context->preference('UseBranchTransferLimits');
715
716     my $limittype = C4::Context->preference('BranchTransferLimitsType');
717     my ($ccode, $itype) = (undef, undef);
718     if( $limittype eq 'ccode' ){
719         $ccode = $self->ccode;
720     } else {
721         $itype = $self->itype;
722     }
723     my $limits = Koha::Item::Transfer::Limits->search(
724         {
725             fromBranch => $self->holdingbranch,
726             ccode      => $ccode,
727             itemtype   => $itype,
728         },
729         { columns => ['toBranch'] }
730     );
731
732     return $pickup_libraries->search(
733         {
734             pickup_location => 1,
735             branchcode      => {
736                 '-not_in' => $limits->_resultset->as_query
737             }
738         },
739         {
740             order_by => ['branchname']
741         }
742     );
743 }
744
745 =head3 article_request_type
746
747 my $type = $item->article_request_type( $borrower )
748
749 returns 'yes', 'no', 'bib_only', or 'item_only'
750
751 $borrower must be a Koha::Patron object
752
753 =cut
754
755 sub article_request_type {
756     my ( $self, $borrower ) = @_;
757
758     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
759     my $branchcode =
760         $branch_control eq 'homebranch'    ? $self->homebranch
761       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
762       :                                      undef;
763     my $borrowertype = $borrower->categorycode;
764     my $itemtype = $self->effective_itemtype();
765     my $rule = Koha::CirculationRules->get_effective_rule(
766         {
767             rule_name    => 'article_requests',
768             categorycode => $borrowertype,
769             itemtype     => $itemtype,
770             branchcode   => $branchcode
771         }
772     );
773
774     return q{} unless $rule;
775     return $rule->rule_value || q{}
776 }
777
778 =head3 current_holds
779
780 =cut
781
782 sub current_holds {
783     my ( $self ) = @_;
784     my $attributes = { order_by => 'priority' };
785     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
786     my $params = {
787         itemnumber => $self->itemnumber,
788         suspend => 0,
789         -or => [
790             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
791             waitingdate => { '!=' => undef },
792         ],
793     };
794     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
795     return Koha::Holds->_new_from_dbic($hold_rs);
796 }
797
798 =head3 stockrotationitem
799
800   my $sritem = Koha::Item->stockrotationitem;
801
802 Returns the stock rotation item associated with the current item.
803
804 =cut
805
806 sub stockrotationitem {
807     my ( $self ) = @_;
808     my $rs = $self->_result->stockrotationitem;
809     return 0 if !$rs;
810     return Koha::StockRotationItem->_new_from_dbic( $rs );
811 }
812
813 =head3 add_to_rota
814
815   my $item = $item->add_to_rota($rota_id);
816
817 Add this item to the rota identified by $ROTA_ID, which means associating it
818 with the first stage of that rota.  Should this item already be associated
819 with a rota, then we will move it to the new rota.
820
821 =cut
822
823 sub add_to_rota {
824     my ( $self, $rota_id ) = @_;
825     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
826     return $self;
827 }
828
829 =head3 has_pending_hold
830
831   my $is_pending_hold = $item->has_pending_hold();
832
833 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
834
835 =cut
836
837 sub has_pending_hold {
838     my ( $self ) = @_;
839     my $pending_hold = $self->_result->tmp_holdsqueues;
840     return $pending_hold->count ? 1: 0;
841 }
842
843 =head3 as_marc_field
844
845     my $field = $item->as_marc_field;
846
847 This method returns a MARC::Field object representing the Koha::Item object
848 with the current mappings configuration.
849
850 =cut
851
852 sub as_marc_field {
853     my ( $self ) = @_;
854
855     my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
856
857     my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
858
859     my @subfields;
860
861     my $item_field = $tagslib->{$itemtag};
862
863     my $more_subfields = $self->additional_attributes->to_hashref;
864     foreach my $subfield (
865         sort {
866                $a->{display_order} <=> $b->{display_order}
867             || $a->{subfield} cmp $b->{subfield}
868         } grep { ref($_) && %$_ } values %$item_field
869     ){
870
871         my $kohafield = $subfield->{kohafield};
872         my $tagsubfield = $subfield->{tagsubfield};
873         my $value;
874         if ( defined $kohafield && $kohafield ne '' ) {
875             next if $kohafield !~ m{^items\.}; # That would be weird!
876             ( my $attribute = $kohafield ) =~ s|^items\.||;
877             $value = $self->$attribute # This call may fail if a kohafield is not a DB column but we don't want to add extra work for that there
878                 if defined $self->$attribute and $self->$attribute ne '';
879         } else {
880             $value = $more_subfields->{$tagsubfield}
881         }
882
883         next unless defined $value
884             and $value ne q{};
885
886         if ( $subfield->{repeatable} ) {
887             my @values = split '\|', $value;
888             push @subfields, ( $tagsubfield => $_ ) for @values;
889         }
890         else {
891             push @subfields, ( $tagsubfield => $value );
892         }
893
894     }
895
896     return unless @subfields;
897
898     return MARC::Field->new(
899         "$itemtag", ' ', ' ', @subfields
900     );
901 }
902
903 =head3 renewal_branchcode
904
905 Returns the branchcode to be recorded in statistics renewal of the item
906
907 =cut
908
909 sub renewal_branchcode {
910
911     my ($self, $params ) = @_;
912
913     my $interface = C4::Context->interface;
914     my $branchcode;
915     if ( $interface eq 'opac' ){
916         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
917         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
918             $branchcode = 'OPACRenew';
919         }
920         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
921             $branchcode = $self->homebranch;
922         }
923         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
924             $branchcode = $self->checkout->patron->branchcode;
925         }
926         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
927             $branchcode = $self->checkout->branchcode;
928         }
929         else {
930             $branchcode = "";
931         }
932     } else {
933         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
934             ? C4::Context->userenv->{branch} : $params->{branch};
935     }
936     return $branchcode;
937 }
938
939 =head3 cover_images
940
941 Return the cover images associated with this item.
942
943 =cut
944
945 sub cover_images {
946     my ( $self ) = @_;
947
948     my $cover_image_rs = $self->_result->cover_images;
949     return unless $cover_image_rs;
950     return Koha::CoverImages->_new_from_dbic($cover_image_rs);
951 }
952
953 =head3 columns_to_str
954
955     my $values = $items->columns_to_str;
956
957 Return a hashref with the string representation of the different attribute of the item.
958
959 This is meant to be used for display purpose only.
960
961 =cut
962
963 sub columns_to_str {
964     my ( $self ) = @_;
965
966     my $frameworkcode = $self->biblio->frameworkcode;
967     my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
968     my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
969
970     my $columns_info = $self->_result->result_source->columns_info;
971
972     my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
973     my $values = {};
974     for my $column ( keys %$columns_info ) {
975
976         next if $column eq 'more_subfields_xml';
977
978         my $value = $self->$column;
979         # Maybe we need to deal with datetime columns here, but so far we have damaged_on, itemlost_on and withdrawn_on, and they are not linked with kohafield
980
981         if ( not defined $value or $value eq "" ) {
982             $values->{$column} = $value;
983             next;
984         }
985
986         my $subfield =
987           exists $mss->{"items.$column"}
988           ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
989           : undef;
990
991         $values->{$column} =
992             $subfield
993           ? $subfield->{authorised_value}
994               ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
995                   $subfield->{tagsubfield}, $value, '', $tagslib )
996               : $value
997           : $value;
998     }
999
1000     my $marc_more=
1001       $self->more_subfields_xml
1002       ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1003       : undef;
1004
1005     my $more_values;
1006     if ( $marc_more ) {
1007         my ( $field ) = $marc_more->fields;
1008         for my $sf ( $field->subfields ) {
1009             my $subfield_code = $sf->[0];
1010             my $value = $sf->[1];
1011             my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1012             next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1013             $value =
1014               $subfield->{authorised_value}
1015               ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1016                 $subfield->{tagsubfield}, $value, '', $tagslib )
1017               : $value;
1018
1019             push @{$more_values->{$subfield_code}}, $value;
1020         }
1021
1022         while ( my ( $k, $v ) = each %$more_values ) {
1023             $values->{$k} = join ' | ', @$v;
1024         }
1025     }
1026
1027     return $values;
1028 }
1029
1030 =head3 additional_attributes
1031
1032     my $attributes = $item->additional_attributes;
1033     $attributes->{k} = 'new k';
1034     $item->update({ more_subfields => $attributes->to_marcxml });
1035
1036 Returns a Koha::Item::Attributes object that represents the non-mapped
1037 attributes for this item.
1038
1039 =cut
1040
1041 sub additional_attributes {
1042     my ($self) = @_;
1043
1044     return Koha::Item::Attributes->new_from_marcxml(
1045         $self->more_subfields_xml,
1046     );
1047 }
1048
1049 =head3 _set_found_trigger
1050
1051     $self->_set_found_trigger
1052
1053 Finds the most recent lost item charge for this item and refunds the patron
1054 appropriately, taking into account any payments or writeoffs already applied
1055 against the charge.
1056
1057 Internal function, not exported, called only by Koha::Item->store.
1058
1059 =cut
1060
1061 sub _set_found_trigger {
1062     my ( $self, $pre_mod_item ) = @_;
1063
1064     ## If item was lost, it has now been found, reverse any list item charges if necessary.
1065     my $no_refund_after_days =
1066       C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1067     if ($no_refund_after_days) {
1068         my $today = dt_from_string();
1069         my $lost_age_in_days =
1070           dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1071           ->in_units('days');
1072
1073         return $self unless $lost_age_in_days < $no_refund_after_days;
1074     }
1075
1076     my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1077         {
1078             item          => $self,
1079             return_branch => C4::Context->userenv
1080             ? C4::Context->userenv->{'branch'}
1081             : undef,
1082         }
1083       );
1084
1085     if ( $lostreturn_policy ) {
1086
1087         # refund charge made for lost book
1088         my $lost_charge = Koha::Account::Lines->search(
1089             {
1090                 itemnumber      => $self->itemnumber,
1091                 debit_type_code => 'LOST',
1092                 status          => [ undef, { '<>' => 'FOUND' } ]
1093             },
1094             {
1095                 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1096                 rows     => 1
1097             }
1098         )->single;
1099
1100         if ( $lost_charge ) {
1101
1102             my $patron = $lost_charge->patron;
1103             if ( $patron ) {
1104
1105                 my $account = $patron->account;
1106                 my $total_to_refund = 0;
1107
1108                 # Use cases
1109                 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1110
1111                     # some amount has been cancelled. collect the offsets that are not writeoffs
1112                     # this works because the only way to subtract from this kind of a debt is
1113                     # using the UI buttons 'Pay' and 'Write off'
1114                     my $credit_offsets = $lost_charge->debit_offsets(
1115                         {
1116                             'credit_id'               => { '!=' => undef },
1117                             'credit.credit_type_code' => { '!=' => 'Writeoff' }
1118                         },
1119                         { join => 'credit' }
1120                     );
1121
1122                     $total_to_refund = ( $credit_offsets->count > 0 )
1123                       ? $credit_offsets->total * -1    # credits are negative on the DB
1124                       : 0;
1125                 }
1126
1127                 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1128
1129                 my $credit;
1130                 if ( $credit_total > 0 ) {
1131                     my $branchcode =
1132                       C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1133                     $credit = $account->add_credit(
1134                         {
1135                             amount      => $credit_total,
1136                             description => 'Item found ' . $self->itemnumber,
1137                             type        => 'LOST_FOUND',
1138                             interface   => C4::Context->interface,
1139                             library_id  => $branchcode,
1140                             item_id     => $self->itemnumber,
1141                             issue_id    => $lost_charge->issue_id
1142                         }
1143                     );
1144
1145                     $credit->apply( { debits => [$lost_charge] } );
1146                     $self->{_refunded} = 1;
1147                 }
1148
1149                 # Update the account status
1150                 $lost_charge->status('FOUND');
1151                 $lost_charge->store();
1152
1153                 # Reconcile balances if required
1154                 if ( C4::Context->preference('AccountAutoReconcile') ) {
1155                     $account->reconcile_balance;
1156                 }
1157             }
1158         }
1159
1160         # restore fine for lost book
1161         if ( $lostreturn_policy eq 'restore' ) {
1162             my $lost_overdue = Koha::Account::Lines->search(
1163                 {
1164                     itemnumber      => $self->itemnumber,
1165                     debit_type_code => 'OVERDUE',
1166                     status          => 'LOST'
1167                 },
1168                 {
1169                     order_by => { '-desc' => 'date' },
1170                     rows     => 1
1171                 }
1172             )->single;
1173
1174             if ( $lost_overdue ) {
1175
1176                 my $patron = $lost_overdue->patron;
1177                 if ($patron) {
1178                     my $account = $patron->account;
1179
1180                     # Update status of fine
1181                     $lost_overdue->status('FOUND')->store();
1182
1183                     # Find related forgive credit
1184                     my $refund = $lost_overdue->credits(
1185                         {
1186                             credit_type_code => 'FORGIVEN',
1187                             itemnumber       => $self->itemnumber,
1188                             status           => [ { '!=' => 'VOID' }, undef ]
1189                         },
1190                         { order_by => { '-desc' => 'date' }, rows => 1 }
1191                     )->single;
1192
1193                     if ( $refund ) {
1194                         # Revert the forgive credit
1195                         $refund->void({ interface => 'trigger' });
1196                         $self->{_restored} = 1;
1197                     }
1198
1199                     # Reconcile balances if required
1200                     if ( C4::Context->preference('AccountAutoReconcile') ) {
1201                         $account->reconcile_balance;
1202                     }
1203                 }
1204             }
1205         } elsif ( $lostreturn_policy eq 'charge' ) {
1206             $self->{_charge} = 1;
1207         }
1208     }
1209
1210     return $self;
1211 }
1212
1213 =head3 public_read_list
1214
1215 This method returns the list of publicly readable database fields for both API and UI output purposes
1216
1217 =cut
1218
1219 sub public_read_list {
1220     return [
1221         'itemnumber',     'biblionumber',    'homebranch',
1222         'holdingbranch',  'location',        'collectioncode',
1223         'itemcallnumber', 'copynumber',      'enumchron',
1224         'barcode',        'dateaccessioned', 'itemnotes',
1225         'onloan',         'uri',             'itype',
1226         'notforloan',     'damaged',         'itemlost',
1227         'withdrawn',      'restricted'
1228     ];
1229 }
1230
1231 =head3 to_api
1232
1233 Overloaded to_api method to ensure item-level itypes is adhered to.
1234
1235 =cut
1236
1237 sub to_api {
1238     my ($self, $params) = @_;
1239
1240     my $response = $self->SUPER::to_api($params);
1241     my $overrides = {};
1242
1243     $overrides->{effective_item_type_id} = $self->effective_itemtype;
1244
1245     return { %$response, %$overrides };
1246 }
1247
1248 =head3 to_api_mapping
1249
1250 This method returns the mapping for representing a Koha::Item object
1251 on the API.
1252
1253 =cut
1254
1255 sub to_api_mapping {
1256     return {
1257         itemnumber               => 'item_id',
1258         biblionumber             => 'biblio_id',
1259         biblioitemnumber         => undef,
1260         barcode                  => 'external_id',
1261         dateaccessioned          => 'acquisition_date',
1262         booksellerid             => 'acquisition_source',
1263         homebranch               => 'home_library_id',
1264         price                    => 'purchase_price',
1265         replacementprice         => 'replacement_price',
1266         replacementpricedate     => 'replacement_price_date',
1267         datelastborrowed         => 'last_checkout_date',
1268         datelastseen             => 'last_seen_date',
1269         stack                    => undef,
1270         notforloan               => 'not_for_loan_status',
1271         damaged                  => 'damaged_status',
1272         damaged_on               => 'damaged_date',
1273         itemlost                 => 'lost_status',
1274         itemlost_on              => 'lost_date',
1275         withdrawn                => 'withdrawn',
1276         withdrawn_on             => 'withdrawn_date',
1277         itemcallnumber           => 'callnumber',
1278         coded_location_qualifier => 'coded_location_qualifier',
1279         issues                   => 'checkouts_count',
1280         renewals                 => 'renewals_count',
1281         reserves                 => 'holds_count',
1282         restricted               => 'restricted_status',
1283         itemnotes                => 'public_notes',
1284         itemnotes_nonpublic      => 'internal_notes',
1285         holdingbranch            => 'holding_library_id',
1286         timestamp                => 'timestamp',
1287         location                 => 'location',
1288         permanent_location       => 'permanent_location',
1289         onloan                   => 'checked_out_date',
1290         cn_source                => 'call_number_source',
1291         cn_sort                  => 'call_number_sort',
1292         ccode                    => 'collection_code',
1293         materials                => 'materials_notes',
1294         uri                      => 'uri',
1295         itype                    => 'item_type_id',
1296         more_subfields_xml       => 'extended_subfields',
1297         enumchron                => 'serial_issue_number',
1298         copynumber               => 'copy_number',
1299         stocknumber              => 'inventory_number',
1300         new_status               => 'new_status'
1301     };
1302 }
1303
1304 =head3 itemtype
1305
1306     my $itemtype = $item->itemtype;
1307
1308     Returns Koha object for effective itemtype
1309
1310 =cut
1311
1312 sub itemtype {
1313     my ( $self ) = @_;
1314     return Koha::ItemTypes->find( $self->effective_itemtype );
1315 }
1316
1317 =head3 orders
1318
1319   my $orders = $item->orders();
1320
1321 Returns a Koha::Acquisition::Orders object
1322
1323 =cut
1324
1325 sub orders {
1326     my ( $self ) = @_;
1327
1328     my $orders = $self->_result->item_orders;
1329     return Koha::Acquisition::Orders->_new_from_dbic($orders);
1330 }
1331
1332 =head3 tracked_links
1333
1334   my $tracked_links = $item->tracked_links();
1335
1336 Returns a Koha::TrackedLinks object
1337
1338 =cut
1339
1340 sub tracked_links {
1341     my ( $self ) = @_;
1342
1343     my $tracked_links = $self->_result->linktrackers;
1344     return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1345 }
1346
1347 =head3 move_to_biblio
1348
1349   $item->move_to_biblio($to_biblio[, $params]);
1350
1351 Move the item to another biblio and update any references in other tables.
1352
1353 The final optional parameter, C<$params>, is expected to contain the
1354 'skip_record_index' key, which is relayed down to Koha::Item->store.
1355 There it prevents calling index_records, which takes most of the
1356 time in batch adds/deletes. The caller must take care of calling
1357 index_records separately.
1358
1359 $params:
1360     skip_record_index => 1|0
1361
1362 Returns undef if the move failed or the biblionumber of the destination record otherwise
1363
1364 =cut
1365
1366 sub move_to_biblio {
1367     my ( $self, $to_biblio, $params ) = @_;
1368
1369     $params //= {};
1370
1371     return if $self->biblionumber == $to_biblio->biblionumber;
1372
1373     my $from_biblionumber = $self->biblionumber;
1374     my $to_biblionumber = $to_biblio->biblionumber;
1375
1376     # Own biblionumber and biblioitemnumber
1377     $self->set({
1378         biblionumber => $to_biblionumber,
1379         biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1380     })->store({ skip_record_index => $params->{skip_record_index} });
1381
1382     unless ($params->{skip_record_index}) {
1383         my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1384         $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1385     }
1386
1387     # Acquisition orders
1388     $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1389
1390     # Holds
1391     $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1392
1393     # hold_fill_target (there's no Koha object available yet)
1394     my $hold_fill_target = $self->_result->hold_fill_target;
1395     if ($hold_fill_target) {
1396         $hold_fill_target->update({ biblionumber => $to_biblionumber });
1397     }
1398
1399     # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1400     # and can't even fake one since the significant columns are nullable.
1401     my $storage = $self->_result->result_source->storage;
1402     $storage->dbh_do(
1403         sub {
1404             my ($storage, $dbh, @cols) = @_;
1405
1406             $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1407         }
1408     );
1409
1410     # tracked_links
1411     $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1412
1413     return $to_biblionumber;
1414 }
1415
1416 =head2 Internal methods
1417
1418 =head3 _after_item_action_hooks
1419
1420 Helper method that takes care of calling all plugin hooks
1421
1422 =cut
1423
1424 sub _after_item_action_hooks {
1425     my ( $self, $params ) = @_;
1426
1427     my $action = $params->{action};
1428
1429     Koha::Plugins->call(
1430         'after_item_action',
1431         {
1432             action  => $action,
1433             item    => $self,
1434             item_id => $self->itemnumber,
1435         }
1436     );
1437 }
1438
1439 =head3 _type
1440
1441 =cut
1442
1443 sub _type {
1444     return 'Item';
1445 }
1446
1447 =head1 AUTHOR
1448
1449 Kyle M Hall <kyle@bywatersolutions.com>
1450
1451 =cut
1452
1453 1;