From 0a76db84fefa903ec5ad8112e165fdbe075101ba 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 | 6 +- Koha/BackgroundJob/BatchDeleteItem.pm | 227 ++++++++ Koha/BackgroundJob/BatchUpdateItem.pm | 193 +++++++ Koha/Item.pm | 84 ++- Koha/Item/Attributes.pm | 149 +++++ Koha/Items.pm | 213 +++++++ Koha/UI/Form/Builder/Item.pm | 52 +- Koha/UI/Table/Builder/Items.pm | 147 +++++ cataloguing/additem.pl | 13 +- .../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 | 45 +- .../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 | 26 +- 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 | 536 ++++-------------- 24 files changed, 2393 insertions(+), 807 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 2275fc3b211..ab55376d336 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 Koha::BackgroundJob::BatchCancelHold; 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 @@ -256,6 +258,8 @@ sub type_to_class_mapping { batch_authority_record_modification => 'Koha::BackgroundJob::BatchUpdateAuthority', batch_biblio_record_deletion => 'Koha::BackgroundJob::BatchDeleteBiblio', batch_biblio_record_modification => 'Koha::BackgroundJob::BatchUpdateBiblio', + batch_item_record_deletion => 'Koha::BackgroundJob::BatchDeleteItem', + batch_item_record_modification => 'Koha::BackgroundJob::BatchUpdateItem', batch_hold_cancel => 'Koha::BackgroundJob::BatchCancelHold', }; } 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 08e527df101..86d82f86370 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -38,6 +38,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; @@ -850,8 +851,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. @@ -859,37 +859,54 @@ with the current mappings configuration. =cut sub as_marc_field { - my ( $self, $params ) = @_; + my ( $self ) = @_; - my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } ); - my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield}; + my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" ); - my @subfields; + my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 }); - my @columns = $self->_result->result_source->columns; + my @subfields; - 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 $item_field = $tagslib->{$itemtag}; + + 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 + ){ + + 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} + } - push @subfields, $tagsubfield => $self->$item_field - if defined $self->$item_field and $item_field ne ''; - } + 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 @@ -1019,6 +1036,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 fd93030ffe4..8d5a01cdc76 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 Koha::CirculationRules; @@ -149,6 +155,7 @@ sub filter_out_lost { return $self->search( $params ); } + =head3 move_to_biblio $items->move_to_biblio($to_biblio); @@ -171,6 +178,212 @@ sub move_to_biblio { } } +=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 diff --git a/Koha/UI/Form/Builder/Item.pm b/Koha/UI/Form/Builder/Item.pm index 9c10032a760..56ba5d808b1 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}; @@ -380,28 +381,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( @@ -441,9 +429,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 @@ -470,7 +463,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}; @@ -495,10 +489,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 = (); @@ -546,6 +551,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/cataloguing/additem.pl b/cataloguing/additem.pl index 6851e2c8b77..2326d5c2ea8 100755 --- a/cataloguing/additem.pl +++ b/cataloguing/additem.pl @@ -509,7 +509,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} } ) { @@ -561,12 +561,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 ) @@ -595,8 +604,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_biblio_record_modification.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_biblio_record_modification.inc index 1d5888714e2..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 4b4e81d22ea..f73e9222ae9 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 %] [% USE KohaDates %] [% SET footerjs = 1 %] @@ -19,16 +20,24 @@ [% END -%] [% END %] [% BLOCK show_job_type %] - [% SWITCH job.type %] - [% CASE "batch_biblio_record_modification" %] - Batch bibliographic record modification - [% CASE "batch_authority_record_modification" %] - Batch authority record modification - [% CASE "batch_hold_cancel" %] - Batch hold cancellation - [% CASE # Default case %] - [% job.type | 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 'batch_item_record_modification' %] + Batch item record modification + [% CASE 'batch_item_record_deletion' %] + Batch item record deletion + [% CASE "batch_hold_cancel" %] + Batch hold cancellation + [% CASE %]Unknown job type '[% job_type | html %]' [% END %] + [% END %] [% INCLUDE 'doc-head-open.inc' %] @@ -107,7 +116,7 @@ <li><label for="job_progress">Progress: </label>[% job.progress || 0 | html %] / [% job.size | html %]</li> <li> <label for="job_type">Type: </label> - [% PROCESS show_job_type %] + [% PROCESS show_job_type job_type => job.type %] </li> <li> <label for="job_enqueued_on">Queued: </label> @@ -164,14 +173,7 @@ </td> <td>[% job.progress || 0 | html %] / [% job.size | html %]</td> <td> - [% 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_hold_cancel" %]Batch hold cancellation - [% CASE %][% job.type | html %] - [% END %] + [% PROCESS show_job_type job_type => job.type %] </td> <td>[% job.enqueued_on | $KohaDates with_hours = 1 %]</td> <td>[% job.started_on| $KohaDates with_hours = 1 %]</td> @@ -217,10 +219,11 @@ "sPaginationType": "full_numbers" })); - [% IF op == 'view' %] - [% PROCESS 'js' %] - [% END %] }); </script> + [% IF op == 'view' %] + [% PROCESS 'js' %] + [% 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 a6a1f3f82dc..b6fb2317bb6 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt @@ -184,8 +184,6 @@ </fieldset> [% ELSE %] - <input type="hidden" name="tag" value="[% itemtagfield | html %]" /> - <input type="hidden" name="subfield" value="[% itemtagsubfield | html %]" /> [% IF op != 'add_item' %] <input type="hidden" name="itemnumber" value="[% itemnumber | html %]" /> [% 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 @@ <div class="main container-fluid"> - [% IF ( show ) %]<h1>Batch item deletion</h1>[% ELSE %]<h1>Batch item deletion results</h1>[% END %] - [% IF ( barcode_not_unique ) %]<div class="dialog alert"><strong>Error saving item</strong>: Barcode must be unique.</div>[% END %] - [% IF ( no_next_barcode ) %]<div class="dialog alert"><strong>Error saving items</strong>: Unable to automatically determine values for barcodes. No item has been inserted.</div>[% END %] - [% IF ( book_on_loan ) %]<div class="dialog alert"><strong>Cannot delete</strong>: item is checked out.</div>[% END %] - [% IF ( book_reserved ) %]<div class="dialogalert"><strong>Cannot delete</strong>: item has a waiting hold.</div>[% END %] + <h1>Batch item deletion</h1> + + [% FOREACH message IN messages %] + [% IF message.type == 'success' %] + <div class="dialog message"> + [% ELSIF message.type == 'warning' %] + <div class="dialog alert"> + [% ELSIF message.type == 'error' %] + <div class="dialog alert" style="margin:auto;"> + [% 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 %] + </div> + [% END %] + [% UNLESS ( action ) %] @@ -93,94 +108,13 @@ <input type="hidden" name="biblionumber" id="biblionumber" value="[% biblionumber | html %]" /> <input type="hidden" name="op" value="[% op | html %]" /> <input type="hidden" name="searchid" value="[% searchid | html %]" /> - <input type="hidden" name="uploadedfileid" id="uploadedfileid" value="" /> - <input type="hidden" name="completedJobID" id="completedJobID" value="" /> <input type="hidden" name="src" id="src" value="[% src | html %]" /> [% IF biblionumber %] <input type="hidden" name="biblionumber" id="biblionumber" value="[% biblionumber | html %]" /> [% END %] -[% IF ( item_loop ) %] - [% IF ( show ) %]<div id="toolbar"><a id="selectallbutton" href="#"><i class="fa fa-check"></i> Select all</a> | <a id="clearallbutton" href="#"><i class="fa fa-remove"></i> Clear all</a></div>[% END %] - <div id="cataloguing_additem_itemlist"> - - <p id="selections"><strong>Show/hide columns:</strong> <span class="selected"><input type="checkbox" checked="checked" id="showall"/><label for="showall">Show all columns</label></span> <span><input type="checkbox" id="hideall"/><label for="hideall">Hide all columns</label></span> - [% FOREACH item_header_loo IN item_header_loop %] - <span class="selected"><input id="checkheader[% loop.count | html %]" type="checkbox" checked="checked" /> <label for="checkheader[% loop.count | html %]">[% item_header_loo.header_value | html %]</label> </span> - [% END %] - </p> - - <table id="itemst"> - <thead> - <tr> - [% IF ( show ) %]<th> </th>[% END %] - <th class="anti-the">Title</th> - <th class="holds_count" title="Item holds / Total holds">Holds</th> - [% FOREACH item_header_loo IN item_header_loop %] - <th> [% item_header_loo.header_value | html %] </th> - [% END %] - </tr> - </thead> - <tbody> - [% FOREACH item_loo IN item_loop %] - <tr> - [% IF show %] - [% IF item_loo.nomod %] - <td class="error">Cannot delete</td> - [% ELSE %] - [% IF item_loo.safe_to_delete == 1 %] - <td><input type="checkbox" name="itemnumber" value="[% item_loo.itemnumber | html %]" id="row[% item_loo.itemnumber | html %]" checked="checked" /></td> - [% 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 %] - - <td><input type="checkbox" name="itemnumber" value="[% item_loo.itemnumber | html %]" id="row[% item_loo.itemnumber | html %]" disabled="disabled" title="[% cannot_delete_reason | html %]"/></td> - [% END %] - [% END %] - [% ELSE %] - <td> </td> - [% END %] - <td> - <label for="row[% item_loo.itemnumber | html %]"> - <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% item_loo.biblionumber | uri %]"> - [% item_loo.title | html %] - </a> - [% IF ( item_loo.author ) %], by [% item_loo.author | html %][% END %] - </label> - </td> - <td class="holds_count"> - [% IF item_loo.holds %] - [% IF item_loo.item_holds %] - <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item_loo.biblionumber | uri %]" title="Holds on this item: [% item_loo.item_holds | html %] / Total holds on this record: [% item_loo.holds | html -%]" > - [% ELSE %] - <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item_loo.biblionumber | uri %]" title="No holds on this item / Total holds on this record: [% item_loo.holds | html -%]" > - [% END %] - [% ELSE %] - [% IF item_loo.holds %] - <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item_loo.biblionumber | uri %]" title="Holds on this record: [% item_loo.holds | html -%]" > - [% ELSE %] - <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item_loo.biblionumber | uri %]" title="No holds on this record" > - [% END %] - [% END %] - [% IF item_loo.holds %] - [% item_loo.item_holds | html %]/[% item_loo.holds | html %] - [% ELSE %] - [% item_loo.holds | html %] - [% END %] - </a> - </td> - [% FOREACH item_valu IN item_loo.item_value %] <td>[% item_valu.field | html %]</td> - [% END %] </tr> - [% END %] - </tbody> - </table> - </div> +[% 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 ) %] <div id="cataloguing_additem_newitem"> <input type="hidden" name="op" value="[% op | html %]" /> <p>This will delete [% IF ( too_many_items_display ) %]all the[% ELSE %]the selected[% END %] items.</p> @@ -226,87 +160,29 @@ </form> [% END %] -[% IF ( action ) %] - [% IF deletion_failed %] - <div class="dialog alert"> - At least one item blocked the deletion. The operation rolled back and nothing happened! - </div> - [% ELSE %] + + [% IF op == 'enqueued' %] <div class="dialog message"> - <p>[% deleted_items | html %] item(s) deleted.</p> - [% IF delete_records %] <p>[% deleted_records | html %] record(s) deleted.</p> [% END %] + <p>The job has been enqueued! It will be processed as soon as possible.</p> + <p><a href="/cgi-bin/koha/admin/background_jobs.pl?op=view&id=[% job_id | uri %]" title="View detail of the enqueued job">View detail of the enqueued job</a> + | <a href="/cgi-bin/koha/tools/batchMod.pl?del=1" title="New batch item deletion">New batch item deletion</a></p> + </div> + + <fieldset class="action"> [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Delete items in a batch%] - [% IF biblio_deleted %] - [% IF searchid %] - <div id="previous_search_link"></div> - [% END %] - <a href="/cgi-bin/koha/cataloguing/addbooks.pl">Return to the cataloging module</a> + [% IF searchid %] + <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]&searchid=[% searchid | uri %]">Return to the record</a> [% ELSE %] - [% IF searchid %] - <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]&searchid=[% searchid | uri %]">Return to the record</a> - [% ELSE %] - <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]">Return to the record</a> - [% END %] + <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]">Return to the record</a> [% END %] [% ELSIF src %] <a href="[% src | url %]">Return to where you were</a> [% ELSE %] <a href="/cgi-bin/koha/tools/batchMod.pl?del=1">Return to batch item deletion</a> [% END %] - </div> - [% END %] - [% IF ( not_deleted_items ) %] - <div style="width:55%;margin:auto;"> - <p>[% not_deleted_items | html %] item(s) could not be deleted: [% FOREACH not_deleted_itemnumber IN not_deleted_itemnumbers %][% not_deleted_itemnumber.itemnumber | html %][% END %]</p> - [% IF ( not_deleted_loop ) %] - <table id="itemst"> - <thead> - <tr> - <th>Itemnumber</th> - <th>Barcode</th> - <th>Reason</th> - </tr> - </thead> - <tbody> - [% FOREACH not_deleted_loo IN not_deleted_loop %] - <tr> - <td>[% not_deleted_loo.itemnumber | html %]</td> - <td>[% IF ( CAN_user_editcatalogue_edit_items ) %]<a href="/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% not_deleted_loo.biblionumber | uri %]&itemnumber=[% not_deleted_loo.itemnumber | uri %]">[% not_deleted_loo.barcode | html %]</a>[% ELSE %][% not_deleted_loo.barcode | html %][% END %]</td> - <td> - [% 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 %] - </td> - </tr> - [% END %] - </tbody> - </table> - [% END %] - </div> + </fieldset> [% END %] - <p> - [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Delete items in a batch%] - [% IF biblio_deleted %] - <a class="btn btn-default" href="/cgi-bin/koha/cataloguing/addbooks.pl"><i class="fa fa-check-square-o"></i> Return to the cataloging module</a> - [% ELSIF searchid %] - <a class="btn btn-default" href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]&searchid=[% searchid | uri %]"><i class="fa fa-check-square-o"></i> Return to the record</a> - [% ELSE %] - <a class="btn btn-default" href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]"><i class="fa fa-check-square-o"></i> Return to the record</a> - [% END %] - [% ELSIF src %] - <a class="btn btn-default" href="[% src | url %]"><i class="fa fa-check-square-o"></i> Return to where you were</a> - [% ELSE %] - <a class="btn btn-default" href="/cgi-bin/koha/tools/batchMod.pl?del=1"><i class="fa fa-check-square-o"></i> Return to batch item deletion</a> - [% END %] - </p> -[% END %] </div> [% MACRO jsinclude BLOCK %] @@ -315,18 +191,6 @@ [% Asset.js("js/pages/batchMod.js") | $raw %] [% Asset.js("js/browser.js") | $raw %] <script> - // Prepare array of all column headers, incrementing each index by - // two to accommodate control and title columns - var allColumns = new Array([% FOREACH item_header_loo IN item_header_loop %]'[% loop.count | html %]'[% UNLESS ( loop.last ) %],[% END %][% END %]); - for( x=0; x<allColumns.length; x++ ){ - allColumns[x] = Number(allColumns[x]) + 2; - } - $(document).ready(function(){ - $("#mainformsubmit").on("click",function(){ - return submitBackgroundJob(this.form); - }); - }); - [% IF searchid %] browser = KOHA.browser('[% searchid | html %]'); browser.show_back_link(); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-edit.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-edit.tt index 05399cf7e05..5660a2cb459 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-edit.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batchMod-edit.tt @@ -1,6 +1,7 @@ [% USE raw %] [% USE Asset %] [% USE Koha %] +[% USE KohaDates %] [% SET footerjs = 1 %] [% INCLUDE 'doc-head-open.inc' %] <title>Batch item modification › Tools › Koha @@ -31,31 +32,46 @@
- [% IF ( show ) %] -

