From 9ac9561ee49b51a6785015d0984837f99c6f1d95 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Tue, 27 Jul 2021 11:05:54 +0200 Subject: [PATCH] Bug 28445: Use the task queue for the batch delete and update items tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Here we go! Disclaimer: this patch is huge and does many things, but splitting it in several chunks would be time consuming and painful to rebase. However it adds many tests and isolate/refactor code to make it way more reusable. This patchset will make the "batch item modification" and "batch item deletion" features use the task queue (reminder: Since bug 28158, and so 21.05.00, we do no longer use the old "background job" functionality and the user does not get any info about the progress of the job). More than that, more of the code to build an item form and a list of items is now isolated in module (.pm) and include files (.inc) We are reusing the changes made by bug 27526 that simplifies the way we edit/create items (no more unecessary serialization Koha > MARC > MARCXML > XML > HTML) New module: * Koha::BackgroundJob::BatchDeleteItem Subclass for process item deletion in batch * Koha::BackgroundJob::BatchUpdateItem Subclass for process item modification in batch * Koha::Item::Attributes We needed an object to represent item's attributes that are not mapped with a koha field (aka "more subfields xml") This module will help us to create the marcxml from a hashref and the reverse. * Koha::UI::Form::Builder::Item The code that was used to build the add/edit item form is centralised in this module. In conjunction with the subfields_for_item BLOCK (from html_helpers.inc) it will be really easy to reuse this code in other places where the item form is used (acquisition and serials modules) * Koha::UI::Table::Builder::Items Same as previously for the table. We are now using this table from 3 different places (batch item mod, batch item del, backgroung job detail view) and the code is only in one place. To use with items_table_batchmod BLOCK (still from html_helpers.inc) This patch is fixing some bugs about repeatable subfields and regex. A UI change will reflect the limitation: if you want to apply a regex on a subfield you cannot add several subfields for the same subfield code. Test plan: Prepare the ground: - Make sure you are always using a bibliographic/item record using the framework you are modifying! - Add some subfields for items that are not mapped with a koha field (note that you can use 'é' for more fun, don't try more funny characters) - Make some subfields (mapped and not mapped with a kohafield) repeatable - Add default values to some of your subfields There are 4 main screens to test: 1. Add/edit item form The behaviour should be the same before and after this patch. See test plan from bug 27526. Those 2 prefs must be tested: * SubfieldsToAllowForRestrictedEditing * SubfieldsToUseWhenPrefill 2. Batch modification a. Fill some values, play with repeatable and regex. Note that the behaviour in master was buggy, only the first value was modified by the regex: * With subfield = "a | b" 1 value added with "new" => "new | b" * With subfield = "a | b" 2 new fields "new1","new2" => "new2 | b" Important note: For repeatable subfields, a regex will apply on the subfields in the "concatenated form". To apply the regex on all the different subfields of a given subfield code you must use the "g" modifier. This could be improved later, but keep in mind that it's not a regression or behaviour change. b. Play with the "Populate fields with default values from default framework" checkbox c. Use this tool to modify items and play with the different sysprefs that interfer with it: * NewItemsDefaultLocation * SubfieldsToAllowForRestrictedBatchmod * MaxItemsToDisplayForBatchMod * MaxItemsToProcessForBatchMod 3. Batch deletion a. Batch delete some items b. Check items out and try to delete them c. Use the "Delete records if no items remain" checkbox to delete bibliographic records without remaining items. d. Play with the following sysprefs and confirm that it works as expected: * MaxItemsToDisplayForBatchDel e. Stress the tool: Go to the confirmation screen with items that can be deleted, don't request the job to be processed right away, but check the item out before. 4. Background job detail view You must have seen it already if you are curious and tested the above. When a new modification or deletion batch is requested, the confirmation screen will tell you that the job has enqueued. A link to the progress of the job can be followed. On this screen you will be able to see the result of the job once it's fully processed. QA notes: * There are some FIXME's that are not blocker in my opinion. Feel free to discuss them if you have suggestions. * Do we still need MaxItemsToProcessForBatchMod? * Prior to this patchset we had a "Return to the cataloging module" link if we went from the cataloguing module and that the biblio was deleted. We cannot longer know if the biblio will be deleted but we could display a "Go to the cataloging module" link on the "job has been enqueued" screen regardless from where we were coming from. --- Koha/BackgroundJob.pm | 8 +- Koha/BackgroundJob/BatchDeleteItem.pm | 227 ++++++++ Koha/BackgroundJob/BatchUpdateItem.pm | 193 +++++++ Koha/Item.pm | 83 ++- Koha/Item/Attributes.pm | 149 +++++ Koha/Items.pm | 213 +++++++ Koha/UI/Form/Builder/Item.pm | 52 +- Koha/UI/Table/Builder/Items.pm | 147 +++++ admin/background_jobs.pl | 10 +- cataloguing/additem.pl | 13 +- .../batch_authority_record_modification.inc | 2 +- .../batch_biblio_record_modification.inc | 30 +- .../batch_item_record_deletion.inc | 58 ++ .../batch_item_record_modification.inc | 38 ++ .../prog/en/includes/html_helpers.inc | 130 +++++ .../prog/en/modules/admin/background_jobs.tt | 31 +- .../prog/en/modules/cataloguing/additem.tt | 2 - .../prog/en/modules/tools/batchMod-del.tt | 206 ++----- .../prog/en/modules/tools/batchMod-edit.tt | 158 ++--- .../intranet-tmpl/prog/js/pages/batchMod.js | 25 +- misc/background_jobs_worker.pl | 2 + t/db_dependent/Koha/Item.t | 22 +- t/db_dependent/Koha/Item/Attributes.t | 151 +++++ t/db_dependent/Koha/Items/BatchUpdate.t | 383 +++++++++++++ t/db_dependent/Koha/UI/Form/Builder/Item.t | 329 +++++++++++ tools/batchMod.pl | 539 ++++-------------- 26 files changed, 2390 insertions(+), 811 deletions(-) create mode 100644 Koha/BackgroundJob/BatchDeleteItem.pm create mode 100644 Koha/BackgroundJob/BatchUpdateItem.pm create mode 100644 Koha/Item/Attributes.pm create mode 100644 Koha/UI/Table/Builder/Items.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_deletion.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc create mode 100755 t/db_dependent/Koha/Item/Attributes.t create mode 100755 t/db_dependent/Koha/Items/BatchUpdate.t create mode 100755 t/db_dependent/Koha/UI/Form/Builder/Item.t diff --git a/Koha/BackgroundJob.pm b/Koha/BackgroundJob.pm index 553d7ce6e64..dc65739a3a8 100644 --- a/Koha/BackgroundJob.pm +++ b/Koha/BackgroundJob.pm @@ -26,8 +26,10 @@ use Koha::DateUtils qw( dt_from_string ); use Koha::Exceptions; use Koha::BackgroundJob::BatchUpdateBiblio; use Koha::BackgroundJob::BatchUpdateAuthority; +use Koha::BackgroundJob::BatchUpdateItem; use Koha::BackgroundJob::BatchDeleteBiblio; use Koha::BackgroundJob::BatchDeleteAuthority; +use Koha::BackgroundJob::BatchDeleteItem; use base qw( Koha::Object ); @@ -198,7 +200,7 @@ sub report { my ( $self ) = @_; my $data_dump = decode_json $self->data; - return $data_dump->{report}; + return $data_dump->{report} || {}; } =head3 additional_report @@ -235,10 +237,14 @@ sub _derived_class { ? Koha::BackgroundJob::BatchUpdateBiblio->new : $job_type eq 'batch_authority_record_modification' ? Koha::BackgroundJob::BatchUpdateAuthority->new + : $job_type eq 'batch_item_record_modification' + ? Koha::BackgroundJob::BatchUpdateItem->new : $job_type eq 'batch_biblio_record_deletion' ? Koha::BackgroundJob::BatchDeleteBiblio->new : $job_type eq 'batch_authority_record_deletion' ? Koha::BackgroundJob::BatchDeleteAuthority->new + : $job_type eq 'batch_item_record_deletion' + ? Koha::BackgroundJob::BatchDeleteItem->new : Koha::Exceptions::Exception->throw($job_type . ' is not a valid job_type') } diff --git a/Koha/BackgroundJob/BatchDeleteItem.pm b/Koha/BackgroundJob/BatchDeleteItem.pm new file mode 100644 index 00000000000..9fb6969e33e --- /dev/null +++ b/Koha/BackgroundJob/BatchDeleteItem.pm @@ -0,0 +1,227 @@ +package Koha::BackgroundJob::BatchDeleteItem; + +# 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 . + +=head1 NAME + +Koha::BackgroundJob::BatchDeleteItem - Background job derived class to process item deletion in batch + +=cut + +use Modern::Perl; +use JSON qw( encode_json decode_json ); +use List::MoreUtils qw( uniq ); +use Try::Tiny; + +use Koha::BackgroundJobs; +use Koha::DateUtils qw( dt_from_string ); + +use base 'Koha::BackgroundJob'; + +=head1 API + +=head2 Class methods + +=head3 job_type + +Return the job type 'batch_item_record_deletion'. + +=cut + +sub job_type { + return 'batch_item_record_deletion'; +} + +=head3 process + + Koha::BackgroundJobs->find($id)->process( + { + record_ids => \@itemnumbers, + deleted_biblios => 0|1, + } + ); + +Will delete all the items that have been passed for deletion. + +When deleted_biblios is passed, if we deleted the last item of a biblio, +the bibliographic record will be deleted as well. + +The search engine's index will be updated according to the changes made +to the deleted bibliographic recods. + +The generated report will be: + { + deleted_itemnumbers => \@list_of_itemnumbers, + not_deleted_itemnumbers => \@list_of_itemnumbers, + deleted_biblionumbers=> \@list_of_biblionumbers, + } + +=cut + +sub process { + my ( $self, $args ) = @_; + + my $job_type = $args->{job_type}; + + my $job = Koha::BackgroundJobs->find( $args->{job_id} ); + + if ( !exists $args->{job_id} || !$job || $job->status eq 'cancelled' ) { + return; + } + + # FIXME If the job has already been started, but started again (worker has been restart for instance) + # Then we will start from scratch and so double delete the same records + + my $job_progress = 0; + $job->started_on(dt_from_string)->progress($job_progress) + ->status('started')->store; + + my @record_ids = @{ $args->{record_ids} }; + my $delete_biblios = $args->{delete_biblios}; + + my $report = { + total_records => scalar @record_ids, + total_success => 0, + }; + my @messages; + my $schema = Koha::Database->new->schema; + my ( @deleted_itemnumbers, @not_deleted_itemnumbers, + @deleted_biblionumbers ); + + try { + my $schema = Koha::Database->new->schema; + $schema->txn_do( + sub { + my (@biblionumbers); + for my $record_id ( sort { $a <=> $b } @record_ids ) { + + last if $job->get_from_storage->status eq 'cancelled'; + + my $item = Koha::Items->find($record_id) || next; + + my $return = $item->safe_delete; + unless ( ref($return) ) { + + # FIXME Do we need to rollback the whole transaction if a deletion failed? + push @not_deleted_itemnumbers, $item->itemnumber; + push @messages, + { + type => 'error', + code => 'item_not_deleted', + itemnumber => $item->itemnumber, + biblionumber => $item->biblionumber, + barcode => $item->barcode, + title => $item->biblio->title, + reason => $return, + }; + + next; + } + + push @deleted_itemnumbers, $item->itemnumber; + push @biblionumbers, $item->biblionumber; + + $report->{total_success}++; + $job->progress( ++$job_progress )->store; + } + + # If there are no items left, delete the biblio + if ( $delete_biblios && @biblionumbers ) { + for my $biblionumber ( uniq @biblionumbers ) { + my $items_count = + Koha::Biblios->find($biblionumber)->items->count; + if ( $items_count == 0 ) { + my $error = C4::Biblio::DelBiblio( $biblionumber, + { skip_record_index => 1 } ); + unless ($error) { + push @deleted_biblionumbers, $biblionumber; + } + } + } + + if (@deleted_biblionumbers) { + my $indexer = Koha::SearchEngine::Indexer->new( + { index => $Koha::SearchEngine::BIBLIOS_INDEX } ); + + $indexer->index_records( \@deleted_biblionumbers, + 'recordDelete', "biblioserver", undef ); + } + } + } + ); + } + catch { + + warn $_; + + push @messages, + { + type => 'error', + code => 'unknown', + error => $_, + }; + + die "Something terrible has happened!" + if ( $_ =~ /Rollback failed/ ); # Rollback failed + }; + + $report->{deleted_itemnumbers} = \@deleted_itemnumbers; + $report->{not_deleted_itemnumbers} = \@not_deleted_itemnumbers; + $report->{deleted_biblionumbers} = \@deleted_biblionumbers; + + my $job_data = decode_json $job->data; + $job_data->{messages} = \@messages; + $job_data->{report} = $report; + + $job->ended_on(dt_from_string)->data( encode_json $job_data); + $job->status('finished') if $job->status ne 'cancelled'; + $job->store; +} + +=head3 enqueue + + Koha::BackgroundJob::BatchDeleteItem->new->enqueue( + { + record_ids => \@itemnumbers, + deleted_biblios => 0|1, + } + ); + +Enqueue the job. + +=cut + +sub enqueue { + my ( $self, $args ) = @_; + + # TODO Raise exception instead + return unless exists $args->{record_ids}; + + my @record_ids = @{ $args->{record_ids} }; + my $delete_biblios = @{ $args->{delete_biblios} || [] }; + + $self->SUPER::enqueue( + { + job_size => scalar @record_ids, + job_args => { + record_ids => \@record_ids, + delete_biblios => $delete_biblios, + } + } + ); +} + +1; diff --git a/Koha/BackgroundJob/BatchUpdateItem.pm b/Koha/BackgroundJob/BatchUpdateItem.pm new file mode 100644 index 00000000000..405e71456f1 --- /dev/null +++ b/Koha/BackgroundJob/BatchUpdateItem.pm @@ -0,0 +1,193 @@ +package Koha::BackgroundJob::BatchUpdateItem; + +# 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 . + +use Modern::Perl; +use JSON qw( encode_json decode_json ); +use List::MoreUtils qw( uniq ); +use Try::Tiny; + +use MARC::Record; +use MARC::Field; + +use C4::Biblio; +use C4::Items; + +use Koha::BackgroundJobs; +use Koha::DateUtils qw( dt_from_string ); +use Koha::SearchEngine::Indexer; +use Koha::Items; +use Koha::UI::Table::Builder::Items; + +use base 'Koha::BackgroundJob'; + +=head1 NAME + +Koha::BackgroundJob::BatchUpdateItem - Background job derived class to process item modification in batch + +=head1 API + +=head2 Class methods + +=head3 job_type + +Define the job type of this job: batch_item_record_modification + +=cut + +sub job_type { + return 'batch_item_record_modification'; +} + +=head3 process + + Koha::BackgroundJobs->find($id)->process( + { + record_ids => \@itemnumbers, + 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 + } + ); + +Process the modification. + +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. + +regex_mod allows to modify existing subfield's values using a regular expression. + +=cut + +sub process { + my ( $self, $args ) = @_; + + my $job = Koha::BackgroundJobs->find( $args->{job_id} ); + + if ( !exists $args->{job_id} || !$job || $job->status eq 'cancelled' ) { + return; + } + + # FIXME If the job has already been started, but started again (worker has been restart for instance) + # Then we will start from scratch and so double process the same records + + my $job_progress = 0; + $job->started_on(dt_from_string)->progress($job_progress) + ->status('started')->store; + + my @record_ids = @{ $args->{record_ids} }; + my $regex_mod = $args->{regex_mod}; + my $new_values = $args->{new_values}; + my $exclude_from_local_holds_priority = + $args->{exclude_from_local_holds_priority}; + + my $report = { + total_records => scalar @record_ids, + modified_itemitemnumbers => [], + modified_fields => 0, + }; + + try { + my $schema = Koha::Database->new->schema; + $schema->txn_do( + sub { + my ($results) = + Koha::Items->search( { itemnumber => \@record_ids } ) + ->batch_update( + { + regex_mod => $regex_mod, + new_values => $new_values, + exclude_from_local_holds_priority => + $exclude_from_local_holds_priority, + callback => sub { + my ($progress) = @_; + $job->progress($progress)->store; + }, + } + ); + $report->{modified_itemnumbers} = $results->{modified_itemnumbers}; + $report->{modified_fields} = $results->{modified_fields}; + } + ); + } + catch { + warn $_; + die "Something terrible has happened!" + if ( $_ =~ /Rollback failed/ ); # Rollback failed + }; + + my $job_data = decode_json $job->data; + $job_data->{report} = $report; + + $job->ended_on(dt_from_string)->data( encode_json $job_data); + $job->status('finished') if $job->status ne 'cancelled'; + $job->store; +} + +=head3 enqueue + +Enqueue the new job + +=cut + +sub enqueue { + my ( $self, $args ) = @_; + + # TODO Raise exception instead + return unless exists $args->{record_ids}; + + my @record_ids = @{ $args->{record_ids} }; + + $self->SUPER::enqueue( + { + job_size => scalar @record_ids, + job_args => {%$args}, + } + ); +} + +=head3 additional_report + +Sent the infos to generate the table containing the details of the modified items. + +=cut + +sub additional_report { + my ( $self, $args ) = @_; + + my $job = Koha::BackgroundJobs->find( $args->{job_id} ); + + my $itemnumbers = $job->report->{modified_itemnumbers}; + my $items_table = + Koha::UI::Table::Builder::Items->new( { itemnumbers => $itemnumbers } ) + ->build_table; + + return { + items => $items_table->{items}, + item_header_loop => $items_table->{headers}, + }; +} + +1; diff --git a/Koha/Item.pm b/Koha/Item.pm index a4e04972a36..cd6a3b86af2 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -26,6 +26,7 @@ use Koha::Database; use Koha::DateUtils qw( dt_from_string output_pref ); use C4::Context; +use C4::Biblio qw( GetMarcStructure GetMarcSubfieldStructure GetMarcFromKohaField ); use C4::Circulation qw( GetBranchItemRule ); use C4::Reserves; use C4::ClassSource qw( GetClassSort ); @@ -38,6 +39,7 @@ use Koha::SearchEngine::Indexer; use Koha::Exceptions::Item::Transfer; use Koha::Item::Transfer::Limits; use Koha::Item::Transfers; +use Koha::Item::Attributes; use Koha::ItemTypes; use Koha::Patrons; use Koha::Plugins; @@ -847,8 +849,7 @@ sub has_pending_hold { =head3 as_marc_field - my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } ); - my $field = $item->as_marc_field({ [ mss => $mss ] }); + my $field = $item->as_marc_field; This method returns a MARC::Field object representing the Koha::Item object with the current mappings configuration. @@ -856,37 +857,54 @@ with the current mappings configuration. =cut sub as_marc_field { - my ( $self, $params ) = @_; + my ( $self ) = @_; + + my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" ); - my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } ); - my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield}; + my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 }); my @subfields; - my @columns = $self->_result->result_source->columns; + my $item_field = $tagslib->{$itemtag}; - foreach my $item_field ( @columns ) { - my $mapping = $mss->{ "items.$item_field"}[0]; - my $tagfield = $mapping->{tagfield}; - my $tagsubfield = $mapping->{tagsubfield}; - next if !$tagfield; # TODO: Should we raise an exception instead? - # Feels like safe fallback is better + my $more_subfields = $self->additional_attributes->to_hashref; + foreach my $subfield ( + sort { + $a->{display_order} <=> $b->{display_order} + || $a->{subfield} cmp $b->{subfield} + } grep { ref($_) && %$_ } values %$item_field + ){ - push @subfields, $tagsubfield => $self->$item_field - if defined $self->$item_field and $item_field ne ''; - } + my $kohafield = $subfield->{kohafield}; + my $tagsubfield = $subfield->{tagsubfield}; + my $value; + if ( defined $kohafield ) { + next if $kohafield !~ m{^items\.}; # That would be weird! + ( my $attribute = $kohafield ) =~ s|^items\.||; + $value = $self->$attribute # This call may fail if a kohafield is not a DB column but we don't want to add extra work for that there + if defined $self->$attribute and $self->$attribute ne ''; + } else { + $value = $more_subfields->{$tagsubfield} + } + + next unless defined $value + and $value ne q{}; - my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml); - push( @subfields, @{$unlinked_item_subfields} ) - if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1; + if ( $subfield->{repeatable} ) { + my @values = split '\|', $value; + push @subfields, ( $tagsubfield => $_ ) for @values; + } + else { + push @subfields, ( $tagsubfield => $value ); + } - my $field; + } - $field = MARC::Field->new( - "$item_tag", ' ', ' ', @subfields - ) if @subfields; + return unless @subfields; - return $field; + return MARC::Field->new( + "$itemtag", ' ', ' ', @subfields + ); } =head3 renewal_branchcode @@ -1010,6 +1028,25 @@ sub columns_to_str { return $values; } +=head3 additional_attributes + + my $attributes = $item->additional_attributes; + $attributes->{k} = 'new k'; + $item->update({ more_subfields => $attributes->to_marcxml }); + +Returns a Koha::Item::Attributes object that represents the non-mapped +attributes for this item. + +=cut + +sub additional_attributes { + my ($self) = @_; + + return Koha::Item::Attributes->new_from_marcxml( + $self->more_subfields_xml, + ); +} + =head3 _set_found_trigger $self->_set_found_trigger diff --git a/Koha/Item/Attributes.pm b/Koha/Item/Attributes.pm new file mode 100644 index 00000000000..d75a5c44f6a --- /dev/null +++ b/Koha/Item/Attributes.pm @@ -0,0 +1,149 @@ +package Koha::Item::Attributes; + +# 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 . + +use Modern::Perl; +use MARC::Record; +use MARC::Field; +use List::MoreUtils qw( uniq ); + +use C4::Biblio; +use C4::Charset qw( StripNonXmlChars ); + +=head1 NAME + +Koha::Item::Attributes - Class to represent the additional attributes of items. + +Additional attributes are 'more subfields xml' + +=head1 API + +=head2 Class methods + +=cut + +=head3 new_from_marcxml + + my $attributes = Koha::Item::Attributes->new_from_marcxml( $item->more_subfield_xml ); + +Constructor that takes a MARCXML. + +=cut + +# FIXME maybe this needs to care about repeatable but don't from batchMod - To implement later? +sub new_from_marcxml { + my ( $class, $more_subfields_xml ) = @_; + + my $self = {}; + if ($more_subfields_xml) { + # FIXME MARC::Record->new_from_xml (vs MARC::Record::new_from_xml) does not return the correctly encoded subfield code (??) + my $marc_more = + MARC::Record::new_from_xml( + C4::Charset::StripNonXmlChars($more_subfields_xml), 'UTF-8' ); + + # use of tag 999 is arbitrary, and doesn't need to match the item tag + # used in the framework + my $field = $marc_more->field('999'); + my $more_subfields = [ uniq map { $_->[0] } $field->subfields ]; + for my $more_subfield (@$more_subfields) { + my @s = $field->subfield($more_subfield); + $self->{$more_subfield} = join '|', @s; + } + } + return bless $self, $class; +} + +=head3 new + +Constructor + +=cut + +# FIXME maybe this needs to care about repeatable but don't from batchMod - To implement later? +sub new { + my ( $class, $attributes ) = @_; + + my $self = $attributes; + return bless $self, $class; +} + +=head3 to_marcxml + + $attributes->to_marcxml; + + $item->more_subfields_xml( $attributes->to_marcxml ); + +Return the MARCXML representation of the attributes. + +=cut + +sub to_marcxml { + my ( $self, $frameworkcode ) = @_; + + return unless keys %$self; + + my $tagslib = + C4::Biblio::GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } ); + + my ( $itemtag, $itemtagsubfield ) = + C4::Biblio::GetMarcFromKohaField("items.itemnumber"); + my @subfields; + for my $tagsubfield ( + sort { + $tagslib->{$itemtag}->{$a}->{display_order} <=> $tagslib->{$itemtag}->{$b}->{display_order} + || $tagslib->{$itemtag}->{$a}->{subfield} cmp $tagslib->{$itemtag}->{$b}->{subfield} + } keys %$self + ) + { + next + if not defined $self->{$tagsubfield} + or $self->{$tagsubfield} eq ""; + + if ( $tagslib->{$itemtag}->{$tagsubfield}->{repeatable} ) { + my @values = split '\|', $self->{$tagsubfield}; + push @subfields, ( $tagsubfield => $_ ) for @values; + } + else { + push @subfields, ( $tagsubfield => $self->{$tagsubfield} ); + } + } + + return unless @subfields; + + my $marc_more = MARC::Record->new(); + + # use of tag 999 is arbitrary, and doesn't need to match the item tag + # used in the framework + $marc_more->append_fields( + MARC::Field->new( '999', ' ', ' ', @subfields ) ); + $marc_more->encoding("UTF-8"); + return $marc_more->as_xml("USMARC"); +} + +=head3 to_hashref + + $attributes->to_hashref; + +Returns the hashref representation of the attributes. + +=cut + +sub to_hashref { + my ($self) = @_; + return { map { $_ => $self->{$_} } keys %$self }; +} + +1; diff --git a/Koha/Items.pm b/Koha/Items.pm index 2643223d3bb..787c0c73d50 100644 --- a/Koha/Items.pm +++ b/Koha/Items.pm @@ -18,10 +18,16 @@ package Koha::Items; # along with Koha; if not, see . use Modern::Perl; +use Array::Utils qw( array_minus ); +use List::MoreUtils qw( uniq ); +use C4::Context; +use C4::Biblio qw( GetMarcStructure GetMarcFromKohaField ); use Koha::Database; +use Koha::SearchEngine::Indexer; +use Koha::Item::Attributes; use Koha::Item; use base qw(Koha::Objects); @@ -108,6 +114,213 @@ sub filter_out_lost { return $self->search( $params ); } +=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 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 $callback = $params->{callback}; + + my (@modified_itemnumbers, $modified_fields); + my $i; + while ( my $item = $self->next ) { + + my $modified_holds_priority = 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}); + + LostItem( + $item->itemnumber, 'batchmod', undef, + { skip_record_index => 1 } + ) if $item->itemlost + and not $itemlost_pre; + + push @modified_itemnumbers, $item->itemnumber if $modified || $modified_holds_priority; + $modified_fields += $modified + $modified_holds_priority; + } + + if ( $callback ) { + $callback->(++$i); + } + } + + if (@modified_itemnumbers) { + my @biblionumbers = uniq( + Koha::Items->search( { itemnumber => \@modified_itemnumbers } ) + ->get_column('biblionumber')); + + my $indexer = Koha::SearchEngine::Indexer->new( + { index => $Koha::SearchEngine::BIBLIOS_INDEX } ); + $indexer->index_records( \@biblionumbers, 'specialUpdate', + "biblioserver", undef ) + if @biblionumbers; + } + + return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self ); +} + +sub apply_regex { # FIXME Should be moved outside of Koha::Items + my ($params) = @_; + my $search = $params->{search}; + my $replace = $params->{replace}; + my $modifiers = $params->{modifiers} || q{}; + my $value = $params->{value}; + + 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/ig; + } + elsif ( $retained_modifiers eq 'i' ) { + $value =~ s/$search/$replace/i; + } + elsif ( $retained_modifiers eq 'g' ) { + $value =~ s/$search/$replace/g; + } + else { + $value =~ s/$search/$replace/; + } + + return $value; +} + + =head2 Internal methods =head3 _type diff --git a/Koha/UI/Form/Builder/Item.pm b/Koha/UI/Form/Builder/Item.pm index b747508f705..c11cd9cb5f4 100644 --- a/Koha/UI/Form/Builder/Item.pm +++ b/Koha/UI/Form/Builder/Item.pm @@ -82,6 +82,7 @@ sub generate_subfield_form { my $prefill_with_default_values = $params->{prefill_with_default_values}; my $branch_limit = $params->{branch_limit}; my $default_branches_empty = $params->{default_branches_empty}; + my $readonly = $params->{readonly}; my $item = $self->{item}; my $subfield = $tagslib->{$tag}{$subfieldtag}; @@ -379,28 +380,15 @@ sub generate_subfield_form { }; } - # Getting list of subfields to keep when restricted editing is enabled - # FIXME Improve the following block, no need to do it for every subfields - my $subfieldsToAllowForRestrictedEditing = - C4::Context->preference('SubfieldsToAllowForRestrictedEditing'); - my $allowAllSubfields = ( - not defined $subfieldsToAllowForRestrictedEditing - or $subfieldsToAllowForRestrictedEditing eq q|| - ) ? 1 : 0; - my @subfieldsToAllow = split( / /, $subfieldsToAllowForRestrictedEditing ); - -# If we're on restricted editing, and our field is not in the list of subfields to allow, -# then it is read-only - $subfield_data{marc_value}->{readonly} = - ( not $allowAllSubfields - and $restricted_edition - and !grep { $tag . '$' . $subfieldtag eq $_ } @subfieldsToAllow ) - ? 1 - : 0; + # If we're on restricted editing, and our field is not in the list of subfields to allow, + # then it is read-only + $subfield_data{marc_value}->{readonly} = $readonly; return \%subfield_data; } +=head3 edit_form + my $subfields = Koha::UI::Form::Builder::Item->new( { biblionumber => $biblionumber, item => $current_item } )->edit_form( @@ -440,9 +428,14 @@ List of subfields to prefill (value of syspref SubfieldsToUseWhenPrefill) =item subfields_to_allow -List of subfields to allow (value of syspref SubfieldsToAllowForRestrictedBatchmod) +List of subfields to allow (value of syspref SubfieldsToAllowForRestrictedBatchmod or SubfieldsToAllowForRestrictedEditing) + +=item ignore_not_allowed_subfields + +If set, the subfields in subfields_to_allow will be ignored (ie. they will not be part of the subfield list. +If not set, the subfields in subfields_to_allow will be marked as readonly. -=item subfields_to_ignore +=item kohafields_to_ignore List of subfields to ignore/skip @@ -469,7 +462,8 @@ sub edit_form { my $restricted_edition = $params->{restricted_editition}; my $subfields_to_prefill = $params->{subfields_to_prefill} || []; my $subfields_to_allow = $params->{subfields_to_allow} || []; - my $subfields_to_ignore= $params->{subfields_to_ignore} || []; + my $ignore_not_allowed_subfields = $params->{ignore_not_allowed_subfields}; + my $kohafields_to_ignore = $params->{kohafields_to_ignore} || []; my $prefill_with_default_values = $params->{prefill_with_default_values}; my $branch_limit = $params->{branch_limit}; my $default_branches_empty = $params->{default_branches_empty}; @@ -494,10 +488,21 @@ sub edit_form { next if IsMarcStructureInternal($subfield); next if $subfield->{tab} ne "10"; - next if @$subfields_to_allow && !grep { $subfield->{kohafield} eq $_ } @$subfields_to_allow; next if grep { $subfield->{kohafield} && $subfield->{kohafield} eq $_ } - @$subfields_to_ignore; + @$kohafields_to_ignore; + + my $readonly; + if ( + @$subfields_to_allow && !grep { + sprintf( "%s\$%s", $subfield->{tagfield}, $subfield->{tagsubfield} ) eq $_ + } @$subfields_to_allow + ) + { + + next if $ignore_not_allowed_subfields; + $readonly = 1 if $restricted_edition; + } my @values = (); @@ -545,6 +550,7 @@ sub edit_form { prefill_with_default_values => $prefill_with_default_values, branch_limit => $branch_limit, default_branches_empty => $default_branches_empty, + readonly => $readonly } ); push @subfields, $subfield_data; diff --git a/Koha/UI/Table/Builder/Items.pm b/Koha/UI/Table/Builder/Items.pm new file mode 100644 index 00000000000..516414d2648 --- /dev/null +++ b/Koha/UI/Table/Builder/Items.pm @@ -0,0 +1,147 @@ +package Koha::UI::Table::Builder::Items; + +# 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 . + +use Modern::Perl; +use List::MoreUtils qw( uniq ); +use C4::Biblio qw( GetMarcStructure GetMarcFromKohaField IsMarcStructureInternal ); +use Koha::Items; + +=head1 NAME + +Koha::UI::Table::Builder::Items + +Helper to build a table with a list of items with all their information. + +Items' attributes that are mapped and not mapped will be listed in the table. + +Only attributes that have been defined only once will be displayed (empty string is considered as not defined). + +=head1 API + +=head2 Class methods + +=cut + +=head3 new + + my $table = Koha::UI::Table::Builder::Items->new( { itemnumbers => \@itemnumbers } ); + +Constructor. + +=cut + +sub new { + my ( $class, $params ) = @_; + + my $self; + $self->{itemnumbers} = $params->{itemnumbers} || []; + + bless $self, $class; + return $self; +} + +=head3 build_table + + my $items_table = Koha::UI::Table::Builder::Items->new( { itemnumbers => \@itemnumbers } ) + ->build_table; + + my $items = $items_table->{items}; + my $headers = $items_table->{headers}; + +Build the headers and rows for the table. + +Use it with: + [% PROCESS items_table_batchmod headers => headers, items => items %] + +=cut + +sub build_table { + my ( $self, $params ) = @_; + + my $items = Koha::Items->search( { itemnumber => $self->{itemnumbers} } ); + + my @items; + while ( my $item = $items->next ) { + my $item_info = $item->columns_to_str; + $item_info = { + %$item_info, + biblio => $item->biblio, + safe_to_delete => $item->safe_to_delete, + holds => $item->biblio->holds->count, + item_holds => $item->holds->count, + is_checked_out => $item->checkout || 0, + }; + push @items, $item_info; + } + + $self->{headers} = $self->_build_headers( \@items ); + $self->{items} = \@items; + return $self; +} + +=head2 Internal methods + +=cut + +=head3 _build_headers + +Build the headers given the items' info. + +=cut + +sub _build_headers { + my ( $self, $items ) = @_; + + my @witness_attributes = uniq map { + my $item = $_; + map { defined $item->{$_} && $item->{$_} ne "" ? $_ : () } keys %$item + } @$items; + + my ( $itemtag, $itemsubfield ) = + C4::Biblio::GetMarcFromKohaField("items.itemnumber"); + my $tagslib = C4::Biblio::GetMarcStructure(1); + my $subfieldcode_attribute_mappings; + for my $subfield_code ( keys %{ $tagslib->{$itemtag} } ) { + + my $subfield = $tagslib->{$itemtag}->{$subfield_code}; + + next if IsMarcStructureInternal($subfield); + next unless $subfield->{tab} eq 10; # Is this really needed? + + my $attribute; + if ( $subfield->{kohafield} ) { + ( $attribute = $subfield->{kohafield} ) =~ s|^items\.||; + } + else { + $attribute = $subfield_code; # It's in more_subfields_xml + } + next unless grep { $attribute eq $_ } @witness_attributes; + $subfieldcode_attribute_mappings->{$subfield_code} = $attribute; + } + + return [ + map { + { + header_value => $tagslib->{$itemtag}->{$_}->{lib}, + attribute => $subfieldcode_attribute_mappings->{$_}, + subfield_code => $_, + } + } sort keys %$subfieldcode_attribute_mappings + ]; +} + +1 diff --git a/admin/background_jobs.pl b/admin/background_jobs.pl index 9a474857b6b..4dadcc1784c 100755 --- a/admin/background_jobs.pl +++ b/admin/background_jobs.pl @@ -51,14 +51,8 @@ if ( $op eq 'view' ) { } else { $template->param( job => $job, ); - $template->param( - lists => scalar Koha::Virtualshelves->search( - [ - { category => 1, owner => $loggedinuser }, - { category => 2 } - ] - ) - ) if $job->type eq 'batch_biblio_record_modification'; + my $report = $job->additional_report() || {}; + $template->param( %$report ); } } else { $op = 'list'; diff --git a/cataloguing/additem.pl b/cataloguing/additem.pl index ec5f3aea5af..7811842c46d 100755 --- a/cataloguing/additem.pl +++ b/cataloguing/additem.pl @@ -508,7 +508,7 @@ my @witness_attributes = uniq map { map { defined $item->{$_} && $item->{$_} ne "" ? $_ : () } keys %$item } @items; -our ( $itemtagfield, $itemtagsubfield ) = &GetMarcFromKohaField("items.itemnumber"); +our ( $itemtagfield, $itemtagsubfield ) = GetMarcFromKohaField("items.itemnumber"); my $subfieldcode_attribute_mappings; for my $subfield_code ( keys %{ $tagslib->{$itemtagfield} } ) { @@ -560,12 +560,21 @@ if ( $nextop eq 'additem' && $prefillitem ) { # Setting to 1 element if SubfieldsToUseWhenPrefill is empty to prevent all the subfields to be prefilled @subfields_to_prefill = split(' ', C4::Context->preference('SubfieldsToUseWhenPrefill')) || (""); } + +# Getting list of subfields to keep when restricted editing is enabled +my @subfields_to_allow = $restrictededition ? split ' ', C4::Context->preference('SubfieldsToAllowForRestrictedEditing') : (); + my $subfields = Koha::UI::Form::Builder::Item->new( { biblionumber => $biblionumber, item => $current_item } )->edit_form( { branchcode => $branchcode, restricted_editition => $restrictededition, + ( + @subfields_to_allow + ? ( subfields_to_allow => \@subfields_to_allow ) + : () + ), ( @subfields_to_prefill ? ( subfields_to_prefill => \@subfields_to_prefill ) @@ -594,8 +603,6 @@ $template->param( subfields => $subfields, itemnumber => $itemnumber, barcode => $current_item->{barcode}, - itemtagfield => $itemtagfield, - itemtagsubfield => $itemtagsubfield, op => $nextop, popup => scalar $input->param('popup') ? 1: 0, C4::Search::enabled_staff_search_views, diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_authority_record_modification.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_authority_record_modification.inc index 47bfc08a0a5..58291f570d4 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_authority_record_modification.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_authority_record_modification.inc @@ -27,7 +27,7 @@ [% END %] [% SWITCH m.code %] [% CASE 'authority_not_modified' %] - Authority record [% m.authid | html %] has not been modified. An error occurred on modifying it[% IF m.error %] ([% m.error %])[% END %]. + Authority record [% m.authid | html %] has not been modified. An error occurred on modifying it[% IF m.error %] ([% m.error | html %])[% END %]. [% CASE 'authority_modified' %] Authority record [% m.authid | html %] has successfully been modified.. [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_biblio_record_modification.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_biblio_record_modification.inc index 51a447e3b2c..89fd36ac6b2 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_biblio_record_modification.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_biblio_record_modification.inc @@ -46,19 +46,21 @@ [% END %] [% BLOCK js %] - $("#add_bibs_to_list").change(function(){ - var selected = $("#add_bibs_to_list").find("option:selected"); - if ( selected.attr("class") == "shelf" ){ - var shelfnumber = selected.attr("value"); - var bibs = new Array(); - [% FOREACH message IN job.messages %] - [% IF message.code == 'biblio_modified' %] - bibs.push("biblionumber="+[% message.biblionumber | html %]); + [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_deletion.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_deletion.inc new file mode 100644 index 00000000000..c4d677918aa --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_deletion.inc @@ -0,0 +1,58 @@ +[% BLOCK report %] + [% SET report = job.report %] + [% IF report %] +
+ [% IF report.deleted_itemnumbers.size %] +

