Bug 23463: (follow-up) Fix timestamp default value
[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::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
34 use C4::ClassSource; # FIXME We would like to avoid that
35 use C4::Log qw( logaction );
36
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Patrons;
42 use Koha::Plugins;
43 use Koha::Libraries;
44 use Koha::StockRotationItem;
45 use Koha::StockRotationRotas;
46
47 use base qw(Koha::Object);
48
49 =head1 NAME
50
51 Koha::Item - Koha Item object class
52
53 =head1 API
54
55 =head2 Class methods
56
57 =cut
58
59 =head3 store
60
61 =cut
62
63 sub store {
64     my ($self, $params) = @_;
65
66     my $log_action = $params->{log_action} // 1;
67
68     # We do not want to oblige callers to pass this value
69     # Dev conveniences vs performance?
70     unless ( $self->biblioitemnumber ) {
71         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
72     }
73
74     # See related changes from C4::Items::AddItem
75     unless ( $self->itype ) {
76         $self->itype($self->biblio->biblioitem->itemtype);
77     }
78
79     my %updated_columns = $self->_result->get_dirty_columns;
80     if (   exists $updated_columns{itemcallnumber}
81         or exists $updated_columns{cn_source} )
82     {
83         my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
84         $self->cn_sort($cn_sort);
85     }
86
87     my $today = dt_from_string;
88     unless ( $self->in_storage ) { #AddItem
89         unless ( $self->permanent_location ) {
90             $self->permanent_location($self->location);
91         }
92         unless ( $self->replacementpricedate ) {
93             $self->replacementpricedate($today);
94         }
95         unless ( $self->datelastseen ) {
96             $self->datelastseen($today);
97         }
98
99         unless ( $self->dateaccessioned ) {
100             $self->dateaccessioned($today);
101         }
102
103         C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
104
105         logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
106           if $log_action && C4::Context->preference("CataloguingLog");
107
108         $self->_after_item_action_hooks({ action => 'create' });
109
110     } else { # ModItem
111
112         { # Update *_on  fields if needed
113           # Why not for AddItem as well?
114             my @fields = qw( itemlost withdrawn damaged );
115
116             # Only retrieve the item if we need to set an "on" date field
117             if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
118                 my $pre_mod_item = $self->get_from_storage;
119                 for my $field (@fields) {
120                     if (    $self->$field
121                         and not $pre_mod_item->$field )
122                     {
123                         my $field_on = "${field}_on";
124                         $self->$field_on(
125                           DateTime::Format::MySQL->format_datetime( dt_from_string() )
126                         );
127                     }
128                 }
129             }
130
131             # If the field is defined but empty, we are removing and,
132             # and thus need to clear out the 'on' field as well
133             for my $field (@fields) {
134                 if ( defined( $self->$field ) && !$self->$field ) {
135                     my $field_on = "${field}_on";
136                     $self->$field_on(undef);
137                 }
138             }
139         }
140
141         %updated_columns = $self->_result->get_dirty_columns;
142         return $self->SUPER::store unless %updated_columns;
143         if (    exists $updated_columns{location}
144             and $self->location ne 'CART'
145             and $self->location ne 'PROC'
146             and not exists $updated_columns{permanent_location} )
147         {
148             $self->permanent_location( $self->location );
149         }
150
151         $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
152
153         C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
154
155         $self->_after_item_action_hooks({ action => 'modify' });
156
157         logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
158           if $log_action && C4::Context->preference("CataloguingLog");
159     }
160
161     unless ( $self->dateaccessioned ) {
162         $self->dateaccessioned($today);
163     }
164
165     return $self->SUPER::store;
166 }
167
168 =head3 delete
169
170 =cut
171
172 sub delete {
173     my ( $self ) = @_;
174
175     # FIXME check the item has no current issues
176     # i.e. raise the appropriate exception
177
178     C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
179
180     $self->_after_item_action_hooks({ action => 'delete' });
181
182     logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
183       if C4::Context->preference("CataloguingLog");
184
185     return $self->SUPER::delete;
186 }
187
188 =head3 safe_delete
189
190 =cut
191
192 sub safe_delete {
193     my ($self) = @_;
194
195     my $safe_to_delete = $self->safe_to_delete;
196     return $safe_to_delete unless $safe_to_delete eq '1';
197
198     $self->move_to_deleted;
199
200     return $self->delete;
201 }
202
203 =head3 safe_to_delete
204
205 returns 1 if the item is safe to delete,
206
207 "book_on_loan" if the item is checked out,
208
209 "not_same_branch" if the item is blocked by independent branches,
210
211 "book_reserved" if the there are holds aganst the item, or
212
213 "linked_analytics" if the item has linked analytic records.
214
215 =cut
216
217 sub safe_to_delete {
218     my ($self) = @_;
219
220     return "book_on_loan" if $self->checkout;
221
222     return "not_same_branch"
223       if defined C4::Context->userenv
224       and !C4::Context->IsSuperLibrarian()
225       and C4::Context->preference("IndependentBranches")
226       and ( C4::Context->userenv->{branch} ne $self->homebranch );
227
228     # check it doesn't have a waiting reserve
229     return "book_reserved"
230       if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
231
232     return "linked_analytics"
233       if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
234
235     return 1;
236 }
237
238 =head3 move_to_deleted
239
240 my $is_moved = $item->move_to_deleted;
241
242 Move an item to the deleteditems table.
243 This can be done before deleting an item, to make sure the data are not completely deleted.
244
245 =cut
246
247 sub move_to_deleted {
248     my ($self) = @_;
249     my $item_infos = $self->unblessed;
250     delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
251     return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
252 }
253
254
255 =head3 effective_itemtype
256
257 Returns the itemtype for the item based on whether item level itemtypes are set or not.
258
259 =cut
260
261 sub effective_itemtype {
262     my ( $self ) = @_;
263
264     return $self->_result()->effective_itemtype();
265 }
266
267 =head3 home_branch
268
269 =cut
270
271 sub home_branch {
272     my ($self) = @_;
273
274     $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
275
276     return $self->{_home_branch};
277 }
278
279 =head3 holding_branch
280
281 =cut
282
283 sub holding_branch {
284     my ($self) = @_;
285
286     $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
287
288     return $self->{_holding_branch};
289 }
290
291 =head3 biblio
292
293 my $biblio = $item->biblio;
294
295 Return the bibliographic record of this item
296
297 =cut
298
299 sub biblio {
300     my ( $self ) = @_;
301     my $biblio_rs = $self->_result->biblio;
302     return Koha::Biblio->_new_from_dbic( $biblio_rs );
303 }
304
305 =head3 biblioitem
306
307 my $biblioitem = $item->biblioitem;
308
309 Return the biblioitem record of this item
310
311 =cut
312
313 sub biblioitem {
314     my ( $self ) = @_;
315     my $biblioitem_rs = $self->_result->biblioitem;
316     return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
317 }
318
319 =head3 checkout
320
321 my $checkout = $item->checkout;
322
323 Return the checkout for this item
324
325 =cut
326
327 sub checkout {
328     my ( $self ) = @_;
329     my $checkout_rs = $self->_result->issue;
330     return unless $checkout_rs;
331     return Koha::Checkout->_new_from_dbic( $checkout_rs );
332 }
333
334 =head3 holds
335
336 my $holds = $item->holds();
337 my $holds = $item->holds($params);
338 my $holds = $item->holds({ found => 'W'});
339
340 Return holds attached to an item, optionally accept a hashref of params to pass to search
341
342 =cut
343
344 sub holds {
345     my ( $self,$params ) = @_;
346     my $holds_rs = $self->_result->reserves->search($params);
347     return Koha::Holds->_new_from_dbic( $holds_rs );
348 }
349
350 =head3 get_transfer
351
352 my $transfer = $item->get_transfer;
353
354 Return the transfer if the item is in transit or undef
355
356 =cut
357
358 sub get_transfer {
359     my ( $self ) = @_;
360     my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
361     return unless $transfer_rs;
362     return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
363 }
364
365 =head3 last_returned_by
366
367 Gets and sets the last borrower to return an item.
368
369 Accepts and returns Koha::Patron objects
370
371 $item->last_returned_by( $borrowernumber );
372
373 $last_returned_by = $item->last_returned_by();
374
375 =cut
376
377 sub last_returned_by {
378     my ( $self, $borrower ) = @_;
379
380     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
381
382     if ($borrower) {
383         return $items_last_returned_by_rs->update_or_create(
384             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
385     }
386     else {
387         unless ( $self->{_last_returned_by} ) {
388             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
389             if ($result) {
390                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
391             }
392         }
393
394         return $self->{_last_returned_by};
395     }
396 }
397
398 =head3 can_article_request
399
400 my $bool = $item->can_article_request( $borrower )
401
402 Returns true if item can be specifically requested
403
404 $borrower must be a Koha::Patron object
405
406 =cut
407
408 sub can_article_request {
409     my ( $self, $borrower ) = @_;
410
411     my $rule = $self->article_request_type($borrower);
412
413     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
414     return q{};
415 }
416
417 =head3 hidden_in_opac
418
419 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
420
421 Returns true if item fields match the hidding criteria defined in $rules.
422 Returns false otherwise.
423
424 Takes HASHref that can have the following parameters:
425     OPTIONAL PARAMETERS:
426     $rules : { <field> => [ value_1, ... ], ... }
427
428 Note: $rules inherits its structure from the parsed YAML from reading
429 the I<OpacHiddenItems> system preference.
430
431 =cut
432
433 sub hidden_in_opac {
434     my ( $self, $params ) = @_;
435
436     my $rules = $params->{rules} // {};
437
438     return 1
439         if C4::Context->preference('hidelostitems') and
440            $self->itemlost > 0;
441
442     my $hidden_in_opac = 0;
443
444     foreach my $field ( keys %{$rules} ) {
445
446         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
447             $hidden_in_opac = 1;
448             last;
449         }
450     }
451
452     return $hidden_in_opac;
453 }
454
455 =head3 can_be_transferred
456
457 $item->can_be_transferred({ to => $to_library, from => $from_library })
458 Checks if an item can be transferred to given library.
459
460 This feature is controlled by two system preferences:
461 UseBranchTransferLimits to enable / disable the feature
462 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
463                          for setting the limitations
464
465 Takes HASHref that can have the following parameters:
466     MANDATORY PARAMETERS:
467     $to   : Koha::Library
468     OPTIONAL PARAMETERS:
469     $from : Koha::Library  # if not given, item holdingbranch
470                            # will be used instead
471
472 Returns 1 if item can be transferred to $to_library, otherwise 0.
473
474 To find out whether at least one item of a Koha::Biblio can be transferred, please
475 see Koha::Biblio->can_be_transferred() instead of using this method for
476 multiple items of the same biblio.
477
478 =cut
479
480 sub can_be_transferred {
481     my ($self, $params) = @_;
482
483     my $to   = $params->{to};
484     my $from = $params->{from};
485
486     $to   = $to->branchcode;
487     $from = defined $from ? $from->branchcode : $self->holdingbranch;
488
489     return 1 if $from eq $to; # Transfer to current branch is allowed
490     return 1 unless C4::Context->preference('UseBranchTransferLimits');
491
492     my $limittype = C4::Context->preference('BranchTransferLimitsType');
493     return Koha::Item::Transfer::Limits->search({
494         toBranch => $to,
495         fromBranch => $from,
496         $limittype => $limittype eq 'itemtype'
497                         ? $self->effective_itemtype : $self->ccode
498     })->count ? 0 : 1;
499 }
500
501 =head3 pickup_locations
502
503 @pickup_locations = $item->pickup_locations( {patron => $patron } )
504
505 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)
506 and if item can be transferred to each pickup location.
507
508 =cut
509
510 sub pickup_locations {
511     my ($self, $params) = @_;
512
513     my $patron = $params->{patron};
514
515     my $circ_control_branch =
516       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
517     my $branchitemrule =
518       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
519
520     my @libs;
521     if(defined $patron) {
522         return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
523         return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
524     }
525
526     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
527         @libs  = $self->home_branch->get_hold_libraries;
528         push @libs, $self->home_branch unless scalar(@libs) > 0;
529     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
530         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
531         @libs  = $plib->get_hold_libraries;
532         push @libs, $self->home_branch unless scalar(@libs) > 0;
533     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
534         push @libs, $self->home_branch;
535     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
536         push @libs, $self->holding_branch;
537     } else {
538         @libs = Koha::Libraries->search({
539             pickup_location => 1
540         }, {
541             order_by => ['branchname']
542         })->as_list;
543     }
544
545     my @pickup_locations;
546     foreach my $library (@libs) {
547         if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
548             push @pickup_locations, $library;
549         }
550     }
551
552     return wantarray ? @pickup_locations : \@pickup_locations;
553 }
554
555 =head3 article_request_type
556
557 my $type = $item->article_request_type( $borrower )
558
559 returns 'yes', 'no', 'bib_only', or 'item_only'
560
561 $borrower must be a Koha::Patron object
562
563 =cut
564
565 sub article_request_type {
566     my ( $self, $borrower ) = @_;
567
568     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
569     my $branchcode =
570         $branch_control eq 'homebranch'    ? $self->homebranch
571       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
572       :                                      undef;
573     my $borrowertype = $borrower->categorycode;
574     my $itemtype = $self->effective_itemtype();
575     my $rule = Koha::CirculationRules->get_effective_rule(
576         {
577             rule_name    => 'article_requests',
578             categorycode => $borrowertype,
579             itemtype     => $itemtype,
580             branchcode   => $branchcode
581         }
582     );
583
584     return q{} unless $rule;
585     return $rule->rule_value || q{}
586 }
587
588 =head3 current_holds
589
590 =cut
591
592 sub current_holds {
593     my ( $self ) = @_;
594     my $attributes = { order_by => 'priority' };
595     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
596     my $params = {
597         itemnumber => $self->itemnumber,
598         suspend => 0,
599         -or => [
600             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
601             waitingdate => { '!=' => undef },
602         ],
603     };
604     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
605     return Koha::Holds->_new_from_dbic($hold_rs);
606 }
607
608 =head3 stockrotationitem
609
610   my $sritem = Koha::Item->stockrotationitem;
611
612 Returns the stock rotation item associated with the current item.
613
614 =cut
615
616 sub stockrotationitem {
617     my ( $self ) = @_;
618     my $rs = $self->_result->stockrotationitem;
619     return 0 if !$rs;
620     return Koha::StockRotationItem->_new_from_dbic( $rs );
621 }
622
623 =head3 add_to_rota
624
625   my $item = $item->add_to_rota($rota_id);
626
627 Add this item to the rota identified by $ROTA_ID, which means associating it
628 with the first stage of that rota.  Should this item already be associated
629 with a rota, then we will move it to the new rota.
630
631 =cut
632
633 sub add_to_rota {
634     my ( $self, $rota_id ) = @_;
635     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
636     return $self;
637 }
638
639 =head3 has_pending_hold
640
641   my $is_pending_hold = $item->has_pending_hold();
642
643 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
644
645 =cut
646
647 sub has_pending_hold {
648     my ( $self ) = @_;
649     my $pending_hold = $self->_result->tmp_holdsqueues;
650     return $pending_hold->count ? 1: 0;
651 }
652
653 =head3 as_marc_field
654
655     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
656     my $field = $item->as_marc_field({ [ mss => $mss ] });
657
658 This method returns a MARC::Field object representing the Koha::Item object
659 with the current mappings configuration.
660
661 =cut
662
663 sub as_marc_field {
664     my ( $self, $params ) = @_;
665
666     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
667     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
668
669     my @subfields;
670
671     my @columns = $self->_result->result_source->columns;
672
673     foreach my $item_field ( @columns ) {
674         my $mapping = $mss->{ "items.$item_field"}[0];
675         my $tagfield    = $mapping->{tagfield};
676         my $tagsubfield = $mapping->{tagsubfield};
677         next if !$tagfield; # TODO: Should we raise an exception instead?
678                             # Feels like safe fallback is better
679
680         push @subfields, $tagsubfield => $self->$item_field;
681     }
682
683     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
684     push( @subfields, @{$unlinked_item_subfields} )
685         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
686
687     my $field;
688
689     $field = MARC::Field->new(
690         "$item_tag", ' ', ' ', @subfields
691     ) if @subfields;
692
693     return $field;
694 }
695
696 =head3 to_api_mapping
697
698 This method returns the mapping for representing a Koha::Item object
699 on the API.
700
701 =cut
702
703 sub to_api_mapping {
704     return {
705         itemnumber               => 'item_id',
706         biblionumber             => 'biblio_id',
707         biblioitemnumber         => undef,
708         barcode                  => 'external_id',
709         dateaccessioned          => 'acquisition_date',
710         booksellerid             => 'acquisition_source',
711         homebranch               => 'home_library_id',
712         price                    => 'purchase_price',
713         replacementprice         => 'replacement_price',
714         replacementpricedate     => 'replacement_price_date',
715         datelastborrowed         => 'last_checkout_date',
716         datelastseen             => 'last_seen_date',
717         stack                    => undef,
718         notforloan               => 'not_for_loan_status',
719         damaged                  => 'damaged_status',
720         damaged_on               => 'damaged_date',
721         itemlost                 => 'lost_status',
722         itemlost_on              => 'lost_date',
723         withdrawn                => 'withdrawn',
724         withdrawn_on             => 'withdrawn_date',
725         itemcallnumber           => 'callnumber',
726         coded_location_qualifier => 'coded_location_qualifier',
727         issues                   => 'checkouts_count',
728         renewals                 => 'renewals_count',
729         reserves                 => 'holds_count',
730         restricted               => 'restricted_status',
731         itemnotes                => 'public_notes',
732         itemnotes_nonpublic      => 'internal_notes',
733         holdingbranch            => 'holding_library_id',
734         paidfor                  => undef,
735         timestamp                => 'timestamp',
736         location                 => 'location',
737         permanent_location       => 'permanent_location',
738         onloan                   => 'checked_out_date',
739         cn_source                => 'call_number_source',
740         cn_sort                  => 'call_number_sort',
741         ccode                    => 'collection_code',
742         materials                => 'materials_notes',
743         uri                      => 'uri',
744         itype                    => 'item_type',
745         more_subfields_xml       => 'extended_subfields',
746         enumchron                => 'serial_issue_number',
747         copynumber               => 'copy_number',
748         stocknumber              => 'inventory_number',
749         new_status               => 'new_status'
750     };
751 }
752
753 =head2 Internal methods
754
755 =head3 _after_item_action_hooks
756
757 Helper method that takes care of calling all plugin hooks
758
759 =cut
760
761 sub _after_item_action_hooks {
762     my ( $self, $params ) = @_;
763
764     my $action = $params->{action};
765
766     if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
767
768         my @plugins = Koha::Plugins->new->GetPlugins({
769             method => 'after_item_action',
770         });
771
772         if (@plugins) {
773
774             foreach my $plugin ( @plugins ) {
775                 try {
776                     $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
777                 }
778                 catch {
779                     warn "$_";
780                 };
781             }
782         }
783     }
784 }
785
786 =head3 _type
787
788 =cut
789
790 sub _type {
791     return 'Item';
792 }
793
794 =head1 AUTHOR
795
796 Kyle M Hall <kyle@bywatersolutions.com>
797
798 =cut
799
800 1;