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