Batch item modification

- [% ELSE %] -

Batch item modification results

+

Batch item modification

+ [% IF op == 'enqueued' %]
- [% IF (modified_items) %] - [% modified_items | html %] item(s) modified (with [% modified_fields | html %] field(s) modified). - [% ELSE %] - No items modified. - [% END %] -
- [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Edit items in a batch%] - [% IF searchid %] - Return to the record - [% ELSE %] - Return to the record - [% END %] - [% ELSIF src %] - Return to where you were +

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

+

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

+
+ +
+ [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Edit items in a batch%] + [% IF searchid %] + Return to the record [% ELSE %] - Return to batch item modification + Return to the record [% END %] -
-
- [% END # /IF show %] + [% ELSIF src %] + Return to where you were + [% ELSE %] + Return to batch item modification + [% END %] + + [% END %] + + [% 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 %] + [% IF ( barcode_not_unique ) %]
@@ -123,99 +139,9 @@ [% END %] - [% IF ( item_loop ) %] - [% IF show %] - - [% END %] - -
- -

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

- - - - - - - - [% FOREACH item_header_loo IN item_header_loop %] - - [% END %] - - - - [% FOREACH item_loo IN item_loop %] - - [% IF show %] - [% IF item_loo.nomod %] - - [% ELSE %] - - [% END %] - [% ELSE %] - - [% END %] - - - [% FOREACH item_valu IN item_loo.item_value %] - - [% END %] - - [% END # /FOREACH item_loo %] - -
 TitleHolds [% item_header_loo.header_value | html %]
Cannot edit - -   - - - [% IF item_loo.holds %] - [% IF item_loo.item_holds %] - - [% ELSE %] - - [% END %] - [% ELSE %] - [% IF item_loo.holds %] - - [% ELSE %] - - [% END %] - [% END # /IF item_loo.holds %] - [% IF item_loo.holds %] - [% item_loo.item_holds | html %]/[% item_loo.holds | html %] - [% ELSE %] - [% item_loo.holds | html %] - [% END %] - - - [% item_valu.field | html %] -
-
- [% END # /IF item_loop %] + [% IF items.size %] + [% PROCESS items_table_batchmod headers => item_header_loop, items => items, checkboxes_edit => 1, display_columns_selection => 1 %] + [% END %] [% IF ( simple_items_display || job_completed ) %] [% IF ( too_many_items_display ) %] @@ -290,7 +216,7 @@ Return to batch item modification [% END %] - [% END #/IF show %] + [% END %] [% MACRO jsinclude BLOCK %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/pages/batchMod.js b/koha-tmpl/intranet-tmpl/prog/js/pages/batchMod.js index 4816036d3ea..d9432e74f51 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/pages/batchMod.js +++ b/koha-tmpl/intranet-tmpl/prog/js/pages/batchMod.js @@ -3,14 +3,26 @@ var date = new Date(); date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000)); +function guess_nb_cols() { + // This is a bit ugly, we are trying to know if there are checkboxes in the first column of the table + if ( $("#itemst tr:first th:first").html() == "" ) { + // First header is empty, it's a checkbox + return 3; + } else { + // First header is not empty, there are no checkboxes + return 2; + } +} + function hideColumns() { var valCookie = Cookies.get("showColumns"); + var nb_cols = guess_nb_cols(); if (valCookie) { valCookie = valCookie.split("/"); $("#showall").prop("checked", false).parent().removeClass("selected"); for ( var i = 0; i < valCookie.length; i++ ) { if (valCookie[i] !== '') { - var index = valCookie[i] - 3; + var index = valCookie[i] - nb_cols; $("#itemst td:nth-child(" + valCookie[i] + "),#itemst th:nth-child(" + valCookie[i] + ")").toggle(); $("#checkheader" + index).prop("checked", false).parent().removeClass("selected"); } @@ -20,10 +32,11 @@ function hideColumns() { function hideColumn(num) { $("#hideall,#showall").prop("checked", false).parent().removeClass("selected"); + var nb_cols = guess_nb_cols(); var valCookie = Cookies.get("showColumns"); // set the index of the table column to hide $("#" + num).parent().removeClass("selected"); - var hide = Number(num.replace("checkheader", "")) + 3; + var hide = Number(num.replace("checkheader", "")) + nb_cols; // hide header and cells matching the index $("#itemst td:nth-child(" + hide + "),#itemst th:nth-child(" + hide + ")").toggle(); // set or modify cookie with the hidden column's index @@ -59,7 +72,8 @@ function showColumn(num) { $("#" + num).parent().addClass("selected"); var valCookie = Cookies.get("showColumns"); // set the index of the table column to hide - var show = Number(num.replace("checkheader", "")) + 3; + var nb_cols = guess_nb_cols(); + var show = Number(num.replace("checkheader", "")) + nb_cols; // hide header and cells matching the index $("#itemst td:nth-child(" + show + "),#itemst th:nth-child(" + show + ")").toggle(); // set or modify cookie with the hidden column's index @@ -80,21 +94,23 @@ function showColumn(num) { } function showAllColumns() { + var nb_cols = guess_nb_cols(); $("#selections input:checkbox").each(function () { $(this).prop("checked", true); }); $("#selections span").addClass("selected"); - $("#itemst td:nth-child(3),#itemst tr th:nth-child(3)").nextAll().show(); + $("#itemst td:nth-child("+nb_cols+"),#itemst tr th:nth-child("+nb_cols+")").nextAll().show(); $.removeCookie("showColumns", { path: '/' }); $("#hideall").prop("checked", false).parent().removeClass("selected"); } function hideAllColumns() { + var nb_cols = guess_nb_cols(); $("#selections input:checkbox").each(function () { $(this).prop("checked", false); }); $("#selections span").removeClass("selected"); - $("#itemst td:nth-child(3),#itemst th:nth-child(3)").nextAll().hide(); + $("#itemst td:nth-child("+nb_cols+"),#itemst tr th:nth-child("+nb_cols+")").nextAll().hide(); $("#hideall").prop("checked", true).parent().addClass("selected"); var cookieString = allColumns.join("/"); Cookies.set("showColumns", cookieString, { expires: date, path: '/' }); diff --git a/misc/background_jobs_worker.pl b/misc/background_jobs_worker.pl index 8c1b6ded3fb..5a84d342baa 100755 --- a/misc/background_jobs_worker.pl +++ b/misc/background_jobs_worker.pl @@ -31,8 +31,10 @@ try { my @job_types = qw( batch_biblio_record_modification batch_authority_record_modification + batch_item_record_modification batch_biblio_record_deletion batch_authority_record_deletion + batch_item_record_deletion batch_hold_cancel ); diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t index 1625e0132c6..db3ff9a987d 100755 --- a/t/db_dependent/Koha/Item.t +++ b/t/db_dependent/Koha/Item.t @@ -109,11 +109,12 @@ subtest 'has_pending_hold() tests' => sub { subtest "as_marc_field() tests" => sub { my $mss = C4::Biblio::GetMarcSubfieldStructure( '' ); + my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" ); my @schema_columns = $schema->resultset('Item')->result_source->columns; my @mapped_columns = grep { exists $mss->{'items.'.$_} } @schema_columns; - plan tests => 2 * (scalar @mapped_columns + 1) + 2; + plan tests => 2 * (scalar @mapped_columns + 1) + 3; $schema->storage->txn_begin; @@ -126,7 +127,7 @@ subtest "as_marc_field() tests" => sub { is( $marc_field->tag, - $mss->{'items.itemnumber'}[0]->{tagfield}, + $itemtag, 'Generated field set the right tag number' ); @@ -141,7 +142,7 @@ subtest "as_marc_field() tests" => sub { is( $marc_field->tag, - $mss->{'items.itemnumber'}[0]->{tagfield}, + $itemtag, 'Generated field set the right tag number' ); @@ -154,25 +155,36 @@ subtest "as_marc_field() tests" => sub { my $unmapped_subfield = Koha::MarcSubfieldStructure->new( { frameworkcode => '', - tagfield => $mss->{'items.itemnumber'}[0]->{tagfield}, + tagfield => $itemtag, tagsubfield => 'X', } )->store; - $mss = C4::Biblio::GetMarcSubfieldStructure( '' ); my @unlinked_subfields; push @unlinked_subfields, X => 'Something weird'; $item->more_subfields_xml( C4::Items::_get_unlinked_subfields_xml( \@unlinked_subfields ) )->store; + Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" ); + Koha::MarcSubfieldStructures->search( + { frameworkcode => '', tagfield => $itemtag } ) + ->update( { display_order => \['FLOOR( 1 + RAND( ) * 10 )'] } ); + $marc_field = $item->as_marc_field; + my $tagslib = C4::Biblio::GetMarcStructure(1, ''); my @subfields = $marc_field->subfields; my $result = all { defined $_->[1] } @subfields; ok( $result, 'There are no undef subfields' ); + my @ordered_subfields = sort { + $tagslib->{$itemtag}->{ $a->[0] }->{display_order} + <=> $tagslib->{$itemtag}->{ $b->[0] }->{display_order} + } @subfields; + is_deeply(\@subfields, \@ordered_subfields); is( scalar $marc_field->subfield('X'), 'Something weird', 'more_subfield_xml is considered' ); $schema->storage->txn_rollback; + Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" ); }; subtest 'pickup_locations' => sub { diff --git a/t/db_dependent/Koha/Item/Attributes.t b/t/db_dependent/Koha/Item/Attributes.t new file mode 100755 index 00000000000..c78a397a52d --- /dev/null +++ b/t/db_dependent/Koha/Item/Attributes.t @@ -0,0 +1,151 @@ +#!/usr/bin/perl + +# 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 Test::More tests=> 10; +use utf8; + +use Koha::Database; +use Koha::Caches; + +use C4::Biblio; +use Koha::Item::Attributes; +use Koha::MarcSubfieldStructures; + +use t::lib::TestBuilder; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; +my $biblio = $builder->build_sample_biblio({ frameworkcode => '' }); +my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber }); + +my $cache = Koha::Caches->get_instance; +$cache->clear_from_cache("MarcStructure-0-"); +$cache->clear_from_cache("MarcStructure-1-"); +$cache->clear_from_cache("default_value_for_mod_marc-"); +$cache->clear_from_cache("MarcSubfieldStructure-"); + +# 952 $x $é $y are not linked with a kohafield +# $952$x $é repeatable +# $952$y is not repeatable +setup_mss(); + +$item->more_subfields_xml(undef)->store; # Shouldn't be needed, but we want to make sure +my $attributes = $item->additional_attributes; +is( ref($attributes), 'Koha::Item::Attributes' ); +is( $attributes->to_marcxml, undef ); +is_deeply($attributes->to_hashref, {}); + +my $some_marc_xml = q{ + + + + a + + value for x 1 + value for x 2 + value for y + value for é 1 + value for é 2 + value for z 1|value for z 2 + + + +}; + +$item->more_subfields_xml($some_marc_xml)->store; + +$attributes = $item->additional_attributes; +is( ref($attributes), 'Koha::Item::Attributes' ); +is( $attributes->{'x'}, "value for x 1|value for x 2"); +is( $attributes->{'y'}, "value for y"); +is( $attributes->{'é'}, "value for é 1|value for é 2"); +is( $attributes->{'z'}, "value for z 1|value for z 2"); + +is( $attributes->to_marcxml, $some_marc_xml ); +is_deeply( + $attributes->to_hashref, + { + 'x' => "value for x 1|value for x 2", + 'y' => "value for y", + 'é' => "value for é 1|value for é 2", + 'z' => "value for z 1|value for z 2", + } +); + +Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" ); + +sub setup_mss { + + my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => 'é', + } + )->delete; # In case it exist already + + Koha::MarcSubfieldStructure->new( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => 'é', + kohafield => undef, + repeatable => 1, + tab => 10, + } + )->store; + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => [ 'x', 'y' ] + } + )->update( { kohafield => undef } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => [ 'x', 'é' ], + } + )->update( { repeatable => 1 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => ['y'], + } + )->update( { repeatable => 0 } ); + + my $i = 0; + for my $sf ( qw( x y é z ) ) { + Koha::MarcSubfieldStructures->search( + { frameworkcode => '', tagfield => $itemtag, tagsubfield => $sf } ) + ->update( { display_order => $i++ } ); + } + +} diff --git a/t/db_dependent/Koha/Items/BatchUpdate.t b/t/db_dependent/Koha/Items/BatchUpdate.t new file mode 100755 index 00000000000..b07d0bf7cf2 --- /dev/null +++ b/t/db_dependent/Koha/Items/BatchUpdate.t @@ -0,0 +1,383 @@ +#!/usr/bin/perl + +# 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 Test::More tests=> 7; +use utf8; + +use Koha::Database; +use Koha::Caches; + +use C4::Biblio; +use Koha::Item::Attributes; +use Koha::MarcSubfieldStructures; + +use t::lib::TestBuilder; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" ); + +# 952 $x $é $y are not linked with a kohafield +# $952$x $é repeatable +# $952$t is not repeatable +# 952$z is linked with items.itemnotes and is repeatable +# 952$t is linked with items.copynumber and is not repeatable +setup_mss(); + +my $biblio = $builder->build_sample_biblio({ frameworkcode => '' }); +my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber }); + +my $items = Koha::Items->search({itemnumber => $item->itemnumber}); + +subtest 'MARC subfield linked with kohafield' => sub { + plan tests => 9; + + $items->batch_update({ + new_values => {itemnotes => 'new note'} + }); + $items->reset; + + $item = $item->get_from_storage; + is( $item->itemnotes, 'new note' ); + is( $item->as_marc_field->subfield('t'), undef ); + + is( $items->batch_update({ + new_values => {itemnotes=> 'another note'} + })->count, 1, 'Can be chained'); + $items->reset; + + $items->batch_update({new_values => {itemnotes=> undef }})->reset; + $item = $item->get_from_storage; + is( $item->itemnotes, undef, "blank" ); + is( $item->as_marc_field->subfield('t'), undef, '' ); + + $items->batch_update({new_values => {itemnotes=> 'new note', copynumber => 'new copynumber'}})->reset; + $item = $item->get_from_storage; + is( $item->itemnotes, 'new note', "multi" ); + is( $item->as_marc_field->subfield('z'), 'new note', '' ); + is( $item->copynumber, 'new copynumber', "multi" ); + is( $item->as_marc_field->subfield('t'), 'new copynumber', '' ); +}; + +subtest 'More marc subfields (no linked)' => sub { + plan tests => 1; + + $items->batch_update({new_values => {x => 'new xxx' }})->reset; + is( $item->get_from_storage->as_marc_field->subfield('x'), 'new xxx' ); +}; + +subtest 'repeatable' => sub { + plan tests => 2; + + subtest 'linked' => sub { + plan tests => 4; + + $items->batch_update({new_values => {itemnotes => 'new zzz 1|new zzz 2' }})->reset; + is( $item->get_from_storage->itemnotes, 'new zzz 1|new zzz 2'); + is_deeply( [$item->get_from_storage->as_marc_field->subfield('z')], ['new zzz 1', 'new zzz 2'], 'z is repeatable' ); + + $items->batch_update({new_values => {copynumber => 'new ttt 1|new ttt 2' }})->reset; + is( $item->get_from_storage->copynumber, 'new ttt 1|new ttt 2'); + is_deeply( [$item->get_from_storage->as_marc_field->subfield('t')], ['new ttt 1|new ttt 2'], 't is not repeatable' ); + }; + + subtest 'not linked' => sub { + plan tests => 2; + + $items->batch_update({new_values => {x => 'new xxx 1|new xxx 2' }})->reset; + is_deeply( [$item->get_from_storage->as_marc_field->subfield('x')], ['new xxx 1', 'new xxx 2'], 'i is repeatable' ); + + $items->batch_update({new_values => {y => 'new yyy 1|new yyy 2' }})->reset; + is_deeply( [$item->get_from_storage->as_marc_field->subfield('y')], ['new yyy 1|new yyy 2'], 'y is not repeatable' ); + }; +}; + +subtest 'blank' => sub { + plan tests => 5; + + $items->batch_update( + { + new_values => { + itemnotes => 'new notes 1|new notes 2', + copynumber => 'new cn 1|new cn 2', + x => 'new xxx 1|new xxx 2', + y => 'new yyy 1|new yyy 2', + + } + } + )->reset; + + $items->batch_update( + { + new_values => { + itemnotes => undef, + copynumber => undef, + x => undef, + } + } + )->reset; + + $item = $item->get_from_storage; + is( $item->itemnotes, undef ); + is( $item->copynumber, undef ); + is( $item->as_marc_field->subfield('x'), undef ); + is_deeply( [ $item->as_marc_field->subfield('y') ], + ['new yyy 1|new yyy 2'] ); + + $items->batch_update( + { + new_values => { + y => undef, + } + } + )->reset; + + is( $item->get_from_storage->more_subfields_xml, undef ); + +}; + +subtest 'regex' => sub { + plan tests => 6; + + $items->batch_update( + { + new_values => { + itemnotes => 'new notes 1|new notes 2', + copynumber => 'new cn 1|new cn 2', + x => 'new xxx 1|new xxx 2', + y => 'new yyy 1|new yyy 2', + + } + } + )->reset; + + my $re = { + search => 'new', + replace => 'awesome', + modifiers => '', + }; + $items->batch_update( + { + regex_mod => + { itemnotes => $re, copynumber => $re, x => $re, y => $re } + } + )->reset; + $item = $item->get_from_storage; + is( $item->itemnotes, 'awesome notes 1|new notes 2' ); + is_deeply( + [ $item->as_marc_field->subfield('z') ], + [ 'awesome notes 1', 'new notes 2' ], + 'z is repeatable' + ); + + is( $item->copynumber, 'awesome cn 1|new cn 2' ); + is_deeply( [ $item->as_marc_field->subfield('t') ], + ['awesome cn 1|new cn 2'], 't is not repeatable' ); + + is_deeply( + [ $item->as_marc_field->subfield('x') ], + [ 'awesome xxx 1', 'new xxx 2' ], + 'i is repeatable' + ); + + is_deeply( + [ $item->as_marc_field->subfield('y') ], + ['awesome yyy 1|new yyy 2'], + 'y is not repeatable' + ); +}; + +subtest 'encoding' => sub { + plan tests => 1; + + $items->batch_update({ + new_values => { 'é' => 'new note é'} + }); + $items->reset; + + $item = $item->get_from_storage; + is( $item->as_marc_field->subfield('é'), 'new note é', ); +}; + +subtest 'report' => sub { + plan tests => 5; + + my $item_1 = + $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); + my $item_2 = + $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); + + my $items = Koha::Items->search( + { itemnumber => [ $item_1->itemnumber, $item_2->itemnumber ] } ); + + my ($report) = $items->batch_update( + { + new_values => { itemnotes => 'new note' } + } + ); + $items->reset; + is_deeply( + $report, + { + modified_itemnumbers => + [ $item_1->itemnumber, $item_2->itemnumber ], + modified_fields => 2 + } + ); + + ($report) = $items->batch_update( + { + new_values => { itemnotes => 'new note', copynumber => 'new cn' } + } + ); + $items->reset; + + is_deeply( + $report, + { + modified_itemnumbers => + [ $item_1->itemnumber, $item_2->itemnumber ], + modified_fields => 2 + } + ); + + $item_1->get_from_storage->update( { itemnotes => 'not new note' } ); + ($report) = $items->batch_update( + { + new_values => { itemnotes => 'new note', copynumber => 'new cn' } + } + ); + $items->reset; + + is_deeply( + $report, + { + modified_itemnumbers => [ $item_1->itemnumber ], + modified_fields => 1 + } + ); + + ($report) = $items->batch_update( + { + new_values => { x => 'new xxx', y => 'new yyy' } + } + ); + $items->reset; + + is_deeply( + $report, + { + modified_itemnumbers => + [ $item_1->itemnumber, $item_2->itemnumber ], + modified_fields => 4 + } + ); + + my $re = { + search => 'new', + replace => 'awesome', + modifiers => '', + }; + + $item_2->get_from_storage->update( { itemnotes => 'awesome note' } ); + ($report) = $items->batch_update( + { + regex_mod => + { itemnotes => $re, copynumber => $re, x => $re, y => $re } + } + ); + $items->reset; + + is_deeply( + $report, + { + modified_itemnumbers => + [ $item_1->itemnumber, $item_2->itemnumber ], + modified_fields => 7 + } + ); + +}; + +Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" ); + +sub setup_mss { + + my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => 'é', + } + )->delete; # In case it exist already + + Koha::MarcSubfieldStructure->new( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => 'é', + kohafield => undef, + repeatable => 1, + tab => 10, + } + )->store; + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => [ 'x', 'y' ] + } + )->update( { kohafield => undef } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => [ 'x', 'é' ], + } + )->update( { repeatable => 1 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => ['t'], + } + )->update( { repeatable => 0 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => ['z'], + } + )->update( { kohafield => 'items.itemnotes', repeatable => 1 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + } + )->update( { display_order => \['FLOOR( 1 + RAND( ) * 10 )'] } ); +} diff --git a/t/db_dependent/Koha/UI/Form/Builder/Item.t b/t/db_dependent/Koha/UI/Form/Builder/Item.t new file mode 100755 index 00000000000..413d837c8c1 --- /dev/null +++ b/t/db_dependent/Koha/UI/Form/Builder/Item.t @@ -0,0 +1,329 @@ +#!/usr/bin/perl + +# 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 Test::More tests => 6; +use utf8; + +use List::MoreUtils qw( uniq ); + +use Koha::Libraries; +use Koha::MarcSubfieldStructures; +use Koha::UI::Form::Builder::Item; +use t::lib::TestBuilder; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +my $cache = Koha::Caches->get_instance(); +$cache->clear_from_cache("MarcStructure-0-"); +$cache->clear_from_cache("MarcStructure-1-"); +$cache->clear_from_cache("default_value_for_mod_marc-"); +$cache->clear_from_cache("MarcSubfieldStructure-"); + +# 952 $x $é are not linked with a kohafield +# $952$x $é repeatable +# $952$t is not repeatable +# 952$z is linked with items.itemnotes and is repeatable +# 952$t is linked with items.copynumber and is not repeatable +setup_mss(); + +subtest 'authorised values' => sub { + #plan tests => 1; + + my $biblio = $builder->build_sample_biblio({ value => {frameworkcode => ''}}); + my $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form; + + my @display_orders = uniq map { $_->{display_order} } @$subfields; + is_deeply( \@display_orders, [sort {$a <=> $b} @display_orders], 'subfields are sorted by display order' ); + + subtest 'normal AV' => sub { + plan tests => 2; + my ($subfield) = + grep { $_->{kohafield} eq 'items.notforloan' } @$subfields; + my $avs = Koha::AuthorisedValues->search( { category => 'NOT_LOAN' } ); + + is_deeply( + $subfield->{marc_value}->{values}, + [ + "", + map { $_->authorised_value } + sort { $a->lib cmp $b->lib } + $avs->as_list + ], + 'AVs are sorted by lib and en empty option is created first' + ); + is_deeply( + $subfield->{marc_value}->{labels}, + { + map { $_->authorised_value => $_->lib } + sort { $a->lib cmp $b->lib } + $avs->as_list + } + ); + }; + + subtest 'cn_source' => sub { + plan tests => 2; + my ( $subfield ) = grep { $_->{kohafield} eq 'items.cn_source' } @$subfields; + is_deeply( $subfield->{marc_value}->{values}, [ '', 'ddc', 'lcc' ] ); + is_deeply( + $subfield->{marc_value}->{labels}, + { + ddc => "Dewey Decimal Classification", + lcc => "Library of Congress Classification", + } + ); + }; + subtest 'branches' => sub { + plan tests => 2; + my ( $subfield ) = grep { $_->{kohafield} eq 'items.homebranch' } @$subfields; + my $libraries = Koha::Libraries->search; + is_deeply( + $subfield->{marc_value}->{values}, + [ $libraries->get_column('branchcode') ] + ); + is_deeply( + $subfield->{marc_value}->{labels}, + { map { $_->branchcode => $_->branchname } $libraries->as_list } + ); + }; + + subtest 'itemtypes' => sub { + plan tests => 2; + my ($subfield) = grep { $_->{kohafield} eq 'items.itype' } @$subfields; + my $itemtypes = Koha::ItemTypes->search; + + is_deeply( + $subfield->{marc_value}->{values}, + [ + "", + map { $_->itemtype } + # We need to sort using uc or perl won't be case insensitive + sort { uc($a->translated_description) cmp uc($b->translated_description) } + $itemtypes->as_list + ], + "Item types should be sorted by description and an empty entries should be shown" + ); + is_deeply( $subfield->{marc_value}->{labels}, + { map { $_->itemtype => $_->description } $itemtypes->as_list }, + 'Labels should be correctly displayed' + ); + }; +}; + +subtest 'prefill_with_default_values' => sub { + plan tests => 2; + + my $biblio = $builder->build_sample_biblio({ value => {frameworkcode => ''}}); + my $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form; + + + my ($subfield) = grep { $_->{subfield} eq 'é' } @$subfields; + is( $subfield->{marc_value}->{value}, '', 'no default value if prefill_with_default_values not passed' ); + + $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form({ prefill_with_default_values => 1 }); + + + ($subfield) = grep { $_->{subfield} eq 'é' } @$subfields; + is( $subfield->{marc_value}->{value}, 'ééé', 'default value should be set if prefill_with_default_values passed'); + + +}; + +subtest 'branchcode' => sub { + plan tests => 2; + + my $biblio = $builder->build_sample_biblio({ value => {frameworkcode => ''}}); + my $library = $builder->build_object({ class => 'Koha::Libraries' }); + my $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form; + + my ( $subfield ) = grep { $_->{kohafield} eq 'items.homebranch' } @$subfields; + is( $subfield->{marc_value}->{default}, '', 'no library preselected if no branchcode passed'); + + $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form({ branchcode => $library->branchcode }); + + ( $subfield ) = grep { $_->{kohafield} eq 'items.homebranch' } @$subfields; + is( $subfield->{marc_value}->{default}, $library->branchcode, 'the correct library should be preselected if branchcode is passed'); +}; + +subtest 'default_branches_empty' => sub { + plan tests => 2; + + my $biblio = $builder->build_sample_biblio({ value => {frameworkcode => ''}}); + my $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form; + + my ( $subfield ) = grep { $_->{kohafield} eq 'items.homebranch' } @$subfields; + isnt( $subfield->{marc_value}->{values}->[0], "", 'No empty option for branches' ); + + $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form({ default_branches_empty => 1 }); + + ( $subfield ) = grep { $_->{kohafield} eq 'items.homebranch' } @$subfields; + is( $subfield->{marc_value}->{values}->[0], "", 'empty option for branches if default_branches_empty passed' ); +}; + +subtest 'kohafields_to_ignore' => sub { + plan tests => 2; + + my $biblio = + $builder->build_sample_biblio( { value => { frameworkcode => '' } } ); + my $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form; + + my ($subfield) = grep { $_->{kohafield} eq 'items.barcode' } @$subfields; + isnt( $subfield, undef, 'barcode subfield should be in the subfield list' ); + + $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } ) + ->edit_form( { kohafields_to_ignore => ['items.barcode'] } ); + + ($subfield) = grep { $_->{kohafield} eq 'items.barcode' } @$subfields; + is( $subfield, undef, + 'barcode subfield should have not been built if passed to kohafields_to_ignore' + ); +}; + +subtest 'subfields_to_allow & ignore_not_allowed_subfields' => sub { + plan tests => 6; + + my ( $tag_cn, $subtag_cn ) = C4::Biblio::GetMarcFromKohaField("items.itemcallnumber"); + my ( $tag_notes, $subtag_notes ) = C4::Biblio::GetMarcFromKohaField("items.itemnotes"); + my $biblio = $builder->build_sample_biblio( { value => { frameworkcode => '' } } ); + my $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form( + { + subfields_to_allow => [ + sprintf( '%s$%s', $tag_cn, $subtag_cn ), + sprintf( '%s$%s', $tag_notes, $subtag_notes ) + ] + } + ); + + isnt( scalar(@$subfields), 2, "There are more than the 2 subfields we allowed" ); + my ($subfield) = grep { $_->{kohafield} eq 'items.itemcallnumber' } @$subfields; + is( $subfield->{marc_value}->{readonly}, undef, "subfields to allowed are not marked as readonly" ); + ($subfield) = grep { $_->{kohafield} eq 'items.copynumber' } @$subfields; + isnt( $subfield->{marc_value}->{readonly}, 1, "subfields that are not in the allow list are marked as readonly" ); + + $subfields = + Koha::UI::Form::Builder::Item->new( + { biblionumber => $biblio->biblionumber } )->edit_form( + { + subfields_to_allow => [ + sprintf( '%s$%s', $tag_cn, $subtag_cn ), + sprintf( '%s$%s', $tag_notes, $subtag_notes ) + ], + ignore_not_allowed_subfields => 1, + } + ); + + is( scalar(@$subfields), 2, "With ignore_not_allowed_subfields, only the subfields to ignore are returned" ); + ($subfield) = + grep { $_->{kohafield} eq 'items.itemcallnumber' } @$subfields; + is( $subfield->{marc_value}->{readonly}, undef, "subfields to allowed are not marked as readonly" ); + ($subfield) = grep { $_->{kohafield} eq 'items.copynumber' } @$subfields; + is( $subfield, undef, "subfield that is not in the allow list is not returned" ); +}; + + +$cache->clear_from_cache("MarcStructure-0-"); +$cache->clear_from_cache("MarcStructure-1-"); +$cache->clear_from_cache("default_value_for_mod_marc-"); +$cache->clear_from_cache("MarcSubfieldStructure-"); + +sub setup_mss { + + my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => 'é', + } + )->delete; # In case it exist already + + Koha::MarcSubfieldStructure->new( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => 'é', + kohafield => undef, + repeatable => 1, + defaultvalue => 'ééé', + tab => 10, + } + )->store; + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => [ 'x' ] + } + )->update( { kohafield => undef } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => [ 'x', 'é' ], + } + )->update( { repeatable => 1 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => ['t'], + } + )->update( { repeatable => 0 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + tagsubfield => ['z'], + } + )->update( { kohafield => 'items.itemnotes', repeatable => 1 } ); + + Koha::MarcSubfieldStructures->search( + { + frameworkcode => '', + tagfield => $itemtag, + } + )->update( { display_order => \['FLOOR( 1 + RAND( ) * 10 )'] } ); +} diff --git a/tools/batchMod.pl b/tools/batchMod.pl index 0988571b5d4..e5ba87cfbb8 100755 --- a/tools/batchMod.pl +++ b/tools/batchMod.pl @@ -24,33 +24,23 @@ use Try::Tiny qw( catch try ); use C4::Auth qw( get_template_and_user haspermission ); use C4::Output qw( output_html_with_http_headers ); -use C4::Biblio qw( - DelBiblio - GetAuthorisedValueDesc - GetMarcFromKohaField - GetMarcStructure - IsMarcStructureInternal - TransformHtmlToXml -); -use C4::Items qw( GetItemsInfo Item2Marc ModItemFromMarc ); -use C4::Circulation qw( barcodedecode LostItem IsItemIssued ); +use C4::Circulation qw( barcodedecode ); use C4::Context; -use C4::Koha; -use C4::BackgroundJob; -use C4::ClassSource qw( GetClassSources GetClassSource ); use MARC::File::XML; use List::MoreUtils qw( uniq ); +use Encode qw( encode_utf8 ); use Koha::Database; +use Koha::DateUtils qw( dt_from_string ); use Koha::Exceptions::Exception; -use Koha::AuthorisedValues; use Koha::Biblios; -use Koha::DateUtils qw( dt_from_string ); use Koha::Items; -use Koha::ItemTypes; use Koha::Patrons; -use Koha::SearchEngine::Indexer; +use Koha::Item::Attributes; +use Koha::BackgroundJob::BatchDeleteItem; +use Koha::BackgroundJob::BatchUpdateItem; use Koha::UI::Form::Builder::Item; +use Koha::UI::Table::Builder::Items; my $input = CGI->new; my $dbh = C4::Context->dbh; @@ -90,253 +80,125 @@ my $restrictededition = $uid ? haspermission($uid, {'tools' => 'items_batchmod_ # In case user is a superlibrarian, edition is not restricted $restrictededition = 0 if ($restrictededition != 0 && C4::Context->IsSuperLibrarian()); -$template->param(del => $del); - my $nextop=""; -my @errors; # store errors found while checking data BEFORE saving item. -my $items_display_hashref; -our $tagslib = &GetMarcStructure(1); - -my $deleted_items = 0; # Number of deleted items -my $deleted_records = 0; # Number of deleted records ( with no items attached ) -my $not_deleted_items = 0; # Number of items that could not be deleted -my @not_deleted; # List of the itemnumbers that could not be deleted -my $modified_items = 0; # Numbers of modified items -my $modified_fields = 0; # Numbers of modified fields +my $display_items; my %cookies = parse CGI::Cookie($cookie); my $sessionID = $cookies{'CGISESSID'}->value; +my @messages; -#--- ---------------------------------------------------------------------------- -if ($op eq "action") { -#------------------------------------------------------------------------------- - my @tags = $input->multi_param('tag'); - my @subfields = $input->multi_param('subfield'); - my @values = $input->multi_param('field_value'); - my @searches = $input->multi_param('regex_search'); - my @replaces = $input->multi_param('regex_replace'); - my @modifiers = $input->multi_param('regex_modifiers'); - - my $upd_biblionumbers; - my $del_biblionumbers; - if ( $del ) { +if ( $op eq "action" ) { + + if ($del) { try { - my $schema = Koha::Database->new->schema; - $schema->txn_do( - sub { - foreach my $itemnumber (@itemnumbers) { - my $item = Koha::Items->find($itemnumber); - next - unless $item - ; # Should have been tested earlier, but just in case... - my $itemdata = $item->unblessed; - my $return = $item->safe_delete; - if ( ref( $return ) ) { - $deleted_items++; - push @$upd_biblionumbers, $itemdata->{'biblionumber'}; - } - else { - $not_deleted_items++; - push @not_deleted, - { - biblionumber => $itemdata->{'biblionumber'}, - itemnumber => $itemdata->{'itemnumber'}, - barcode => $itemdata->{'barcode'}, - title => $itemdata->{'title'}, - reason => $return, - }; - } - - # If there are no items left, delete the biblio - if ($del_records) { - my $itemscount = Koha::Biblios->find( $itemdata->{'biblionumber'} )->items->count; - if ( $itemscount == 0 ) { - my $error = DelBiblio( $itemdata->{'biblionumber'}, { skip_record_index => 1 } ); - unless ($error) { - $deleted_records++; - push @$del_biblionumbers, $itemdata->{'biblionumber'}; - if ( $src eq 'CATALOGUING' ) { - # We are coming catalogue/detail.pl, there were items from a single bib record - $template->param( biblio_deleted => 1 ); - } - } - } - } - } - if (@not_deleted) { - Koha::Exceptions::Exception->throw( - 'Some items have not been deleted, rolling back'); - } - } - ); + my $params = { + record_ids => \@itemnumbers, + delete_biblios => $del_records, + }; + my $job_id = + Koha::BackgroundJob::BatchDeleteItem->new->enqueue($params); + $nextop = 'enqueued'; + $template->param( job_id => $job_id, ); } catch { warn $_; - if ( $_->isa('Koha::Exceptions::Exception') ) { - $template->param( deletion_failed => 1 ); - } - die "Something terrible has happened!" - if ($_ =~ /Rollback failed/); # Rollback failed + push @messages, + { + type => 'error', + code => 'cannot_enqueue_job', + error => $_, + }; + $template->param( view => 'errors' ); }; } - else { # modification + else { # modification - my @columns = Koha::Items->columns; + my @item_columns = Koha::Items->columns; my $new_item_data; - my @columns_with_regex; - for my $c ( @columns ) { - if ( $c eq 'more_subfields_xml' ) { - my @more_subfields_xml = $input->multi_param("items.more_subfields_xml"); - my @unlinked_item_subfields; - for my $subfield ( @more_subfields_xml ) { - my $v = $input->param('items.more_subfields_xml_' . $subfield); - push @unlinked_item_subfields, $subfield, $v; - } - if ( @unlinked_item_subfields ) { - my $marc = MARC::Record->new(); - # use of tag 999 is arbitrary, and doesn't need to match the item tag - # used in the framework - $marc->append_fields(MARC::Field->new('999', ' ', ' ', @unlinked_item_subfields)); - $marc->encoding("UTF-8"); - # FIXME This is WRONG! We need to use the values that haven't been modified by the batch tool! - $new_item_data->{more_subfields_xml} = $marc->as_xml("USMARC"); - next; + my ( $columns_with_regex ); + my @subfields_to_blank = $input->multi_param('disable_input'); + my @more_subfields = $input->multi_param("items.more_subfields_xml"); + for my $item_column (@item_columns) { + my @attributes = ($item_column); + my $cgi_param_prefix = 'items.'; + if ( $item_column eq 'more_subfields_xml' ) { + @attributes = (); + $cgi_param_prefix = 'items.more_subfields_xml_'; + for my $subfield (@more_subfields) { + push @attributes, $subfield; } - $new_item_data->{more_subfields_xml} = undef; - # FIXME deal with more_subfields_xml and @subfields_to_blank - } elsif ( grep { $c eq $_ } @subfields_to_blank ) { - # Empty this column - $new_item_data->{$c} = undef - } else { + } - my @v = grep { $_ ne "" } - uniq $input->multi_param( "items." . $c ); + for my $attr (@attributes) { - next unless @v; + my $cgi_var_name = $cgi_param_prefix + . encode_utf8($attr) + ; # We need to deal correctly with encoding on subfield codes - $new_item_data->{$c} = join ' | ', @v; - } + if ( grep { $attr eq $_ } @subfields_to_blank ) { + + # Empty this column + $new_item_data->{$attr} = undef; + } + elsif ( my $regex_search = + $input->param( $cgi_var_name . '_regex_search' ) ) + { + $columns_with_regex->{$attr} = { + search => $regex_search, + replace => + $input->param( $cgi_var_name . '_regex_replace' ), + modifiers => + $input->param( $cgi_var_name . '_regex_modifiers' ) + }; + } + else { + my @v = + grep { $_ ne "" } uniq $input->multi_param($cgi_var_name); - if ( my $regex_search = $input->param('items.'.$c.'_regex_search') ) { - push @columns_with_regex, $c; + next unless @v; + + $new_item_data->{$attr} = join '|', @v; + } } } + my $params = { + record_ids => \@itemnumbers, + regex_mod => $columns_with_regex, + new_values => $new_item_data, + exclude_from_local_holds_priority => ( + defined $exclude_from_local_holds_priority + && $exclude_from_local_holds_priority ne "" + ) + ? $exclude_from_local_holds_priority + : undef, + + }; try { - my $schema = Koha::Database->new->schema; - $schema->txn_do( - sub { - - foreach my $itemnumber (@itemnumbers) { - my $item = Koha::Items->find($itemnumber); - next - unless $item - ; # Should have been tested earlier, but just in case... - my $itemdata = $item->unblessed; - - my $modified_holds_priority = 0; - if ( defined $exclude_from_local_holds_priority && $exclude_from_local_holds_priority ne "" ) { - 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; - for my $c ( @columns_with_regex ) { - my $regex_search = $input->param('items.'.$c.'_regex_search'); - my $old_value = $item->$c; - - my $value = apply_regex( - { - search => $regex_search, - replace => $input->param( - 'items' . $c . '_regex_replace' - ), - modifiers => $input->param( - 'items' . $c . '_regex_modifiers' - ), - value => $old_value, - } - ); - unless ( $old_value eq $value ) { - $modified++; - $item->$c($value); - } - } - - $modified += scalar(keys %$new_item_data); # FIXME This is incorrect if old value == new value. Should we loop of the keys and compare the before/after values? - if ( $modified) { - my $itemlost_pre = $item->itemlost; - $item->set($new_item_data)->store({skip_record_index => 1}); - - push @$upd_biblionumbers, $itemdata->{'biblionumber'}; - - LostItem( - $item->itemnumber, 'batchmod', undef, - { skip_record_index => 1 } - ) if $item->itemlost - and not $itemlost_pre; - } - - $modified_items++ if $modified || $modified_holds_priority; - $modified_fields += $modified + $modified_holds_priority; - } - } - ); + my $job_id = + Koha::BackgroundJob::BatchUpdateItem->new->enqueue($params); + $nextop = 'enqueued'; + $template->param( job_id => $job_id, ); } catch { - warn $_; - die "Something terrible has happened!" - if ($_ =~ /Rollback failed/); # Rollback failed + push @messages, + { + type => 'error', + code => 'cannot_enqueue_job', + error => $_, + }; + $template->param( view => 'errors' ); }; } - $upd_biblionumbers = [ uniq @$upd_biblionumbers ]; # Only update each bib once - - # Don't send specialUpdate for records we are going to delete - my %del_bib_hash = map{ $_ => undef } @$del_biblionumbers; - @$upd_biblionumbers = grep( ! exists( $del_bib_hash{$_} ), @$upd_biblionumbers ); - - my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX }); - $indexer->index_records( $upd_biblionumbers, 'specialUpdate', "biblioserver", undef ) if @$upd_biblionumbers; - $indexer->index_records( $del_biblionumbers, 'recordDelete', "biblioserver", undef ) if @$del_biblionumbers; - - # Once the job is done - # If we have a reasonable amount of items, we display them - my $max_items = $del ? C4::Context->preference("MaxItemsToDisplayForBatchDel") : C4::Context->preference("MaxItemsToDisplayForBatchMod"); - if (scalar(@itemnumbers) <= $max_items ){ - if (scalar(@itemnumbers) <= 1000 ) { - $items_display_hashref=BuildItemsData(@itemnumbers); - } else { - # Else, we only display the barcode - my @simple_items_display = map { - my $itemnumber = $_; - my $item = Koha::Items->find($itemnumber); - { - itemnumber => $itemnumber, - barcode => $item ? ( $item->barcode // q{} ) : q{}, - biblionumber => $item ? $item->biblio->biblionumber : q{}, - }; - } @itemnumbers; - $template->param("simple_items_display" => \@simple_items_display); - } - } else { - $template->param( "too_many_items_display" => scalar(@itemnumbers) ); - $template->param( "job_completed" => 1 ); - } - - - # Calling the template - $template->param( - modified_items => $modified_items, - modified_fields => $modified_fields, - ); - } + +$template->param( + messages => \@messages, +); # #------------------------------------------------------------------------------- # build screen with existing items. and "new" one @@ -372,10 +234,8 @@ if ($op eq "show"){ } } else { if (defined $biblionumber && !@itemnumbers){ - my @all_items = GetItemsInfo( $biblionumber ); - foreach my $itm (@all_items) { - push @itemnumbers, $itm->{itemnumber}; - } + my $biblio = Koha::Biblios->find($biblionumber); + @itemnumbers = $biblio ? $biblio->items->get_column('itemnumber') : (); } if ( my $list = $input->param('barcodelist') ) { my @barcodelist = grep /\S/, ( split /[$split_chars]/, $list ); @@ -398,7 +258,7 @@ if ($op eq "show"){ : C4::Context->preference("MaxItemsToDisplayForBatchMod"); $template->param("too_many_items_process" => scalar(@itemnumbers)) if !$del && scalar(@itemnumbers) > C4::Context->preference("MaxItemsToProcessForBatchMod"); if (scalar(@itemnumbers) <= ( $max_display_items // 1000 ) ) { - $items_display_hashref=BuildItemsData(@itemnumbers); + $display_items = 1; } else { $template->param("too_many_items_display" => scalar(@itemnumbers)); # Even if we do not display the items, we need the itemnumbers @@ -406,10 +266,6 @@ if ($op eq "show"){ } # now, build the item form for entering a new item - my @loop_data =(); - my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; - - my $pref_itemcallnumber = C4::Context->preference('itemcallnumber'); # Getting list of subfields to keep when restricted batchmod edit is enabled my @subfields_to_allow = $restrictededition ? split ' ', C4::Context->preference('SubfieldsToAllowForRestrictedBatchmod') : (); @@ -422,7 +278,8 @@ if ($op eq "show"){ ? ( subfields_to_allow => \@subfields_to_allow ) : () ), - subfields_to_ignore => ['items.barcode'], + ignore_not_allowed_subfields => 1, + kohafields_to_ignore => ['items.barcode'], prefill_with_default_values => $use_default_values, default_branches_empty => 1, } @@ -437,187 +294,22 @@ if ($op eq "show"){ $nextop="action" } # -- End action="show" -$template->param(%$items_display_hashref) if $items_display_hashref; -$template->param( - op => $nextop, -); -$template->param( $op => 1 ) if $op; - -if ($op eq "action") { - - #my @not_deleted_loop = map{{itemnumber=>$_}}@not_deleted; - +if ( $display_items ) { + my $items_table = + Koha::UI::Table::Builder::Items->new( { itemnumbers => \@itemnumbers } ) + ->build_table; $template->param( - not_deleted_items => $not_deleted_items, - deleted_items => $deleted_items, - delete_records => $del_records, - deleted_records => $deleted_records, - not_deleted_loop => \@not_deleted + items => $items_table->{items}, + item_header_loop => $items_table->{headers}, ); } -foreach my $error (@errors) { - $template->param($error => 1) if $error; -} -$template->param(src => $src); -$template->param(biblionumber => $biblionumber); -output_html_with_http_headers $input, $cookie, $template->output; -exit; - - -# ---------------- Functions - -sub BuildItemsData{ - my @itemnumbers=@_; - # now, build existiing item list - my %witness; #---- stores the list of subfields used at least once, with the "meaning" of the code - my @big_array; - #---- finds where items.itemnumber is stored - my ( $itemtagfield, $itemtagsubfield) = &GetMarcFromKohaField( "items.itemnumber" ); - my ($branchtagfield, $branchtagsubfield) = &GetMarcFromKohaField( "items.homebranch" ); - foreach my $itemnumber (@itemnumbers){ - my $itemdata = Koha::Items->find($itemnumber); - next unless $itemdata; # Should have been tested earlier, but just in case... - $itemdata = $itemdata->unblessed; - my $itemmarc=Item2Marc($itemdata); - my %this_row; - foreach my $field (grep {$_->tag() eq $itemtagfield} $itemmarc->fields()) { - # loop through each subfield - my $itembranchcode=$field->subfield($branchtagsubfield); - if ($itembranchcode && C4::Context->preference("IndependentBranches")) { - #verifying rights - my $userenv = C4::Context->userenv(); - unless (C4::Context->IsSuperLibrarian() or (($userenv->{'branch'} eq $itembranchcode))){ - $this_row{'nomod'}=1; - } - } - my $tag=$field->tag(); - foreach my $subfield ($field->subfields) { - my ($subfcode,$subfvalue)=@$subfield; - next if ($tagslib->{$tag}->{$subfcode}->{tab} ne 10 - && $tag ne $itemtagfield - && $subfcode ne $itemtagsubfield); - - $witness{$subfcode} = $tagslib->{$tag}->{$subfcode}->{lib} if ($tagslib->{$tag}->{$subfcode}->{tab} eq 10); - if ($tagslib->{$tag}->{$subfcode}->{tab} eq 10) { - $this_row{$subfcode}=GetAuthorisedValueDesc( $tag, - $subfcode, $subfvalue, '', $tagslib) - || $subfvalue; - } - - $this_row{itemnumber} = $subfvalue if ($tag eq $itemtagfield && $subfcode eq $itemtagsubfield); - } - } - - # grab title, author, and ISBN to identify bib that the item - # belongs to in the display - my $biblio = Koha::Biblios->find( $itemdata->{biblionumber} ); - $this_row{title} = $biblio->title; - $this_row{author} = $biblio->author; - $this_row{isbn} = $biblio->biblioitem->isbn; - $this_row{biblionumber} = $biblio->biblionumber; - $this_row{holds} = $biblio->holds->count; - $this_row{item_holds} = Koha::Holds->search( { itemnumber => $itemnumber } )->count; - $this_row{item} = Koha::Items->find($itemnumber); - - if (%this_row) { - push(@big_array, \%this_row); - } - } - @big_array = sort {$a->{0} cmp $b->{0}} @big_array; - - # now, construct template ! - # First, the existing items for display - my @item_value_loop; - my @witnesscodessorted=sort keys %witness; - for my $row ( @big_array ) { - my %row_data; - my @item_fields = map +{ field => $_ || '' }, @$row{ @witnesscodessorted }; - $row_data{item_value} = [ @item_fields ]; - $row_data{itemnumber} = $row->{itemnumber}; - #reporting this_row values - $row_data{'nomod'} = $row->{'nomod'}; - $row_data{bibinfo} = $row->{bibinfo}; - $row_data{author} = $row->{author}; - $row_data{title} = $row->{title}; - $row_data{isbn} = $row->{isbn}; - $row_data{biblionumber} = $row->{biblionumber}; - $row_data{holds} = $row->{holds}; - $row_data{item_holds} = $row->{item_holds}; - $row_data{item} = $row->{item}; - $row_data{safe_to_delete} = $row->{item}->safe_to_delete; - my $is_on_loan = C4::Circulation::IsItemIssued( $row->{itemnumber} ); - $row_data{onloan} = $is_on_loan ? 1 : 0; - push(@item_value_loop,\%row_data); - } - my @header_loop=map { { header_value=> $witness{$_}} } @witnesscodessorted; - - my @cannot_be_deleted = map { - $_->{safe_to_delete} == 1 ? () : $_->{item}->barcode - } @item_value_loop; - return { - item_loop => \@item_value_loop, - cannot_be_deleted => \@cannot_be_deleted, - item_header_loop => \@header_loop - }; -} - -#BE WARN : it is not the general case -# This function can be OK in the item marc record special case -# Where subfield is not repeated -# And where we are sure that field should correspond -# And $tag>10 -sub UpdateMarcWith { - my ($marcfrom,$marcto)=@_; - my ( $itemtag, $itemtagsubfield) = &GetMarcFromKohaField( "items.itemnumber" ); - my $fieldfrom=$marcfrom->field($itemtag); - my @fields_to=$marcto->field($itemtag); - my $modified = 0; - - return $modified unless $fieldfrom; - - foreach my $subfield ( $fieldfrom->subfields() ) { - foreach my $field_to_update ( @fields_to ) { - if ( $subfield->[1] ) { - unless ( $field_to_update->subfield($subfield->[0]) eq $subfield->[1] ) { - $modified++; - $field_to_update->update( $subfield->[0] => $subfield->[1] ); - } - } - else { - $modified++; - $field_to_update->delete_subfield( code => $subfield->[0] ); - } - } - } - return $modified; -} - -sub apply_regex { - my ($params) = @_; - my $search = $params->{search}; - my $replace = $params->{replace}; - my $modifiers = $params->{modifiers} || []; - 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/; - } +$template->param( + op => $nextop, + del => $del, + ( $op ? ( $op => 1 ) : () ), + src => $src, + biblionumber => $biblionumber, +); - return $value; -} +output_html_with_http_headers $input, $cookie, $template->output; -- 2.25.1