Bug 24846: Add new tool to batch extend due dates

With events sometimes leading to unforeseen branch closures (think Coronavirus as
an example), it would be helpful to have a tool that would allow librarians to
update due dates in bulk based on branch and current due date of the material.

It allows to select checkouts given the following parameters:
 * libraries
 * patron's categories
 * range of the due date

You can set a hard due date, or define a number of days to extend the
due date.

Test plan:
Check some items out
Use the new tool to extend the due dates
Test the different filters to make sure they all work

Note: What about holidays?

Signed-off-by: Bernardo Gonzalez Kriegel <bgkriegel@gmail.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
This commit is contained in:
Jonathan Druart 2020-03-16 11:45:44 +01:00 committed by Martin Renvoize
parent 18a56d0146
commit c7084c4c27
Signed by: martin.renvoize
GPG key ID: 422B469130441A0F
2 changed files with 414 additions and 0 deletions

View file

@ -0,0 +1,269 @@
[% 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>Koha &rsaquo; Tools &rsaquo; Batch extend due dates</title>
[% INCLUDE 'doc-head-close.inc' %]
[% Asset.css("css/humanmsg.css") | $raw %]
</head>
<body id="tools_batch_extend_due_dates" class="tools">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs">
<a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
<a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> &rsaquo;
<a href="/cgi-bin/koha/tools/batch_extend_due_dates.pl">Batch extend due dates</a>
</div>
<div class="main container-fluid">
<div class="row">
<div class="col-sm-10 col-sm-push-2">
<main>
<h1>Batch extend due dates</h1>
[% IF ( messages ) %]
<div class="dialog message">
[% FOREACH message IN messages %]
[% IF message.type == 'success' %]
<div><i class="fa fa-check success"></i>
[% ELSIF message.type == 'warning' %]
<div><i class="fa fa-warning warn"></i>
[% ELSIF message.type == 'error' %]
<div><i class="fa fa-exclamation error"></i>
[% END %]
[% IF message.error %]
(The error was: [% message.error | html %]. See the Koha logfile for more information).
[% END %]
</div>
[% END %]
</div> <!-- .dialog.message -->
[% END %]
[% IF view == 'form' %]
<form method="post" enctype="multipart/form-data" action="/cgi-bin/koha/tools/batch_extend_due_dates.pl" id="extend_due_dates_form">
<fieldset class="rows">
<legend>Checkout criteria:</legend>
<ol>
<li>
<label for="categorycodes">Patrons' categories: </label>
[% SET categories = Categories.all() %]
<select id="categorycodes" name="categorycodes" multiple="multiple">
[% FOREACH cat IN categories %]
<option value="[% cat.categorycode | html %]">[% cat.description | html %]</option>
[% END %]
</select>
</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="from_due_date">Due date from: </label>
<input type="text" size="10" id="from" name="from_due_date" class="datepickerfrom" />
</li>
<li>
<label for="to_due_date">Due date to:</label>
<input type="text" size="10" id="to" name="to_due_date" class="datepickerto" />
</li>
</ol>
</fieldset>
<fieldset class="rows">
<legend>New due date:</legend>
<ol>
<li>
<label for="new_hard_due_date">Hard due date: </label>
<input type="text" size="10" id="new_hard_due_date" name="new_hard_due_date" class="datepicker" />
</li>
<li>
<label for="due_date_days">Or add number of days:</label>
<input type="text" size="10" id="due_date_days" name="due_date_days"/>
</li>
</ol>
</fieldset>
<fieldset class="action">
<input type="hidden" name="op" value="list" />
<input type="submit" value="Continue" class="button" />
<a class="cancel" href="/cgi-bin/koha/tools/tools-home.pl">Cancel</a>
</fieldset>
</form> <!-- /#extend_due_dates_form -->
[% ELSIF view == 'list' %]
[% IF checkouts.count %]
<form action="/cgi-bin/koha/tools/batch_extend_due_dates.pl" method="post" id="process">
<div id="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="checkouts">
<thead>
<tr>
<th>&nbsp;</th>
<th>Due date</th>
<th>Title</th>
<th>Item type</th>
<th>Home library</th>
<th>Checked out on</th>
<th>Checked out from</th>
<th>New due date</th>
</tr>
</thead>
<tbody>
[% FOR checkout IN checkouts %]
<tr>
<td><input type="checkbox" name="issue_id" value="[% checkout.issue_id | html %]" /></td>
<td>[% checkout.date_due | $KohaDates as_due_date => 1 %]</td>
<td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% checkout.item.biblio.biblionumber | uri %]">[% checkout.item.biblio.title | html %]</a></td>
<td>[% ItemTypes.GetDescription( checkout.item.effective_itemtype ) | html %]</td>
<td>[% checkout.item.home_branch.branchname | html %]</td>
<td>[% checkout.issuedate | $KohaDates %]</td>
<td>[% Branches.GetName( checkout.branchcode ) | html %]</td>
<td>
[% IF new_hard_due_date %]
[% new_hard_due_date | $KohaDates %]
[% ELSE %]
[% new_due_dates.shift | $KohaDates %]
[% END %]
</td>
</tr>
[% END %]
</tbody>
</table> <!-- /#checkouts -->
<div class="note"><i class="fa fa-exclamation"></i> Reminder: this action will modify all selected checkouts!</div>
<fieldset class="action">
<input type="hidden" name="op" value="modify" />
<input type="hidden" name="new_hard_due_date" value="[% new_hard_due_date | $KohaDates %]" />
<input type="hidden" name="due_date_days" value="[% due_date_days | html %]" />
<input type="submit" value="Modify selected checkouts" class="button" />
<a class="cancel" href="/cgi-bin/koha/tools/batch_extend_due_dates.pl">Cancel</a>
</fieldset>
</form> <!-- /#process -->
[% ELSE %]
<div class="dialog message">
No checkouts for the selected filters.
</div>
[% END %]
[% ELSIF view == 'report' %]
<div class="dialog message">
Due dates have been modified!
</div>
<table id="checkouts_result">
<thead>
<tr>
<th>Due date</th>
<th>Title</th>
<th>Item type</th>
<th>Home library</th>
<th>Checked out on</th>
<th>Checked out from</th>
</tr>
</thead>
<tbody>
[% FOR checkout IN checkouts %]
<tr>
<td>[% checkout.date_due | $KohaDates as_due_date => 1 %]</td>
<td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% checkout.item.biblio.biblionumber | uri %]">[% checkout.item.biblio.title | html %]</a></td>
<td>[% ItemTypes.GetDescription( checkout.item.effective_itemtype ) | html %]</td>
<td>[% checkout.item.home_branch.branchname | html %]</td>
<td>[% checkout.issuedate | $KohaDates %]</td>
<td>[% Branches.GetName( checkout.branchcode ) | html %]</td>
</tr>
[% END %]
</tbody>
</table> <!-- /#checkouts_result -->
[% 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/jquery.checkboxes.min.js") | $raw %]
[% Asset.js("lib/jquery/plugins/humanmsg.js") | $raw %]
<script>
$(document).ready(function() {
$("#selectall").click(function(e) {
e.preventDefault();
$("#checkouts").checkCheckboxes();
});
$("#clearall").click(function(e) {
e.preventDefault();
$("#checkouts").unCheckCheckboxes();
});
$("#selectall").click();
$("table#checkouts").dataTable($.extend(true, {}, dataTablesDefaults, {
"aoColumnDefs": [
{ "aTargets": [0, 3], "bSortable": false, "bSearchable": false },
{ "aTargets": [1], "sType": "num-html" }
],
"sDom": 't',
"aaSorting": [],
"bPaginate": false
}));
$("table#checkouts_result").dataTable($.extend(true, {}, dataTablesDefaults, {
"aoColumnDefs": [
{ "aTargets": [0, 3], "bSortable": false, "bSearchable": false },
{ "aTargets": [1], "sType": "num-html" }
],
"sDom": 't',
"aaSorting": [],
"bPaginate": false
}));
$("#extend_due_dates_form").on('submit', function(e) {
var new_hard_due_date = $("#new_hard_due_date").val();
var due_date_days = $("#due_date_days").val();
if (new_hard_due_date && due_date_days ) {
e.preventDefault();
alert(_("You must fill only one of the two due date options"));
return false;
} else if ( !new_hard_due_date && !due_date_days ) {
e.preventDefault();
alert(_("You must fill at least one of the two due date options"));
return false;
}
return true;
});
$("#process").on('submit', function(e) {
if ($("input[type=checkbox][name='issue_id']:checked").length == 0) {
e.preventDefault();
alert(_("Please select at least one checkout to process"));
return false;
}
return true;
});
});
</script>
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]

