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