Bug 32336: (QA follow-up) Use $metadata->schema
[srvgit] / Koha / Items.pm
index 4d3de57..3f3ba3f 100644 (file)
@@ -18,11 +18,20 @@ package Koha::Items;
 # 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);
 
@@ -42,13 +51,52 @@ Koha::Items - Koha Item object set class
 
     my $filtered_items = $items->filter_by_for_hold;
 
-Return the items of the set that are holdable
+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) = @_;
-    return $self->search( { notforloan => { '<=' => 0 } } ); # items with negative or zero notforloan value are holdable
+
+    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
@@ -80,7 +128,7 @@ sub filter_by_visible_in_opac {
 
         my $rules_params;
         foreach my $field ( keys %$rules ) {
-            $rules_params->{$field} =
+            $rules_params->{'me.'.$field} =
               [ { '-not_in' => $rules->{$field} }, undef ];
         }
 
@@ -110,6 +158,7 @@ sub filter_out_lost {
     return $self->search( $params );
 }
 
+
 =head3 move_to_biblio
 
  $items->move_to_biblio($to_biblio);
@@ -121,14 +170,298 @@ Move items to a given biblio.
 sub move_to_biblio {
     my ( $self, $to_biblio ) = @_;
 
-    while (my $item = $self->next()) {
-        $item->move_to_biblio($to_biblio, { skip_record_index => 1 });
+    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 });
-    $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" );
-    $indexer->index_records( $to_biblio->biblionumber, "specialUpdate", "biblioserver" );
+    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',
+                    \"LPAD( me.copynumber, 8, '0' )",
+                    {-desc => 'me.dateaccessioned'}
+                ],
+                join => ['homebranch']
+            }
+        );
+    }
+}
 
 =head2 Internal methods