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