[open-ils-commits] r18630 - in trunk/Open-ILS/src: perlmods/OpenILS/Application/Storage/Publisher sql/Pg sql/Pg/upgrade (dbs)

svn at svn.open-ils.org svn at svn.open-ils.org
Fri Nov 5 13:35:42 EDT 2010


Author: dbs
Date: 2010-11-05 13:35:38 -0400 (Fri, 05 Nov 2010)
New Revision: 18630

Added:
   trunk/Open-ILS/src/sql/Pg/upgrade/0460.schema.recall.sql
Modified:
   trunk/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm
   trunk/Open-ILS/src/sql/Pg/002.schema.config.sql
   trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql
Log:
Initial support for recall of items triggered by placing a hold

In academic libraries, it is common for groups like faculty and graduate
students to have extended loan periods (for example, 120 days), while
others have more common loan periods such as 3 weeks. In these environments,
it is desirable to have a hold placed on an item that has been loaned out
for an extended period to trigger a "recall": truncating the loan period,
setting available renewals to 0, optionally changing the fines associated
with overdues for the new due date, and notifying the current patron of
the recall.

New actor.org_unit_setting entries and hold targeting logic enable libraries to
control whether a hold will trigger a recall of a circulating item as follows:

  * "Recalls: Circulation duration that triggers a recall" (recall threshold)
     is specified as an interval (for example, "21 days"); any items with a
     loan duration of less that this interval will not be considered for a
     recall
  * "Recalls: Truncated loan period" (return interval) is specified as an
     interval (for example, "7 days"), such that the user who currently
     has the item checked out will get the greater of either the recall
     threshold, or the return interval.
  * "Recalls: An array of fine amount, fine interval, and maximum fine"
     is an optional setting that applied the new fine rules to the current
     circulation period.
 
When a hold is placed and no available copies are found by the hold targeter,
the recall logic checks to see if the recall threshold and return interval
settings are set; if so, then the hold targeter checks the currently
checked-out copies to determine if any of the currently circulating items at
the designated pickup library have a loan duration longer than the recall
threshold. If so, then the eligible item with the nearest due date will be
recalled.


Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm	2010-11-05 17:18:35 UTC (rev 18629)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm	2010-11-05 17:35:38 UTC (rev 18630)
@@ -1,7 +1,10 @@
 package OpenILS::Application::Storage::Publisher::action;
-use base qw/OpenILS::Application::Storage::Publisher/;
+use parent qw/OpenILS::Application::Storage/;
+use strict;
+use warnings;
 use OpenSRF::Utils::Logger qw/:level/;
 use OpenSRF::Utils qw/:datetime/;
+use OpenSRF::Utils::JSON;
 use OpenSRF::AppSession;
 use OpenSRF::EX qw/:try/;
 use OpenILS::Utils::Fieldmapper;
@@ -1406,6 +1409,7 @@
 			} else {
 				$hold->update( { prev_check_time => 'now' } );
 				$log->info( "\tThere were no targetable copies for the hold" );
+				process_recall($actor, $log, $hold, \@good_copies);
 			}
 
 			$self->method_lookup('open-ils.storage.transaction.commit')->run;
@@ -1435,6 +1439,88 @@
 	method		=> 'new_hold_copy_targeter',
 );
 
