[open-ils-commits] [GIT] Evergreen ILS branch master updated. eabd8160c6b88dd6e04e22b0d2b26e62f0d118cb

Evergreen Git git at git.evergreen-ils.org
Tue Feb 16 12:47:49 EST 2016


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  eabd8160c6b88dd6e04e22b0d2b26e62f0d118cb (commit)
       via  831a808746308a174f1209d3bec9c2284602798b (commit)
       via  63205ed42a72a3cb6b3c7d7d3d76154dab40be1f (commit)
      from  c3fe2f44128f83720777052745015b2b7265887a (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit eabd8160c6b88dd6e04e22b0d2b26e62f0d118cb
Author: Jason Stephenson <jason at sigio.com>
Date:   Sun Oct 11 14:30:11 2015 -0400

    LP 1499123: Add release notes.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/standing_penalty_ignore_proximity.txt b/docs/RELEASE_NOTES_NEXT/Circulation/standing_penalty_ignore_proximity.txt
new file mode 100644
index 0000000..7bdc5b8
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/standing_penalty_ignore_proximity.txt
@@ -0,0 +1,30 @@
+Standing Penalty Ignore Proximity
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Standing penalties now have an ignore_proximity field that takes an
+integer value.  When set, the value of this field represents the
+proximity from the user's home organizational unit where this penalty
+will be ignored for purposes of circulation and holds.  Typical values
+for this field would be 0, 1, or 2 when using a standard hierarchy of
+Consortium -> System -> Branch -> Sublibrary/Bookmoblie.  A value of 1
+would cause the penalty to be ignored at the user's home organization
+unit, it's parent and/or immediate child.  A value of 2 should cause
+it to be ignored at the above as well as all sibling organizational
+units to the user's home.  In all cases, a value of zero causes the
+penalty to be ignored at the user's home and to apply at all other
+organizational units.  If the value of this field is left unset (or
+set to a negative value), the penalty will still take effect
+everywhere using the normal organizational unit and depth values.  If
+you use a custom hierarchy, you will need to figure out any values
+greater than 0 on your own.
+
+The ignore_proximity does not affect where penalties are applied. It
+is used when determining whether or not a penalty blocks an activity
+at the current organizational unit or the organizational unit that
+owns the copy involved in the current transaction.  For instance, if
+you set the ignore_proximity to 0 on patron exceeds overdue fines,
+then the patron will still be able to place holds on and checkout
+copies owned by their home organizational unit at their home
+organizational unit.  They will not, however, be able to receive
+copies form other organizational units, nor use other organizational
+units as a patron.

commit 831a808746308a174f1209d3bec9c2284602798b
Author: Jason Stephenson <jason at sigio.com>
Date:   Sat Sep 26 11:42:35 2015 -0400

    LP 1499123: Modify Perl code for csp.ignore_proximity field.
    
    * Add get_org_unit_proximity function to AppUtils.
    
    First, we add a helper function to OpenILS::Application::AppUtils that
    returns the proximity value between a "from" org_unit and a "to"
    org_unit.  It takes a CStoreEditor and the ids of the two org_units as
    arguments.
    
    * Use csp.ignore_proximity in O::A::Circ::Circulate::Circulator.
    
    Modify the check_hold_fulfill_blocks method of the Circulator object
    to take the csp.ignore_proximity into account.
    
    The new code first calculates the proximity of the circ_lib and the
    copy's circ_lib with the patron's home_ou.  It then modifies the main
    query in the function to check if the csp object's ignore_proximity
    is null or greater than either of the two calculated proximity values.
    
    * Teach SIP::Patron about csp.ignore_proximity.
    
    We modify SIP::Patron::flesh_user_penalties to not report penalties
    within csp.ignore_proximity of the user's home_ou.
    
    In order to have a notion of "here" for the proximity check, we modify
    SIP::Patron->new to assign its authtoken argument, if any, to the
    CStoreEditor.  We then use this authtoken to retrieve the authsession
    user so that we may use the authsession user's ws_ou or home_ou as a
    context ou for penalty lookup and filtering based on the
    csp.ignore_proximity in flesh_user_penalties. If we're not given the
    authtoken, we fall back to using the patron's home_ou and the
    penalty's context ou for the proximity lookup.
    
    This assumes, of course, that the authsession user's ws_ou or home_ou
    have a logical relationship with the desired transaction ou.  For most
    self-checks this will likely be true.  For other uses of the SIP
    protocol, this is less likely to be true.
    
    * Add Perl live tests.
    
    Add tests for basic checkout and hold functionality as well as for
    the OpenILS::SIP::Patron->flesh_user_penalties() changes.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
index 3d0ec38..ab89ac4 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
@@ -1496,6 +1496,23 @@ sub org_unit_ancestor_at_depth {
     return ($resp) ? $resp->{id} : undef;
 }
 
+# Returns the proximity value between two org units.
+sub get_org_unit_proximity {
+    my ($class, $e, $from_org, $to_org) = @_;
+    $e = OpenILS::Utils::CStoreEditor->new unless ($e);
+    my $r = $e->json_query(
+        {
+            select => {aoup => ['prox']},
+            from => 'aoup',
+            where => {from_org => $from_org, to_org => $to_org}
+        }
+    );
+    if (ref($r) eq 'ARRAY' && @$r) {
+        return $r->[0]->{prox};
+    }
+    return undef;
+}
+
 # returns the user's configured locale as a string.  Defaults to en-US if none is configured.
 sub get_user_locale {
     my($self, $user_id, $e) = @_;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
index c729abc..befe1bc 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
@@ -1482,6 +1482,22 @@ sub bail_on_events {
 sub check_hold_fulfill_blocks {
     my $self = shift;
 
+    # With the addition of ignore_proximity in csp, we need to fetch
+    # the proximity of both the circ_lib and the copy's circ_lib to
+    # the patron's home_ou.
+    my ($ou_prox, $copy_prox);
+    my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
+    $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
+    $ou_prox = -1 unless (defined($ou_prox));
+    my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
+    if ($copy_ou == $self->circ_lib) {
+        # Save us the time of an extra query.
+        $copy_prox = $ou_prox;
+    } else {
+        $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
+        $copy_prox = -1 unless (defined($copy_prox));
+    }
+
     # See if the user has any penalties applied that prevent hold fulfillment
     my $pens = $self->editor->json_query({
         select => {csp => ['name', 'label']},
@@ -1495,7 +1511,14 @@ sub check_hold_fulfill_blocks {
                     {stop_date => {'>' => 'now'}}
                 ]
             },
-            '+csp' => {block_list => {'like' => '%FULFILL%'}}
+            '+csp' => {
+                block_list => {'like' => '%FULFILL%'},
+                '-or' => [
+                    {ignore_proximity => undef},
+                    {ignore_proximity => {'<' => $ou_prox}},
+                    {ignore_proximity => {'<' => $copy_prox}}
+                ]
+            }
         }
     });
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
index ac4f05c..1600db1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
@@ -51,6 +51,12 @@ sub new {
     syslog("LOG_DEBUG", "OILS: new OpenILS Patron(%s => %s): searching...", $key, $patron_id);
 
     my $e = OpenILS::SIP->editor();
+    # Pass the authtoken, if any, to the editor so that we can use it
+    # to fake a context org_unit for the csp.ignore_proximity in
+    # flesh_user_penalties, below.
+    unless ($e->authtoken()) {
+        $e->authtoken($args{authtoken}) if ($args{authtoken});
+    }
 
     my $usr_flesh = {
         flesh => 2,
@@ -143,9 +149,22 @@ sub get_act_who {
 sub flesh_user_penalties {
     my ($self, $user, $e) = @_;
 
-    $user->standing_penalties(
+    # Use the ws_ou or home_ou of the authsession user, if any, as a
+    # context org_unit for the penalties and the csp.ignore_proximity.
+    my $here;
+    if ($e->authtoken()) {
+        my $auth_usr = $e->checkauth();
+        if ($auth_usr) {
+            $here = $auth_usr->ws_ou() || $auth_usr->home_ou();
+        }
+    }
+
+    # Get the "raw" list of user's penalties and flesh the
+    # standing_penalty field, so we can filter them based on
+    # csp.ignore_proximity.
+    my $raw_penalties =
         $e->search_actor_user_standing_penalty([
-            {   
+            {
                 usr => $user->id,
                 '-or' => [
 
@@ -158,21 +177,20 @@ sub flesh_user_penalties {
                     in  => {
                         select => {
                             aou => [{
-                                column => 'id', 
-                                transform => 'actor.org_unit_ancestors', 
+                                column => 'id',
+                                transform => 'actor.org_unit_ancestors',
                                 result_field => 'id'
                             }]
                         },
                         from => 'aou',
 
-                        # at this point, there is no concept of "here", so fetch penalties 
-                        # for the patron's home lib plus ancestors
-                        where => {id => $user->home_ou}, 
+                        # Use "here" or user's home_ou.
+                        where => {id => ($here) ? $here : $user->home_ou},
                         distinct => 1
                     }
                 },
 
-                # in addition to fines and excessive overdue penalties, 
+                # in addition to fines and excessive overdue penalties,
                 # we only care about penalties that result in blocks
                 standing_penalty => {
                     in => {
@@ -187,8 +205,28 @@ sub flesh_user_penalties {
                     }
                 }
             },
-        ])
-    );
+            {
+                flesh => 1,
+                flesh_fields => {ausp => ['standing_penalty']}
+            }
+        ]);
+    # We filter the raw penalties that apply into this array.
+    my $applied_penalties = [];
+    if (ref($raw_penalties) eq 'ARRAY' && @$raw_penalties) {
+        my $here_prox = ($here) ? $U->get_org_unit_proximity($e, $here, $user->home_ou())
+            : undef;
+        # Filter out those that do not apply and deflesh the standing_penalty.
+        $applied_penalties = [map
+            { $_->standing_penalty($_->standing_penalty->id()) }
+                grep {
+                    !defined($_->standing_penalty->ignore_proximity())
+                    || ((defined($here_prox))
+                        ? $_->standing_penalty->ignore_proximity() < $here_prox
+                        : $_->standing_penalty->ignore_proximity() <
+                            $U->get_org_unit_proximity($e, $_->org_unit(), $user->home_ou()))
+                } @$raw_penalties];
+    }
+    $user->standing_penalties($applied_penalties);
 }
 
 sub id {
diff --git a/Open-ILS/src/perlmods/live_t/12-lp1499123_csp_ignore_proximity.t b/Open-ILS/src/perlmods/live_t/12-lp1499123_csp_ignore_proximity.t
new file mode 100644
index 0000000..2b993bc
--- /dev/null
+++ b/Open-ILS/src/perlmods/live_t/12-lp1499123_csp_ignore_proximity.t
@@ -0,0 +1,252 @@
+#!perl
+use strict; use warnings;
+
+use Test::More tests => 28;
+use Data::Dumper;
+
+diag("Test config.standing_penalty.ignore_proximity feature.");
+
+use OpenILS::Utils::TestUtils;
+use OpenILS::SIP::Patron;
+my $script = OpenILS::Utils::TestUtils->new();
+our $apputils = 'OpenILS::Application::AppUtils';
+
+use constant WORKSTATION_NAME => 'BR1-test-lp1499123_csp_ignore_proximity.t';
+use constant WORKSTATION_LIB => 4;
+
+# Because this may run multiple times, without a DB reload, we search
+# for the workstation before registering it.
+sub find_workstation {
+    my $r = $apputils->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.workstation.list',
+        $script->authtoken,
+        WORKSTATION_LIB
+    );
+    if ($r->{&WORKSTATION_LIB}) {
+        return scalar(grep {$_->name() eq WORKSTATION_NAME} @{$r->{&WORKSTATION_LIB}});
+    }
+    return 0;
+}
+
+sub retrieve_staff_chr {
+    my $e = shift;
+    my $staff_chr = $e->retrieve_config_standing_penalty(25);
+    return $staff_chr;
+}
+
+sub update_staff_chr {
+    my $e = shift;
+    my $penalty = shift;
+    $e->xact_begin;
+    my $r = $e->update_config_standing_penalty($penalty) || $e->event();
+    if (ref($r)) {
+        $e->rollback();
+    } else {
+        $e->commit;
+    }
+    return $r;
+}
+
+sub retrieve_user_by_barcode {
+    my $barcode = shift;
+    return $apputils->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.user.fleshed.retrieve_by_barcode',
+        $script->authtoken,
+        $barcode
+    );
+}
+
+sub retrieve_copy_by_barcode {
+    my $editor = shift;
+    my $barcode = shift;
+    my $r = $editor->search_asset_copy({barcode => $barcode});
+    if (ref($r) eq 'ARRAY' && @$r) {
+        return $r->[0];
+    }
+    return undef;
+}
+
+sub apply_staff_chr_to_patron {
+    my ($staff, $patron) = @_;
+    my $penalty = Fieldmapper::actor::user_standing_penalty->new();
+    $penalty->standing_penalty(25);
+    $penalty->usr($patron->id());
+    $penalty->set_date('now');
+    $penalty->staff($staff->id());
+    $penalty->org_unit(1); # Consortium-wide.
+    $penalty->note('LP 1499123 csp.ignore_proximity test');
+    my $r = $apputils->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.user.penalty.apply',
+        $script->authtoken,
+        $penalty
+    );
+    if (ref($r)) {
+        undef($penalty);
+    } else {
+        $penalty->id($r);
+    }
+    return $penalty;
+}
+
+sub remove_staff_chr_from_patron {
+    my $penalty = shift;
+    return $apputils->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.user.penalty.remove',
+        $script->authtoken,
+        $penalty
+    );
+}
+
+sub checkout_permit_test {
+    my $patron = shift;
+    my $copy_barcode = shift;
+    my $r = $apputils->simplereq(
+        'open-ils.circ',
+        'open-ils.circ.checkout.permit',
+        $script->authtoken,
+        {
+            patron => $patron->id(),
+            barcode => $copy_barcode
+        }
+    );
+    if (ref($r) eq 'HASH' && $r->{textcode} eq 'SUCCESS') {
+        return 1;
+    }
+    return 0;
+}
+
+sub copy_hold_permit_test {
+    my $editor = shift;
+    my $patron = shift;
+    my $copy_barcode = shift;
+    my $copy = retrieve_copy_by_barcode($editor, $copy_barcode);
+    if ($copy) {
+        my $r = $apputils->simplereq(
+            'open-ils.circ',
+            'open-ils.circ.title_hold.is_possible',
+            $script->authtoken,
+            {
+                patronid => $patron->id(),
+                pickup_lib => 4,
+                copy_id => $copy->id(),
+                hold_type => 'C'
+            }
+        );
+        if (ref($r) && defined $r->{success}) {
+            return $r->{success};
+        }
+    }
+    return undef;
+}
+
+sub patron_sip_test {
+    my $patron_id = shift;
+    my $patron = OpenILS::SIP::Patron->new(usr => $patron_id, authtoken => $script->authtoken);
+    return scalar(@{$patron->{user}->standing_penalties()});
+}
+
+# In concerto, we need to register a workstation.
+$script->authenticate({
+    username => 'admin',
+    password => 'demo123',
+    type => 'staff',
+});
+ok($script->authtoken, 'Initial Login');
+
+SKIP: {
+    my $ws = find_workstation();
+    skip 'Workstation exists', 1 if ($ws);
+    $ws = $script->register_workstation(WORKSTATION_NAME, WORKSTATION_LIB) unless ($ws);
+    ok(! ref $ws, 'Registered a new workstation');
+}
+
+$script->logout();
+$script->authenticate({
+    username => 'admin',
+    password => 'demo123',
+    type => 'staff',
+    workstation => WORKSTATION_NAME
+});
+ok($script->authtoken, 'Login with workstaion');
+
+# Get a CStoreEditor for later use.
+my $editor = $script->editor(authtoken=>$script->authtoken);
+my $staff = $editor->checkauth();
+ok(ref($staff), 'Got a staff user');
+
+# We retrieve STAFF_CHR block and check that it has an undefined
+# ignore_proximity.
+my $staff_chr = retrieve_staff_chr($editor);
+isa_ok($staff_chr, 'Fieldmapper::config::standing_penalty', 'STAFF_CHR');
+is($staff_chr->name, 'STAFF_CHR', 'Penalty name is STAFF_CHR');
+is($staff_chr->ignore_proximity, undef, 'STAFF_CHR ignore_proximity is undefined');
+
+# We set the ignore_proximity to 0.
+$staff_chr->ignore_proximity(0);
+ok(! ref update_staff_chr($editor, $staff_chr), 'Update of STAFF_CHR');
+
+# We need a patron with no penalties to test holds and circulation.
+my $patron = retrieve_user_by_barcode("99999350419");
+isa_ok($patron, 'Fieldmapper::actor::user', 'Patron');
+
+# Patron should have no penalties.
+ok(! scalar(@{$patron->standing_penalties()}), 'Patron has no penalties');
+
+# Add the STAFF_CHR to the patron
+my $penalty = apply_staff_chr_to_patron($staff, $patron);
+ok(ref $penalty, 'Added STAFF_CHR to patron');
+is(patron_sip_test($patron->id()), 0, 'SIP says patron has no penalties');
+
+# See if we can place a hold on a copy owned by BR1.
+is(copy_hold_permit_test($editor, $patron, "CONC4300036"), 1, 'Can place hold on copy from BR1');
+# We should not be able to place a  hold on a copy owned by a different branch.
+is(copy_hold_permit_test($editor, $patron, "CONC51000636"), 0, 'Cannot place hold on copy from BR2');
+
+# See if we can check out a copy owned by branch 4 out to the patron.
+# This should succeed.
+ok(checkout_permit_test($patron, "CONC4300036"), 'Can checkout copy from BR1');
+
+# We should not be able to checkout a copy owned by a different branch.
+ok(!checkout_permit_test($patron, "CONC51000636"), 'Cannot checkout copy from BR2');
+
+# We reset the ignore_proximity of STAFF_CHR.
+$staff_chr->clear_ignore_proximity();
+ok(! ref update_staff_chr($editor, $staff_chr), 'Reset of STAFF_CHR');
+is(patron_sip_test($patron->id()), 1, 'SIP says patron has one penalty');
+
+# See if we can place a hold on a copy owned by BR1.
+is(copy_hold_permit_test($editor, $patron, "CONC4300036"), 0, 'Cannot place hold on copy from BR1');
+# We should not be able to place a  hold on a copy owned by a different branch.
+is(copy_hold_permit_test($editor, $patron, "CONC51000636"), 0, 'Cannot place hold on copy from BR2');
+
+# See if we can check out a copy owned by branch 4 out to the patron.
+# This should succeed.
+ok(!checkout_permit_test($patron, "CONC4300036"), 'Cannot checkout copy from BR1');
+
+# We should not be able to checkout a copy owned by a different branch.
+ok(!checkout_permit_test($patron, "CONC51000636"), 'Cannot checkout copy from BR2');
+
+# We remove the STAFF_CHR from our test patron.
+my $r = remove_staff_chr_from_patron($penalty);
+ok( ! ref $r, 'STAFF_CHR removed from patron');
+
+# Do the checks again, all should pass.
+is(patron_sip_test($patron->id()), 0, 'SIP says patron has no penalties');
+
+# See if we can place a hold on a copy owned by BR1.
+is(copy_hold_permit_test($editor, $patron, "CONC4300036"), 1, 'Can place hold on copy from BR1');
+# We should now be able to place a  hold on a copy owned by a different branch.
+is(copy_hold_permit_test($editor, $patron, "CONC51000636"), 1, 'Can place hold on copy from BR2');
+
+# See if we can check out a copy owned by branch 4 out to the patron.
+# This should succeed.
+ok(checkout_permit_test($patron, "CONC4300036"), 'Can checkout copy from BR1');
+
+# We should not be able to checkout a copy owned by a different branch.
+ok(checkout_permit_test($patron, "CONC51000636"), 'Can checkout copy from BR2');
+
+$script->logout();

commit 63205ed42a72a3cb6b3c7d7d3d76154dab40be1f
Author: Jason Stephenson <jason at sigio.com>
Date:   Thu Sep 24 20:35:50 2015 -0400

    LP 1499123: Add ignore_proximity to config.standing_penalty.
    
    This commit adds the integer column ignore_proximity to the
    config.standing_penalty table.  It also adds the column to the
    csp class entry in the IDL.
    
    It also modifies the action.hold_permit_test() function from
    110.hold_matrix.sql to use the ignore_proximity field from
    config.standing_penalty when checking the user's penalties to see if
    they block the hold.
    
    We also modify the action.item_user_circ_test() function from
    100.circ_matrix.sql to use the ignore_proximity field from the
    config.standing_penalty table when checking to see if the user's
    penalties block the circulation.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 75c7f12..421b967 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3758,6 +3758,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field name="block_list" reporter:datatype="text"/>
 			<field name="staff_alert" reporter:datatype="bool"/>
 			<field name="org_depth" reporter:datatype="int"/>
+			<field name="ignore_proximity" reporter:datatype="int"/>
 		</fields>
 		<links/>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 5381cfd..f46affe 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -128,7 +128,8 @@ CREATE TABLE config.standing_penalty (
 	label		TEXT	NOT NULL,
 	block_list	TEXT,
 	staff_alert	BOOL	NOT NULL DEFAULT FALSE,
-	org_depth	INTEGER
+	org_depth	INTEGER,
+	ignore_proximity INTEGER
 );
 
 CREATE TABLE config.xml_transform (
diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
index 837c34e..4226a27 100644
--- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql
+++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
@@ -427,6 +427,8 @@ DECLARE
     items_out               INT;
     context_org_list        INT[];
     done                    BOOL := FALSE;
+    item_prox               INT;
+    home_prox               INT;
 BEGIN
     -- Assume success unless we hit a failure condition
     result.success := TRUE;
@@ -522,6 +524,12 @@ BEGIN
     -- Use Circ OU for penalties and such
     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
 
+    -- Proximity of user's home_ou to circ_ou to see if penalties should be ignored.
+    SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = circ_ou;
+
+    -- Proximity of user's home_ou to item circ_lib to see if penalties should be ignored.
+    SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
+
     IF renewal THEN
         penalty_type = '%RENEW%';
     ELSE
@@ -535,6 +543,9 @@ BEGIN
           WHERE usr = match_user
                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND (csp.ignore_proximity IS NULL
+                     OR csp.ignore_proximity < home_prox
+                     OR csp.ignore_proximity < item_prox)
                 AND csp.block_list LIKE penalty_type LOOP
 
         result.fail_part := standing_penalty.name;
diff --git a/Open-ILS/src/sql/Pg/110.hold_matrix.sql b/Open-ILS/src/sql/Pg/110.hold_matrix.sql
index 8f6518b..fe17a9a 100644
--- a/Open-ILS/src/sql/Pg/110.hold_matrix.sql
+++ b/Open-ILS/src/sql/Pg/110.hold_matrix.sql
@@ -241,6 +241,8 @@ DECLARE
     hold_penalty TEXT;
     v_pickup_ou ALIAS FOR pickup_ou;
     v_request_ou ALIAS FOR request_ou;
+    item_prox INT;
+    pickup_prox INT;
 BEGIN
     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
@@ -358,6 +360,15 @@ BEGIN
         END IF;
     END IF;
  
+    -- Proximity of user's home_ou to the pickup_lib to see if penalty should be ignored.
+    SELECT INTO pickup_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = v_pickup_ou;
+    -- Proximity of user's home_ou to the items' lib to see if penalty should be ignored.
+    IF hold_test.distance_is_from_owner THEN
+        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_cn_object.owning_lib;
+    ELSE
+        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
+    END IF;
+
     FOR standing_penalty IN
         SELECT  DISTINCT csp.*
           FROM  actor.usr_standing_penalty usp
@@ -365,6 +376,8 @@ BEGIN
           WHERE usr = match_user
                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND (csp.ignore_proximity IS NULL OR csp.ignore_proximity < item_prox
+                     OR csp.ignore_proximity < pickup_prox)
                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
 
         result.fail_part := standing_penalty.name;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql
new file mode 100644
index 0000000..615350e
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql
@@ -0,0 +1,478 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+ALTER TABLE config.standing_penalty
+      ADD COLUMN ignore_proximity INTEGER;
+
+CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
+DECLARE
+    matchpoint_id        INT;
+    user_object        actor.usr%ROWTYPE;
+    age_protect_object    config.rule_age_hold_protect%ROWTYPE;
+    standing_penalty    config.standing_penalty%ROWTYPE;
+    transit_range_ou_type    actor.org_unit_type%ROWTYPE;
+    transit_source        actor.org_unit%ROWTYPE;
+    item_object        asset.copy%ROWTYPE;
+    item_cn_object     asset.call_number%ROWTYPE;
+    item_status_object  config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    ou_skip              actor.org_unit_setting%ROWTYPE;
+    result            action.matrix_test_result;
+    hold_test        config.hold_matrix_matchpoint%ROWTYPE;
+    use_active_date   TEXT;
+    age_protect_date  TIMESTAMP WITH TIME ZONE;
+    hold_count        INT;
+    hold_transit_prox    INT;
+    frozen_hold_count    INT;
+    context_org_list    INT[];
+    done            BOOL := FALSE;
+    hold_penalty TEXT;
+    v_pickup_ou ALIAS FOR pickup_ou;
+    v_request_ou ALIAS FOR request_ou;
+    item_prox INT;
+    pickup_prox INT;
+BEGIN
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
+
+    result.success := TRUE;
+
+    -- The HOLD penalty block only applies to new holds.
+    -- The CAPTURE penalty block applies to existing holds.
+    hold_penalty := 'HOLD';
+    IF retargetting THEN
+        hold_penalty := 'CAPTURE';
+    END IF;
+
+    -- Fail if we couldn't find a user
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
+
+    -- Fail if we couldn't find a copy
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
+    result.matchpoint := matchpoint_id;
+
+    SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
+
+    -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
+    IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
+        result.fail_part := 'circ.holds.target_skip_me';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- Fail if user is barred
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
+    SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+
+    -- Fail if we couldn't find any matchpoint (requires a default)
+    IF matchpoint_id IS NULL THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
+
+    IF hold_test.holdable IS FALSE THEN
+        result.fail_part := 'config.hold_matrix_test.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF item_object.holdable IS FALSE THEN
+        result.fail_part := 'item.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF item_status_object.holdable IS FALSE THEN
+        result.fail_part := 'status.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF item_location_object.holdable IS FALSE THEN
+        result.fail_part := 'location.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF hold_test.transit_range IS NOT NULL THEN
+        SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
+        ELSE
+            SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
+        END IF;
+
+        PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
+
+        IF NOT FOUND THEN
+            result.fail_part := 'transit_range';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+ 
+    -- Proximity of user's home_ou to the pickup_lib to see if penalty should be ignored.
+    SELECT INTO pickup_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = v_pickup_ou;
+    -- Proximity of user's home_ou to the items' lib to see if penalty should be ignored.
+    IF hold_test.distance_is_from_owner THEN
+        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_cn_object.owning_lib;
+    ELSE
+        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
+    END IF;
+
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND (csp.ignore_proximity IS NULL OR csp.ignore_proximity < item_prox
+                     OR csp.ignore_proximity < pickup_prox)
+                AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    IF hold_test.stop_blocked_user IS TRUE THEN
+        FOR standing_penalty IN
+            SELECT  DISTINCT csp.*
+              FROM  actor.usr_standing_penalty usp
+                    JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+              WHERE usr = match_user
+                    AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                    AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                    AND csp.block_list LIKE '%CIRC%' LOOP
+    
+            result.fail_part := standing_penalty.name;
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END LOOP;
+    END IF;
+
+    IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
+        SELECT    INTO hold_count COUNT(*)
+          FROM    action.hold_request
+          WHERE    usr = match_user
+            AND fulfillment_time IS NULL
+            AND cancel_time IS NULL
+            AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
+
+        IF hold_count >= hold_test.max_holds THEN
+            result.fail_part := 'config.hold_matrix_test.max_holds';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    IF item_object.age_protect IS NOT NULL THEN
+        SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
+        ELSE
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
+        END IF;
+        IF use_active_date = 'true' THEN
+            age_protect_date := COALESCE(item_object.active_date, NOW());
+        ELSE
+            age_protect_date := item_object.create_date;
+        END IF;
+        IF age_protect_date + age_protect_object.age > NOW() THEN
+            IF hold_test.distance_is_from_owner THEN
+                SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
+                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = v_pickup_ou;
+            ELSE
+                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = v_pickup_ou;
+            END IF;
+
+            IF hold_transit_prox > age_protect_object.prox THEN
+                result.fail_part := 'config.rule_age_hold_protect.prox';
+                result.success := FALSE;
+                done := TRUE;
+                RETURN NEXT result;
+            END IF;
+        END IF;
+    END IF;
+
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+DECLARE
+    user_object             actor.usr%ROWTYPE;
+    standing_penalty        config.standing_penalty%ROWTYPE;
+    item_object             asset.copy%ROWTYPE;
+    item_status_object      config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    result                  action.circ_matrix_test_result;
+    circ_test               action.found_circ_matrix_matchpoint;
+    circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
+    circ_limit_set          config.circ_limit_set%ROWTYPE;
+    hold_ratio              action.hold_stats%ROWTYPE;
+    penalty_type            TEXT;
+    items_out               INT;
+    context_org_list        INT[];
+    done                    BOOL := FALSE;
+    item_prox               INT;
+    home_prox               INT;
+BEGIN
+    -- Assume success unless we hit a failure condition
+    result.success := TRUE;
+
+    -- Need user info to look up matchpoints
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
+
+    -- (Insta)Fail if we couldn't find the user
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- Need item info to look up matchpoints
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
+
+    -- (Insta)Fail if we couldn't find the item 
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
+
+    circ_matchpoint             := circ_test.matchpoint;
+    result.matchpoint           := circ_matchpoint.id;
+    result.circulate            := circ_matchpoint.circulate;
+    result.duration_rule        := circ_matchpoint.duration_rule;
+    result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
+    result.max_fine_rule        := circ_matchpoint.max_fine_rule;
+    result.hard_due_date        := circ_matchpoint.hard_due_date;
+    result.renewals             := circ_matchpoint.renewals;
+    result.grace_period         := circ_matchpoint.grace_period;
+    result.buildrows            := circ_test.buildrows;
+
+    -- (Insta)Fail if we couldn't find a matchpoint
+    IF circ_test.success = false THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- All failures before this point are non-recoverable
+    -- Below this point are possibly overridable failures
+
+    -- Fail if the user is barred
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate
+    IF item_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item isn't in a circulateable status on a non-renewal
+    IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    -- Alternately, fail if the item isn't checked out on a renewal
+    ELSIF renewal AND item_object.status <> 1 THEN
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate because of the shelving location
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+    IF item_location_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy_location.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Use Circ OU for penalties and such
+    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
+
+    -- Proximity of user's home_ou to circ_ou to see if penalties should be ignored.
+    SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = circ_ou;
+
+    -- Proximity of user's home_ou to item circ_lib to see if penalties should be ignored.
+    SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
+
+    IF renewal THEN
+        penalty_type = '%RENEW%';
+    ELSE
+        penalty_type = '%CIRC%';
+    END IF;
+
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND (csp.ignore_proximity IS NULL
+                     OR csp.ignore_proximity < home_prox
+                     OR csp.ignore_proximity < item_prox)
+                AND csp.block_list LIKE penalty_type LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    -- Fail if the test is set to hard non-circulating
+    IF circ_matchpoint.circulate IS FALSE THEN
+        result.fail_part := 'config.circ_matrix_test.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the total copy-hold ratio is too low
+    IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
+        SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the available copy-hold ratio is too low
+    IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
+        IF hold_ratio.hold_count IS NULL THEN
+            SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        END IF;
+        IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the user has too many items out by defined limit sets
+    FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
+      JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
+      WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
+        ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
+        ) LOOP
+            IF circ_limit_set.items_out > 0 AND NOT renewal THEN
+                SELECT INTO context_org_list ARRAY_AGG(aou.id)
+                  FROM actor.org_unit_full_path( circ_ou ) aou
+                    JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
+                  WHERE aout.depth >= circ_limit_set.depth;
+                IF circ_limit_set.global THEN
+                    WITH RECURSIVE descendant_depth AS (
+                        SELECT  ou.id,
+                            ou.parent_ou
+                        FROM  actor.org_unit ou
+                        WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
+                            UNION
+                        SELECT  ou.id,
+                            ou.parent_ou
+                        FROM  actor.org_unit ou
+                            JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
+                    ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
+                END IF;
+                SELECT INTO items_out COUNT(DISTINCT circ.id)
+                  FROM action.circulation circ
+                    JOIN asset.copy copy ON (copy.id = circ.target_copy)
+                    LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
+                  WHERE circ.usr = match_user
+                    AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
+                    AND circ.checkin_time IS NULL
+                    AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
+                    AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
+                        OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
+                        OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
+                    );
+                IF items_out >= circ_limit_set.items_out THEN
+                    result.fail_part := 'config.circ_matrix_circ_mod_test';
+                    result.success := FALSE;
+                    done := TRUE;
+                    RETURN NEXT result;
+                END IF;
+            END IF;
+            SELECT INTO result.limit_groups result.limit_groups || ARRAY_AGG(limit_group) FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id AND NOT check_only;
+    END LOOP;
+
+    -- If we passed everything, return the successful matchpoint
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+COMMIT;

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |    1 +
 .../perlmods/lib/OpenILS/Application/AppUtils.pm   |   17 +
 .../lib/OpenILS/Application/Circ/Circulate.pm      |   25 +-
 Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm    |   58 ++-
 .../live_t/12-lp1499123_csp_ignore_proximity.t     |  252 ++++++++++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    3 +-
 Open-ILS/src/sql/Pg/100.circ_matrix.sql            |   11 +
 Open-ILS/src/sql/Pg/110.hold_matrix.sql            |   13 +
 ...ma.config.standing_penalty.ignore_proximity.sql |  478 ++++++++++++++++++++
 .../standing_penalty_ignore_proximity.txt          |   30 ++
 10 files changed, 876 insertions(+), 12 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/live_t/12-lp1499123_csp_ignore_proximity.t
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/standing_penalty_ignore_proximity.txt


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list