Bugzilla – Attachment 164059 Details for
Bug 36135
Add tool to batch modify holds
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 36135: Add new tool to batch modify holds
Bug-36135-Add-new-tool-to-batch-modify-holds.patch (text/plain), 25.00 KB, created by
Lucas Gass (lukeg)
on 2024-03-28 14:12:58 UTC
(
hide
)
Description:
Bug 36135: Add new tool to batch modify holds
Filename:
MIME Type:
Creator:
Lucas Gass (lukeg)
Created:
2024-03-28 14:12:58 UTC
Size:
25.00 KB
patch
obsolete
>From 42cbbaa195c87c2563209cbe7856df1450e18f83 Mon Sep 17 00:00:00 2001 >From: Emmi Takkinen <emmi.takkinen@koha-suomi.fi> >Date: Fri, 8 Mar 2024 13:46:29 +0200 >Subject: [PATCH] Bug 36135: Add new tool to batch modify holds > >We sometimes need to modify holds due library closing down etc. >For this we need tool to batch modify holds which allows us >to filter holds and modify them. > >Tool allows users to select holds with following parameters: > - expiration date > -> if patron_expiration_date is set, it is used as main search filter > -> otherwise expirationdate is used > - library > - found status > - suspended status > - suspend until date > - hold note > >Users are able to modify following with tool: > - expiration date (not expiration date set by patron) > - pick up library > - suspend status (only if hold has no found status) > - suspend until date (only if suspend status is set) > - hold note > >To test: >1. Apply this patch. >2. Navigate to new tool Tools->Batch modify holds. >3. Fill in some criteria to search holds with. >=> Confirm that holds found match your search criteria. >4. Set e.g. new expiration date for all or just selected holds. >=> Confirm that holds were modified as they should have. >=> Confirm this also from database. > >To test suspending holds with found status: >1. Find holds with found status. >2. Attempt to modify their suspend status. >=> Alert box with text "One or more holds have found status >and can't be suspended." should be displayed. > >Play around, test different search compinations and modifications. > >Sponsored-by: Koha-Suomi Oy >Signed-off-by: Laura Escamilla <laura.escamilla@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> >--- > .../en/modules/tools/batch_modify_holds.tt | 313 ++++++++++++++++++ > tools/batch_modify_holds.pl | 175 ++++++++++ > 2 files changed, 488 insertions(+) > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt > create mode 100755 tools/batch_modify_holds.pl > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt >new file mode 100644 >index 0000000000..7705c36beb >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt >@@ -0,0 +1,313 @@ >+[% USE raw %] >+[% USE Asset %] >+[% SET footerjs = 1 %] >+[% USE Branches %] >+[% USE Categories %] >+[% USE KohaDates %] >+[% USE ItemTypes %] >+[% PROCESS 'html_helpers.inc' %] >+ >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Batch modify holds › Tools › Koha</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% Asset.css("css/humanmsg.css") | $raw %] >+</head> >+ >+<body id="tools_batch_extend_due_dates" class="tools"> >+ [% WRAPPER 'header.inc' %] >+ [% INCLUDE 'cat-search.inc' %] >+[% END %] >+ >+ [% WRAPPER 'sub-header.inc' %] >+ <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb"> >+ <ol> >+ <li> >+ <a href="/cgi-bin/koha/mainpage.pl">Home</a> >+ </li> >+ <li> >+ <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> >+ </li> >+ <li> >+ <a href="#" aria-current="page">Batch modify holds</a> >+ </li> >+ </ol> >+ </nav> >+ [% END %] >+ >+ <div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-sm-10 col-sm-push-2"> >+ <main> >+ >+ <h1>Batch modify holds</h1> >+ >+ [% IF view == 'form' %] >+ <form method="post" action="/cgi-bin/koha/tools/batch_modify_holds.pl" id="modify_holds_form"> >+ <fieldset class="rows"> >+ <legend>Hold search criteria:</legend> >+ <span class="hint">NOTE! Column patron_expiration_date is used as a main filter for expiration date field. Otherwise column expirationdate is used.</span> >+ <ol> >+ <li> >+ <label for="suspend_until">Expiration date:</label> >+ <li> >+ <label for="expirationdate_from">From</label> >+ <input type="text" size="10" id="from" name="expirationdate_from" class="flatpickr" data-date_to="to" /> >+ </li> >+ <li> >+ <label for="expirationdate_to">To</label> >+ <input type="text" size="10" id="to" name="expirationdate_to" class="flatpickr"/> >+ </li> >+ </li> >+ <li> >+ <label for="branchcodes">Libraries:</label> >+ <select name="branchcodes" id="branchcodes" multiple="multiple"> >+ [% PROCESS options_for_libraries libraries => Branches.all() %] >+ </select> >+ </li> >+ <li> >+ <label for="found_status">Found status:</label> >+ <select name="found_status" id="found_status" multiple="multiple"> >+ <option value="NULL">No status</option> >+ <option value="T">In transit</option> >+ <option value="P">In processing</option> >+ <option value="W">Waiting</option> >+ </select> >+ </li> >+ <li> >+ <label for="suspend_status">Suspended:</label> >+ <select name="suspend_status" id="suspend_status"> >+ <option value="none" selected="selected"></option> >+ <option value="0">Not suspended</option> >+ <option value="1">Suspended</option> >+ </select> >+ </li> >+ <li> >+ <label for="suspend_until">Suspended until:</label> >+ <li> >+ <label for="suspend_until_from">From</label> >+ <input type="text" size="10" id="from" name="suspend_until_from" class="flatpickr" data-date_to="to" /> >+ </li> >+ <li> >+ <label for="suspend_until_to">To</label> >+ <input type="text" size="10" id="to" name="suspend_until_to" class="flatpickr"/> >+ </li> >+ </li> >+ <li> >+ <label for="reservenotes">Hold note:</label> >+ <input type="text" id="reservenotes" name="reservenotes"/> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="hidden" name="op" value="list"/> >+ <input type="submit" class="btn btn-primary" value="Continue"/> >+ <a class="cancel" href="/cgi-bin/koha/tools/tools-home.pl">Cancel</a> >+ </fieldset> >+ </form> <!-- /#modify_holds_form --> >+ [% ELSIF view == 'list' %] >+ <form action="/cgi-bin/koha/tools/batch_modify_holds.pl" method="post" id="process"> >+ [% IF holds.count %] >+ <div class="page-section"> >+ <div class="btn-toolbar selections-toolbar"> >+ <a id="selectall" href="#"><i class="fa fa-check"></i> Select all</a> >+ <a id="clearall" href="#"><i class="fa fa-remove"></i> Clear all</a> >+ </div> >+ <table id="found_holds"> >+ <thead> >+ <tr> >+ <th> </th> >+ <th>Expiration date</th> >+ <th>Patron expiration date</th> >+ <th>Status</th> >+ <th>Hold pickup library</th> >+ <th>Suspended</th> >+ <th>Suspended until</th> >+ <th>Note</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH hold IN holds %] >+ <tr> >+ <td><input type="checkbox" name="hold_id" value="[% hold.reserve_id | html %]"/></td> >+ <td>[% hold.expirationdate | $KohaDates %]</td> >+ <td>[% hold.patron_expiration_date | $KohaDates %]</td> >+ <td class="found_status" data-found="[% hold.found | html %]"> >+ [% IF hold.found == "T" %] >+ In transit >+ [% ELSIF hold.found == "P" %] >+ In processing >+ [% ELSIF hold.found == "W" %] >+ Waiting >+ [% ELSE %] >+ No status >+ [% END %] >+ </td> >+ <td>[% Branches.GetName( hold.branchcode ) | html %]</td> >+ <td>[% IF hold.suspend %]Yes[% ELSE %]No[% END %]</td> >+ <td>[% hold.suspend_until | $KohaDates %]</td> >+ <td>[% hold.reservenotes | html %]</td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> <!-- /#holds --> >+ </div> <!-- /.page-section --> >+ <h2>Modify holds</h2> >+ <table id="hold_modifies"> >+ <thead> >+ <tr> >+ <th>New expiration date</th> >+ <th>New pickup library</th> >+ <th>Suspend holds</th> >+ <th>Suspend until</th> >+ <th>New reserve note</th> >+ </tr> >+ </thead> >+ <tbody> >+ <tr> >+ <td> >+ <input type="text" id="new_expiration_date" name="new_expiration_date" class="flatpickr" data-flatpickr-futuredate="true"/> >+ </td> >+ <td> >+ <select name="new_pickup_loc" id="new_pickup_loc"> >+ [% PROCESS options_for_libraries libraries => Branches.pickup_locations({ selected => hold.branchcode }) %] >+ </select> >+ </td> >+ <td> >+ <select id="new_suspend_status" name="new_suspend_status"> >+ <option value="" selected="selected"></option> >+ <option value="not_suspended">Not suspended</option> >+ <option value="suspend">Suspend</option> >+ </select> >+ </td> >+ <td> >+ <input type="text" id="new_suspend_date" name="new_suspend_date" class="flatpickr" data-flatpickr-futuredate="true"/> >+ </td> >+ <td> >+ <input type="text" id="new_reserve_note" name="new_reserve_note"/> >+ </td> >+ </tr> >+ </tbody> >+ </table> <!-- /#hold_modifies --> >+ <fieldset class="action"> >+ <input type="hidden" name="op" value="modify"/> >+ <input type="submit" class="btn btn-primary" value="Modify selected holds"/> >+ <a class="cancel" href="/cgi-bin/koha/tools/batch_modify_holds.pl">Cancel</a> >+ </fieldset> >+ </form> <!-- /#process --> >+ [% ELSE %] >+ <div class="dialog message"> >+ No holds were found for the selected filters. >+ </div> >+ <a class="cancel" href="/cgi-bin/koha/tools/batch_modify_holds.pl">Return</a> >+ [% END %] >+ [% ELSIF view == 'report' %] >+ <div class="dialog message"> >+ Holds have been modified! >+ </div> >+ <div class="page-section"> >+ <div class="btn-toolbar selections-toolbar"> >+ <a id="selectall" href="#"><i class="fa fa-check"></i> Select all</a> >+ <a id="clearall" href="#"><i class="fa fa-remove"></i> Clear all</a> >+ </div> >+ <table id="holds"> >+ <thead> >+ <tr> >+ <th>Expiration date</th> >+ <th>Hold pickup library</th> >+ <th>Suspended</th> >+ <th>Suspended until</th> >+ <th>Note</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH hold IN updated_holds %] >+ <tr> >+ <td>[% hold.expirationdate | $KohaDates %]</td> >+ <td>[% Branches.GetName( hold.branchcode ) | html %]</td> >+ <td>[% IF hold.suspend == 0 %]No[% ELSE %]Yes[% END %]</td> >+ <td>[% hold.suspend_until | $KohaDates %]</td> >+ <td>[% hold.reservenotes | html %]</td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> <!-- /#holds --> >+ </div> <!-- /.page-section --> >+ <a class="cancel" href="/cgi-bin/koha/tools/batch_modify_holds.pl">Return to batch hold modification</a> >+ [% END %] >+ </main> >+ </div> <!-- /.col-sm-10 col-sm-push-2 --> >+ >+ >+ <div class="col-sm-2 col-sm-pull-10"> >+ <aside> >+ [% INCLUDE 'tools-menu.inc' %] >+ </aside> >+ </div> <!-- /.col-sm-2 col-sm-pull-10 --> >+ </div> <!-- /.row --> >+ >+[% MACRO jsinclude BLOCK %] >+ [% Asset.js("js/tools-menu.js") | $raw %] >+ [% INCLUDE 'calendar.inc' %] >+ [% INCLUDE 'datatables.inc' %] >+ [% Asset.js("lib/jquery/plugins/humanmsg.js") | $raw %] >+ <script> >+ $(document).ready(function() { >+ >+ $("#selectall").click(function(e){ >+ e.preventDefault(); >+ $("#found_holds input[type='checkbox']").each(function(){ >+ $(this).prop("checked", true); >+ }); >+ }); >+ >+ $("#clearall").click(function(e){ >+ e.preventDefault(); >+ $("#found_holds input[type='checkbox']").each(function(){ >+ $(this).prop("checked", false); >+ }); >+ }); >+ >+ $("#selectall").click(); >+ >+ $("#found_holds").dataTable($.extend(true, {}, dataTablesDefaults, { >+ "aoColumnDefs": [ >+ { "aTargets": [0], "bSortable": false, "bSearchable": false }, >+ ], >+ "sDom": 't', >+ "aaSorting": [], >+ "bPaginate": false >+ })); >+ >+ $("#process").on('submit', function(e) { >+ var reserve_ids = $("input[type=checkbox][name='hold_id']:checked"); >+ >+ var new_suspend_status = $("#new_suspend_status").val(); >+ var new_suspend_date = $("#new_suspend_date").val(); >+ >+ if ( reserve_ids.length == 0 ) { >+ e.preventDefault(); >+ alert(_("Please select at least one hold to process.")); >+ return false; >+ } >+ if( new_suspend_status ){ >+ reserve_ids.each(function(){ >+ if($(this).parents("tr").children(".found_status").data("found") != ""){ >+ e.preventDefault(); >+ alert(_("One or more holds have found status and can't be suspended.")); >+ return false; >+ } >+ }) >+ } >+ if( ( !new_suspend_status || new_suspend_status == "not_suspended") && new_suspend_date ){ >+ e.preventDefault(); >+ alert(_('You have to suspend holds if new suspend until date is set.')); >+ return false; >+ } >+ >+ return true; >+ }); >+ }); >+ </script> >+[% END %] >+ >+[% INCLUDE 'intranet-bottom.inc' %] >\ No newline at end of file >diff --git a/tools/batch_modify_holds.pl b/tools/batch_modify_holds.pl >new file mode 100755 >index 0000000000..ddc08e3adc >--- /dev/null >+++ b/tools/batch_modify_holds.pl >@@ -0,0 +1,175 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Copyright 2024 Koha Development Team >+# >+# 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 CGI; >+ >+use C4::Auth qw( get_template_and_user ); >+use C4::Output qw( output_html_with_http_headers ); >+ >+use Koha::DateUtils qw( dt_from_string ); >+use Koha::Holds; >+ >+my $input = CGI->new; >+my $op = $input->param('op') // q|form|; >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => 'tools/batch_modify_holds.tt', >+ query => $input, >+ type => "intranet", >+ flagsrequired => { tools => 'batch_modify_holds.tt' }, >+ } >+); >+ >+my @hold_ids; >+ >+if ( $op eq 'form' ) { >+ $template->param( view => 'form', ); >+} >+elsif ( $op eq 'list' ) { >+ >+ my @reserve_ids = $input->multi_param('reserve_ids'); >+ my $expirationdate_from = $input->param('expirationdate_from'); >+ my $expirationdate_to = $input->param('expirationdate_to'); >+ my @branchcodes = $input->multi_param('branchcodes'); >+ my @found_status = $input->multi_param('found_status'); >+ my $suspend_status = $input->param('suspend_status'); >+ my $suspend_until_from = $input->param('suspend_until_from'); >+ my $suspend_until_to = $input->param('suspend_until_to'); >+ my $reservenotes = $input->param('reservenotes'); >+ >+ my $search_params; >+ >+ if( @reserve_ids ){ >+ $search_params->{'me.reserve_id'} = { -in => \@reserve_ids }; >+ } else { >+ if( $expirationdate_from && $expirationdate_to ){ >+ my $expirationdate_params = { -or => { >+ patron_expiration_date => { >+ -between => [ >+ $expirationdate_from, >+ $expirationdate_to >+ ] >+ }, >+ expirationdate => { >+ -between => [ >+ $expirationdate_from, >+ $expirationdate_to >+ ] >+ } >+ }}; >+ >+ $search_params = $expirationdate_params; >+ } >+ >+ if ( @branchcodes ) { >+ $search_params->{'me.branchcode'} = { -in => \@branchcodes }; >+ } >+ >+ if ( @found_status ) { >+ my $found_params = {}; >+ # if NULL is used as filter, use it as "is" param >+ if( grep {$_ eq "NULL"} @found_status ) { >+ $found_params->{'-or'}{'-is'} = undef; >+ $found_params->{'-or'}{'-in'} = \@found_status; >+ } else { >+ $found_params = { -in => \@found_status }; >+ } >+ >+ $search_params->{'me.found'} = $found_params; >+ } >+ >+ if( $suspend_status ne "none" ){ >+ $search_params->{'me.suspend'} = $suspend_status; >+ } >+ >+ if( $suspend_until_from && $suspend_until_to ){ >+ $search_params->{'me.suspend_until'} = { >+ -between => [ >+ $suspend_until_from, >+ $suspend_until_to, >+ ] >+ }; >+ } >+ >+ if( $reservenotes ){ >+ $search_params->{'me.reservenotes'} = { -like => "%".$reservenotes."%" } >+ } >+ } >+ >+ my $holds = Koha::Holds->search( $search_params ); >+ >+ $template->param( >+ holds => $holds, >+ view => 'list', >+ ); >+} >+ >+if ( $op eq 'modify' ) { >+ >+ my $new_expiration_date = $input->param('new_expiration_date'); >+ my $new_pickup_loc = $input->param('new_pickup_loc'); >+ my $new_suspend_status = $input->param('new_suspend_status'); >+ my $new_suspend_date = $input->param('new_suspend_date'); >+ my $new_reserve_note = $input->param('new_reserve_note'); >+ >+ @hold_ids = $input->multi_param('hold_id'); >+ >+ my $holds_to_update = Koha::Holds->search( { reserve_id => { -in => \@hold_ids } } ); >+ >+ while ( my $hold = $holds_to_update->next ) { >+ >+ if( $new_expiration_date ){ >+ $hold->expirationdate($new_expiration_date)->store; >+ } >+ >+ unless( $hold->branchcode eq $new_pickup_loc){ >+ $hold->branchcode($new_pickup_loc)->store; >+ } >+ >+ if( $new_suspend_status && !$hold->is_found ){ >+ if( $new_suspend_status eq "suspend" ){ >+ $hold->suspend(1)->store; >+ if( $new_suspend_date ){ >+ $hold->suspend_until( $new_suspend_date )->store; >+ } else { >+ $hold->suspend_until( undef )->store; >+ } >+ } else { >+ $hold->suspend(0)->store; >+ $hold->suspend_until( undef )->store; >+ } >+ } >+ >+ if( $new_reserve_note ){ >+ $hold->reservenotes($new_reserve_note)->store; >+ } >+ } >+ >+ $template->param( >+ updated_holds => $holds_to_update, >+ view => 'report', >+ ); >+ >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >-- >2.30.2
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 36135
:
163021
|
163022
|
163023
|
163024
|
163529
|
163530
|
163540
|
163541
|
163542
|
163543
|
163544
|
163796
|
164037
|
164051
|
164057
|
164058
| 164059 |
164060
|
164061
|
164062
|
164063