Koha/Koha/Items.pm
Jonathan Druart db74a8112a
Bug 33568: Remove LPAD ordering
This is coming from bug 3521 it seems. And we are going to deal with
this later if people complains.

kidclamp> the performance issue for sites with many copies is bigger than sorting issues imho
kidclamp> drop the LPAD I say :-)

Signed-off-by: Laurence Rault <laurence.rault@biblibre.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
2024-04-05 15:35:12 +02:00

506 lines
14 KiB
Perl

package Koha::Items;
# Copyright ByWater Solutions 2014
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Array::Utils qw( array_minus );
use List::MoreUtils qw( uniq );
use Try::Tiny;
use C4::Context;
use C4::Biblio qw( GetMarcStructure GetMarcFromKohaField );
use C4::Circulation;
use Koha::Database;
use Koha::SearchEngine::Indexer;
use Koha::Item::Attributes;
use Koha::Item;
use Koha::CirculationRules;
use base qw(Koha::Objects);
use Koha::SearchEngine::Indexer;
=head1 NAME
Koha::Items - Koha Item object set class
=head1 API
=head2 Class methods
=cut
=head3 filter_by_for_hold
my $filtered_items = $items->filter_by_for_hold;
Return the items of the set that are *potentially* holdable.
Caller has the responsibility to call C4::Reserves::CanItemBeReserved before
placing a hold on one of those items.
=cut
sub filter_by_for_hold {
my ($self) = @_;
my @hold_not_allowed_itypes = Koha::CirculationRules->search(
{
rule_name => 'holdallowed',
branchcode => undef,
categorycode => undef,
rule_value => 'not_allowed',
}
)->get_column('itemtype');
push @hold_not_allowed_itypes, Koha::ItemTypes->search({ notforloan => 1 })->get_column('itemtype');
my $params = {
itemlost => 0,
withdrawn => 0,
notforloan => { '<=' => 0 }, # items with negative or zero notforloan value are holdable
( C4::Context->preference('AllowHoldsOnDamagedItems')? (): ( damaged => 0 ) ),
( C4::Context->only_my_library() ? ( homebranch => C4::Context::mybranch() ) : () ),
};
if ( C4::Context->preference("item-level_itypes") ) {
return $self->search(
{
%$params,
itype => { -not_in => \@hold_not_allowed_itypes },
}
);
} else {
return $self->search(
{
%$params,
'biblioitem.itemtype' => { -not_in => \@hold_not_allowed_itypes },
},
{
join => 'biblioitem',
}
);
}
}
=head3 filter_by_visible_in_opac
my $filered_items = $items->filter_by_visible_in_opac(
{
[ patron => $patron ]
}
);
Returns a new resultset, containing those items that are not expected to be hidden in OPAC
for the passed I<Koha::Patron> object that is passed.
The I<OpacHiddenItems>, I<hidelostitems> and I<OpacHiddenItemsExceptions> system preferences
are honoured.
=cut
sub filter_by_visible_in_opac {
my ($self, $params) = @_;
my $patron = $params->{patron};
my $result = $self;
# Filter out OpacHiddenItems unless disabled by OpacHiddenItemsExceptions
unless ( $patron and $patron->category->override_hidden_items ) {
my $rules = C4::Context->yaml_preference('OpacHiddenItems') // {};
my $rules_params;
foreach my $field ( keys %$rules ) {
$rules_params->{'me.'.$field} =
[ { '-not_in' => $rules->{$field} }, undef ];
}
$result = $result->search( $rules_params );
}
if (C4::Context->preference('hidelostitems')) {
$result = $result->filter_out_lost;
}
return $result;
}
=head3 filter_out_lost
my $filered_items = $items->filter_out_lost;
Returns a new resultset, containing those items that are not marked as lost.
=cut
sub filter_out_lost {
my ($self) = @_;
my $params = { itemlost => 0 };
return $self->search( $params );
}
=head3 filter_by_bookable
my $filterd_items = $items->filter_by_bookable;
Returns a new resultset, containing only those items that are allowed to be booked.
=cut
sub filter_by_bookable {
my ($self) = @_;
my $params = { bookable => 1 };
return $self->search($params);
}
=head3 move_to_biblio
$items->move_to_biblio($to_biblio);
Move items to a given biblio.
=cut
sub move_to_biblio {
my ( $self, $to_biblio ) = @_;
my $biblionumbers = { $to_biblio->biblionumber => 1 };
while ( my $item = $self->next() ) {
$biblionumbers->{ $item->biblionumber } = 1;
$item->move_to_biblio( $to_biblio, { skip_record_index => 1 } );
}
my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
for my $biblionumber ( keys %{$biblionumbers} ) {
$indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
}
}
=head3 batch_update
Koha::Items->search->batch_update
{
new_values => {
itemnotes => $new_item_notes,
k => $k,
},
regex_mod => {
itemnotes_nonpublic => {
search => 'foo',
replace => 'bar',
modifiers => 'gi',
},
},
exclude_from_local_holds_priority => 1|0,
callback => sub {
# increment something here
},
}
);
Batch update the items.
Returns ( $report, $self )
Report has 2 keys:
* modified_itemnumbers - list of the modified itemnumbers
* modified_fields - number of fields modified
Parameters:
=over
=item new_values
Allows to set a new value for given fields.
The key can be one of the item's column name, or one subfieldcode of a MARC subfields not linked with a Koha field
=item regex_mod
Allows to modify existing subfield's values using a regular expression
=item exclude_from_local_holds_priority
Set the passed boolean value to items.exclude_from_local_holds_priority
=item mark_items_returned
Move issues on these items to the old issues table, do not mark items found, or
adjust damaged/withdrawn statuses, or fines, or locations.
=item callback
Callback function to call after an item has been modified
=back
=cut
sub batch_update {
my ( $self, $params ) = @_;
my $regex_mod = $params->{regex_mod} || {};
my $new_values = $params->{new_values} || {};
my $exclude_from_local_holds_priority = $params->{exclude_from_local_holds_priority};
my $mark_items_returned = $params->{mark_items_returned};
my $callback = $params->{callback};
my (@modified_itemnumbers, $modified_fields);
my $i;
my $schema = Koha::Database->new->schema;
while ( my $item = $self->next ) {
try {$schema->txn_do(sub {
my $modified_holds_priority = 0;
my $item_returned = 0;
if ( defined $exclude_from_local_holds_priority ) {
if(!defined $item->exclude_from_local_holds_priority || $item->exclude_from_local_holds_priority != $exclude_from_local_holds_priority) {
$item->exclude_from_local_holds_priority($exclude_from_local_holds_priority)->store;
$modified_holds_priority = 1;
}
}
my $modified = 0;
my $new_values = {%$new_values}; # Don't modify the original
my $old_values = $item->unblessed;
if ( $item->more_subfields_xml ) {
$old_values = {
%$old_values,
%{$item->additional_attributes->to_hashref},
};
}
for my $attr ( keys %$regex_mod ) {
my $old_value = $old_values->{$attr};
next unless $old_value;
my $value = apply_regex(
{
%{ $regex_mod->{$attr} },
value => $old_value,
}
);
$new_values->{$attr} = $value;
}
for my $attribute ( keys %$new_values ) {
next if $attribute eq 'more_subfields_xml'; # Already counted before
my $old = $old_values->{$attribute};
my $new = $new_values->{$attribute};
$modified++
if ( defined $old xor defined $new )
|| ( defined $old && defined $new && $new ne $old );
}
{ # Dealing with more_subfields_xml
my $frameworkcode = $item->biblio->frameworkcode;
my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 });
my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
my @more_subfield_tags = map {
(
ref($_)
&& %$_
&& !$_->{kohafield} # Get subfields that are not mapped
)
? $_->{tagsubfield}
: ()
} values %{ $tagslib->{$itemtag} };
my $more_subfields_xml = Koha::Item::Attributes->new(
{
map {
exists $new_values->{$_} ? ( $_ => $new_values->{$_} )
: exists $old_values->{$_}
? ( $_ => $old_values->{$_} )
: ()
} @more_subfield_tags
}
)->to_marcxml($frameworkcode);
$new_values->{more_subfields_xml} = $more_subfields_xml;
delete $new_values->{$_} for @more_subfield_tags; # Clean the hash
}
if ( $modified ) {
my $itemlost_pre = $item->itemlost;
$item->set($new_values)->store({skip_record_index => 1});
C4::Circulation::LostItem(
$item->itemnumber, 'batchmod', undef,
{ skip_record_index => 1 }
) if $item->itemlost
and not $itemlost_pre;
}
if ( $mark_items_returned ){
my $issue = $item->checkout;
if( $issue ){
$item_returned = 1;
C4::Circulation::MarkIssueReturned(
$issue->borrowernumber,
$item->itemnumber,
undef,
$issue->patron->privacy,
{
skip_record_index => 1,
skip_holds_queue => 1,
}
);
}
}
push @modified_itemnumbers, $item->itemnumber if $modified || $modified_holds_priority || $item_returned;
$modified_fields += $modified + $modified_holds_priority + $item_returned;
})}
catch {
warn $_
};
if ( $callback ) {
$callback->(++$i);
}
}
if (@modified_itemnumbers) {
my @biblionumbers = uniq(
Koha::Items->search( { itemnumber => \@modified_itemnumbers } )
->get_column('biblionumber'));
if ( @biblionumbers ) {
my $indexer = Koha::SearchEngine::Indexer->new(
{ index => $Koha::SearchEngine::BIBLIOS_INDEX } );
$indexer->index_records( \@biblionumbers, 'specialUpdate',
"biblioserver", undef );
}
}
return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self );
}
sub apply_regex {
# FIXME Should be moved outside of Koha::Items
# FIXME This is nearly identical to Koha::SimpleMARC::_modify_values
my ($params) = @_;
my $search = $params->{search};
my $replace = $params->{replace};
my $modifiers = $params->{modifiers} || q{};
my $value = $params->{value};
$replace =~ s/"/\\"/g; # Protection from embedded code
$replace = '"' . $replace . '"'; # Put in a string for /ee
my @available_modifiers = qw( i g );
my $retained_modifiers = q||;
for my $modifier ( split //, $modifiers ) {
$retained_modifiers .= $modifier
if grep { /$modifier/ } @available_modifiers;
}
if ( $retained_modifiers =~ m/^(ig|gi)$/ ) {
$value =~ s/$search/$replace/igee;
}
elsif ( $retained_modifiers eq 'i' ) {
$value =~ s/$search/$replace/iee;
}
elsif ( $retained_modifiers eq 'g' ) {
$value =~ s/$search/$replace/gee;
}
else {
$value =~ s/$search/$replace/ee;
}
return $value;
}
=head3 search_ordered
$items->search_ordered;
Search and sort items in a specific order, depending if serials are present or not
=cut
sub search_ordered {
my ($self, $params, $attributes) = @_;
$self = $self->search($params, $attributes);
my @biblionumbers = uniq $self->get_column('biblionumber');
if ( scalar ( @biblionumbers ) == 1
&& Koha::Biblios->find( $biblionumbers[0] )->serial )
{
return $self->search(
{},
{
order_by => [ 'serialid.publisheddate', 'me.enumchron' ],
join => { serialitem => 'serialid' }
}
);
} else {
return $self->search(
{},
{
order_by => [
'homebranch.branchname',
'me.enumchron',
{-desc => 'me.dateaccessioned'}
],
join => ['homebranch']
}
);
}
}
=head2 Internal methods
=head3 _type
=cut
sub _type {
return 'Item';
}
=head3 object_class
=cut
sub object_class {
return 'Koha::Item';
}
=head1 AUTHOR
Kyle M Hall <kyle@bywatersolutions.com>
Tomas Cohen Arazi <tomascohen@theke.io>
Martin Renvoize <martin.renvoize@ptfs-europe.com>
=cut
1;