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