From ed727566db5ddff167fa484c5f0342997a3ef028 Mon Sep 17 00:00:00 2001 From: Jake Deery Date: Mon, 1 Dec 2025 13:09:20 +0000 Subject: [PATCH] Bug 14962: On Display code for BackgroundJobs This patch adds code required to make BackgroundJobs accept and process batch addition and removal of items to and from displays. Please see the test files commit for instructions on how to test this bundle of patches. Sponsored-by: ByWater Solutions Signed-of-by: Martin Renvoize --- Koha/BackgroundJob.pm | 2 + Koha/BackgroundJob/BatchAddDisplayItems.pm | 208 ++++++++++++++++++ Koha/BackgroundJob/BatchDeleteDisplayItems.pm | 193 ++++++++++++++++ .../batch_add_display_items.inc | 76 +++++++ .../batch_delete_display_items.inc | 76 +++++++ .../prog/en/modules/admin/background_jobs.tt | 8 + 6 files changed, 563 insertions(+) create mode 100644 Koha/BackgroundJob/BatchAddDisplayItems.pm create mode 100644 Koha/BackgroundJob/BatchDeleteDisplayItems.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_add_display_items.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_delete_display_items.inc diff --git a/Koha/BackgroundJob.pm b/Koha/BackgroundJob.pm index 0ff204281d6..b4f46904649 100644 --- a/Koha/BackgroundJob.pm +++ b/Koha/BackgroundJob.pm @@ -449,6 +449,8 @@ sub core_types_to_classes { batch_biblio_record_modification => 'Koha::BackgroundJob::BatchUpdateBiblio', batch_item_record_deletion => 'Koha::BackgroundJob::BatchDeleteItem', batch_item_record_modification => 'Koha::BackgroundJob::BatchUpdateItem', + batch_add_display_items => 'Koha::BackgroundJob::BatchAddDisplayItems', + batch_delete_display_items => 'Koha::BackgroundJob::BatchDeleteDisplayItems', erm_sushi_harvester => 'Koha::BackgroundJob::ErmSushiHarvester', batch_hold_cancel => 'Koha::BackgroundJob::BatchCancelHold', create_eholdings_from_biblios => 'Koha::BackgroundJob::CreateEHoldingsFromBiblios', diff --git a/Koha/BackgroundJob/BatchAddDisplayItems.pm b/Koha/BackgroundJob/BatchAddDisplayItems.pm new file mode 100644 index 00000000000..065defcd1ba --- /dev/null +++ b/Koha/BackgroundJob/BatchAddDisplayItems.pm @@ -0,0 +1,208 @@ +package Koha::BackgroundJob::BatchAddDisplayItems; + +# Copyright 2025-2026 Open Fifth Ltd +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Try::Tiny qw( catch try ); +use C4::Context; + +use Koha::DateUtils qw(dt_from_string); +use Koha::DisplayItem; +use Koha::DisplayItems; +use Koha::Items; +use Koha::Displays; + +use base 'Koha::BackgroundJob'; + +=head1 NAME + +Koha::BackgroundJob::BatchAddDisplayItems - Background job to add multiple items to a display + +=head1 API + +=head2 Class methods + +=head3 job_type + +Return the job type 'batch_add_display_items'. + +=cut + +sub job_type { + return 'batch_add_display_items'; +} + +=head3 process + +Process the batch addition of items to a display + +=cut + +sub process { + my ( $self, $args ) = @_; + + if ( $self->status eq 'cancelled' ) { + return; + } + + $self->start; + + my $display_id = $args->{display_id}; + my @barcodes = @{ $args->{barcodes} }; + my $date_remove = $args->{date_remove}; + + my $report = { + total_records => scalar @barcodes, + total_success => 0, + total_errors => 0, + }; + + my @messages; + my @added_items; + my @failed_items; + + # Validate display exists + my $display = Koha::Displays->find($display_id); + unless ($display) { + push @messages, { + type => 'error', + code => 'display_not_found', + display_id => $display_id, + }; + $self->finish( { messages => \@messages, report => $report } ); + return; + } + + # Calculate date_remove from display_days if not provided + unless ($date_remove) { + if ( $display->display_days ) { + my $dt = dt_from_string(); + $dt->add( days => $display->display_days ); + $date_remove = $dt->ymd; + } + } + + try { + my $schema = Koha::Database->new->schema; + $schema->txn_do( + sub { + for my $barcode ( sort { $a <=> $b } @barcodes ) { + + last if $self->get_from_storage->status eq 'cancelled'; + + my $item = Koha::Items->find( { barcode => $barcode } ); + unless ($item) { + push @failed_items, { + barcode => $barcode, + error => 'Item with barcode not found' + }; + $report->{total_errors}++; + next; + } + my $itemnumber = $item->itemnumber; + + # Check if item is already in this display + my $existing = Koha::DisplayItems->search( + { + display_id => $display_id, + itemnumber => $itemnumber, + } + )->next; + + if ($existing) { + push @failed_items, { + itemnumber => $itemnumber, + barcode => $barcode, + error => 'Item already in display' + }; + $report->{total_errors}++; + next; + } + + # Create display item + my $display_item = Koha::DisplayItem->new( + { + display_id => $display_id, + itemnumber => $itemnumber, + biblionumber => $item->biblionumber, + date_remove => $date_remove, + } + )->store; + + push @added_items, { + display_item_id => $display_item->display_item_id, + itemnumber => $itemnumber, + barcode => $barcode, + biblionumber => $item->biblionumber, + }; + + $report->{total_success}++; + $self->step; + } + } + ); + } catch { + warn $_; + push @messages, { + type => 'error', + code => 'unknown', + error => $_, + }; + die "Something terrible has happened!" if ( $_ =~ /Rollback failed/ ); + }; + + $report->{display_id} = $display_id; + $report->{added_items} = \@added_items; + $report->{failed_items} = \@failed_items; + + my $data = $self->decoded_data; + $data->{messages} = \@messages; + $data->{report} = $report; + + $self->finish($data); +} + +=head3 enqueue + +Enqueue the job. + +=cut + +sub enqueue { + my ( $self, $args ) = @_; + + return unless exists $args->{barcodes} && exists $args->{display_id}; + + my @barcodes = @{ $args->{barcodes} }; + my $display_id = $args->{display_id}; + my $date_remove = $args->{date_remove}; + + $self->SUPER::enqueue( + { + job_size => scalar @barcodes, + job_args => { + display_id => $display_id, + barcodes => \@barcodes, + date_remove => $date_remove, + }, + job_queue => 'long_tasks', + } + ); +} + +1; diff --git a/Koha/BackgroundJob/BatchDeleteDisplayItems.pm b/Koha/BackgroundJob/BatchDeleteDisplayItems.pm new file mode 100644 index 00000000000..c24c29c1f83 --- /dev/null +++ b/Koha/BackgroundJob/BatchDeleteDisplayItems.pm @@ -0,0 +1,193 @@ +package Koha::BackgroundJob::BatchDeleteDisplayItems; + +# Copyright 2025-2026 Open Fifth Ltd +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Try::Tiny qw( catch try ); + +use Koha::DisplayItem; +use Koha::DisplayItems; +use Koha::Items; + +use base 'Koha::BackgroundJob'; + +=head1 NAME + +Koha::BackgroundJob::BatchDeleteDisplayItems - Background job to remove multiple items from displays + +=head1 API + +=head2 Class methods + +=head3 job_type + +Return the job type 'batch_delete_display_items'. + +=cut + +sub job_type { + return 'batch_delete_display_items'; +} + +=head3 process + +Process the batch removal of items from displays + +=cut + +sub process { + my ( $self, $args ) = @_; + + if ( $self->status eq 'cancelled' ) { + return; + } + + $self->start; + + my @barcodes = @{ $args->{barcodes} }; + my $display_id = $args->{display_id}; # Optional: if specified, only remove from this display + + my $report = { + total_records => scalar @barcodes, + total_success => 0, + total_errors => 0, + }; + + my @messages; + my @deleted_items; + my @failed_items; + + try { + my $schema = Koha::Database->new->schema; + $schema->txn_do( + sub { + for my $barcode ( sort { $a <=> $b } @barcodes ) { + + last if $self->get_from_storage->status eq 'cancelled'; + + my $item = Koha::Items->find( { barcode => $barcode } ); + unless ($item) { + push @failed_items, { + barcode => $barcode, + error => 'Item with barcode not found' + }; + $report->{total_errors}++; + next; + } + my $itemnumber = $item->itemnumber; + + # Build search criteria + my $search_criteria = { itemnumber => $itemnumber }; + $search_criteria->{display_id} = $display_id + if $display_id; + + my $display_items = Koha::DisplayItems->search($search_criteria); + + unless ( $display_items->count ) { + push @failed_items, { + itemnumber => $itemnumber, + barcode => $barcode, + error => $display_id + ? 'Item not found in specified display' + : 'Item not found in any display' + }; + $report->{total_errors}++; + next; + } + + my $deleted_count = 0; + my @deleted_from_displays; + + while ( my $display_item = $display_items->next ) { + push @deleted_from_displays, { + display_id => $display_item->display_id, + display_item_id => $display_item->display_item_id, + }; + $display_item->delete; + $deleted_count++; + } + + if ( $deleted_count > 0 ) { + push @deleted_items, { + itemnumber => $itemnumber, + barcode => $barcode, + displays_removed_from => \@deleted_from_displays, + count => $deleted_count, + }; + $report->{total_success}++; + } else { + push @failed_items, { + itemnumber => $itemnumber, + barcode => $barcode, + error => 'No display items could be deleted' + }; + $report->{total_errors}++; + } + + $self->step; + } + } + ); + } catch { + warn $_; + push @messages, { + type => 'error', + code => 'unknown', + error => $_, + }; + die "Something terrible has happened!" if ( $_ =~ /Rollback failed/ ); + }; + + $report->{display_id} = $display_id; + $report->{deleted_items} = \@deleted_items; + $report->{failed_items} = \@failed_items; + + my $data = $self->decoded_data; + $data->{messages} = \@messages; + $data->{report} = $report; + + $self->finish($data); +} + +=head3 enqueue + +Enqueue the job. + +=cut + +sub enqueue { + my ( $self, $args ) = @_; + + return unless exists $args->{barcodes}; + + my @barcodes = @{ $args->{barcodes} }; + my $display_id = $args->{display_id}; # Optional + + $self->SUPER::enqueue( + { + job_size => scalar @barcodes, + job_args => { + barcodes => \@barcodes, + display_id => $display_id, + }, + job_queue => 'long_tasks', + } + ); +} + +1; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_add_display_items.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_add_display_items.inc new file mode 100644 index 00000000000..d921cd19d3e --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_add_display_items.inc @@ -0,0 +1,76 @@ +[% USE Koha %] + +[% BLOCK report %] + [% SET report = job.report %] + [% IF report %] + [% IF job.status == 'finished' %] + [% IF report.total_errors > 0 %] +
Processed [% report.total_success | html %] of [% report.total_records | html %] with [% report.total_errors | html %] errors
+ [% ELSE %] +
Processed [% report.total_success | html %] of [% report.total_records | html %]
+ [% END %] + [% ELSIF job.status == 'cancelled' %] + The job has been cancelled before it finished. + [% END %] + [% END %] +[% END %] + +[% BLOCK detail %] + [% SET messages = job.messages %] + [% IF messages && (messages.size > 0) %] +

Messages

+ + + + + + + [% FOR message IN messages %] + + + + + + [% END %] +
TypeCodeError
[% message.type | html %][% message.code | html %][% message.error | html %]
+ [% END %] + [% SET report = job.report %] + [% IF report %] + [% IF (report.added_items.size > 0) %] +

Added items

+ + + + + + [% FOR added_item IN report.added_items %] + + + + + [% END %] +
Item barcodeDisplay number
[% added_item.barcode | html %][% report.display_id | html %]
+ [% END %] + [% IF (report.failed_items.size > 0) %] +

Failed items

+ + + + + + [% FOR failed_item IN report.failed_items %] + + + + + [% END %] +
Item barcodeError message
[% failed_item.barcode | html %][% failed_item.error | html %]
+ [% END %] + [% END %] + [% IF job.status == 'cancelled' %] + The job has been cancelled before it finished. + [% END %] +[% END %] + +[% BLOCK js %] +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_delete_display_items.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_delete_display_items.inc new file mode 100644 index 00000000000..064900baaec --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_delete_display_items.inc @@ -0,0 +1,76 @@ +[% USE Koha %] + +[% BLOCK report %] + [% SET report = job.report %] + [% IF report %] + [% IF job.status == 'finished' %] + [% IF report.total_errors > 0 %] +
Processed [% report.total_success | html %] of [% report.total_records | html %] with [% report.total_errors | html %] errors
+ [% ELSE %] +
Processed [% report.total_success | html %] of [% report.total_records | html %]
+ [% END %] + [% ELSIF job.status == 'cancelled' %] + The job has been cancelled before it finished. + [% END %] + [% END %] +[% END %] + +[% BLOCK detail %] + [% SET messages = job.messages %] + [% IF messages && (messages.size > 0) %] +

Messages

+ + + + + + + [% FOR message IN messages %] + + + + + + [% END %] +
TypeCodeError
[% message.type | html %][% message.code | html %][% message.error | html %]
+ [% END %] + [% SET report = job.report %] + [% IF report %] + [% IF (report.deleted_items.size > 0) %] +

Deleted items

+ + + + + + [% FOR deleted_item IN report.deleted_items %] + + + + + [% END %] +
Item barcodeDisplay number
[% deleted_item.barcode | html %][% report.display_id | html %]
+ [% END %] + [% IF (report.failed_items.size > 0) %] +

Failed items

+ + + + + + [% FOR failed_item IN report.failed_items %] + + + + + [% END %] +
Item barcodeError message
[% failed_item.barcode | html %][% failed_item.error | html %]
+ [% END %] + [% END %] + [% IF job.status == 'cancelled' %] + The job has been cancelled before it finished. + [% END %] +[% END %] + +[% BLOCK js %] +[% 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 eccc21bb977..8a0578e8930 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 @@ -182,6 +182,14 @@ '_id': 'batch_item_record_modification', '_str': _("Batch item record modification").escapeHtml() }, + { + '_id': 'batch_add_display_items', + '_str': _("Batch add display items").escapeHtml() + }, + { + '_id': 'batch_delete_display_items', + '_str': _("Batch delete display items").escapeHtml() + }, { '_id': 'batch_item_record_deletion', '_str': _("Batch item record deletion").escapeHtml() -- 2.53.0