Bugzilla – Attachment 180231 Details for
Bug 23258
Batch tool for placing and cancelling holds
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 23258: Batch place or cancel holds
Bug-23258-Batch-place-or-cancel-holds.patch (text/plain), 21.60 KB, created by
PTFS Europe Sandboxes
on 2025-04-01 22:20:15 UTC
(
hide
)
Description:
Bug 23258: Batch place or cancel holds
Filename:
MIME Type:
Creator:
PTFS Europe Sandboxes
Created:
2025-04-01 22:20:15 UTC
Size:
21.60 KB
patch
obsolete
>From 152c7d076b45ff60237da19bbe0990390fcf8cfb Mon Sep 17 00:00:00 2001 >From: Matthias Meusburger <matthias.meusburger@biblibre.com> >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 > >Signed-off-by: Sam Sowanick <sam.sowanick@corvallisoregon.gov> >--- > .../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 0000000000..bc0d536b00 >--- /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' %] >+<title >+ >[% 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 %]</title >+> >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+ >+<body id="tools_modholds" class="tools"> >+[% WRAPPER 'header.inc' %] >+ [% INCLUDE 'cat-search.inc' %] >+[% END %] >+ >+[% WRAPPER 'sub-header.inc' %] >+ [% WRAPPER breadcrumbs %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> >+ [% END %] >+ [% IF ( op == 'show' or op == 'show_results' ) %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/modholds.pl">Batch holds modification</a> >+ [% 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 %] >+ <span>Batch holds modification</span> >+ [% END %] >+ [% END %] >+ [% END #/ WRAPPER breadcrumbs %] >+[% END #/ WRAPPER sub-header.inc %] >+ >+[% WRAPPER 'main-container.inc' aside='tools-menu' %] >+ [% IF ( op == 'show_form' ) %] >+ <h1>Batch holds modification</h1> >+ <form id="holds_batchmod_form" method="post" enctype="multipart/form-data" action="/cgi-bin/koha/tools/modholds.pl"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="op" value="cud-show" /> >+ <fieldset class="rows"> >+ <legend>Use a file of barcodes</legend> >+ <ol> >+ <li> >+ <label for="barcodesuploadfile">File: </label> <input type="file" id="barcodesuploadfile" name="barcodesuploadfile" /> >+ <div class="hint">File must contain one barcode per line.</div> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend>Or list barcodes one by one</legend> >+ <ol> >+ <li> >+ <label for="barcodeslist">Barcodes list (one barcode per line): </label> >+ <textarea rows="10" cols="30" id="barcodeslist" name="barcodeslist">[% barcodeslist | html %]</textarea> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend>Action</legend> >+ <ol> >+ <li> >+ <input type="radio" name="action" id="action_place_holds" value="place" checked="checked" /> >+ <label for="action_place_holds">Place holds</label> >+ </li> >+ <li> >+ <input type="radio" name="action" id="action_cancel_holds" value="cancel" /> >+ <label for="action_cancel_holds">Cancel holds</label> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend id="action_type_legend">Place holds for</legend> >+ <ol> >+ <li id="patron_search_line"> >+ <label for="find_patron">Patron search: </label> >+ <input autocomplete="off" id="find_patron" type="text" style="width:150px" class="noEnterSubmit required" required="required" /> >+ <input type="hidden" name="borrowernumber" id="borrowernumber" /> >+ </li> >+ <li> >+ <label for="branch">Pickup at:</label> >+ <select name="branch" id="branch"> >+ [% PROCESS options_for_libraries libraries => Branches.all( unfiltered => 1 ) %] >+ </select> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" class="btn btn-primary" value="Continue" /> >+ <a class="cancel" href="/cgi-bin/koha/tools/modholds.pl">Cancel</a> >+ </fieldset> >+ </form> >+ [% END %] >+ >+ [% IF ( op == 'show' || op == 'cud-show' ) && ( !barcodes || !borrowernumber ) # Alert if no barcodes given %] >+ [% op = 'noshow' # Change op to prevent display in code below %] >+ <h1>Batch holds modification</h1> >+ <div class="alert alert-warning"> >+ [% IF (!barcodes) %]<p>No barcodes given.</p>[% END %] >+ [% IF (!borrowernumber) %]<p>No patron given.</p>[% END %] >+ <form action="/cgi-bin/koha/tools/modholds.pl" method="get"> >+ <button type="submit" class="btn btn-default approve"><i class="fa fa-fw fa-check"></i> OK</button> >+ </form> >+ </div> >+ [% END #Alert if no barcodes %] >+ >+ [% BLOCK show_holds_list %] >+ [% IF holds %] >+ <div id="cataloguing_additem_itemlist" class="page-section"> >+ <table id="holdst"> >+ <thead> >+ <tr> >+ [% IF op == 'show' %]<th>Hold</th>[% END %] >+ <th>Title</th> >+ <th>Item</th> >+ <th>Current holds</th> >+ [% IF op == 'show' %]<th>Comment</th>[% END %] >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH hold IN holds %] >+ <tr> >+ [% IF op == 'show' %]<td><input type="checkbox" name="holdable" [% IF !hold.holdable %]disabled="disabled"[% ELSE %]checked="checked"[% END %] value="[% hold.itemnumber | html %]" /></td>[% END %] >+ <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% hold.biblionumber | uri %]">[% hold.title | html %]</a></td> >+ <td><a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% hold.biblionumber | uri %]&itemnumber=[% hold.itemnumber | uri %]">[% hold.barcode | html %]</a></td> >+ <td><a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% hold.biblionumber | uri %]">Show holds</a></td> >+ [% IF op == 'show' %]<td>[% IF !hold.holdable %]<div class="alert alert-warning">This item cannot be placed on hold</div>[% END %]</td>[% END %] >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ </div> >+ [% END %] >+ [% END %] >+ >+ [% IF ( op == 'show' or op == 'show_results' ) %] >+ <h1>Batch holds modification</h1> >+ [% UNLESS ( op == 'show' ) %] >+ <h2>Results</h2> >+ [% END %] >+ [% IF ( notfoundbarcodes ) %] >+ <div class="alert alert-warning"><p>Warning, the following barcodes were not found:</p></div> >+ >+ <div class="page-section"> >+ <table style="margin:auto;"> >+ <thead> >+ <tr><th>Barcodes not found</th></tr> >+ </thead> >+ <tbody> >+ [% FOREACH notfoundbarcode IN notfoundbarcodes %] >+ <tr><td>[% notfoundbarcode.barcode | html %]</td></tr> >+ [% END %] >+ </tbody> >+ </table> >+ </div> >+ <!-- /.page-section --> >+ [% END %] >+ >+ [% IF ( op == 'show_results' ) %] >+ <p> >+ [% IF place %] >+ The following items have been placed on hold >+ [% END %] >+ [% IF cancel %] >+ The following holds were canceled >+ [% END %] >+ for patron: <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% borrowernumber | uri %]">[% borrowersurname | html %][% IF ( borrowerfirstname ) %], [% borrowerfirstname | html %][% END %]</a></p >+ > >+ [% IF place %] >+ <p>Pickup location: [% Branches.GetName( branch ) | html %]</p> >+ [% END %] >+ [% PROCESS show_holds_list %] >+ [% END %] >+ >+ [% IF ( op == 'show' ) %] >+ <form name="f" action="modholds.pl" method="post" id="batchmod-borrowers-form"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="op" value="cud-do" /> >+ [% IF ( holds ) %] >+ <div class="btn-toolbar selections-toolbar"> >+ <a id="selectallbutton" href="#"><i class="fa fa-check"></i> Select all</a> | <a id="clearallbutton" href="#"><i class="fa fa-times"></i> Clear all</a> >+ </div> >+ [% PROCESS show_holds_list %] >+ <input type="hidden" name="op" value="cud-do" /> >+ <input type="hidden" name="action" value="[% action | html %]" /> >+ <input type="hidden" name="branch" value="[% branch | html %]" /> >+ <input type="hidden" name="borrowernumber" value="[% borrowernumber | html %]" /> >+ <fieldset class="action"> >+ <input type="submit" class="submit" value="Submit" /> >+ <a href="/cgi-bin/koha/tool/modholds.pl" class="cancel">Cancel</a> >+ </fieldset> >+ [% END %] >+ </form> >+ [% END %] >+ [% END %] >+ [% IF ( op == 'show_results' ) %] >+ <p> >+ <a href="/cgi-bin/koha/tools/modholds.pl" title="New batch holds modification">New batch holds modification</a> >+ </p> >+ [% 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 %] >+ <script> >+ $(document).ready(function() { >+ >+ [% IF ( op == 'show_form' ) %] >+ patron_autocomplete($("#find_patron"), { >+ 'on-select-callback': function( event, ui ) { >+ var field = ui.item.patron_id; >+ $("#find_patron").val(ui.item.firstname + " " + ( ui.item.middle_name || "" ) + " " + ui.item.surname).focus(); >+ $("#borrowernumber").val(ui.item.patron_id); >+ return false; >+ } >+ }); >+ >+ $("input[name='action']").change(function(){ >+ selected_value = $("input[name='action']:checked").val(); >+ if (selected_value == "place") { >+ $("#action_type_legend").text(__("Place holds for")); >+ $("#branch").prop("disabled", false); >+ } else { >+ $("#action_type_legend").text(__("Cancel holds for")); >+ $("#branch").prop("disabled", true); >+ } >+ }); >+ [% END %] >+ >+ [% IF holds %] >+ $("#selectallbutton").click(function() { >+ $("#holdst").find("input:checkbox").each(function() { >+ $(this).prop("checked", true); >+ }); >+ return false; >+ }); >+ $("#clearallbutton").click(function() { >+ $("#holdst").find("input:checkbox").each(function() { >+ $(this).prop("checked", false); >+ }); >+ return false; >+ }); >+ [% END %] >+ }); >+ </script> >+[% 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 bf1d14de86..74c97be2cf 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 @@ > <dd>Create printable patron cards</dd> > [% END %] > >+ [% IF ( CAN_user_reserveforothers ) %] >+ <dt><a href="/cgi-bin/koha/tools/modholds.pl">Batch place or cancel holds</a></dt> >+ <dd>Batch place holds or cancel holds</dd> >+ [% END %] >+ > [% IF ( CAN_user_tools_delete_anonymize_patrons ) %] > <dt><a href="/cgi-bin/koha/tools/cleanborrowers.pl">Batch patron deletion and anonymization</a></dt> > <dd>Batch delete patrons and delete patron circulation history</dd> >diff --git a/tools/modholds.pl b/tools/modholds.pl >new file mode 100755 >index 0000000000..9838ab990b >--- /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 <http://www.gnu.org/licenses>. >+ >+# 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
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 23258
:
91230
|
106055
|
106077
|
106091
|
106598
|
107408
|
108498
|
108504
|
108628
|
108630
|
180199
| 180231