]> git.koha-community.org Git - koha.git/blob - Koha/Item.pm
Bug 23463: Move plugin hook
[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 use Data::Dumper;
25 use Try::Tiny;
26
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
29
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
34 use C4::ClassSource; # FIXME We would like to avoid that
35 use C4::Log qw( logaction );
36
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Patrons;
42 use Koha::Plugins;
43 use Koha::Libraries;
44 use Koha::StockRotationItem;
45 use Koha::StockRotationRotas;
46
47 use base qw(Koha::Object);
48
49 =head1 NAME
50
51 Koha::Item - Koha Item object class
52
53 =head1 API
54
55 =head2 Class methods
56
57 =cut
58
59 =head3 store
60
61 =cut
62
63 sub store {
64     my ($self, $params) = @_;
65
66     my $log_action = $params->{log_action} // 1;
67
68     # We do not want to oblige callers to pass this value
69     # Dev conveniences vs performance?
70     unless ( $self->biblioitemnumber ) {
71         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
72     }
73
74     # See related changes from C4::Items::AddItem
75     unless ( $self->itype ) {
76         $self->itype($self->biblio->biblioitem->itemtype);
77     }
78
79     if ( $self->itemcallnumber ) { # This could be improved, we should recalculate it only if changed
80         my $cn_sort = GetClassSort($self->cn_source, $self->itemcallnumber, "");
81         $self->cn_sort($cn_sort);
82     }
83
84     my $today = dt_from_string;
85     unless ( $self->in_storage ) { #AddItem
86         unless ( $self->permanent_location ) {
87             $self->permanent_location($self->location);
88         }
89         unless ( $self->replacementpricedate ) {
90             $self->replacementpricedate($today);
91         }
92         unless ( $self->datelastseen ) {
93             $self->datelastseen($today);
94         }
95
96         unless ( $self->dateaccessioned ) {
97             $self->dateaccessioned($today);
98         }
99
100         C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
101
102         logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
103           if $log_action && C4::Context->preference("CataloguingLog");
104
105         $self->_after_item_action_hooks({ action => 'create' });
106
107     } else { # ModItem
108
109         { # Update *_on  fields if needed
110           # Why not for AddItem as well?
111             my @fields = qw( itemlost withdrawn damaged );
112
113             # Only retrieve the item if we need to set an "on" date field
114             if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
115                 my $pre_mod_item = $self->get_from_storage;
116                 for my $field (@fields) {
117                     if (    $self->$field
118                         and not $pre_mod_item->$field )
119                     {
120                         my $field_on = "${field}_on";
121                         $self->$field_on(
122                           DateTime::Format::MySQL->format_datetime( dt_from_string() )
123                         );
124                     }
125                 }
126             }
127
128             # If the field is defined but empty, we are removing and,
129             # and thus need to clear out the 'on' field as well
130             for my $field (@fields) {
131                 if ( defined( $self->$field ) && !$self->$field ) {
132                     my $field_on = "${field}_on";
133                     $self->$field_on(undef);
134                 }
135             }
136         }
137
138         if ( defined $self->location and $self->location ne 'CART' and $self->location ne 'PROC' and not $self->permanent_location ) {
139             $self->permanent_location($self->location);
140         }
141
142         $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
143
144         C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
145
146         $self->_after_item_action_hooks({ action => 'modify' });
147
148         logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
149           if $log_action && C4::Context->preference("CataloguingLog");
150     }
151
152     unless ( $self->dateaccessioned ) {
153         $self->dateaccessioned($today);
154     }
155
156     return $self->SUPER::store;
157 }
158
159 =head3 effective_itemtype
160
161 Returns the itemtype for the item based on whether item level itemtypes are set or not.
162
163 =cut
164
165 sub effective_itemtype {
166     my ( $self ) = @_;
167
168     return $self->_result()->effective_itemtype();
169 }
170
171 =head3 home_branch
172
173 =cut
174
175 sub home_branch {
176     my ($self) = @_;
177
178     $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
179
180     return $self->{_home_branch};
181 }
182
183 =head3 holding_branch
184
185 =cut
186
187 sub holding_branch {
188     my ($self) = @_;
189
190     $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
191
192     return $self->{_holding_branch};
193 }
194
195 =head3 biblio
196
197 my $biblio = $item->biblio;
198
199 Return the bibliographic record of this item
200
201 =cut
202
203 sub biblio {
204     my ( $self ) = @_;
205     my $biblio_rs = $self->_result->biblio;
206     return Koha::Biblio->_new_from_dbic( $biblio_rs );
207 }
208
209 =head3 biblioitem
210
211 my $biblioitem = $item->biblioitem;
212
213 Return the biblioitem record of this item
214
215 =cut
216
217 sub biblioitem {
218     my ( $self ) = @_;
219     my $biblioitem_rs = $self->_result->biblioitem;
220     return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
221 }
222
223 =head3 checkout
224
225 my $checkout = $item->checkout;
226
227 Return the checkout for this item
228
229 =cut
230
231 sub checkout {
232     my ( $self ) = @_;
233     my $checkout_rs = $self->_result->issue;
234     return unless $checkout_rs;
235     return Koha::Checkout->_new_from_dbic( $checkout_rs );
236 }
237
238 =head3 holds
239
240 my $holds = $item->holds();
241 my $holds = $item->holds($params);
242 my $holds = $item->holds({ found => 'W'});
243
244 Return holds attached to an item, optionally accept a hashref of params to pass to search
245
246 =cut
247
248 sub holds {
249     my ( $self,$params ) = @_;
250     my $holds_rs = $self->_result->reserves->search($params);
251     return Koha::Holds->_new_from_dbic( $holds_rs );
252 }
253
254 =head3 get_transfer
255
256 my $transfer = $item->get_transfer;
257
258 Return the transfer if the item is in transit or undef
259
260 =cut
261
262 sub get_transfer {
263     my ( $self ) = @_;
264     my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
265     return unless $transfer_rs;
266     return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
267 }
268
269 =head3 last_returned_by
270
271 Gets and sets the last borrower to return an item.
272
273 Accepts and returns Koha::Patron objects
274
275 $item->last_returned_by( $borrowernumber );
276
277 $last_returned_by = $item->last_returned_by();
278
279 =cut
280
281 sub last_returned_by {
282     my ( $self, $borrower ) = @_;
283
284     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
285
286     if ($borrower) {
287         return $items_last_returned_by_rs->update_or_create(
288             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
289     }
290     else {
291         unless ( $self->{_last_returned_by} ) {
292             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
293             if ($result) {
294                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
295             }
296         }
297
298         return $self->{_last_returned_by};
299     }
300 }
301
302 =head3 can_article_request
303
304 my $bool = $item->can_article_request( $borrower )
305
306 Returns true if item can be specifically requested
307
308 $borrower must be a Koha::Patron object
309
310 =cut
311
312 sub can_article_request {
313     my ( $self, $borrower ) = @_;
314
315     my $rule = $self->article_request_type($borrower);
316
317     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
318     return q{};
319 }
320
321 =head3 hidden_in_opac
322
323 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
324
325 Returns true if item fields match the hidding criteria defined in $rules.
326 Returns false otherwise.
327
328 Takes HASHref that can have the following parameters:
329     OPTIONAL PARAMETERS:
330     $rules : { <field> => [ value_1, ... ], ... }
331
332 Note: $rules inherits its structure from the parsed YAML from reading
333 the I<OpacHiddenItems> system preference.
334
335 =cut
336
337 sub hidden_in_opac {
338     my ( $self, $params ) = @_;
339
340     my $rules = $params->{rules} // {};
341
342     return 1
343         if C4::Context->preference('hidelostitems') and
344            $self->itemlost > 0;
345
346     my $hidden_in_opac = 0;
347
348     foreach my $field ( keys %{$rules} ) {
349
350         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
351             $hidden_in_opac = 1;
352             last;
353         }
354     }
355
356     return $hidden_in_opac;
357 }
358
359 =head3 can_be_transferred
360
361 $item->can_be_transferred({ to => $to_library, from => $from_library })
362 Checks if an item can be transferred to given library.
363
364 This feature is controlled by two system preferences:
365 UseBranchTransferLimits to enable / disable the feature
366 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
367                          for setting the limitations
368
369 Takes HASHref that can have the following parameters:
370     MANDATORY PARAMETERS:
371     $to   : Koha::Library
372     OPTIONAL PARAMETERS:
373     $from : Koha::Library  # if not given, item holdingbranch
374                            # will be used instead
375
376 Returns 1 if item can be transferred to $to_library, otherwise 0.
377
378 To find out whether at least one item of a Koha::Biblio can be transferred, please
379 see Koha::Biblio->can_be_transferred() instead of using this method for
380 multiple items of the same biblio.
381
382 =cut
383
384 sub can_be_transferred {
385     my ($self, $params) = @_;
386
387     my $to   = $params->{to};
388     my $from = $params->{from};
389
390     $to   = $to->branchcode;
391     $from = defined $from ? $from->branchcode : $self->holdingbranch;
392
393     return 1 if $from eq $to; # Transfer to current branch is allowed
394     return 1 unless C4::Context->preference('UseBranchTransferLimits');
395
396     my $limittype = C4::Context->preference('BranchTransferLimitsType');
397     return Koha::Item::Transfer::Limits->search({
398         toBranch => $to,
399         fromBranch => $from,
400         $limittype => $limittype eq 'itemtype'
401                         ? $self->effective_itemtype : $self->ccode
402     })->count ? 0 : 1;
403 }
404
405 =head3 pickup_locations
406
407 @pickup_locations = $item->pickup_locations( {patron => $patron } )
408
409 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)
410 and if item can be transferred to each pickup location.
411
412 =cut
413
414 sub pickup_locations {
415     my ($self, $params) = @_;
416
417     my $patron = $params->{patron};
418
419     my $circ_control_branch =
420       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
421     my $branchitemrule =
422       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
423
424     my @libs;
425     if(defined $patron) {
426         return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
427         return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
428     }
429
430     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
431         @libs  = $self->home_branch->get_hold_libraries;
432         push @libs, $self->home_branch unless scalar(@libs) > 0;
433     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
434         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
435         @libs  = $plib->get_hold_libraries;
436         push @libs, $self->home_branch unless scalar(@libs) > 0;
437     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
438         push @libs, $self->home_branch;
439     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
440         push @libs, $self->holding_branch;
441     } else {
442         @libs = Koha::Libraries->search({
443             pickup_location => 1
444         }, {
445             order_by => ['branchname']
446         })->as_list;
447     }
448
449     my @pickup_locations;
450     foreach my $library (@libs) {
451         if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
452             push @pickup_locations, $library;
453         }
454     }
455
456     return wantarray ? @pickup_locations : \@pickup_locations;
457 }
458
459 =head3 article_request_type
460
461 my $type = $item->article_request_type( $borrower )
462
463 returns 'yes', 'no', 'bib_only', or 'item_only'
464
465 $borrower must be a Koha::Patron object
466
467 =cut
468
469 sub article_request_type {
470     my ( $self, $borrower ) = @_;
471
472     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
473     my $branchcode =
474         $branch_control eq 'homebranch'    ? $self->homebranch
475       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
476       :                                      undef;
477     my $borrowertype = $borrower->categorycode;
478     my $itemtype = $self->effective_itemtype();
479     my $rule = Koha::CirculationRules->get_effective_rule(
480         {
481             rule_name    => 'article_requests',
482             categorycode => $borrowertype,
483             itemtype     => $itemtype,
484             branchcode   => $branchcode
485         }
486     );
487
488     return q{} unless $rule;
489     return $rule->rule_value || q{}
490 }
491
492 =head3 current_holds
493
494 =cut
495
496 sub current_holds {
497     my ( $self ) = @_;
498     my $attributes = { order_by => 'priority' };
499     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
500     my $params = {
501         itemnumber => $self->itemnumber,
502         suspend => 0,
503         -or => [
504             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
505             waitingdate => { '!=' => undef },
506         ],
507     };
508     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
509     return Koha::Holds->_new_from_dbic($hold_rs);
510 }
511
512 =head3 stockrotationitem
513
514   my $sritem = Koha::Item->stockrotationitem;
515
516 Returns the stock rotation item associated with the current item.
517
518 =cut
519
520 sub stockrotationitem {
521     my ( $self ) = @_;
522     my $rs = $self->_result->stockrotationitem;
523     return 0 if !$rs;
524     return Koha::StockRotationItem->_new_from_dbic( $rs );
525 }
526
527 =head3 add_to_rota
528
529   my $item = $item->add_to_rota($rota_id);
530
531 Add this item to the rota identified by $ROTA_ID, which means associating it
532 with the first stage of that rota.  Should this item already be associated
533 with a rota, then we will move it to the new rota.
534
535 =cut
536
537 sub add_to_rota {
538     my ( $self, $rota_id ) = @_;
539     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
540     return $self;
541 }
542
543 =head3 has_pending_hold
544
545   my $is_pending_hold = $item->has_pending_hold();
546
547 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
548
549 =cut
550
551 sub has_pending_hold {
552     my ( $self ) = @_;
553     my $pending_hold = $self->_result->tmp_holdsqueues;
554     return $pending_hold->count ? 1: 0;
555 }
556
557 =head3 as_marc_field
558
559     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
560     my $field = $item->as_marc_field({ [ mss => $mss ] });
561
562 This method returns a MARC::Field object representing the Koha::Item object
563 with the current mappings configuration.
564
565 =cut
566
567 sub as_marc_field {
568     my ( $self, $params ) = @_;
569
570     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
571     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
572
573     my @subfields;
574
575     my @columns = $self->_result->result_source->columns;
576
577     foreach my $item_field ( @columns ) {
578         my $mapping = $mss->{ "items.$item_field"}[0];
579         my $tagfield    = $mapping->{tagfield};
580         my $tagsubfield = $mapping->{tagsubfield};
581         next if !$tagfield; # TODO: Should we raise an exception instead?
582                             # Feels like safe fallback is better
583
584         push @subfields, $tagsubfield => $self->$item_field;
585     }
586
587     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
588     push( @subfields, @{$unlinked_item_subfields} )
589         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
590
591     my $field;
592
593     $field = MARC::Field->new(
594         "$item_tag", ' ', ' ', @subfields
595     ) if @subfields;
596
597     return $field;
598 }
599
600 =head3 to_api_mapping
601
602 This method returns the mapping for representing a Koha::Item object
603 on the API.
604
605 =cut
606
607 sub to_api_mapping {
608     return {
609         itemnumber               => 'item_id',
610         biblionumber             => 'biblio_id',
611         biblioitemnumber         => undef,
612         barcode                  => 'external_id',
613         dateaccessioned          => 'acquisition_date',
614         booksellerid             => 'acquisition_source',
615         homebranch               => 'home_library_id',
616         price                    => 'purchase_price',
617         replacementprice         => 'replacement_price',
618         replacementpricedate     => 'replacement_price_date',
619         datelastborrowed         => 'last_checkout_date',
620         datelastseen             => 'last_seen_date',
621         stack                    => undef,
622         notforloan               => 'not_for_loan_status',
623         damaged                  => 'damaged_status',
624         damaged_on               => 'damaged_date',
625         itemlost                 => 'lost_status',
626         itemlost_on              => 'lost_date',
627         withdrawn                => 'withdrawn',
628         withdrawn_on             => 'withdrawn_date',
629         itemcallnumber           => 'callnumber',
630         coded_location_qualifier => 'coded_location_qualifier',
631         issues                   => 'checkouts_count',
632         renewals                 => 'renewals_count',
633         reserves                 => 'holds_count',
634         restricted               => 'restricted_status',
635         itemnotes                => 'public_notes',
636         itemnotes_nonpublic      => 'internal_notes',
637         holdingbranch            => 'holding_library_id',
638         paidfor                  => undef,
639         timestamp                => 'timestamp',
640         location                 => 'location',
641         permanent_location       => 'permanent_location',
642         onloan                   => 'checked_out_date',
643         cn_source                => 'call_number_source',
644         cn_sort                  => 'call_number_sort',
645         ccode                    => 'collection_code',
646         materials                => 'materials_notes',
647         uri                      => 'uri',
648         itype                    => 'item_type',
649         more_subfields_xml       => 'extended_subfields',
650         enumchron                => 'serial_issue_number',
651         copynumber               => 'copy_number',
652         stocknumber              => 'inventory_number',
653         new_status               => 'new_status'
654     };
655 }
656
657 =head2 Internal methods
658
659 =head3 _after_item_action_hooks
660
661 Helper method that takes care of calling all plugin hooks
662
663 =cut
664
665 sub _after_item_action_hooks {
666     my ( $self, $params ) = @_;
667
668     my $action = $params->{action};
669
670     if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
671
672         my @plugins = Koha::Plugins->new->GetPlugins({
673             method => 'after_item_action',
674         });
675
676         if (@plugins) {
677
678             foreach my $plugin ( @plugins ) {
679                 try {
680                     $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
681                 }
682                 catch {
683                     warn "$_";
684                 };
685             }
686         }
687     }
688 }
689
690 =head3 _type
691
692 =cut
693
694 sub _type {
695     return 'Item';
696 }
697
698 =head1 AUTHOR
699
700 Kyle M Hall <kyle@bywatersolutions.com>
701
702 =cut
703
704 1;