Bug 24846: Add new tool to batch extend due dates
authorJonathan Druart <jonathan.druart@bugs.koha-community.org>
Mon, 16 Mar 2020 10:45:44 +0000 (11:45 +0100)
committerMartin Renvoize <martin.renvoize@ptfs-europe.com>
Thu, 19 Mar 2020 08:56:38 +0000 (08:56 +0000)
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>

koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt [new file with mode: 0644]
tools/batch_extend_due_dates.pl [new file with mode: 0755]

diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt
new file mode 100644 (file)
index 0000000..1286337
--- /dev/null
@@ -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' %]
diff --git a/tools/batch_extend_due_dates.pl b/tools/batch_extend_due_dates.pl
new file mode 100755 (executable)
index 0000000..5f0001d
--- /dev/null
@@ -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;