Grace period auto extension and backdate awareness
authorThomas Berezansky <tsbere@mvlc.org>
Thu, 15 Sep 2011 16:10:12 +0000 (12:10 -0400)
committerMike Rylander <mrylander@gmail.com>
Mon, 10 Oct 2011 17:28:47 +0000 (13:28 -0400)
Grace period auto extension:

Default is "grace periods don't auto extend".

OU setting turns on grace period auto extension. By default they only do so
when the grace period ends on a closed date, but there are two modifiers to
change that.

The first modifier causes grace periods to extend for all closed dates that
they intersect. This is "grace periods are only consumed by open days."

The second modifier causes a grace period that ends just before a closed
day, with or without extension having happened, to include the closed day
(and any following it) as well. This is mainly so that a backdate into the
closed period following the grace period will assume the "best case" of the
item having been returned after hours on the last day of the closed date.

Backdate grace period awareness:

By moving grace period calculations into a shared location backdating can
check the grace period, including extensions. If it finds that the backdate
is within the grace period (before or after extension) then instead of
voiding the fines for after the backdate it instead voids all fines for the
circulation. This emulates what would have happened (no fines) if the item
had been checked in at the time the backdate was made to take effect.

Signed-off-by: Thomas Berezansky <tsbere@mvlc.org>
Signed-off-by: Mike Rylander <mrylander@gmail.com>

Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.grace_period_extend.sql [new file with mode: 0644]

index 1319201..9daa10c 100644 (file)
@@ -18,7 +18,9 @@ my $U = "OpenILS::Application::AppUtils";
 
 # -----------------------------------------------------------------
 # Voids overdue fines on the given circ.  if a backdate is 
-# provided, then we only void back to the backdate
+# provided, then we only void back to the backdate, unless the
+# backdate is to within the grace period, in which case we void all
+# overdue fines.
 # -----------------------------------------------------------------
 sub void_overdues {
     my($class, $e, $circ, $backdate, $note) = @_;
@@ -43,9 +45,15 @@ sub void_overdues {
         my $interval = OpenSRF::Utils->interval_to_seconds($duration);
 
         my $date = DateTime::Format::ISO8601->parse_datetime($backdate);
-        $backdate = $U->epoch2ISO8601($date->epoch + $interval);
-        $logger->info("applying backdate $backdate in overdue voiding");
-        $$bill_search{billing_ts} = {'>=' => $backdate};
+        my $due_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->due_date))->epoch;
+        my $grace_period = extend_grace_period( $class, $circ->circ_lib, $circ->due_date, OpenSRF::Utils->interval_to_seconds($circ->grace_period), $e);
+        if($date->epoch < $due_date + $grace_period) {
+            $logger->info("backdate $backdate is within grace period, voiding all");
+        } else {
+            $backdate = $U->epoch2ISO8601($date->epoch + $interval);
+            $logger->info("applying backdate $backdate in overdue voiding");
+            $$bill_search{billing_ts} = {'>=' => $backdate};
+        }
     }
 
     my $bills = $e->search_money_billing($bill_search);
@@ -106,4 +114,99 @@ sub create_bill {
        return undef;
 }
 
