From 9ac0c248c9da24e5866d5f87dd7fd25a13b90e07 Mon Sep 17 00:00:00 2001
From: Nick Clemens <nick@bywatersolutions.com>
Date: Thu, 14 Jul 2016 06:46:15 -0400
Subject: [PATCH] Bug 16187 - Add a script to cancel unfilled holds after a
 specified number of days

This script takes parameters:
days - how many days waiting to concal an unfilled hold on or after
library - (repeatable) branches to consider
holidays - whether or not to count holidays (default is no)

This patchset adds two methods and covers them with tests:
Koha::Holds->unfilled(); To return holds where found = undef
Koha::Hold->age( $use_calendar ); To return the number of days since a
hold was placed (including or excluding holidays)

To test:
1 - Place some holds with varying reservedates
2 - Run script with different parameters to verify options are respected
(-v for verbosity will assist here)
3 - verify that script does nothing without days parameter

Sponsored by:
    Siskiyou County Library (http://www.siskiyoulibrary.info/)
---
 Koha/Hold.pm                                 |  29 +++++
 Koha/Holds.pm                                |  16 ++-
 misc/cronjobs/holds/cancel_unfilled_holds.pl | 151 +++++++++++++++++++++++++++
 t/db_dependent/Hold.t                        |  10 +-
 t/db_dependent/Holds.t                       |   3 +-
 5 files changed, 205 insertions(+), 4 deletions(-)
 create mode 100755 misc/cronjobs/holds/cancel_unfilled_holds.pl

diff --git a/Koha/Hold.pm b/Koha/Hold.pm
index d708054..4b3cf35 100644
--- a/Koha/Hold.pm
+++ b/Koha/Hold.pm
@@ -30,6 +30,7 @@ use Koha::Patrons;
 use Koha::Biblios;
 use Koha::Items;
 use Koha::Libraries;
+use Koha::Calendar;
 
 use base qw(Koha::Object);
 
@@ -43,6 +44,34 @@ Koha::Hold - Koha Hold object class
 
 =cut
 
+=head3 age
+
+returns the number of days since a hold was placed, optionally
+using the calendar
+
+my $age = $hold->age( $use_calendar );
+
+=cut
+
+sub age {
+    my ( $self, $use_calendar ) = @_;
+
+    my $today = DateTime->now(time_zone => C4::Context->tz );
+    my $age;
+
+    if ( $use_calendar ) {
+        my $calendar = Koha::Calendar->new( branchcode => $self->branchcode );
+        $age = $calendar->days_between( dt_from_string( $self->reservedate ), $today );
+    }
+    else {
+        $age = $today->delta_days( dt_from_string( $self->reservedate ) );
+    }
+
+    $age = $age->in_units( 'days' );
+
+    return $age;
+}
+
 =head3 suspend_hold
 
 my $hold = $hold->suspend_hold( $suspend_until_dt );
diff --git a/Koha/Holds.pm b/Koha/Holds.pm
index 038e31c..ca664c3 100644
--- a/Koha/Holds.pm
+++ b/Koha/Holds.pm
@@ -39,14 +39,26 @@ Koha::Holds - Koha Hold object set class
 
 =head3 waiting
 
-Returns a set of holds that are waiting from an existing set
+returns a set of holds that are waiting from an existing set
 
 =cut
 
 sub waiting {
     my ( $self ) = @_;
 
-    return $self->search( { found => 'W' } );
+    return $self->search( { found => 'w' } );
+}
+
+=head3 unfilled
+
+returns a set of holds that are unfilled from an existing set
+
+=cut
+
+sub unfilled {
+    my ( $self ) = @_;
+
+    return $self->search( { found => undef } );
 }
 
 =head3 forced_hold_level
diff --git a/misc/cronjobs/holds/cancel_unfilled_holds.pl b/misc/cronjobs/holds/cancel_unfilled_holds.pl
new file mode 100755
index 0000000..69aaa6e
--- /dev/null
+++ b/misc/cronjobs/holds/cancel_unfilled_holds.pl
@@ -0,0 +1,151 @@
+#!/usr/bin/perl
+
+#
+# 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>.
+
+use Modern::Perl;
+
+BEGIN {
+    # find Koha's Perl modules
+    # test carefully before changing this
+    use FindBin;
+    eval { require "$FindBin::Bin/../kohalib.pl" };
+}
+
+use Getopt::Long;
+use Pod::Usage;
+
+use C4::Reserves;
+use C4::Log;
+use Koha::Holds;
+use Koha::Calendar;
+use Koha::DateUtils;
+use Koha::Libraries;
+
+cronlogaction();
+
+=head1 NAME
+
+cancel_waiting_holds.pl
+
+=head1 SYNOPSIS
+
+cancel_waiting_holds.pl
+    [-days][-library][-holidays]
+
+ Options:
+    -help                       brief help
+    -days                       cancel holds waiting this many days
+    -library                    [repeatable] limit to specified branch(es)
+    -holidays                   skip holidays when calculating days waiting
+    -v                          verbose
+
+head1 OPTIONS
+
+=over 8
+
+=item B<-help>
+
+Print brief help and exit.
+
+=item B<-man>
+
+Print full documentation and exit.
+
+=item B<-days>
+
+Specify the number of days waiting upon which to cancel holds.
+
+=item B<-library>
+
+Repeatable option to specify which branchcode(s) to cancel holds for.
+
+=item B<-holidays>
+
+This switch specifies whether to count holidays as days waiting. Default is no.
+
+=back
+
+=cut
+
+my $help = 0;
+my $days;
+my @branchcodes;
+my $use_calendar = 0;
+my $verbose      = 0;
+my $confirm      = 0;
+
+GetOptions(
+    'help|?'    => \$help,
+    'days=s'    => \$days,
+    'library=s' => \@branchcodes,
+    'holidays'  => \$use_calendar,
+    'v'         => \$verbose,
+    'confirm'   => \$confirm,
+) or pod2usage(1);
+pod2usage(1) if $help;
+
+unless ( defined $days ) {
+    pod2usage(
+        {
+            -exitval => 1,
+            -msg =>
+qq{\nError: You must specify a value for days waiting to cancel holds.\n},
+        }
+    );
+}
+warn "Running in test mode, no actions will be taken" unless ($confirm);
+
+$verbose and warn "Looking for unfilled holds placed $days or more days ago\n";
+
+unless ( scalar @branchcodes > 0 ) {
+    my $branches = Koha::Libraries->search->get_column('branchcode');
+    while ( my $branch = $branches->next ) {
+        push @branchcodes, $branch->branchcode;
+    }
+}
+$verbose and warn "Running for branch(es): " . join( "|", @branchcodes ) . "\n";
+
+foreach my $branch (@branchcodes) {
+
+    my $holds =
+      Koha::Holds->search( { branchcode => $branch } )->unfilled();
+
+    while ( my $hold = $holds->next ) {
+
+        my $age = $hold->age( $use_calendar );
+
+        $verbose
+          and warn "Hold #"
+          . $hold->reserve_id
+          . " has been unfilled for $age day(s)\n";
+
+        if ( $age >= $days ) {
+            my $action = $confirm ? "Cancelling " : "Would have cancelled ";
+            $verbose
+              and warn $action
+              . "reserve_id: "
+              . $hold->reserve_id
+              . " for borrower: "
+              . $hold->borrowernumber
+              . " on biblio: "
+              . $hold->biblionumber . "\n";
+            CancelReserve( { reserve_id => $hold->reserve_id } ) if $confirm;
+        }
+
+    }
+
+}
diff --git a/t/db_dependent/Hold.t b/t/db_dependent/Hold.t
index 71ce265..de17554 100755
--- a/t/db_dependent/Hold.t
+++ b/t/db_dependent/Hold.t
@@ -22,13 +22,14 @@ use C4::Context;
 use C4::Biblio qw( AddBiblio );
 use Koha::Database;
 use Koha::Libraries;
+use C4::Calendar;
 use Koha::Patrons;
 use Koha::Holds;
 use Koha::Item;
 use Koha::DateUtils;
 use t::lib::TestBuilder;
 
-use Test::More tests => 29;
+use Test::More tests => 31;
 use Test::Warn;
 
 use_ok('Koha::Hold');
@@ -66,6 +67,7 @@ my $hold = Koha::Hold->new(
     {
         biblionumber   => $biblionumber,
         itemnumber     => $item->id(),
+        reservedate    => '2017-01-01',
         waitingdate    => '2000-01-01',
         borrowernumber => $borrower->{borrowernumber},
         branchcode     => $branches[1]->{branchcode},
@@ -74,6 +76,12 @@ my $hold = Koha::Hold->new(
 );
 $hold->store();
 
+my $b1_cal = C4::Calendar->new( branchcode => $branches[1]->{branchcode} );
+$b1_cal->insert_single_holiday( day => 02, month => 01, year => 2017, title => "Morty Day", description => "Rick" ); #Add a holiday
+my $today = DateTime->now(time_zone => C4::Context->tz );
+is( $hold->age(), $today->delta_days( dt_from_string( '2017-01-01' ) )->in_units( 'days')  , "Age of hold is days from reservedate to now if calendar ignored");
+is( $hold->age(1), $today->delta_days( dt_from_string( '2017-01-01' ) )->in_units( 'days' ) - 1 , "Age of hold is days from reservedate to now minus 1 if calendar used");
+
 is( $hold->suspend, 0, "Hold is not suspended" );
 $hold->suspend_hold();
 is( $hold->suspend, 1, "Hold is suspended" );
diff --git a/t/db_dependent/Holds.t b/t/db_dependent/Holds.t
index ad87b26..fe773b0 100755
--- a/t/db_dependent/Holds.t
+++ b/t/db_dependent/Holds.t
@@ -7,7 +7,7 @@ use t::lib::TestBuilder;
 
 use C4::Context;
 
-use Test::More tests => 58;
+use Test::More tests => 59;
 use MARC::Record;
 use C4::Biblio;
 use C4::Items;
@@ -121,6 +121,7 @@ ok( $hold_branch == $hold->branch(), "branch method returns stashed branch" );
 my $hold_found = $hold->found();
 $hold->set({ found => 'W'})->store();
 is( Koha::Holds->waiting()->count(), 1, "Koha::Holds->waiting returns waiting holds" );
+is( Koha::Holds->unfilled()->count(), 4, "Koha::Holds->unfilled returns unfilled holds" );
 
 my $patron = Koha::Patrons->find( $borrowernumbers[0] );
 $holds = $patron->holds;
-- 
2.1.4