Bug 28445: Correctly count modified items
[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
28 use Koha::Database;
29 use Koha::SearchEngine::Indexer;
30
31 use Koha::Item::Attributes;
32 use Koha::Item;
33 use Koha::CirculationRules;
34
35 use base qw(Koha::Objects);
36
37 use Koha::SearchEngine::Indexer;
38
39 =head1 NAME
40
41 Koha::Items - Koha Item object set class
42
43 =head1 API
44
45 =head2 Class methods
46
47 =cut
48
49 =head3 filter_by_for_hold
50
51     my $filtered_items = $items->filter_by_for_hold;
52
53 Return the items of the set that are *potentially* holdable.
54
55 Caller has the responsibility to call C4::Reserves::CanItemBeReserved before
56 placing a hold on one of those items.
57
58 =cut
59
60 sub filter_by_for_hold {
61     my ($self) = @_;
62
63     my @hold_not_allowed_itypes = Koha::CirculationRules->search(
64         {
65             rule_name    => 'holdallowed',
66             branchcode   => undef,
67             categorycode => undef,
68             rule_value   => 'not_allowed',
69         }
70     )->get_column('itemtype');
71     push @hold_not_allowed_itypes, Koha::ItemTypes->search({ notforloan => 1 })->get_column('itemtype');
72
73     my $params = {
74         itemlost   => 0,
75         withdrawn  => 0,
76         notforloan => { '<=' => 0 },    # items with negative or zero notforloan value are holdable
77         ( C4::Context->preference('AllowHoldsOnDamagedItems')? (): ( damaged => 0 ) ),
78     };
79
80     if ( C4::Context->preference("item-level_itypes") ) {
81         return $self->search(
82             {
83                 %$params,
84                 itype        => { -not_in => \@hold_not_allowed_itypes },
85             }
86         );
87     } else {
88         return $self->search(
89             {
90                 %$params,
91                 'biblioitem.itemtype' => { -not_in => \@hold_not_allowed_itypes },
92             },
93             {
94                 join => 'biblioitem',
95             }
96         );
97     }
98 }
99
100 =head3 filter_by_visible_in_opac
101
102     my $filered_items = $items->filter_by_visible_in_opac(
103         {
104             [ patron => $patron ]
105         }
106     );
107
108 Returns a new resultset, containing those items that are not expected to be hidden in OPAC
109 for the passed I<Koha::Patron> object that is passed.
110
111 The I<OpacHiddenItems>, I<hidelostitems> and I<OpacHiddenItemsExceptions> system preferences
112 are honoured.
113
114 =cut
115
116 sub filter_by_visible_in_opac {
117     my ($self, $params) = @_;
118
119     my $patron = $params->{patron};
120
121     my $result = $self;
122
123     # Filter out OpacHiddenItems unless disabled by OpacHiddenItemsExceptions
124     unless ( $patron and $patron->category->override_hidden_items ) {
125         my $rules = C4::Context->yaml_preference('OpacHiddenItems') // {};
126
127         my $rules_params;
128         foreach my $field ( keys %$rules ) {
129             $rules_params->{$field} =
130               [ { '-not_in' => $rules->{$field} }, undef ];
131         }
132
133         $result = $result->search( $rules_params );
134     }
135
136     if (C4::Context->preference('hidelostitems')) {
137         $result = $result->filter_out_lost;
138     }
139
140     return $result;
141 }
142
143 =head3 filter_out_lost
144
145     my $filered_items = $items->filter_out_lost;
146
147 Returns a new resultset, containing those items that are not marked as lost.
148
149 =cut
150
151 sub filter_out_lost {
152     my ($self) = @_;
153
154     my $params = { itemlost => 0 };
155
156     return $self->search( $params );
157 }
158
159
160 =head3 move_to_biblio
161
162  $items->move_to_biblio($to_biblio);
163
164 Move items to a given biblio.
165
166 =cut
167
168 sub move_to_biblio {
169     my ( $self, $to_biblio ) = @_;
170
171     my $biblionumbers = { $to_biblio->biblionumber => 1 };
172     while ( my $item = $self->next() ) {
173         $biblionumbers->{ $item->biblionumber } = 1;
174         $item->move_to_biblio( $to_biblio, { skip_record_index => 1 } );
175     }
176     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
177     for my $biblionumber ( keys %{$biblionumbers} ) {
178         $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
179     }
180 }
181
182 =head3 batch_update
183
184     Koha::Items->search->batch_update
185         {
186             new_values => {
187                 itemnotes => $new_item_notes,
188                 k         => $k,
189             },
190             regex_mod => {
191                 itemnotes_nonpublic => {
192                     search => 'foo',
193                     replace => 'bar',
194                     modifiers => 'gi',
195                 },
196             },
197             exclude_from_local_holds_priority => 1|0,
198             callback => sub {
199                 # increment something here
200             },
201         }
202     );
203
204 Batch update the items.
205
206 Returns ( $report, $self )
207 Report has 2 keys:
208   * modified_itemnumbers - list of the modified itemnumbers
209   * modified_fields - number of fields modified
210
211 Parameters:
212
213 =over
214
215 =item new_values
216
217 Allows to set a new value for given fields.
218 The key can be one of the item's column name, or one subfieldcode of a MARC subfields not linked with a Koha field
219
220 =item regex_mod
221
222 Allows to modify existing subfield's values using a regular expression
223
224 =item exclude_from_local_holds_priority
225
226 Set the passed boolean value to items.exclude_from_local_holds_priority
227
228 =item callback
229
230 Callback function to call after an item has been modified
231
232 =back
233
234 =cut
235
236 sub batch_update {
237     my ( $self, $params ) = @_;
238
239     my $regex_mod = $params->{regex_mod} || {};
240     my $new_values = $params->{new_values} || {};
241     my $exclude_from_local_holds_priority = $params->{exclude_from_local_holds_priority};
242     my $callback = $params->{callback};
243
244     my (@modified_itemnumbers, $modified_fields);
245     my $i;
246     my $schema = Koha::Database->new->schema;
247     while ( my $item = $self->next ) {
248
249         try {$schema->txn_do(sub {
250             my $modified_holds_priority = 0;
251             if ( defined $exclude_from_local_holds_priority ) {
252                 if(!defined $item->exclude_from_local_holds_priority || $item->exclude_from_local_holds_priority != $exclude_from_local_holds_priority) {
253                     $item->exclude_from_local_holds_priority($exclude_from_local_holds_priority)->store;
254                     $modified_holds_priority = 1;
255                 }
256             }
257
258             my $modified = 0;
259             my $new_values = {%$new_values};    # Don't modify the original
260
261             my $old_values = $item->unblessed;
262             if ( $item->more_subfields_xml ) {
263                 $old_values = {
264                     %$old_values,
265                     %{$item->additional_attributes->to_hashref},
266                 };
267             }
268
269             for my $attr ( keys %$regex_mod ) {
270                 my $old_value = $old_values->{$attr};
271
272                 next unless $old_value;
273
274                 my $value = apply_regex(
275                     {
276                         %{ $regex_mod->{$attr} },
277                         value => $old_value,
278                     }
279                 );
280
281                 $new_values->{$attr} = $value;
282             }
283
284             for my $attribute ( keys %$new_values ) {
285                 next if $attribute eq 'more_subfields_xml'; # Already counted before
286
287                 my $old = $old_values->{$attribute};
288                 my $new = $new_values->{$attribute};
289                 $modified++
290                   if ( defined $old xor defined $new )
291                   || ( defined $old && defined $new && $new ne $old );
292             }
293
294             { # Dealing with more_subfields_xml
295
296                 my $frameworkcode = $item->biblio->frameworkcode;
297                 my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 });
298                 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
299
300                 my @more_subfield_tags = map {
301                     (
302                              ref($_)
303                           && %$_
304                           && !$_->{kohafield}    # Get subfields that are not mapped
305                       )
306                       ? $_->{tagsubfield}
307                       : ()
308                 } values %{ $tagslib->{$itemtag} };
309
310                 my $more_subfields_xml = Koha::Item::Attributes->new(
311                     {
312                         map {
313                             exists $new_values->{$_} ? ( $_ => $new_values->{$_} )
314                               : exists $old_values->{$_}
315                               ? ( $_ => $old_values->{$_} )
316                               : ()
317                         } @more_subfield_tags
318                     }
319                 )->to_marcxml($frameworkcode);
320
321                 $new_values->{more_subfields_xml} = $more_subfields_xml;
322
323                 delete $new_values->{$_} for @more_subfield_tags; # Clean the hash
324
325             }
326
327             if ( $modified ) {
328                 my $itemlost_pre = $item->itemlost;
329                 $item->set($new_values)->store({skip_record_index => 1});
330
331                 LostItem(
332                     $item->itemnumber, 'batchmod', undef,
333                     { skip_record_index => 1 }
334                 ) if $item->itemlost
335                       and not $itemlost_pre;
336             }
337
338             push @modified_itemnumbers, $item->itemnumber if $modified || $modified_holds_priority;
339             $modified_fields += $modified + $modified_holds_priority;
340         })};
341
342         if ( $callback ) {
343             $callback->(++$i);
344         }
345     }
346
347     if (@modified_itemnumbers) {
348         my @biblionumbers = uniq(
349             Koha::Items->search( { itemnumber => \@modified_itemnumbers } )
350                        ->get_column('biblionumber'));
351
352         my $indexer = Koha::SearchEngine::Indexer->new(
353             { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
354         $indexer->index_records( \@biblionumbers, 'specialUpdate',
355             "biblioserver", undef )
356           if @biblionumbers;
357     }
358
359     return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self );
360 }
361
362 sub apply_regex { # FIXME Should be moved outside of Koha::Items
363     my ($params) = @_;
364     my $search   = $params->{search};
365     my $replace  = $params->{replace};
366     my $modifiers = $params->{modifiers} || q{};
367     my $value = $params->{value};
368
369     my @available_modifiers = qw( i g );
370     my $retained_modifiers  = q||;
371     for my $modifier ( split //, $modifiers ) {
372         $retained_modifiers .= $modifier
373           if grep { /$modifier/ } @available_modifiers;
374     }
375     if ( $retained_modifiers =~ m/^(ig|gi)$/ ) {
376         $value =~ s/$search/$replace/ig;
377     }
378     elsif ( $retained_modifiers eq 'i' ) {
379         $value =~ s/$search/$replace/i;
380     }
381     elsif ( $retained_modifiers eq 'g' ) {
382         $value =~ s/$search/$replace/g;
383     }
384     else {
385         $value =~ s/$search/$replace/;
386     }
387
388     return $value;
389 }
390
391
392 =head2 Internal methods
393
394 =head3 _type
395
396 =cut
397
398 sub _type {
399     return 'Item';
400 }
401
402 =head3 object_class
403
404 =cut
405
406 sub object_class {
407     return 'Koha::Item';
408 }
409
410 =head1 AUTHOR
411
412 Kyle M Hall <kyle@bywatersolutions.com>
413 Tomas Cohen Arazi <tomascohen@theke.io>
414 Martin Renvoize <martin.renvoize@ptfs-europe.com>
415
416 =cut
417
418 1;