+sub extend_grace_period {
+    my($class, $circ_lib, $due_date, $grace_period, $e, $h) = @_;
+    if ($grace_period >= 86400) { # Only extend grace periods greater than or equal to a full day
+        my $parser = DateTime::Format::ISO8601->new;
+        my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $due_date ) );
+        my $due = $due_dt->epoch;
+
+        my $grace_extend = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend');
+        $e = new_editor() if (!$e);
+        $h = $e->retrieve_actor_org_unit_hours_of_operation($circ_lib) if (!$h);
+        if ($grace_extend and $h) { 
+            my $new_grace_period = $grace_period;
+
+            $logger->info( "Circ lib has an hours-of-operation entry and grace period extension is enabled." );
+
+            my $closed = 0;
+            my %h_closed = {};
+            for my $i (0 .. 6) {
+                my $dow_open = "dow_${i}_open";
+                my $dow_close = "dow_${i}_close";
+                if($h->$dow_open() eq '00:00:00' and $h->$dow_close() eq '00:00:00') {
+                    $closed++;
+                    $h_closed{$i} = 1;
+                } else {
+                    $h_closed{$i} = 0;
+                }
+            }
+
+            if($closed == 7) {
+                $logger->info("Circ lib is closed all week according to hours-of-operation entry. Skipping grace period extension checks.");
+            } else {
+                # Extra nice grace periods
+                # AKA, merge closed dates trailing the grace period into the grace period
+                my $grace_extend_into_closed = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend.into_closed');
+                $due += 86400 if $grace_extend_into_closed;
+
+                my $grace_extend_all = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend.all');
+
+                if ( $grace_extend_all ) {
+                    # Start checking the day after the item was due
+                    # This is "The grace period only counts open days"
+                    # NOTE: Adding 86400 seconds is not the same as adding one day. This uses seconds intentionally.
+                    $due_dt = $due_dt->add( seconds => 86400 );
+                } else {
+                    # Jump to the end of the grace period
+                    # This is "If the grace period ends on a closed day extend it"
+                    # NOTE: This adds grace period as a number of seconds intentionally
+                    $due_dt = $due_dt->add( seconds => $grace_period );
+                }
+
+                my $count = 0; # Infinite loop protection
+                do {
+                    $closed = 0; # Starting assumption for day: We are not closed
+                    $count++; # We limit the number of loops below.
+
+                    # get the day of the week for the day we are looking at
+                    my $dow = $due_dt->day_of_week_0;
+
+                    # Check hours of operation first.
+                    if ($h_closed{$dow}) {
+                        $closed = 1;
+                        $new_grace_period += 86400;
+                        $due_dt->add( seconds => 86400 );
+                    } else {
+                        # Check for closed dates for this period
+                        my $timestamptz = $due_dt->strftime('%FT%T%z');
+                        my $cl = $e->search_actor_org_unit_closed_date(
+                                { close_start => { '<=' => $timestamptz },
+                                  close_end   => { '>=' => $timestamptz },
+                                  org_unit    => $circ_lib }
+                        );
+                        if ($cl and @$cl) {
+                            $closed = 1;
+                            foreach (@$cl) {
+                                my $cl_dt = $parser->parse_datetime( cleanse_ISO8601( $_->close_end ) );
+                                while ($due_dt <= $cl_dt) {
+                                    $due_dt->add( seconds => 86400 );
+                                    $new_grace_period += 86400;
+                                }
+                            }
+                        } else {
+                            $due_dt->add( seconds => 86400 );
+                        }
+                    }
+                } while ( $count <= 366 and ( $closed or $due_dt->epoch <= $due + $new_grace_period ) );
+                if ($new_grace_period > $grace_period) {
+                    $grace_period = $new_grace_period;
+                    $logger->info( "Grace period for circ extended to $grace_period [" . seconds_to_interval( $grace_period ) . "]" );
+                }
+            }
+        }
+    }
+    return $grace_period;
+}
+
 1;
index 6a2b6d6..eff589c 100644 (file)
@@ -13,6 +13,7 @@ use DateTime;
 use DateTime::Format::ISO8601;
 use OpenILS::Utils::Penalty;
 use POSIX qw(ceil);
+use OpenILS::Application::Circ::CircCommon;
 
 sub isTrue {
        my $v = shift;
@@ -886,38 +887,7 @@ sub generate_fines {
                                $log->info( "Potential first billing for circ ".$c->id );
                                $last_fine = $due;
 
-                               if (0) {
-                                       if (my $h = $hoo{$c->$circ_lib_method}) { 
-
-                                               $log->info( "Circ lib has an hours-of-operation entry" );
-                                               # find the day after the due date...
-                                               $due_dt = $due_dt->add( days => 1 );
-
-                                               # get the day of the week for that day...
-                                               my $dow = $due_dt->day_of_week_0;
-                                               my $dow_open = "dow_${dow}_open";
-                                               my $dow_close = "dow_${dow}_close";
-
-                                               my $count = 0;
-                                               while ( $h->$dow_open eq '00:00:00' and $h->$dow_close eq '00:00:00' ) {
-                                                       # if the circ lib is closed, add a day to the grace period...
-
-                                                       $grace_period+=86400;
-                                                       $log->info( "Grace period for circ ".$c->id." extended to $grace_period [" . seconds_to_interval( $grace_period ) . "]" );
-                                                       $log->info( "Day of week $dow open $dow_open, close $dow_close" );
-
-                                                       $due_dt = $due_dt->add( days => 1 );
-                                                       $dow = $due_dt->day_of_week_0;
-                                                       $dow_open = "dow_${dow}_open";
-                                                       $dow_close = "dow_${dow}_close";
-
-                                                       $count++;
-
-                                                       # and check for up to a week
-                                                       last if ($count > 6);
-                                               }
-                                       }
-                               }
+                               $grace_period = OpenILS::Application::Circ::CircCommon->extend_grace_period($c->$circ_lib_method->to_fieldmapper->id,$c->$due_date_method,$grace_period,undef,$hoo{$c->$circ_lib_method});
                        }
 
             next if ($last_fine > $now);
index 09466eb..7f0263b 100644 (file)
@@ -2724,6 +2724,33 @@ INSERT into config.org_unit_setting_type
         'coust', 'description'),
     'bool', null)
 
