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