Bug 22284: (follow-up) Default to circulation control branch when no hold libraries...
[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 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     #warn $branch_control.' '.$branchitemrule->{holdallowed}.' '.$library->branchcode.' '.$patron->branchcode;
320
321     my @libs;
322     if(defined $patron) {
323         return @libs if $branchitemrule->{holdallowed} == 3 && !$library->validate_hold_sibling( {branchcode => $patron->branchcode} );
324         return @libs if $branchitemrule->{holdallowed} == 1 && $library->branchcode ne $patron->branchcode;
325     }
326
327     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
328         @libs  = $library->get_hold_libraries;
329         my $circ_control_library = Koha::Libraries->find($circ_control_branch);
330         push @libs, $circ_control_library unless scalar(@libs) > 0;
331     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
332         push @libs, $self->home_branch;
333     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
334         push @libs, $self->holding_branch;
335     } else {
336         @libs = Koha::Libraries->search({
337             pickup_location => 1
338         }, {
339             order_by => ['branchname']
340         })->as_list;
341     }
342
343     my @pickup_locations;
344     foreach my $library (@libs) {
345         if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
346             push @pickup_locations, $library->unblessed;
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;