3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
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.
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.
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>.
21 use Array::Utils qw( array_minus );
22 use List::MoreUtils qw( uniq );
26 use C4::Biblio qw( GetMarcStructure GetMarcFromKohaField );
30 use Koha::SearchEngine::Indexer;
32 use Koha::Item::Attributes;
34 use Koha::CirculationRules;
36 use base qw(Koha::Objects);
38 use Koha::SearchEngine::Indexer;
42 Koha::Items - Koha Item object set class
50 =head3 filter_by_for_hold
52 my $filtered_items = $items->filter_by_for_hold;
54 Return the items of the set that are *potentially* holdable.
56 Caller has the responsibility to call C4::Reserves::CanItemBeReserved before
57 placing a hold on one of those items.
61 sub filter_by_for_hold {
64 my @hold_not_allowed_itypes = Koha::CirculationRules->search(
66 rule_name => 'holdallowed',
68 categorycode => undef,
69 rule_value => 'not_allowed',
71 )->get_column('itemtype');
72 push @hold_not_allowed_itypes, Koha::ItemTypes->search({ notforloan => 1 })->get_column('itemtype');
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() ) : () ),
82 if ( C4::Context->preference("item-level_itypes") ) {
86 itype => { -not_in => \@hold_not_allowed_itypes },
93 'biblioitem.itemtype' => { -not_in => \@hold_not_allowed_itypes },
102 =head3 filter_by_visible_in_opac
104 my $filered_items = $items->filter_by_visible_in_opac(
106 [ patron => $patron ]
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.
113 The I<OpacHiddenItems>, I<hidelostitems> and I<OpacHiddenItemsExceptions> system preferences
118 sub filter_by_visible_in_opac {
119 my ($self, $params) = @_;
121 my $patron = $params->{patron};
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') // {};
130 foreach my $field ( keys %$rules ) {
131 $rules_params->{'me.'.$field} =
132 [ { '-not_in' => $rules->{$field} }, undef ];
135 $result = $result->search( $rules_params );
138 if (C4::Context->preference('hidelostitems')) {
139 $result = $result->filter_out_lost;
145 =head3 filter_out_lost
147 my $filered_items = $items->filter_out_lost;
149 Returns a new resultset, containing those items that are not marked as lost.
153 sub filter_out_lost {
156 my $params = { itemlost => 0 };
158 return $self->search( $params );
161 =head3 filter_by_bookable
163 my $filterd_items = $items->filter_by_bookable;
165 Returns a new resultset, containing only those items that are allowed to be booked.
169 sub filter_by_bookable {
172 my $params = { bookable => 1 };
174 return $self->search($params);
177 =head3 move_to_biblio
179 $items->move_to_biblio($to_biblio);
181 Move items to a given biblio.
186 my ( $self, $to_biblio ) = @_;
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 } );
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" );
201 Koha::Items->search->batch_update
204 itemnotes => $new_item_notes,
208 itemnotes_nonpublic => {
214 exclude_from_local_holds_priority => 1|0,
216 # increment something here
221 Batch update the items.
223 Returns ( $report, $self )
225 * modified_itemnumbers - list of the modified itemnumbers
226 * modified_fields - number of fields modified
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
239 Allows to modify existing subfield's values using a regular expression
241 =item exclude_from_local_holds_priority
243 Set the passed boolean value to items.exclude_from_local_holds_priority
245 =item mark_items_returned
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.
252 Callback function to call after an item has been modified
259 my ( $self, $params ) = @_;
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};
267 my (@modified_itemnumbers, $modified_fields);
269 my $schema = Koha::Database->new->schema;
270 while ( my $item = $self->next ) {
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;
283 my $new_values = {%$new_values}; # Don't modify the original
285 my $old_values = $item->unblessed;
286 if ( $item->more_subfields_xml ) {
289 %{$item->additional_attributes->to_hashref},
293 for my $attr ( keys %$regex_mod ) {
294 my $old_value = $old_values->{$attr};
296 next unless $old_value;
298 my $value = apply_regex(
300 %{ $regex_mod->{$attr} },
305 $new_values->{$attr} = $value;
308 for my $attribute ( keys %$new_values ) {
309 next if $attribute eq 'more_subfields_xml'; # Already counted before
311 my $old = $old_values->{$attribute};
312 my $new = $new_values->{$attribute};
314 if ( defined $old xor defined $new )
315 || ( defined $old && defined $new && $new ne $old );
318 { # Dealing with more_subfields_xml
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" );
324 my @more_subfield_tags = map {
328 && !$_->{kohafield} # Get subfields that are not mapped
332 } values %{ $tagslib->{$itemtag} };
334 my $more_subfields_xml = Koha::Item::Attributes->new(
337 exists $new_values->{$_} ? ( $_ => $new_values->{$_} )
338 : exists $old_values->{$_}
339 ? ( $_ => $old_values->{$_} )
341 } @more_subfield_tags
343 )->to_marcxml($frameworkcode);
345 $new_values->{more_subfields_xml} = $more_subfields_xml;
347 delete $new_values->{$_} for @more_subfield_tags; # Clean the hash
352 my $itemlost_pre = $item->itemlost;
353 $item->set($new_values)->store({skip_record_index => 1});
355 C4::Circulation::LostItem(
356 $item->itemnumber, 'batchmod', undef,
357 { skip_record_index => 1 }
359 and not $itemlost_pre;
361 if ( $mark_items_returned ){
362 my $issue = $item->checkout;
365 C4::Circulation::MarkIssueReturned(
366 $issue->borrowernumber,
369 $issue->patron->privacy,
371 skip_record_index => 1,
372 skip_holds_queue => 1,
378 push @modified_itemnumbers, $item->itemnumber if $modified || $modified_holds_priority || $item_returned;
379 $modified_fields += $modified + $modified_holds_priority + $item_returned;
390 if (@modified_itemnumbers) {
391 my @biblionumbers = uniq(
392 Koha::Items->search( { itemnumber => \@modified_itemnumbers } )
393 ->get_column('biblionumber'));
395 if ( @biblionumbers ) {
396 my $indexer = Koha::SearchEngine::Indexer->new(
397 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
399 $indexer->index_records( \@biblionumbers, 'specialUpdate',
400 "biblioserver", undef );
404 return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self );
408 # FIXME Should be moved outside of Koha::Items
409 # FIXME This is nearly identical to Koha::SimpleMARC::_modify_values
411 my $search = $params->{search};
412 my $replace = $params->{replace};
413 my $modifiers = $params->{modifiers} || q{};
414 my $value = $params->{value};
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;
424 if ( $retained_modifiers =~ m/^(ig|gi)$/ ) {
425 $value =~ s/$search/$replace/igee;
427 elsif ( $retained_modifiers eq 'i' ) {
428 $value =~ s/$search/$replace/iee;
430 elsif ( $retained_modifiers eq 'g' ) {
431 $value =~ s/$search/$replace/gee;
434 $value =~ s/$search/$replace/ee;
440 =head3 search_ordered
442 $items->search_ordered;
444 Search and sort items in a specific order, depending if serials are present or not
449 my ($self, $params, $attributes) = @_;
451 $self = $self->search($params, $attributes);
453 my @biblionumbers = uniq $self->search(undef,{distinct=>1})->get_column('biblionumber');
455 if ( scalar ( @biblionumbers ) == 1
456 && Koha::Biblios->find( $biblionumbers[0] )->serial )
458 return $self->search(
461 order_by => [ 'serialid.publisheddate', 'me.enumchron' ],
462 join => { serialitem => 'serialid' }
466 return $self->search(
470 'homebranch.branchname',
472 {-desc => 'me.dateaccessioned'}
474 join => ['homebranch']
480 =head2 Internal methods
500 Kyle M Hall <kyle@bywatersolutions.com>
501 Tomas Cohen Arazi <tomascohen@theke.io>
502 Martin Renvoize <martin.renvoize@ptfs-europe.com>