From 0ac38ecaecfb6356cd6846554d364ca7dcf51487 Mon Sep 17 00:00:00 2001 From: Matthias Meusburger Date: Fri, 14 Mar 2025 16:01:42 +0000 Subject: [PATCH] Bug 23258: Batch place or cancel holds Test plan: 1) Apply the patch 2) Go to Tools -> Patrons and circulation -> Batch place or cancel holds (/cgi-bin/koha/tools/modholds.pl) 3) Check that you can either upload a barcode file or use the textarea 4) Check that you can place holds on items 5) Check that you can cancel holds on items --- .../prog/en/modules/tools/modholds.tt | 276 ++++++++++++++++++ .../prog/en/modules/tools/tools-home.tt | 5 + tools/modholds.pl | 204 +++++++++++++ 3 files changed, 485 insertions(+) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/modholds.tt create mode 100755 tools/modholds.pl diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/modholds.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/modholds.tt new file mode 100644 index 00000000000..bc0d536b005 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/modholds.tt @@ -0,0 +1,276 @@ +[% USE raw %] +[% USE Asset %] +[% USE Koha %] +[% USE KohaDates %] +[% USE Branches %] +[% USE Categories %] +[% USE TablesSettings %] +[% PROCESS 'i18n.inc' %] +[% SET footerjs = 1 %] +[% INCLUDE 'doc-head-open.inc' %] +[% FILTER collapse %] + [% IF ( op == 'show' or op == 'show_results' ) %] + [% IF ( op == 'show' ) %] + [% t("Modifications") | html %] + › + [% ELSE %] + [% t("Results") | html %] + › + [% END %] + [% END %] + [% t("Batch holds modification") | html %] + › [% t("Tools") | html %] › [% t("Koha") | html %] + [% END %] +[% INCLUDE 'doc-head-close.inc' %] + + + +[% WRAPPER 'header.inc' %] + [% INCLUDE 'cat-search.inc' %] +[% END %] + +[% WRAPPER 'sub-header.inc' %] + [% WRAPPER breadcrumbs %] + [% WRAPPER breadcrumb_item %] + Tools + [% END %] + [% IF ( op == 'show' or op == 'show_results' ) %] + [% WRAPPER breadcrumb_item %] + Batch holds modification + [% END %] + [% IF ( op == 'show' ) %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + [% t("Modifications") | html %] + [% END %] + [% ELSE %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + [% t("Results")| html %] + [% END %] + [% END %] + [% ELSE %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + Batch holds modification + [% END %] + [% END %] + [% END #/ WRAPPER breadcrumbs %] +[% END #/ WRAPPER sub-header.inc %] + +[% WRAPPER 'main-container.inc' aside='tools-menu' %] + [% IF ( op == 'show_form' ) %] +

Batch holds modification

+
+ [% INCLUDE 'csrf-token.inc' %] + +
+ Use a file of barcodes +
    +
  1. + +
    File must contain one barcode per line.
    +
  2. +
+
+
+ Or list barcodes one by one +
    +
  1. + + +
  2. +
+
+
+ Action +
    +
  1. + + +
  2. +
  3. + + +
  4. +
+
+
+ Place holds for +
    +
  1. + + + +
  2. +
  3. + + +
  4. +
+
+
+ + Cancel +
+
+ [% END %] + + [% IF ( op == 'show' || op == 'cud-show' ) && ( !barcodes || !borrowernumber ) # Alert if no barcodes given %] + [% op = 'noshow' # Change op to prevent display in code below %] +

Batch holds modification

+
+ [% IF (!barcodes) %]

No barcodes given.

[% END %] + [% IF (!borrowernumber) %]

No patron given.

[% END %] +
+ +
+
+ [% END #Alert if no barcodes %] + + [% BLOCK show_holds_list %] + [% IF holds %] +
+ + + + [% IF op == 'show' %][% END %] + + + + [% IF op == 'show' %][% END %] + + + + [% FOREACH hold IN holds %] + + [% IF op == 'show' %][% END %] + + + + [% IF op == 'show' %][% END %] + + [% END %] + +
HoldTitleItemCurrent holdsComment
[% hold.title | html %][% hold.barcode | html %]Show holds[% IF !hold.holdable %]
This item cannot be placed on hold
[% END %]
+
+ [% END %] + [% END %] + + [% IF ( op == 'show' or op == 'show_results' ) %] +

Batch holds modification

+ [% UNLESS ( op == 'show' ) %] +

Results

+ [% END %] + [% IF ( notfoundbarcodes ) %] +

Warning, the following barcodes were not found:

+ +
+ + + + + + [% FOREACH notfoundbarcode IN notfoundbarcodes %] + + [% END %] + +
Barcodes not found
[% notfoundbarcode.barcode | html %]
+
+ + [% END %] + + [% IF ( op == 'show_results' ) %] +

+ [% IF place %] + The following items have been placed on hold + [% END %] + [% IF cancel %] + The following holds were canceled + [% END %] + for patron: [% borrowersurname | html %][% IF ( borrowerfirstname ) %], [% borrowerfirstname | html %][% END %]

+ [% IF place %] +

Pickup location: [% Branches.GetName( branch ) | html %]

+ [% END %] + [% PROCESS show_holds_list %] + [% END %] + + [% IF ( op == 'show' ) %] +
+ [% INCLUDE 'csrf-token.inc' %] + + [% IF ( holds ) %] + + [% PROCESS show_holds_list %] + + + + +
+ + Cancel +
+ [% END %] +
+ [% END %] + [% END %] + [% IF ( op == 'show_results' ) %] +

+ New batch holds modification +

+ [% END %] +[% END %] + +[% MACRO jsinclude BLOCK %] + [% INCLUDE 'calendar.inc' %] + [% INCLUDE 'datatables.inc' %] + [% Asset.js("js/tools-menu.js") | $raw %] + [% Asset.js("js/members-patron-selections.js") | $raw %] + +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt index bf1d14de861..74c97be2cfd 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt @@ -73,6 +73,11 @@
Create printable patron cards
[% END %] + [% IF ( CAN_user_reserveforothers ) %] +
Batch place or cancel holds
+
Batch place holds or cancel holds
+ [% END %] + [% IF ( CAN_user_tools_delete_anonymize_patrons ) %]
Batch patron deletion and anonymization
Batch delete patrons and delete patron circulation history
diff --git a/tools/modholds.pl b/tools/modholds.pl new file mode 100755 index 00000000000..9838ab990ba --- /dev/null +++ b/tools/modholds.pl @@ -0,0 +1,204 @@ +#!/usr/bin/perl + +# Copyright 2025 BibLibre +# +# 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 . + +# modborrowers.pl +# +# Batch Edit Patrons +# Modification for patron's fields: +# surname firstname branchcode categorycode city state zipcode country sort1 +# sort2 dateenrolled dateexpiry borrowernotes protected +# And for patron attributes. + +use Modern::Perl; +use CGI qw ( -utf8 ); +use C4::Auth qw( get_template_and_user ); +use C4::Members; +use C4::Reserves qw( CanItemBeReserved AddReserve ); +use C4::Output qw( output_html_with_http_headers ); +use C4::Context; +use Koha::DateUtils qw( dt_from_string ); +use Koha::List::Patron qw( GetPatronLists ); +use Koha::Libraries; +use Koha::Patron::Categories; +use Koha::Patron::Debarments qw( AddDebarment DelDebarment ); +use Koha::Patrons; +use List::MoreUtils qw(uniq); +use Koha::Patron::Messages; + +my $input = CGI->new; +my $op = $input->param('op') || 'show_form'; +my $action = $input->param('action'); +my $branch = $input->param('branch'); +my $borrowernumber = $input->param('borrowernumber'); + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "tools/modholds.tt", + query => $input, + type => "intranet", + flagsrequired => { reserveforothers => "*" }, + } +); + +my $logged_in_user = Koha::Patrons->find($loggedinuser); + +if ( $logged_in_user->is_superlibrarian ) { + $template->param( CAN_user_reserveforothers => 1 ); +} + +my $dbh = C4::Context->dbh; + +# Show borrower informations +if ( $op eq 'cud-show' || $op eq 'show' ) { + my @barcodes; + my @notfoundbarcodes; + my @itemnumbers; + my $borrower = Koha::Patrons->find( { borrowernumber => $borrowernumber } ); + + # Get cardnumbers from a file or the input area + if ( my $barcodeslist = $input->param('barcodeslist') ) { + + # User submitted a list of barcodes + push @barcodes, split( /\s\n/, $barcodeslist ); + } elsif ( my $barcodesuploadfile = $input->param('barcodesuploadfile') ) { + + # User uploaded a file of card numbers + binmode $barcodesuploadfile, ':encoding(UTF-8)'; + while ( my $content = <$barcodesuploadfile> ) { + next unless $content; + $content =~ s/[\r\n]*$//; + push @barcodes, $content if $content; + } + } + + @barcodes = uniq(@barcodes); + + for my $barcode (@barcodes) { + chomp $barcode; + my $item = Koha::Items->find( { barcode => $barcode } ); + if ($item) { + push @itemnumbers, $item->itemnumber; + } else { + push @notfoundbarcodes, $barcode; + } + + if (@notfoundbarcodes) { + my @notfoundbarcodesloop = map { { barcode => $_ } } @notfoundbarcodes; + $template->param( notfoundbarcodes => \@notfoundbarcodesloop ); + } + my @holdloop; + if (@itemnumbers) { + foreach my $itemnumber (@itemnumbers) { + my $item = Koha::Items->find( { itemnumber => $itemnumber } ); + my $biblio = Koha::Biblios->find( { biblionumber => $item->biblionumber } ); + my $holdable = CanItemBeReserved( $borrower, $item )->{status}; + push @holdloop, + { + 'biblionumber' => $biblio->biblionumber, + 'title' => $biblio->title, + 'itemnumber' => $item->itemnumber, + 'barcode' => $item->barcode, + 'holdable' => ( $action eq "cancel" || $holdable eq 'OK' ) + ? 1 + : 0, + }; + } + + } + $op = "show"; + $template->param( + show => 1, + holds => \@holdloop, + barcodes => \@barcodes, + borrowernumber => $borrowernumber, + borrowersurname => $borrower->surname, + borrowerfirstname => $borrower->firstname, + branch => $branch, + action => $action, + op => $op + ); + } +} + +# Process modifications +if ( $op eq 'cud-do' ) { + + $op = "show_results"; # We have process modifications, the user want to view its + + my $branch = $input->param('branch'); + my $borrowernumber = $input->param('borrowernumber'); + my $action = $input->param('action'); + my @itemnumbers = $input->multi_param('holdable'); + my @holdloop; + foreach my $itemnumber (@itemnumbers) { + my $item = Koha::Items->find( { itemnumber => $itemnumber } ); + my $biblionumber = $item->biblionumber; + my $biblio = Koha::Biblios->find( { biblionumber => $item->biblionumber } ); + + if ( $action eq "place" ) { + my $reserve_id = AddReserve( + { + branchcode => $branch, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + itemnumber => $itemnumber + } + ); + } else { + my $holds = Koha::Holds->search( + { + itemnumber => $itemnumber, + borrowernumber => $borrowernumber + } + ); + if ($holds) { + while ( my $hold = $holds->next ) { + $hold->cancel; + } + } + } + push @holdloop, + { + 'biblionumber' => $biblio->biblionumber, + 'title' => $biblio->title, + 'itemnumber' => $item->itemnumber, + 'barcode' => $item->barcode, + }; + } + my $borrower = Koha::Patrons->find( { borrowernumber => $borrowernumber } ); + $template->param( + result => 1, + holds => \@holdloop, + borrowernumber => $borrowernumber, + borrowersurname => $borrower->surname, + borrowerfirstname => $borrower->firstname, + branch => $branch, + cancel => $action eq "cancel" ? 1 : 0, + place => $action eq "place" ? 1 : 0, + op => $op + ); + + # $template->param( errors => \@errors ); +} + +$template->param( + op => $op, +); +output_html_with_http_headers $input, $cookie, $template->output; +exit; -- 2.39.5