+sub process_recall {
+    my ($actor, $log, $hold, $good_copies) = @_;
+
+    # Bail early if we don't have required settings to avoid spurious requests
+    my $recall_threshold = $actor->request(
+        'open-ils.actor.ou_setting.ancestor_default', ''.$hold->pickup_lib, 'circ.holds.recall_threshold'
+    )->gather(1)->{value};
+
+    if (!$recall_threshold) {
+        $log->info("Recall threshold was not set; bailing out on hold ".$hold->id." processing.");
+        return;
+    }
+
+    my $return_interval = $actor->request(
+        'open-ils.actor.ou_setting.ancestor_default', ''.$hold->pickup_lib, 'circ.holds.recall_return_interval'
+    )->gather(1)->{value};
+
+    if (!$return_interval) {
+        $log->info("Recall return interval was not set; bailing out on hold ".$hold->id." processing.");
+        return;
+    }
+
+    my $fine_rules = $actor->request(
+        'open-ils.actor.ou_setting.ancestor_default', ''.$hold->pickup_lib, 'circ.holds.recall_fine_rules'
+    )->gather(1)->{value};
+
+    $log->info("Recall threshold: $recall_threshold; return interval: $return_interval");
+
+    # We want checked out copies (status = 1) at the hold pickup lib
+    my $all_copies = [grep { $_->status == 1 } grep {''.$_->circ_lib eq ''.$hold->pickup_lib } @$good_copies];
+
+    my @copy_ids = map { $_->id } @$all_copies;
+
+    $log->info("Found " . scalar(@$all_copies) . " eligible checked-out copies for recall");
+
+    my $return_date = DateTime->now(time_zone => 'local')->add(seconds => interval_to_seconds($return_interval))->iso8601();
+
+    # Iterate over the checked-out copies to find a copy with a
+    # loan period longer than the recall threshold:
+    my $circs = [ action::circulation->search_where(
+        { target_copy => \@copy_ids, checkin_time => undef, duration => { '>' => $recall_threshold } },
+        { order_by => 'due_date ASC' }
+    )];
+
+    # If we have a candidate copy, then:
+    if ($circs) {
+        my $circ = $circs->[0];
+        $log->info("Recalling circ ID : " . $circ->id);
+
+        # Give the user a new due date of either a full recall threshold,
+        # or the return interval, whichever is further in the future
+        my $threshold_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->xact_start))->add(seconds => interval_to_seconds($recall_threshold))->iso8601();
+        if (DateTime->compare(DateTime::Format::ISO8601->parse_datetime($threshold_date), DateTime::Format::ISO8601->parse_datetime($return_date)) == 1) {
+            $return_date = $threshold_date;
+        }
+
+        my $update_fields = {
+            due_date => $return_date,
+            renewal_remaining => 0,
+        };
+
+        # If the OU hasn't defined new fine rules for recalls, keep them
+        # as they were
+        if ($fine_rules) {
+            $log->info("Apply recall fine rules: $fine_rules");
+            my $rules = OpenSRF::Utils::JSON->JSON2perl($fine_rules);
+            $update_fields->{recurring_fine} = $rules->[0];
+            $update_fields->{fine_interval} = $rules->[1];
+            $update_fields->{max_fine} = $rules->[2];
+        }
+
+        # Adjust circ for current user
+        $circ->update($update_fields);
+
+        # Create trigger event for notifying current user
+        my $ses = OpenSRF::AppSession->create('open-ils.trigger');
+        $ses->request('open-ils.trigger.event.autocreate', 'circ.recall.target', $circ, $circ->circ_lib->id);
+    }
+
+    $log->info("Processing of hold ".$hold->id." for recall is now complete.");
+}
+
 sub reservation_targeter {
 	my $self = shift;
 	my $client = shift;

Modified: trunk/Open-ILS/src/sql/Pg/002.schema.config.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/002.schema.config.sql	2010-11-05 17:18:35 UTC (rev 18629)
+++ trunk/Open-ILS/src/sql/Pg/002.schema.config.sql	2010-11-05 17:35:38 UTC (rev 18630)
@@ -70,7 +70,7 @@
     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0459'); -- gmc
+INSERT INTO config.upgrade_log (version) VALUES ('0460'); -- dbs
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,

Modified: trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql	2010-11-05 17:18:35 UTC (rev 18629)
+++ trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql	2010-11-05 17:35:38 UTC (rev 18630)
@@ -7074,3 +7074,69 @@
     )
 ;
 
+-- 0460 schema for recalls triggered by holds
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
+    VALUES
+        ('circ.holds.recall_threshold',
+        oils_i18n_gettext( 'circ.holds.recall_threshold',
+            'Recalls: Circulation duration that triggers a recall.', 'coust', 'label'),
+        oils_i18n_gettext( 'circ.holds.recall_threshold',
+            'Recalls: A hold placed on an item with a circulation duration longer than this will trigger a recall. For example, "14 days" or "3 weeks".', 'coust', 'description'),
+        'interval')
+;
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
+    VALUES
+        ('circ.holds.recall_return_interval',
+        oils_i18n_gettext( 'circ.holds.recall_return_interval',
+            'Recalls: Truncated loan period.', 'coust', 'label'),
+        oils_i18n_gettext( 'circ.holds.recall_return_interval',
+            'Recalls: When a recall is triggered, this defines the adjusted loan period for the item. For example, "4 days" or "1 week".', 'coust', 'description'),
+        'interval')
+;
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
+    VALUES
+        ('circ.holds.recall_fine_rules',
+        oils_i18n_gettext( 'circ.holds.recall_fine_rules',
+            'Recalls: An array of fine amount, fine interval, and maximum fine.', 'coust', 'label'),
+        oils_i18n_gettext( 'circ.holds.recall_fine_rules',
+            'Recalls: An array of fine amount, fine interval, and maximum fine. For example, to specify a new fine rule of $5.00 per day, with a maximum fine of $50.00, use: [5.00,"1 day",50.00]', 'coust', 'description'),
+        'array')
+;
+
+INSERT INTO action_trigger.hook (key,core_type,description)
+    VALUES ('circ.recall.target', 'circ', 'A checked-out copy has been recalled for a hold.');
+
+INSERT INTO action_trigger.event_definition (id, owner, name, hook, validator, reactor, group_field, template)
+    VALUES (37, 1, 'Item Recall Email Notice', 'circ.recall.target', 'NOOP_True', 'SendEmail', 'usr', 
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Subject: Item Recall Notification 
+
+Dear [% user.family_name %], [% user.first_given_name %]
+
+The following item which you have checked out has been recalled so that
+another patron can have access to the item:
+
+[% FOR circ IN target %]
+    Title: [% circ.target_copy.call_number.record.simple_record.title %] 
+    Barcode: [% circ.target_copy.barcode %] 
+    Now Due: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
+    Library: [% circ.circ_lib.name %]
+
+    If this item is not returned by the new due date, fines will be assessed at
+    the rate of [% circ.recurring_fine %] every [% circ.fine_interval %].
+[% END %]
+$$
+);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES
+    (37, 'target_copy.call_number.record.simple_record'),
+    (37, 'usr'),
+    (37, 'circ_lib.billing_address')
+;
+