+,( 'circ.grace.extend', 'circ',
+    oils_i18n_gettext('circ.grace.extend',
+        'Auto-Extend Grace Periods',
+        'coust', 'label'),
+    oils_i18n_gettext('circ.grace.extend',
+        'When enabled grace periods will auto-extend. By default this will be only when they are a full day or more and end on a closed date, though other options can alter this.',
+        'coust', 'description'),
+    'bool', null)
+
+,( 'circ.grace.extend.all', 'circ',
+    oils_i18n_gettext('circ.grace.extend.all',
+        'Auto-Extending Grace Periods extend for all closed dates',
+        'coust', 'label'),
+    oils_i18n_gettext('circ.grace.extend.all',
+        'If enabled and Grace Periods auto-extending is turned on grace periods will extend past all closed dates they intersect, within hard-coded limits. This basically becomes "grace periods can only be consumed by closed dates".',
+        'coust', 'description'),
+    'bool', null)
+
+,( 'circ.grace.extend.into_closed', 'circ',
+    oils_i18n_gettext('circ.grace.extend.into_closed',
+        'Auto-Extending Grace Periods include trailing closed dates',
+        'coust', 'label'),
+    oils_i18n_gettext('circ.grace.extend.into_closed',
+         'If enabled and Grace Periods auto-extending is turned on grace periods will include closed dates that directly follow the last day of the grace period, to allow a backdate into the closed dates to assume "returned after hours on the last day of the grace period, and thus still within it" automatically.',
+        'coust', 'description'),
+    'bool', null)
+
 ,( 'circ.hold_boundary.hard', 'holds',
     oils_i18n_gettext('circ.hold_boundary.hard',
         'Hard boundary',
@@ -4475,6 +4502,7 @@ INSERT INTO actor.org_unit_setting (org_unit, name, value) VALUES (
     ,(1, 'cat.label.font.family', '"monospace"')
     ,(1, 'cat.label.font.size', 10)
     ,(1, 'cat.label.font.weight', '"normal"')
+    ,(1, 'circ.grace.extend', 'true')
 ;
 
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.grace_period_extend.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.grace_period_extend.sql
new file mode 100644 (file)
index 0000000..84ed345
--- /dev/null
@@ -0,0 +1,28 @@
+INSERT INTO config.org_unit_setting_type(name, grp, label, description, datatype) VALUES
+
+( 'circ.grace.extend', 'circ',
+    oils_i18n_gettext('circ.grace.extend',
+        'Auto-Extend Grace Periods',
+        'coust', 'label'),
+    oils_i18n_gettext('circ.grace.extend',
+        'When enabled grace periods will auto-extend. By default this will be only when they are a full day or more and end on a closed date, though other options can alter this.',
+        'coust', 'description'),
+    'bool', null)
+
+,( 'circ.grace.extend.all', 'circ',
+    oils_i18n_gettext('circ.grace.extend.all',
+        'Auto-Extending Grace Periods extend for all closed dates',
+        'coust', 'label'),
+    oils_i18n_gettext('circ.grace.extend.all',
+        'If enabled and Grace Periods auto-extending is turned on grace periods will extend past all closed dates they intersect, within hard-coded limits. This basically becomes "grace periods can only be consumed by closed dates".',
+        'coust', 'description'),
+    'bool', null)
+
+,( 'circ.grace.extend.into_closed', 'circ',
+    oils_i18n_gettext('circ.grace.extend.into_closed',
+        'Auto-Extending Grace Periods include trailing closed dates',
+        'coust', 'label'),
+    oils_i18n_gettext('circ.grace.extend.into_closed',
+         'If enabled and Grace Periods auto-extending is turned on grace periods will include closed dates that directly follow the last day of the grace period, to allow a backdate into the closed dates to assume "returned after hours on the last day of the grace period, and thus still within it" automatically.',
+        'coust', 'description'),
+    'bool', null);