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