Added: trunk/Open-ILS/src/sql/Pg/upgrade/0460.schema.recall.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/upgrade/0460.schema.recall.sql	                        (rev 0)
+++ trunk/Open-ILS/src/sql/Pg/upgrade/0460.schema.recall.sql	2010-11-05 17:35:38 UTC (rev 18630)
@@ -0,0 +1,70 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0460'); -- dbs
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
+    VALUES
+        ('circ.holds.recall_threshold',
+        oils_i18n_gettext( 'circ.holds.recall_threshold',
+            'Recalls: Circulation duration that triggers a recall.', 'coust', 'label'),
+        oils_i18n_gettext( 'circ.holds.recall_threshold',
+            'Recalls: A hold placed on an item with a circulation duration longer than this will trigger a recall. For example, "14 days" or "3 weeks".', 'coust', 'description'),
+        'interval')
+;
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
+    VALUES
+        ('circ.holds.recall_return_interval',
+        oils_i18n_gettext( 'circ.holds.recall_return_interval',
+            'Recalls: Truncated loan period.', 'coust', 'label'),
+        oils_i18n_gettext( 'circ.holds.recall_return_interval',
+            'Recalls: When a recall is triggered, this defines the adjusted loan period for the item. For example, "4 days" or "1 week".', 'coust', 'description'),
+        'interval')
+;
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
+    VALUES
+        ('circ.holds.recall_fine_rules',
+        oils_i18n_gettext( 'circ.holds.recall_fine_rules',
+            'Recalls: An array of fine amount, fine interval, and maximum fine.', 'coust', 'label'),
+        oils_i18n_gettext( 'circ.holds.recall_fine_rules',
+            'Recalls: An array of fine amount, fine interval, and maximum fine. For example, to specify a new fine rule of $5.00 per day, with a maximum fine of $50.00, use: [5.00,"1 day",50.00]', 'coust', 'description'),
+        'array')
+;
+
+INSERT INTO action_trigger.hook (key,core_type,description)
+    VALUES ('circ.recall.target', 'circ', 'A checked-out copy has been recalled for a hold.');
+
+INSERT INTO action_trigger.event_definition (id, owner, name, hook, validator, reactor, group_field, template)
+    VALUES (37, 1, 'Item Recall Email Notice', 'circ.recall.target', 'NOOP_True', 'SendEmail', 'usr', 
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Subject: Item Recall Notification 
+
+Dear [% user.family_name %], [% user.first_given_name %]
+
+The following item which you have checked out has been recalled so that
+another patron can have access to the item:
+
+[% FOR circ IN target %]
+    Title: [% circ.target_copy.call_number.record.simple_record.title %] 
+    Barcode: [% circ.target_copy.barcode %] 
+    Now Due: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
+    Library: [% circ.circ_lib.name %]
+
+    If this item is not returned by the new due date, fines will be assessed at
+    the rate of [% circ.recurring_fine %] every [% circ.fine_interval %].
+[% END %]
+$$
+);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES
+    (37, 'target_copy.call_number.record.simple_record'),
+    (37, 'usr'),
+    (37, 'circ_lib.billing_address')
+;
+
+COMMIT;



More information about the open-ils-commits mailing list