Bugzilla – Attachment 126330 Details for
Bug 28445
Use the task queue for the batch delete and update items tool
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 28445: Use the task queue for the batch delete and update items tool
Bug-28445-Use-the-task-queue-for-the-batch-delete-.patch (text/plain), 151.37 KB, created by
Jonathan Druart
on 2021-10-15 14:02:25 UTC
(
hide
)
Description:
Bug 28445: Use the task queue for the batch delete and update items tool
Filename:
MIME Type:
Creator:
Jonathan Druart
Created:
2021-10-15 14:02:25 UTC
Size:
151.37 KB
patch
obsolete
>From 0a76db84fefa903ec5ad8112e165fdbe075101ba Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >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 <http://www.gnu.org/licenses>. >+ >+=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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. > > 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 <http://www.gnu.org/licenses>. >+ >+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 %]); >+ <script> >+ $("#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 %] > [% END %] >- [% END %] >- var bibstring = bibs.join("&"); >- window.open('/cgi-bin/koha/virtualshelves/addbybiblionumber.pl?shelfnumber='+shelfnumber+'&confirm=1&'+bibstring, 'popup', 'width=500,height=500,toolbar=false,scrollbars=yes,resizable=yes'); >- return false; >- } >- }); >+ var bibstring = bibs.join("&"); >+ window.open('/cgi-bin/koha/virtualshelves/addbybiblionumber.pl?shelfnumber='+shelfnumber+'&confirm=1&'+bibstring, 'popup', 'width=500,height=500,toolbar=false,scrollbars=yes,resizable=yes'); >+ return false; >+ } >+ }); >+ </script> > [% 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 %] >+ <div class="dialog message"> >+ [% IF report.deleted_itemnumbers.size %] >+ <p>[% report.deleted_itemnumbers.size | html %] item(s) deleted.</p> >+ [% IF report.deleted_biblionumbers.size %] >+ <p>[% report.deleted_biblionumbers.size | html %] record(s) deleted.</p> >+ [% END %] >+ [% ELSE %] >+ No items deleted. >+ [% END %] >+ </div> >+ >+ [% IF report.not_deleted_itemnumbers.size %] >+ <div class="dialog error"> >+ [% 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 %] >+ </div> >+ [% END %] >+ >+ [% IF job.status == 'cancelled' %] >+ <div class="dialog error"> >+ The job has been cancelled before it finished. >+ <a href="/cgi-bin/koha/tools/batchMod.pl" title="New batch item modification">New batch item modification</a> >+ </div> >+ [% END %] >+ [% END %] >+[% END %] >+ >+[% BLOCK detail %] >+ [% FOR m IN job.messages %] >+ <div class="dialog message"> >+ [% IF m.type == 'success' %] >+ <i class="fa fa-check success"></i> >+ [% ELSIF m.type == 'warning' %] >+ <i class="fa fa-warning warn"></i> >+ [% ELSIF m.type == 'error' %] >+ <i class="fa fa-exclamation error"></i> >+ [% END %] >+ [% SWITCH m.code %] >+ [% CASE 'item_not_deleted' %] >+ Item with barcode <a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% m.biblionumber | uri %]&itemnumber=[% m.itemnumber | uri %]">[% m.barcode | html %]</a> 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 %] >+ </div> >+ [% 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 %] >+ <div class="dialog message"> >+ [% 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 %] >+ <a href="/cgi-bin/koha/tools/batchMod.pl" title="New batch item modification">New batch item modification</a> >+ </div> >+ [% END %] >+[% END %] >+ >+[% BLOCK detail %] >+ [% FOR m IN job.messages %] >+ <div class="dialog message"> >+ [% IF m.type == 'success' %] >+ <i class="fa fa-check success"></i> >+ [% ELSIF m.type == 'warning' %] >+ <i class="fa fa-warning warn"></i> >+ [% ELSIF m.type == 'error' %] >+ <i class="fa fa-exclamation error"></i> >+ [% END %] >+ [% SWITCH m.code %] >+ [% CASE %]Unknown message '[% m.code | html %]' >+ [% END %] >+ </div> >+ [% 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 %] > </ol> > [% END %] >+ >+[% BLOCK items_table_batchmod %] >+ >+ [% IF display_columns_selection %][%# Needs js/pages/batchMod.js %] >+ [% IF checkboxes_edit OR checkboxes_delete %] >+ <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> | <a id="clearonloanbutton" href="#">Clear on loan</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 header IN item_header_loop %] >+ <span class="selected"> >+ <input id="checkheader[% loop.count | html %]" type="checkbox" checked="checked" /> >+ <label for="checkheader[% loop.count | html %]">[% header.header_value | html %]</label> >+ </span> >+ [% END %] >+ </p> <!-- /#selections --> >+ </div> >+ [% END %] >+ [% SET date_fields = [ 'dateaccessioned', 'onloan', 'datelastseen', 'datelastborrowed', 'replacementpricedate' ] %] >+ <table id="itemst"> >+ <thead> >+ <tr> >+ [% IF checkboxes_edit OR checkboxes_delete %] >+ <th></th> >+ [% END %] >+ <th class="anti-the">Title</th> >+ <th class="holds_count" title="Item holds / Total holds">Holds</th> >+ [% FOREACH item_header IN headers %] >+ [% IF item_header.column_name %] >+ <th data-colname="[% item_header.column_name | html %]"> >+ [% ELSE %] >+ <th> >+ [% END %] >+ [% item_header.header_value | html %] >+ </th> >+ [% END %] >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH item IN items %] >+ [% SET can_be_edited = ! ( Koha.Preference('IndependentBranches') && ! logged_in_user && item.homebranch != Branches.GetLoggedInBranchcode() ) %] >+ >+ <tr> >+ [% IF checkboxes_edit %] >+ [% UNLESS can_be_edited%] >+ <td class="error">Cannot edit</td> >+ [% ELSE %] >+ <td> >+ <input type="checkbox" name="itemnumber" value="[% item.itemnumber | html %]" id="row[% item.itemnumber | html %]" checked="checked" data-is-onloan="[% item.is_checked_out | html %]" /> >+ </td> >+ [% END %] >+ [% ELSIF checkboxes_delete %] >+ [% UNLESS can_be_edited %] >+ <td class="error">Cannot delete</td> >+ [% ELSE %] >+ [% IF item.safe_to_delete == 1 %] >+ <td><input type="checkbox" name="itemnumber" value="[% item.itemnumber | html %]" id="row[% item.itemnumber | html %]" checked="checked" /></td> >+ [% 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 %] >+ >+ <td><input type="checkbox" name="itemnumber" value="[% item.itemnumber | html %]" id="row[% item.itemnumber | html %]" disabled="disabled" title="[% cannot_delete_reason | html %]"/></td> >+ [% END %] >+ >+ [% END %] >+ [% END %] >+ <td> >+ <label for="row[% item.itemnumber | html %]"> >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% item.biblionumber | uri %]"> >+ [% item.biblio.title | html %] >+ </a> >+ [% IF ( item.biblio.author ) %], by [% item.biblio.author | html %][% END %] >+ </label> >+ </td> >+ <td class="holds_count"> >+ [% IF item.holds %] >+ [% IF item.item_holds %] >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item.biblionumber | uri %]" title="Holds on this item: [% item.item_holds | html %] / Total holds on this record: [% item.holds | html -%]" > >+ [% ELSE %] >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item.biblionumber | uri %]" title="No holds on this item / Total holds on this record: [% item.holds | html -%]" > >+ [% END %] >+ [% ELSE %] >+ [% IF item.holds %] >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item.biblionumber | uri %]" title="Holds on this record: [% item.holds | html -%]" > >+ [% ELSE %] >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% item.biblionumber | uri %]" title="No holds on this record" > >+ [% END %] >+ [% END # /IF item.holds %] >+ [% IF item.holds %] >+ [% item.item_holds | html %]/[% item.holds | html %] >+ [% ELSE %] >+ [% item.holds | html %] >+ [% END %] >+ </a> >+ </td> >+ [% FOREACH header IN headers %] >+ [% SET attribute = header.attribute %] >+ [% IF header.attribute AND date_fields.grep('^' _ attribute _ '$').size %] >+ <td data-order="[% item.$attribute | html %]">[% item.$attribute | $KohaDates %]</td> >+ [% ELSE %] >+ <td>[% item.$attribute | html %]</td> >+ [% END %] >+ [% END %] >+ >+ </tr> >+ [% END # /FOREACH items %] >+ </tbody> >+ </table> <!-- /#itemst --> >+ >+[% 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' %] > <title> >@@ -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</title> >@@ -31,31 +32,46 @@ > > <div class="main container-fluid"> > >- [% IF ( show ) %] >- <h1>Batch item modification</h1> >- [% ELSE %] >- <h1>Batch item modification results</h1> >+ <h1>Batch item modification</h1> >+ [% IF op == 'enqueued' %] > <div class="dialog message"> >- [% IF (modified_items) %] >- [% modified_items | html %] item(s) modified (with [% modified_fields | html %] field(s) modified). >- [% ELSE %] >- No items modified. >- [% END %] >- <fieldset class="action"> >- [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Edit items in a batch%] >- [% 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 %] >- [% ELSIF src %] >- <a href="[% src | url %]">Return to where you were</a> >+ <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" title="New batch item modification">New batch item modification</a></p> >+ </div> >+ >+ <fieldset class="action"> >+ [% IF src == 'CATALOGUING' # from catalogue/detail.pl > Edit items in a batch%] >+ [% 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/tools/batchMod.pl">Return to batch item modification</a> >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber | uri %]">Return to the record</a> > [% END %] >- </fieldset> >- </div> <!-- /.dialog.message --> >- [% END # /IF show %] >+ [% ELSIF src %] >+ <a href="[% src | url %]">Return to where you were</a> >+ [% ELSE %] >+ <a href="/cgi-bin/koha/tools/batchMod.pl">Return to batch item modification</a> >+ [% END %] >+ </fieldset> >+ [% END %] >+ >+ [% 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 %] >+ > > [% IF ( barcode_not_unique ) %] > <div class="dialog alert"> >@@ -123,99 +139,9 @@ > <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> | <a id="clearonloanbutton" href="#">Clear on loan</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> <!-- /#selections --> >- >- <table id="itemst"> >- <thead> >- <tr> >- <th> </th> >- <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 edit</td> >- [% ELSE %] >- <td> >- <input type="checkbox" name="itemnumber" value="[% item_loo.itemnumber | html %]" id="row[% item_loo.itemnumber | html %]" checked="checked" data-is-onloan="[% item_loo.onloan | html %]" /> >- </td> >- [% 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 %] >- [% 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 # /FOREACH item_loo %] >- </tbody> >- </table> <!-- /#itemst --> >- </div> <!-- /#cataloguing_additem_itemlist --> >- [% 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 @@ > <a class="btn btn-default" href="/cgi-bin/koha/tools/batchMod.pl"><i class="fa fa-check-square-o"></i> Return to batch item modification</a> > [% END %] > </fieldset> <!-- /.action --> >- [% END #/IF show %] >+ [% END %] > </form> > > [% 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 <http://www.gnu.org/licenses>. >+ >+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{<?xml version="1.0" encoding="UTF-8"?> >+<collection >+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >+ xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd" >+ xmlns="http://www.loc.gov/MARC21/slim"> >+ >+<record> >+ <leader> a </leader> >+ <datafield tag="999" ind1=" " ind2=" "> >+ <subfield code="x">value for x 1</subfield> >+ <subfield code="x">value for x 2</subfield> >+ <subfield code="y">value for y</subfield> >+ <subfield code="é">value for é 1</subfield> >+ <subfield code="é">value for é 2</subfield> >+ <subfield code="z">value for z 1|value for z 2</subfield> >+ </datafield> >+</record> >+ >+</collection>}; >+ >+$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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 28445
:
121372
|
123221
|
123222
|
123223
|
123224
|
123225
|
123226
|
123227
|
123228
|
123229
|
123239
|
123240
|
125801
|
125802
|
125803
|
125804
|
125805
|
125808
|
125810
|
125811
|
125812
|
125817
|
125865
|
126046
|
126047
|
126136
|
126137
|
126138
|
126139
|
126140
|
126141
|
126142
|
126217
|
126218
|
126219
|
126223
|
126328
|
126329
|
126330
|
126331
|
126332
|
126333
|
126334
|
126335
|
126336
|
126337
|
126338
|
126339
|
126352
|
126353
|
126354
|
126355
|
126356
|
126357
|
126358
|
126359
|
126360
|
126361
|
126362
|
126363
|
126364
|
126365
|
126366
|
126367
|
126374
|
126375
|
126376
|
126377
|
126378
|
126379
|
126380
|
126381
|
126382
|
126383
|
126384
|
126385
|
126386
|
126387
|
126388
|
126389
|
127033
|
127037
|
127038
|
127039
|
128007
|
128008
|
128009