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