Bug 24321: Clean /biblios
[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 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 C4::Reserves;
31 use Koha::Checkouts;
32 use Koha::IssuingRules;
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 $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $borrowertype, itemtype => $itemtype, branchcode => $branchcode });
373
374     return q{} unless $issuing_rule;
375     return $issuing_rule->article_requests || q{}
376 }
377
378 =head3 current_holds
379
380 =cut
381
382 sub current_holds {
383     my ( $self ) = @_;
384     my $attributes = { order_by => 'priority' };
385     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
386     my $params = {
387         itemnumber => $self->itemnumber,
388         suspend => 0,
389         -or => [
390             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
391             waitingdate => { '!=' => undef },
392         ],
393     };
394     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
395     return Koha::Holds->_new_from_dbic($hold_rs);
396 }
397
398 =head3 stockrotationitem
399
400   my $sritem = Koha::Item->stockrotationitem;
401
402 Returns the stock rotation item associated with the current item.
403
404 =cut
405
406 sub stockrotationitem {
407     my ( $self ) = @_;
408     my $rs = $self->_result->stockrotationitem;
409     return 0 if !$rs;
410     return Koha::StockRotationItem->_new_from_dbic( $rs );
411 }
412
413 =head3 add_to_rota
414
415   my $item = $item->add_to_rota($rota_id);
416
417 Add this item to the rota identified by $ROTA_ID, which means associating it
418 with the first stage of that rota.  Should this item already be associated
419 with a rota, then we will move it to the new rota.
420
421 =cut
422
423 sub add_to_rota {
424     my ( $self, $rota_id ) = @_;
425     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
426     return $self;
427 }
428
429 =head3 has_pending_hold
430
431   my $is_pending_hold = $item->has_pending_hold();
432
433 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
434
435 =cut
436
437 sub has_pending_hold {
438     my ( $self ) = @_;
439     my $pending_hold = $self->_result->tmp_holdsqueues;
440     return $pending_hold->count ? 1: 0;
441 }
442
443 =head3 as_marc_field
444
445     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
446     my $field = $item->as_marc_field({ [ mss => $mss ] });
447
448 This method returns a MARC::Field object representing the Koha::Item object
449 with the current mappings configuration.
450
451 =cut
452
453 sub as_marc_field {
454     my ( $self, $params ) = @_;
455
456     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
457     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
458
459     my @subfields;
460
461     my @columns = $self->_result->result_source->columns;
462
463     foreach my $item_field ( @columns ) {
464         my $mapping = $mss->{ "items.$item_field"}[0];
465         my $tagfield    = $mapping->{tagfield};
466         my $tagsubfield = $mapping->{tagsubfield};
467         next if !$tagfield; # TODO: Should we raise an exception instead?
468                             # Feels like safe fallback is better
469
470         push @subfields, $tagsubfield => $self->$item_field;
471     }
472
473     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
474     push( @subfields, @{$unlinked_item_subfields} )
475         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
476
477     my $field;
478
479     $field = MARC::Field->new(
480         "$item_tag", ' ', ' ', @subfields
481     ) if @subfields;
482
483     return $field;
484 }
485
486 =head3 to_api_mapping
487
488 This method returns the mapping for representing a Koha::Item object
489 on the API.
490
491 =cut
492
493 sub to_api_mapping {
494     return {
495         itemnumber               => 'item_id',
496         biblionumber             => 'biblio_id',
497         biblioitemnumber         => undef,
498         barcode                  => 'external_id',
499         dateaccessioned          => 'acquisition_date',
500         booksellerid             => 'acquisition_source',
501         homebranch               => 'home_library_id',
502         price                    => 'purchase_price',
503         replacementprice         => 'replacement_price',
504         replacementpricedate     => 'replacement_price_date',
505         datelastborrowed         => 'last_checkout_date',
506         datelastseen             => 'last_seen_date',
507         stack                    => undef,
508         notforloan               => 'not_for_loan_status',
509         damaged                  => 'damaged_status',
510         damaged_on               => 'damaged_date',
511         itemlost                 => 'lost_status',
512         itemlost_on              => 'lost_date',
513         withdrawn                => 'withdrawn',
514         withdrawn_on             => 'withdrawn_date',
515         itemcallnumber           => 'callnumber',
516         coded_location_qualifier => 'coded_location_qualifier',
517         issues                   => 'checkouts_count',
518         renewals                 => 'renewals_count',
519         reserves                 => 'holds_count',
520         restricted               => 'restricted_status',
521         itemnotes                => 'public_notes',
522         itemnotes_nonpublic      => 'internal_notes',
523         holdingbranch            => 'holding_library_id',
524         paidfor                  => undef,
525         timestamp                => 'timestamp',
526         location                 => 'location',
527         permanent_location       => 'permanent_location',
528         onloan                   => 'checked_out_date',
529         cn_source                => 'call_number_source',
530         cn_sort                  => 'call_number_sort',
531         ccode                    => 'collection_code',
532         materials                => 'materials_notes',
533         uri                      => 'uri',
534         itype                    => 'item_type',
535         more_subfields_xml       => 'extended_subfields',
536         enumchron                => 'serial_issue_number',
537         copynumber               => 'copy_number',
538         stocknumber              => 'inventory_number',
539         new_status               => 'new_status'
540     };
541 }
542
543 =head2 Internal methods
544
545 =head3 _type
546
547 =cut
548
549 sub _type {
550     return 'Item';
551 }
552
553 =head1 AUTHOR
554
555 Kyle M Hall <kyle@bywatersolutions.com>
556
557 =cut
558
559 1;