[% report.deleted_itemnumbers.size | html %] item(s) deleted.

+ [% IF report.deleted_biblionumbers.size %] +

[% report.deleted_biblionumbers.size | html %] record(s) deleted.

+ [% END %] + [% ELSE %] + No items deleted. + [% END %] +
+ + [% IF report.not_deleted_itemnumbers.size %] +
+ [% report.not_deleted_itemnumbers.size | html %] item(s) could not be deleted: [% FOREACH not_deleted_itemnumber IN not_deleted_itemnumbers %][% not_deleted_itemnumber.itemnumber | html %][% END %] +
+ [% END %] + + [% IF job.status == 'cancelled' %] +
+ The job has been cancelled before it finished. + New batch item modification +
+ [% END %] + [% END %] +[% END %] + +[% BLOCK detail %] + [% FOR m IN job.messages %] +
+ [% IF m.type == 'success' %] + + [% ELSIF m.type == 'warning' %] + + [% ELSIF m.type == 'error' %] + + [% END %] + [% SWITCH m.code %] + [% CASE 'item_not_deleted' %] + Item with barcode [% m.barcode | html %] cannot be deleted: + [% SWITCH m.reason %] + [% CASE "book_on_loan" %]Item is checked out + [% CASE "not_same_branch" %]Item does not belong to your library + [% CASE "book_reserved" %]Item has a waiting hold + [% CASE "linked_analytics" %]Item has linked analytics + [% CASE "last_item_for_hold" %]Last item for bibliographic record with biblio-level hold on it + [% CASE %]Unknown reason '[% m.reason | html %]' + [% END %] + [% CASE %]Unknown message '[% m.code | html %]' + [% END %] +
+ [% END %] +[% END %] + +[% BLOCK js %] +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc new file mode 100644 index 00000000000..25bf4f8c24c --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc @@ -0,0 +1,38 @@ +[% BLOCK report %] + [% SET report = job.report %] + [% IF report %] +
+ [% IF report.modified_itemnumbers.size %] + [% report.modified_itemnumbers.size | html %] item(s) modified (with [% report.modified_fields | html %] field(s) modified). + [% ELSE %] + No items modified. + [% END %] + + [% IF job.status == 'cancelled' %]The job has been cancelled before it finished.[% END %] + New batch item modification +
+ [% END %] +[% END %] + +[% BLOCK detail %] + [% FOR m IN job.messages %] +
+ [% IF m.type == 'success' %] + + [% ELSIF m.type == 'warning' %] + + [% ELSIF m.type == 'error' %] + + [% END %] + [% SWITCH m.code %] + [% CASE %]Unknown message '[% m.code | html %]' + [% END %] +
+ [% END %] + + [% PROCESS items_table_batchmod headers => item_header_loop, items => items, display_columns_selection => 1 %] +[% END %] + +[% BLOCK js %] + [% Asset.js("js/pages/batchMod.js") | $raw %] +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers.inc index 0d90b493251..52f2c8faf0d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers.inc @@ -181,3 +181,133 @@ [% END %] [% END %] + +[% BLOCK items_table_batchmod %] + + [% IF display_columns_selection %][%# Needs js/pages/batchMod.js %] + [% IF checkboxes_edit OR checkboxes_delete %] + + [% END %] + +
+ +

+ Show/hide columns: + + + + + + + + + + [% FOREACH header IN item_header_loop %] + + + + + [% END %] +

+
+ [% END %] + [% SET date_fields = [ 'dateaccessioned', 'onloan', 'datelastseen', 'datelastborrowed', 'replacementpricedate' ] %] + + + + [% IF checkboxes_edit OR checkboxes_delete %] + + [% END %] + + + [% FOREACH item_header IN headers %] + [% IF item_header.column_name %] + + [% END %] + + + + [% FOREACH item IN items %] + [% SET can_be_edited = ! ( Koha.Preference('IndependentBranches') && ! logged_in_user && item.homebranch != Branches.GetLoggedInBranchcode() ) %] + + + [% IF checkboxes_edit %] + [% UNLESS can_be_edited%] + + [% ELSE %] + + [% END %] + [% ELSIF checkboxes_delete %] + [% UNLESS can_be_edited %] + + [% ELSE %] + [% IF item.safe_to_delete == 1 %] + + [% ELSE %] + [% SWITCH item.safe_to_delete%] + [% CASE "book_on_loan" %][% SET cannot_delete_reason = t("Item is checked out") %] + [% CASE "not_same_branch" %][% SET cannot_delete_reason = t("Item does not belong to your library") %] + [% CASE "book_reserved" %][% SET cannot_delete_reason = t("Item has a waiting hold") %] + [% CASE "linked_analytics" %][% SET cannot_delete_reason = t("Item has linked analytics") %] + [% CASE "last_item_for_hold" %][% SET cannot_delete_reason = t("Last item for bibliographic record with biblio-level hold on it") %] + [% CASE %][% SET cannot_delete_reason = t("Unknown reason") _ '(' _ item.safe_to_delete _ ')' %] + [% END %] + + + [% END %] + + [% END %] + [% END %] + + + [% FOREACH header IN headers %] + [% SET attribute = header.attribute %] + [% IF header.attribute AND date_fields.grep('^' _ attribute _ '$').size %] + + [% ELSE %] + + [% END %] + [% END %] + + + [% END # /FOREACH items %] + +
TitleHolds + [% ELSE %] + + [% END %] + [% item_header.header_value | html %] +
Cannot edit + + Cannot delete + + + [% IF item.holds %] + [% IF item.item_holds %] + + [% ELSE %] + + [% END %] + [% ELSE %] + [% IF item.holds %] + + [% ELSE %] + + [% END %] + [% END # /IF item.holds %] + [% IF item.holds %] + [% item.item_holds | html %]/[% item.holds | html %] + [% ELSE %] + [% item.holds | html %] + [% END %] + + [% item.$attribute | $KohaDates %][% item.$attribute | html %]
+ +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/background_jobs.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/background_jobs.tt index ba12fc31c0a..b032b8df7bc 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/background_jobs.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/background_jobs.tt @@ -1,4 +1,5 @@ [% USE raw %] +[% USE KohaDates %] [% USE Asset %] [% SET footerjs = 1 %] [% INCLUDE 'doc-head-open.inc' %] @@ -64,7 +65,7 @@ Administration › Koha
  • Job ID: [% job.id | html %]
  • [% job.status | html %]
  • [% job.progress || 0 | html %] / [% job.size | html %]
  • -
  • [% job.type | html %]
  • +
  • [% PROCESS display_job_type job_type => job.type %]
  • [% job.enqueued_on | html %]
  • [% job.started_on | html %]
  • [% job.ended_on | html %]
  • @@ -106,15 +107,7 @@ Administration › Koha [% job.id | html %] [% job.status | html %] [% job.progress || 0 | html %] / [% job.size | html %] - - [% SWITCH job.type %] - [% CASE 'batch_biblio_record_modification' %]Batch bibliographic record modification - [% CASE 'batch_biblio_record_deletion' %]Batch bibliographic record record deletion - [% CASE 'batch_authority_record_modification' %]Batch authority record modification - [% CASE 'batch_authority_record_deletion' %]Batch authority record deletion - [% CASE %][% job.type | html %] - [% END %] - + [% PROCESS display_job_type job_type => job.type %] [% job.enqueued_on | html %] [% job.started_on| html %] [% job.ended_on| html %] @@ -159,10 +152,22 @@ Administration › Koha "sPaginationType": "full_numbers" })); - [% IF op == 'view' %] - [% PROCESS 'js' %] - [% END %] }); + [% IF op == 'view' %] + [% PROCESS 'js' %] + [% END %] [% END %] +[% BLOCK display_job_type %] + [% SWITCH job_type %] + [% CASE 'batch_biblio_record_modification' %]Batch bibliographic record modification + [% CASE 'batch_biblio_record_deletion' %]Batch bibliographic record record deletion + [% CASE 'batch_authority_record_modification' %]Batch authority record modification + [% CASE 'batch_authority_record_deletion' %]Batch authority record deletion + [% CASE 'batch_item_record_modification' %]Batch item record modification + [% CASE 'batch_item_record_deletion' %]Batch item record deletion + [% CASE %]Unknown job type '[% job_type | html %]' + [% END %] +[% END %] + [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt index 0988065fb6b..7ed467c2f07 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt @@ -182,8 +182,6 @@ [% ELSE %] - - [% IF op != 'add_item' %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-del.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-del.tt index 5269cf003eb..4c09f219d98 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-del.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-del.tt @@ -1,4 +1,5 @@ [% USE raw %] +[% USE KohaDates %] [% USE Asset %] [% SET footerjs = 1 %] [% PROCESS 'i18n.inc' %] @@ -29,11 +30,25 @@
    - [% IF ( show ) %]

    Batch item deletion

    [% ELSE %]

    Batch item deletion results

    [% END %] - [% IF ( barcode_not_unique ) %]
    Error saving item: Barcode must be unique.
    [% END %] - [% IF ( no_next_barcode ) %]
    Error saving items: Unable to automatically determine values for barcodes. No item has been inserted.
    [% END %] - [% IF ( book_on_loan ) %]
    Cannot delete: item is checked out.
    [% END %] - [% IF ( book_reserved ) %]
    Cannot delete: item has a waiting hold.
    [% END %] +

    Batch item deletion

    + + [% FOREACH message IN messages %] + [% IF message.type == 'success' %] +
    + [% ELSIF message.type == 'warning' %] +
    + [% ELSIF message.type == 'error' %] +
    + [% END %] + [% IF message.code == 'cannot_enqueue_job' %] + Cannot enqueue this job. + [% END %] + [% IF message.error %] + (The error was: [% message.error | html %], see the Koha log file for more information). + [% END %] +
    + [% END %] + [% UNLESS ( action ) %] @@ -93,94 +108,13 @@ - - [% IF biblionumber %] [% END %] -[% IF ( item_loop ) %] - [% IF ( show ) %][% END %] -
    - -

    Show/hide columns: - [% FOREACH item_header_loo IN item_header_loop %] - - [% END %] -

    - - - - - [% IF ( show ) %][% END %] - - - [% FOREACH item_header_loo IN item_header_loop %] - - [% END %] - - - - [% FOREACH item_loo IN item_loop %] - - [% IF show %] - [% IF item_loo.nomod %] - - [% ELSE %] - [% IF item_loo.safe_to_delete == 1 %] - - [% ELSE %] - [% SWITCH item_loo.safe_to_delete%] - [% CASE "book_on_loan" %][% SET cannot_delete_reason = t("Item is checked out") %] - [% CASE "not_same_branch" %][% SET cannot_delete_reason = t("Item does not belong to your library") %] - [% CASE "book_reserved" %][% SET cannot_delete_reason = t("Item has a waiting hold") %] - [% CASE "linked_analytics" %][% SET cannot_delete_reason = t("Item has linked analytics") %] - [% CASE "last_item_for_hold" %][% SET cannot_delete_reason = t("Last item for bibliographic record with biblio-level hold on it") %] - [% CASE %][% SET cannot_delete_reason = t("Unknown reason") _ '(' _ item_loo.safe_to_delete _ ')' %] - [% END %] - - - [% END %] - [% END %] - [% ELSE %] - - [% END %] - - - [% FOREACH item_valu IN item_loo.item_value %] - [% END %] - [% END %] - -
     TitleHolds [% item_header_loo.header_value | html %]
    Cannot delete  - - - [% IF item_loo.holds %] - [% IF item_loo.item_holds %] - - [% ELSE %] - - [% END %] - [% ELSE %] - [% IF item_loo.holds %] - - [% ELSE %] - - [% END %] - [% END %] - [% IF item_loo.holds %] - [% item_loo.item_holds | html %]/[% item_loo.holds | html %] - [% ELSE %] - [% item_loo.holds | html %] - [% END %] - - [% item_valu.field | html %]
    -
    +[% IF items.size %] + [% PROCESS items_table_batchmod headers => item_header_loop, items => items, checkboxes_delete => 1, display_columns_selection => 1 %] [% END %] [% IF ( simple_items_display ) %] @@ -203,7 +137,7 @@ [% END %] [% END %] -[% IF ( itemresults ) %] + [% IF ( itemresults ) %]

    This will delete [% IF ( too_many_items_display ) %]all the[% ELSE %]the selected[% END %] items.

    @@ -226,87 +160,29 @@ [% END %] -[% IF ( action ) %] - [% IF deletion_failed %] -
    - At least one item blocked the deletion. The operation rolled back and nothing happened! -
    - [% ELSE %] + + [% IF op == 'enqueued' %]
    -

    [% deleted_items | html %] item(s) deleted.

    - [% IF delete_records %]

    [% deleted_records | html %] record(s) deleted.

    [% END %] +

    The job has been enqueued! It will be processed as soon as possible.

    +

    View detail of the enqueued job + | New batch item deletion

    +
    + +
    [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Delete items in a batch%] - [% IF biblio_deleted %] - [% IF searchid %] - - [% END %] - Return to the cataloging module + [% IF searchid %] + Return to the record [% ELSE %] - [% IF searchid %] - Return to the record - [% ELSE %] - Return to the record - [% END %] + Return to the record [% END %] [% ELSIF src %] Return to where you were [% ELSE %] Return to batch item deletion [% END %] -
    - [% END %] - [% IF ( not_deleted_items ) %] -
    -

    [% not_deleted_items | html %] item(s) could not be deleted: [% FOREACH not_deleted_itemnumber IN not_deleted_itemnumbers %][% not_deleted_itemnumber.itemnumber | html %][% END %]

    - [% IF ( not_deleted_loop ) %] - - - - - - - - - - [% FOREACH not_deleted_loo IN not_deleted_loop %] - - - - - - [% END %] - -
    ItemnumberBarcodeReason
    [% not_deleted_loo.itemnumber | html %][% IF ( CAN_user_editcatalogue_edit_items ) %][% not_deleted_loo.barcode | html %][% ELSE %][% not_deleted_loo.barcode | html %][% END %] - [% SWITCH not_deleted_loo.reason %] - [% CASE "book_on_loan" %][% SET cannot_delete_reason = t("Item is checked out") %] - [% CASE "not_same_branch" %][% SET cannot_delete_reason = t("Item does not belong to your library") %] - [% CASE "book_reserved" %][% SET cannot_delete_reason = t("Item has a waiting hold") %] - [% CASE "linked_analytics" %][% SET cannot_delete_reason = t("Item has linked analytics") %] - [% CASE "last_item_for_hold" %][% SET cannot_delete_reason = t("Last item for bibliographic record with biblio-level hold on it") %] - [% CASE %][% SET cannot_delete_reason = t("Unknown reason") _ '(' _ can_be_deleted _ ')' %] - [% END %] - [% cannot_delete_reason | html %] -
    - [% END %] -
    + [% END %] -

    - [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Delete items in a batch%] - [% IF biblio_deleted %] - Return to the cataloging module - [% ELSIF searchid %] - Return to the record - [% ELSE %] - Return to the record - [% END %] - [% ELSIF src %] - Return to where you were - [% ELSE %] - Return to batch item deletion - [% END %] -

    -[% END %]
    [% MACRO jsinclude BLOCK %] @@ -315,18 +191,6 @@ [% Asset.js("js/pages/batchMod.js") | $raw %] [% Asset.js("js/browser.js") | $raw %]