145
tools/batch_extend_due_dates.pl Executable file
View file

@ -0,0 +1,145 @@
#!/usr/bin/perl
# This file is part of Koha.
#
# Copyright 2020 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::Checkouts;
use Koha::DateUtils qw( dt_from_string );
my $input = new CGI;
my $op = $input->param('op') // q|form|;
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => 'tools/batch_extend_due_dates.tt',
query => $input,
type => "intranet",
authnotrequired => 0,
flagsrequired => { tools => 'batch_extend_due_dates' },
}
);
if ( $op eq 'form' ) {
$template->param( view => 'form', );
}
elsif ( $op eq 'list' ) {
my @categorycodes = $input->multi_param('categorycodes');
my @branchcodes = $input->multi_param('branchcodes');
my $from_due_date = $input->param('from_due_date');
my $to_due_date = $input->param('to_due_date');
my $new_hard_due_date = $input->param('new_hard_due_date');
my $due_date_days = $input->param('due_date_days');
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
my $search_params;
if (@categorycodes) {
$search_params->{'borrower.categorycode'} = { -in => \@categorycodes };
}
if (@branchcodes) {
$search_params->{'me.branchcode'} = { -in => \@branchcodes };
}
if ( $from_due_date and $to_due_date ) {
my $to_due_date_endday = dt_from_string($to_due_date);
$to_due_date_endday
->set( # We set last second of day to see all checkouts from that day
hour => 23,
minute => 59,
second => 59
);
$search_params->{'me.date_due'} = {
-between => [
$dtf->format_datetime( dt_from_string($from_due_date) ),
$dtf->format_datetime($to_due_date_endday),
]
};
}
elsif ($from_due_date) {
$search_params->{'me.date_due'} =
{ '>=' => $dtf->format_datetime( dt_from_string($from_due_date) ) };
}
elsif ($to_due_date) {
my $to_due_date_endday = dt_from_string($to_due_date);
$to_due_date_endday
->set( # We set last second of day to see all checkouts from that day
hour => 23,
minute => 59,
second => 59
);
$search_params->{'me.date_due'} =
{ '<=' => $dtf->format_datetime($to_due_date_endday) };
}
my $checkouts = Koha::Checkouts->search(
$search_params,
{
join => [ 'item', 'borrower' ]
}
);
my @new_due_dates;
if ( not $new_hard_due_date && $due_date_days ) {
while ( my $checkout = $checkouts->next ) {
my $due_date = dt_from_string( $checkout->date_due );
push @new_due_dates, $due_date->add( days => $due_date_days );
}
}
$template->param(
checkouts => $checkouts,
new_hard_due_date => $new_hard_due_date
? dt_from_string($new_hard_due_date)
: undef,
due_date_days => $due_date_days,
new_due_dates => \@new_due_dates,
view => 'list',
);
}
elsif ( $op eq 'modify' ) {
# We want to modify selected checkouts!
my @issue_ids = $input->multi_param('issue_id');
my $new_hard_due_date = $input->param('new_hard_due_date');
my $due_date_days = $input->param('due_date_days');
$new_hard_due_date &&= dt_from_string($new_hard_due_date);
my $checkouts =
Koha::Checkouts->search( { issue_id => { -in => \@issue_ids } } );
while ( my $checkout = $checkouts->next ) {
if ($new_hard_due_date) {
$checkout->date_due($new_hard_due_date)->store;
}
else {
my $dt = dt_from_string( $checkout->date_due )
->add( days => $due_date_days );
$checkout->date_due($dt)->store;
}
}
$template->param(
view => 'report',
checkouts => $checkouts,
);
}
output_html_with_http_headers $input, $cookie, $template->output;