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