Bug 35086: Add chunk_size option to elasticsearch configuration
[koha.git] / Koha / Items.pm
1 package Koha::Items;
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 use Array::Utils qw( array_minus );
22 use List::MoreUtils qw( uniq );
23 use Try::Tiny;
24
25 use C4::Context;
26 use C4::Biblio qw( GetMarcStructure GetMarcFromKohaField );
27 use C4::Circulation;
28
29 use Koha::Database;
30 use Koha::SearchEngine::Indexer;
31
32 use Koha::Item::Attributes;
33 use Koha::Item;
34 use Koha::CirculationRules;
35
36 use base qw(Koha::Objects);
37
38 use Koha::SearchEngine::Indexer;
39
40 =head1 NAME
41
42 Koha::Items - Koha Item object set class
43
44 =head1 API
45
46 =head2 Class methods
47
48 =cut
49
50 =head3 filter_by_for_hold
51
52     my $filtered_items = $items->filter_by_for_hold;
53
54 Return the items of the set that are *potentially* holdable.
55
56 Caller has the responsibility to call C4::Reserves::CanItemBeReserved before
57 placing a hold on one of those items.
58
59 =cut
60
61 sub filter_by_for_hold {
62     my ($self) = @_;
63
64     my @hold_not_allowed_itypes = Koha::CirculationRules->search(
65         {
66             rule_name    => 'holdallowed',
67             branchcode   => undef,
68             categorycode => undef,
69             rule_value   => 'not_allowed',
70         }
71     )->get_column('itemtype');
72     push @hold_not_allowed_itypes, Koha::ItemTypes->search({ notforloan => 1 })->get_column('itemtype');
73
74     my $params = {
75         itemlost   => 0,
76         withdrawn  => 0,
77         notforloan => { '<=' => 0 },    # items with negative or zero notforloan value are holdable
78         ( C4::Context->preference('AllowHoldsOnDamagedItems')? (): ( damaged => 0 ) ),
79         ( C4::Context->only_my_library() ? ( homebranch => C4::Context::mybranch() ) : () ),
80     };
81
82     if ( C4::Context->preference("item-level_itypes") ) {
83         return $self->search(
84             {
85                 %$params,
86                 itype        => { -not_in => \@hold_not_allowed_itypes },
87             }
88         );
89     } else {
90         return $self->search(
91             {
92                 %$params,
93                 'biblioitem.itemtype' => { -not_in => \@hold_not_allowed_itypes },
94             },
95             {
96                 join => 'biblioitem',
97             }
98         );
99     }
100 }
101
102 =head3 filter_by_visible_in_opac
103
104     my $filered_items = $items->filter_by_visible_in_opac(
105         {
106             [ patron => $patron ]
107         }
108     );
109
110 Returns a new resultset, containing those items that are not expected to be hidden in OPAC
111 for the passed I<Koha::Patron> object that is passed.
112
113 The I<OpacHiddenItems>, I<hidelostitems> and I<OpacHiddenItemsExceptions> system preferences
114 are honoured.
115
116 =cut
117
118 sub filter_by_visible_in_opac {
119     my ($self, $params) = @_;
120
121     my $patron = $params->{patron};
122
123     my $result = $self;
124
125     # Filter out OpacHiddenItems unless disabled by OpacHiddenItemsExceptions
126     unless ( $patron and $patron->category->override_hidden_items ) {
127         my $rules = C4::Context->yaml_preference('OpacHiddenItems') // {};
128
129         my $rules_params;
130         foreach my $field ( keys %$rules ) {
131             $rules_params->{'me.'.$field} =
132               [ { '-not_in' => $rules->{$field} }, undef ];
133         }
134
135         $result = $result->search( $rules_params );
136     }
137
138     if (C4::Context->preference('hidelostitems')) {
139         $result = $result->filter_out_lost;
140     }
141
142     return $result;
143 }
144
145 =head3 filter_out_lost
146
147     my $filered_items = $items->filter_out_lost;
148
149 Returns a new resultset, containing those items that are not marked as lost.
150
151 =cut
152
153 sub filter_out_lost {
154     my ($self) = @_;
155
156     my $params = { itemlost => 0 };
157
158     return $self->search( $params );
159 }
160
161 =head3 filter_by_bookable
162
163   my $filterd_items = $items->filter_by_bookable;
164
165 Returns a new resultset, containing only those items that are allowed to be booked.
166
167 =cut
168
169 sub filter_by_bookable {
170     my ($self) = @_;
171
172     my $params = { bookable => 1 };
173
174     return $self->search($params);
175 }
176
177 =head3 move_to_biblio
178
179  $items->move_to_biblio($to_biblio);
180
181 Move items to a given biblio.
182
183 =cut
184
185 sub move_to_biblio {
186     my ( $self, $to_biblio ) = @_;
187
188     my $biblionumbers = { $to_biblio->biblionumber => 1 };
189     while ( my $item = $self->next() ) {
190         $biblionumbers->{ $item->biblionumber } = 1;
191         $item->move_to_biblio( $to_biblio, { skip_record_index => 1 } );
192     }
193     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
194     for my $biblionumber ( keys %{$biblionumbers} ) {
195         $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
196     }
197 }
198
199 =head3 batch_update
200
201     Koha::Items->search->batch_update
202         {
203             new_values => {
204                 itemnotes => $new_item_notes,
205                 k         => $k,
206             },
207             regex_mod => {
208                 itemnotes_nonpublic => {
209                     search => 'foo',
210                     replace => 'bar',
211                     modifiers => 'gi',
212                 },
213             },
214             exclude_from_local_holds_priority => 1|0,
215             callback => sub {
216                 # increment something here
217             },
218         }
219     );
220
221 Batch update the items.
222
223 Returns ( $report, $self )
224 Report has 2 keys:
225   * modified_itemnumbers - list of the modified itemnumbers
226   * modified_fields - number of fields modified
227
228 Parameters:
229
230 =over
231
232 =item new_values
233
234 Allows to set a new value for given fields.
235 The key can be one of the item's column name, or one subfieldcode of a MARC subfields not linked with a Koha field
236
237 =item regex_mod
238
239 Allows to modify existing subfield's values using a regular expression
240
241 =item exclude_from_local_holds_priority
242
243 Set the passed boolean value to items.exclude_from_local_holds_priority
244
245 =item mark_items_returned
246
247 Move issues on these items to the old issues table, do not mark items found, or
248 adjust damaged/withdrawn statuses, or fines, or locations.
249
250 =item callback
251
252 Callback function to call after an item has been modified
253
254 =back
255
256 =cut
257
258 sub batch_update {
259     my ( $self, $params ) = @_;
260
261     my $regex_mod = $params->{regex_mod} || {};
262     my $new_values = $params->{new_values} || {};
263     my $exclude_from_local_holds_priority = $params->{exclude_from_local_holds_priority};
264     my $mark_items_returned = $params->{mark_items_returned};
265     my $callback = $params->{callback};
266
267     my (@modified_itemnumbers, $modified_fields);
268     my $i;
269     my $schema = Koha::Database->new->schema;
270     while ( my $item = $self->next ) {
271
272         try {$schema->txn_do(sub {
273             my $modified_holds_priority = 0;
274             my $item_returned = 0;
275             if ( defined $exclude_from_local_holds_priority ) {
276                 if(!defined $item->exclude_from_local_holds_priority || $item->exclude_from_local_holds_priority != $exclude_from_local_holds_priority) {
277                     $item->exclude_from_local_holds_priority($exclude_from_local_holds_priority)->store;
278                     $modified_holds_priority = 1;
279                 }
280             }
281
282             my $modified = 0;
283             my $new_values = {%$new_values};    # Don't modify the original
284
285             my $old_values = $item->unblessed;
286             if ( $item->more_subfields_xml ) {
287                 $old_values = {
288                     %$old_values,
289                     %{$item->additional_attributes->to_hashref},
290                 };
291             }
292
293             for my $attr ( keys %$regex_mod ) {
294                 my $old_value = $old_values->{$attr};
295
296                 next unless $old_value;
297
298                 my $value = apply_regex(
299                     {
300                         %{ $regex_mod->{$attr} },
301                         value => $old_value,
302                     }
303                 );
304
305                 $new_values->{$attr} = $value;
306             }
307
308             for my $attribute ( keys %$new_values ) {
309                 next if $attribute eq 'more_subfields_xml'; # Already counted before
310
311                 my $old = $old_values->{$attribute};
312                 my $new = $new_values->{$attribute};
313                 $modified++
314                   if ( defined $old xor defined $new )
315                   || ( defined $old && defined $new && $new ne $old );
316             }
317
318             { # Dealing with more_subfields_xml
319
320                 my $frameworkcode = $item->biblio->frameworkcode;
321                 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 });
322                 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
323
324                 my @more_subfield_tags = map {
325                     (
326                              ref($_)
327                           && %$_
328                           && !$_->{kohafield}    # Get subfields that are not mapped
329                       )
330                       ? $_->{tagsubfield}
331                       : ()
332                 } values %{ $tagslib->{$itemtag} };
333
334                 my $more_subfields_xml = Koha::Item::Attributes->new(
335                     {
336                         map {
337                             exists $new_values->{$_} ? ( $_ => $new_values->{$_} )
338                               : exists $old_values->{$_}
339                               ? ( $_ => $old_values->{$_} )
340                               : ()
341                         } @more_subfield_tags
342                     }
343                 )->to_marcxml($frameworkcode);
344
345                 $new_values->{more_subfields_xml} = $more_subfields_xml;
346
347                 delete $new_values->{$_} for @more_subfield_tags; # Clean the hash
348
349             }
350
351             if ( $modified ) {
352                 my $itemlost_pre = $item->itemlost;
353                 $item->set($new_values)->store({skip_record_index => 1});
354
355                 C4::Circulation::LostItem(
356                     $item->itemnumber, 'batchmod', undef,
357                     { skip_record_index => 1 }
358                 ) if $item->itemlost
359                       and not $itemlost_pre;
360             }
361             if ( $mark_items_returned ){
362                 my $issue = $item->checkout;
363                 if( $issue ){
364                         $item_returned = 1;
365                         C4::Circulation::MarkIssueReturned(
366                         $issue->borrowernumber,
367                         $item->itemnumber,
368                         undef,
369                         $issue->patron->privacy,
370                         {
371                             skip_record_index => 1,
372                             skip_holds_queue  => 1,
373                         }
374                     );
375                 }
376             }
377
378             push @modified_itemnumbers, $item->itemnumber if $modified || $modified_holds_priority || $item_returned;
379             $modified_fields += $modified + $modified_holds_priority + $item_returned;
380         })}
381         catch {
382             warn $_
383         };
384
385         if ( $callback ) {
386             $callback->(++$i);
387         }
388     }
389
390     if (@modified_itemnumbers) {
391         my @biblionumbers = uniq(
392             Koha::Items->search( { itemnumber => \@modified_itemnumbers } )
393                        ->get_column('biblionumber'));
394
395         if ( @biblionumbers ) {
396             my $indexer = Koha::SearchEngine::Indexer->new(
397                 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
398
399             $indexer->index_records( \@biblionumbers, 'specialUpdate',
400                 "biblioserver", undef );
401         }
402     }
403
404     return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self );
405 }
406
407 sub apply_regex {
408     # FIXME Should be moved outside of Koha::Items
409     # FIXME This is nearly identical to Koha::SimpleMARC::_modify_values
410     my ($params) = @_;
411     my $search   = $params->{search};
412     my $replace  = $params->{replace};
413     my $modifiers = $params->{modifiers} || q{};
414     my $value = $params->{value};
415
416     $replace =~ s/"/\\"/g;                    # Protection from embedded code
417     $replace = '"' . $replace . '"'; # Put in a string for /ee
418     my @available_modifiers = qw( i g );
419     my $retained_modifiers  = q||;
420     for my $modifier ( split //, $modifiers ) {
421         $retained_modifiers .= $modifier
422           if grep { /$modifier/ } @available_modifiers;
423     }
424     if ( $retained_modifiers =~ m/^(ig|gi)$/ ) {
425         $value =~ s/$search/$replace/igee;
426     }
427     elsif ( $retained_modifiers eq 'i' ) {
428         $value =~ s/$search/$replace/iee;
429     }
430     elsif ( $retained_modifiers eq 'g' ) {
431         $value =~ s/$search/$replace/gee;
432     }
433     else {
434         $value =~ s/$search/$replace/ee;
435     }
436
437     return $value;
438 }
439
440 =head3 search_ordered
441
442  $items->search_ordered;
443
444 Search and sort items in a specific order, depending if serials are present or not
445
446 =cut
447
448 sub search_ordered {
449     my ($self, $params, $attributes) = @_;
450
451     $self = $self->search($params, $attributes);
452
453     my @biblionumbers = uniq $self->get_column('biblionumber');
454
455     if ( scalar ( @biblionumbers ) == 1
456         && Koha::Biblios->find( $biblionumbers[0] )->serial )
457     {
458         return $self->search(
459             {},
460             {
461                 order_by => [ 'serialid.publisheddate', 'me.enumchron' ],
462                 join     => { serialitem => 'serialid' }
463             }
464         );
465     } else {
466         return $self->search(
467             {},
468             {
469                 order_by => [
470                     'homebranch.branchname',
471                     'me.enumchron',
472                     \"LPAD( me.copynumber, 8, '0' )",
473                     {-desc => 'me.dateaccessioned'}
474                 ],
475                 join => ['homebranch']
476             }
477         );
478     }
479 }
480
481 =head2 Internal methods
482
483 =head3 _type
484
485 =cut
486
487 sub _type {
488     return 'Item';
489 }
490
491 =head3 object_class
492
493 =cut
494
495 sub object_class {
496     return 'Koha::Item';
497 }
498
499 =head1 AUTHOR
500
501 Kyle M Hall <kyle@bywatersolutions.com>
502 Tomas Cohen Arazi <tomascohen@theke.io>
503 Martin Renvoize <martin.renvoize@ptfs-europe.com>
504
505 =cut
506
507 1;