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

Evergreen Git git at git.evergreen-ils.org
Tue Jul 24 08:11:35 EDT 2012


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  8f4f548ad2c13ca62e3a8906a701ceb02b4cabfc (commit)
       via  8ebc2398f1dc28a17bd82809ab2b806766f95499 (commit)
       via  74ffd2aabb28b0655102531ac55c4a7e1ad38b9b (commit)
       via  f60b9829c0a970ac94e001270e2d8bb57c5e1584 (commit)
      from  a91ca6193237a2ee1eb8e640636fb607e73867ab (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 8f4f548ad2c13ca62e3a8906a701ceb02b4cabfc
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Jul 24 08:11:23 2012 -0400

    Stamping upgrade for Copy Location Circ Limits
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index e3d73ee..a527fd3 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -87,7 +87,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0719', :eg_version); -- eeevil/senator
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0720', :eg_version); -- berick/miker
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql b/Open-ILS/src/sql/Pg/upgrade/0720.schema.copy_loc_circ_limits.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
rename to Open-ILS/src/sql/Pg/upgrade/0720.schema.copy_loc_circ_limits.sql
index b10e25e..a603f47 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0720.schema.copy_loc_circ_limits.sql
@@ -1,3 +1,6 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('0720', :eg_version);
 
 ALTER TABLE config.circ_matrix_weights 
     ADD COLUMN copy_location NUMERIC(6,2) NOT NULL DEFAULT 5.0;
@@ -434,4 +437,5 @@ BEGIN
 END;
 $func$ LANGUAGE plpgsql;
 
+COMMIT;
 

commit 8ebc2398f1dc28a17bd82809ab2b806766f95499
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Jun 22 12:04:41 2012 -0400

    Add Copy Location to circ matrix matchpoint
    
    Similar to circulation modifiers, circ policies can now be based on copy
    location.
    
    This also adds copy location to the circ matrix weights.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index d51fc02..a603348 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -1413,6 +1413,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="User Home Lib" name="user_home_ou" reporter:datatype="float"/>
             <field reporter:label="Permission Group" name="grp" reporter:datatype="float"/>
             <field reporter:label="Circulation Modifier" name="circ_modifier" reporter:datatype="float"/>
+            <field reporter:label="Copy Location" name="copy_location" reporter:datatype="float"/>
             <field reporter:label="MARC Type" name="marc_type" reporter:datatype="float"/>
             <field reporter:label="MARC Form" name="marc_form" reporter:datatype="float"/>
             <field reporter:label="MARC Bib Level" name="marc_bib_level" reporter:datatype="float"/>
@@ -1520,6 +1521,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="User Home Lib" name="user_home_ou" reporter:datatype="org_unit"/>
 			<field reporter:label="Permission Group" name="grp" reporter:datatype="link" oils_obj:required="true"/>
 			<field reporter:label="Circulation Modifier" name="circ_modifier" oils_persist:primitive="string" reporter:datatype="link"/>
+			<field reporter:label="Copy Location" name="copy_location" reporter:datatype="link"/>
 			<field reporter:label="MARC Type" name="marc_type" oils_persist:primitive="string" reporter:datatype="link"/>
 			<field reporter:label="MARC Form" name="marc_form" oils_persist:primitive="string" reporter:datatype="link"/>
 			<field reporter:label="MARC Bib Level" name="marc_bib_level" oils_persist:primitive="string" reporter:datatype="link"/>
@@ -1547,6 +1549,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<link field="user_home_ou" reltype="has_a" key="id" map="" class="aou"/>
 			<link field="grp" reltype="has_a" key="id" map="" class="pgt"/>
 			<link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
+			<link field="copy_location" reltype="has_a" key="id" map="" class="acpl"/>
 			<link field="marc_type" reltype="has_a" key="code" map="" class="citm"/>
 			<link field="marc_form" reltype="has_a" key="code" map="" class="cifm"/>
 			<link field="marc_bib_level" reltype="has_a" key="code" map="" class="cblvl"/>
diff --git a/Open-ILS/src/sql/Pg/099.matrix_weights.sql b/Open-ILS/src/sql/Pg/099.matrix_weights.sql
index 5854d3e..2221011 100644
--- a/Open-ILS/src/sql/Pg/099.matrix_weights.sql
+++ b/Open-ILS/src/sql/Pg/099.matrix_weights.sql
@@ -8,6 +8,7 @@ CREATE TABLE config.circ_matrix_weights (
     org_unit                NUMERIC(6,2)   NOT NULL,
     grp                     NUMERIC(6,2)   NOT NULL,
     circ_modifier           NUMERIC(6,2)   NOT NULL,
+    copy_location           NUMERIC(6,2)   NOT NULL,
     marc_type               NUMERIC(6,2)   NOT NULL,
     marc_form               NUMERIC(6,2)   NOT NULL,
     marc_bib_level          NUMERIC(6,2)   NOT NULL,
diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
index 94bc7e3..3dbb328 100644
--- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql
+++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
@@ -57,6 +57,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
     org_unit             INT        NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,    -- Set to the top OU for the matchpoint applicability range; we can use org_unit_prox to choose the "best"
     grp                  INT     NOT NULL REFERENCES permission.grp_tree (id) DEFERRABLE INITIALLY DEFERRED,    -- Set to the top applicable group from the group tree; will need descendents and prox functions for filtering
     circ_modifier        TEXT    REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED,
+    copy_location        INT     REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED,
     marc_type            TEXT,
     marc_form            TEXT,
     marc_bib_level       TEXT,
@@ -84,7 +85,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
 );
 
 -- Nulls don't count for a constraint match, so we have to coalesce them into something that does.
-CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
+CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(copy_location::TEXT, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
 
 -- Limit groups for circ counting
 CREATE TABLE config.circ_limit_group (
@@ -195,6 +196,7 @@ BEGIN
         weights.grp                 := 11.0;
         weights.org_unit            := 10.0;
         weights.circ_modifier       := 5.0;
+        weights.copy_location       := 5.0;
         weights.marc_type           := 4.0;
         weights.marc_form           := 3.0;
         weights.marc_bib_level      := 2.0;
@@ -246,6 +248,7 @@ BEGIN
                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
                 -- Static Item Checks
                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+                AND (m.copy_location            IS NULL OR m.copy_location = item_object.location)
                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
@@ -268,6 +271,7 @@ BEGIN
                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
                 -- Static Item Checks
                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+                CASE WHEN m.copy_location       IS NOT NULL THEN 4^weights.copy_location ELSE 0.0 END +
                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index c41aa9d..312a425 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -2419,11 +2419,11 @@ INSERT INTO asset.call_number VALUES (-1,1,NOW(),1,NOW(),-1,1,'UNCATALOGED');
 -- circ matrix
 INSERT INTO config.circ_matrix_matchpoint (org_unit,grp,circulate,duration_rule,recurring_fine_rule,max_fine_rule) VALUES (1,1,true,11,1,1);
 
-INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound, item_age) VALUES 
-    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
-    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
-    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
-    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, copy_location, marc_type, marc_form, marc_bib_level, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound, item_age) VALUES 
+    ('Default', 10.0, 11.0, 5.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
+    ('Org_Unit_First', 11.0, 10.0, 5.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
+    ('Item_Owner_First', 8.0, 8.0, 5.0, 5.0, 4.0, 3.0, 2.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
 
 -- hold matrix - 110.hold_matrix.sql:
 INSERT INTO config.hold_matrix_matchpoint (requestor_grp) VALUES (1);
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
index 7bd883d..b10e25e 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
@@ -1,4 +1,18 @@
 
+ALTER TABLE config.circ_matrix_weights 
+    ADD COLUMN copy_location NUMERIC(6,2) NOT NULL DEFAULT 5.0;
+UPDATE config.circ_matrix_weights 
+    SET copy_location = 0.0 WHERE name = 'All_Equal';
+ALTER TABLE config.circ_matrix_weights 
+    ALTER COLUMN copy_location DROP DEFAULT; -- for consistency w/ baseline schema
+
+ALTER TABLE config.circ_matrix_matchpoint
+    ADD COLUMN copy_location INTEGER REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED;
+
+DROP INDEX config.ccmm_once_per_paramset;
+
+CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(copy_location::TEXT, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
+
 -- Linkage between limit sets and circ mods
 CREATE TABLE config.circ_limit_set_copy_loc_map (
     id          SERIAL  PRIMARY KEY,
@@ -228,3 +242,196 @@ BEGIN
 END;
 $func$ LANGUAGE plpgsql;
 
+
+-- adding copy_loc to circ_matrix_matchpoint
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
+DECLARE
+    cn_object       asset.call_number%ROWTYPE;
+    rec_descriptor  metabib.rec_descriptor%ROWTYPE;
+    cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
+    matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
+    weights         config.circ_matrix_weights%ROWTYPE;
+    user_age        INTERVAL;
+    my_item_age     INTERVAL;
+    denominator     NUMERIC(6,2);
+    row_list        INT[];
+    result          action.found_circ_matrix_matchpoint;
+BEGIN
+    -- Assume failure
+    result.success = false;
+
+    -- Fetch useful data
+    SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
+
+    -- Pre-generate this so we only calc it once
+    IF user_object.dob IS NOT NULL THEN
+        SELECT INTO user_age age(user_object.dob);
+    END IF;
+
+    -- Ditto
+    SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
+
+    -- Grab the closest set circ weight setting.
+    SELECT INTO weights cw.*
+      FROM config.weight_assoc wa
+           JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
+           JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
+      WHERE active
+      ORDER BY d.distance
+      LIMIT 1;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.grp                 := 11.0;
+        weights.org_unit            := 10.0;
+        weights.circ_modifier       := 5.0;
+        weights.copy_location       := 5.0;
+        weights.marc_type           := 4.0;
+        weights.marc_form           := 3.0;
+        weights.marc_bib_level      := 2.0;
+        weights.marc_vr_format      := 2.0;
+        weights.copy_circ_lib       := 8.0;
+        weights.copy_owning_lib     := 8.0;
+        weights.user_home_ou        := 8.0;
+        weights.ref_flag            := 1.0;
+        weights.juvenile_flag       := 6.0;
+        weights.is_renewal          := 7.0;
+        weights.usr_age_lower_bound := 0.0;
+        weights.usr_age_upper_bound := 0.0;
+        weights.item_age            := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+       	    SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+	)
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- Loop over all the potential matchpoints
+    FOR cur_matchpoint IN
+        SELECT m.*
+          FROM  config.circ_matrix_matchpoint m
+                /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
+                /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+          WHERE m.active
+                -- Permission Groups
+             -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
+                -- Org Units
+             -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
+                AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
+                AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
+                AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
+                -- Circ Type
+                AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
+                -- Static User Checks
+                AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
+                AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
+                AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
+                -- Static Item Checks
+                AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+                AND (m.copy_location            IS NULL OR m.copy_location = item_object.location)
+                AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+                AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+                AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+                AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+                AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+                AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
+          ORDER BY
+                -- Permission Groups
+                CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
+                -- Org Units
+                CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+                -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
+                CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
+                -- Static User Checks
+                CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+                CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
+                CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
+                -- Static Item Checks
+                CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+                CASE WHEN m.copy_location       IS NOT NULL THEN 4^weights.copy_location ELSE 0.0 END +
+                CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+                CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+                CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+                CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
+                -- Item age has a slight adjustment to weight based on value.
+                -- This should ensure that a shorter age limit comes first when all else is equal.
+                -- NOTE: This assumes that intervals will normally be in days.
+                CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
+                -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+                -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+                m.id LOOP
+
+        -- Record the full matching row list
+        row_list := row_list || cur_matchpoint.id;
+
+        -- No matchpoint yet?
+        IF matchpoint.id IS NULL THEN
+            -- Take the entire matchpoint as a starting point
+            matchpoint := cur_matchpoint;
+            CONTINUE; -- No need to look at this row any more.
+        END IF;
+
+        -- Incomplete matchpoint?
+        IF matchpoint.circulate IS NULL THEN
+            matchpoint.circulate := cur_matchpoint.circulate;
+        END IF;
+        IF matchpoint.duration_rule IS NULL THEN
+            matchpoint.duration_rule := cur_matchpoint.duration_rule;
+        END IF;
+        IF matchpoint.recurring_fine_rule IS NULL THEN
+            matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
+        END IF;
+        IF matchpoint.max_fine_rule IS NULL THEN
+            matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
+        END IF;
+        IF matchpoint.hard_due_date IS NULL THEN
+            matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
+        END IF;
+        IF matchpoint.total_copy_hold_ratio IS NULL THEN
+            matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
+        END IF;
+        IF matchpoint.available_copy_hold_ratio IS NULL THEN
+            matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
+        END IF;
+        IF matchpoint.renewals IS NULL THEN
+            matchpoint.renewals := cur_matchpoint.renewals;
+        END IF;
+        IF matchpoint.grace_period IS NULL THEN
+            matchpoint.grace_period := cur_matchpoint.grace_period;
+        END IF;
+    END LOOP;
+
+    -- Check required fields
+    IF matchpoint.circulate             IS NOT NULL AND
+       matchpoint.duration_rule         IS NOT NULL AND
+       matchpoint.recurring_fine_rule   IS NOT NULL AND
+       matchpoint.max_fine_rule         IS NOT NULL THEN
+        -- All there? We have a completed match.
+        result.success := true;
+    END IF;
+
+    -- Include the assembled matchpoint, even if it isn't complete
+    result.matchpoint := matchpoint;
+
+    -- Include (for debugging) the full list of matching rows
+    result.buildrows := row_list;
+
+    -- Hand the result back to caller
+    RETURN result;
+END;
+$func$ LANGUAGE plpgsql;
+
+
diff --git a/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2 b/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2
index 6bdfced..04a83bc 100644
--- a/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2
+++ b/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2
@@ -9,7 +9,7 @@
     <table  jsId="cmGrid"
             style="height: 600px;"
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['id', 'active', 'grp', 'org_unit', 'copy_circ_lib', 'copy_owning_lib', 'user_home_ou', 'is_renewal', 'juvenile_flag', 'circ_modifier', 'marc_type', 'marc_form', 'marc_bib_level', 'marc_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'item_age', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', 'grace_period', 'max_fine_rule', 'available_copy_hold_ratio', 'total_copy_hold_ratio', 'script_test']"
+            fieldOrder="['id', 'active', 'grp', 'org_unit', 'copy_circ_lib', 'copy_owning_lib', 'user_home_ou', 'is_renewal', 'juvenile_flag', 'circ_modifier', 'copy_location', 'marc_type', 'marc_form', 'marc_bib_level', 'marc_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'item_age', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', 'grace_period', 'max_fine_rule', 'available_copy_hold_ratio', 'total_copy_hold_ratio', 'script_test']"
             defaultCellWidth='"auto"'
             query="{id: '*'}"
             fmClass='ccmm'

commit 74ffd2aabb28b0655102531ac55c4a7e1ad38b9b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Apr 13 16:19:03 2012 -0400

    Copy Location Circ Limit Sets Admin UI
    
    Changes are applied to the existing Admin -> Local Administration ->
    Circ Limit Sets interface to support copy location limit groups.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    
    Copy Location Circ Limit Sets : UI 2
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2 b/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2
index 9f9b90f..7541673 100644
--- a/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2
+++ b/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2
@@ -46,6 +46,27 @@
                 </tr>
             </tbody>
         </table>
+        <h3>[% l('Linked Copy Locations') %]</h3>
+        <table class='oils-generic-table'>
+            <tbody>
+                <tr>
+                    <th>[% l('Name') %]</th>
+                    <th>[% l('Remove') %]</th>
+                </tr>
+            </tbody>
+            <tbody name='copy-loc-entry-tbody'>
+                <tr name='copy-loc-entry-row'>
+                    <td name='copy-loc'></td>
+                    <td><a name='remove-copy-loc' href='javascript:void(0);'>[% l('Remove') %]</a></td>
+                </tr>
+            </tbody>
+            <tbody name='copy-loc-entry-new'>
+                <tr>
+                    <td><div name='copy-loc-selector'></div></td>
+                    <td><a href='javascript:void(0);' name='add-copy-loc'>[% l('Add') %]</a></td>
+                </tr>
+            </tbody>
+        </table>
         <h3>Linked Limit Groups</h3>
         <table class='oils-generic-table'>
             <tbody>
diff --git a/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js b/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js
index 49d6aa8..eca7c98 100644
--- a/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js
+++ b/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js
@@ -4,11 +4,14 @@ dojo.require('openils.widget.AutoGrid');
 dojo.require('openils.widget.AutoFieldWidget');
 dojo.require('openils.PermaCrud');
 dojo.require('openils.widget.ProgressDialog');
+dojo.require('openils.User');
 
 var linkedEditor = null;
 var circModEntryCache = [];
+var copyLocEntryCache = [];
 var limitGroupEntryCache = [];
 var circModCache = {};
+var copyLocCache = {};
 var limitGroupCache = {};
 var curLinkedEditor;
 
@@ -25,6 +28,17 @@ function load(){
     dojo.forEach(temp, function(g) { circModCache[g.code()] = g; } );
     temp = pcrud.retrieveAll('cclg');
     dojo.forEach(temp, function(g) { limitGroupCache[g.id()] = g; } );
+
+    // Avoid fetching all copy locations because there can be many 
+    // thousands.  Limit to those within permission range of the user.
+    new openils.User().getPermOrgList(
+        'ADMIN_CIRC_MATRIX_MATCHPOINT',
+        function (orgList) {
+            temp = pcrud.search('acpl', {owning_lib : orgList});
+            dojo.forEach(temp, function(g) { copyLocCache[g.id()] = g; } );
+        }, 
+        true, true 
+    );
 }
 
 function byName(name, ctxt) {
@@ -34,6 +48,7 @@ function byName(name, ctxt) {
 function buildEditPaneAdditions(editPane) {
     circModEntryCache = [];
     limitGroupEntryCache = [];
+    copyLocEntryCache = [];
     var tr = document.createElement('tr');
     var td = document.createElement('td');
     td.setAttribute('colspan','2');
@@ -46,6 +61,7 @@ function buildEditPaneAdditions(editPane) {
     curLinkedEditor = linkedEditor.cloneNode(true);
     td.appendChild(curLinkedEditor);
     var circModTmpl = byName('circ-mod-entry-tbody', curLinkedEditor).removeChild(byName('circ-mod-entry-row', curLinkedEditor));
+    var copyLocTmpl = byName('copy-loc-entry-tbody', curLinkedEditor).removeChild(byName('copy-loc-entry-row', curLinkedEditor));
     var limitGroupTmpl = byName('limit-group-entry-tbody', curLinkedEditor).removeChild(byName('limit-group-entry-row', curLinkedEditor));
 
     var cm_selector = new openils.widget.AutoFieldWidget({
@@ -55,6 +71,13 @@ function buildEditPaneAdditions(editPane) {
     });
     cm_selector.build();
 
+    var cl_selector = new openils.widget.AutoFieldWidget({
+        fmClass : 'cclsacpl',
+        fmField : 'copy_loc',
+        parentNode : byName('copy-loc-selector', curLinkedEditor)
+    });
+    cl_selector.build();
+
     var lg_selector = new openils.widget.AutoFieldWidget({
         fmClass : 'cclsgm',
         fmField : 'limit_group',
@@ -72,6 +95,19 @@ function buildEditPaneAdditions(editPane) {
         byName('circ-mod-entry-tbody', editPane.domNode).appendChild(row);
     }
 
+    function addLoc(id) {
+        var row = copyLocTmpl.cloneNode(true);
+        row.setAttribute('loc_id', id);
+        var copyloc = copyLocCache[id];
+        byName('copy-loc', row).innerHTML = 
+            fieldmapper.aou.findOrgUnit(copyloc.owning_lib()).shortname() +
+            ' : ' + copyloc.name();
+        byName('remove-copy-loc', row).onclick = function() {
+            byName('copy-loc-entry-tbody', clsGrid.editPane.domNode).removeChild(row);
+        }
+        byName('copy-loc-entry-tbody', editPane.domNode).appendChild(row);
+    }
+
     function addGroup(group) {
         var row = limitGroupTmpl.cloneNode(true);
         row.setAttribute('limit_group', group);
@@ -86,6 +122,10 @@ function buildEditPaneAdditions(editPane) {
         addMod(cm_selector.widget.attr('value'));
     }
 
+    byName('add-copy-loc', editPane.domNode).onclick = function() {
+        addLoc(cl_selector.widget.attr('value'));
+    }
+
     byName('add-limit-group', editPane.domNode).onclick = function() {
         addGroup(lg_selector.widget.attr('value'));
     }
@@ -98,8 +138,10 @@ function buildEditPaneAdditions(editPane) {
     if(editPane.mode == 'update') {
         var pcrud = new openils.PermaCrud();
         circModEntryCache = pcrud.search('cclscmm', {limit_set: limitSet});
+        copyLocEntryCache = pcrud.search('cclsacpl', {limit_set: limitSet});
         limitGroupEntryCache = pcrud.search('cclsgm', {limit_set: limitSet});
         dojo.forEach(circModEntryCache, function(g) { addCircMod(circModTmpl, g); } );
+        dojo.forEach(copyLocEntryCache, function(g) { addCopyLoc(copyLocTmpl, g); } );
         dojo.forEach(limitGroupEntryCache, function(g) { addLimitGroup(limitGroupTmpl, g); } );
     } 
 }
@@ -115,6 +157,20 @@ function addCircMod(tmpl, circ_mod_entry) {
     byName('circ-mod-entry-tbody', clsGrid.editPane.domNode).appendChild(row);
 }
 
+function addCopyLoc(tmpl, copy_loc_entry) {
+    var row = tmpl.cloneNode(true);
+    var id = copy_loc_entry.copy_loc();
+    var copyloc = copyLocCache[id];
+    row.setAttribute('loc_id', id);
+    byName('copy-loc', row).innerHTML = 
+        fieldmapper.aou.findOrgUnit(copyloc.owning_lib()).shortname() +
+        ' : ' + copyloc.name();
+    byName('remove-copy-loc', row).onclick = function() {
+        byName('copy-loc-entry-tbody', clsGrid.editPane.domNode).removeChild(row);
+    }
+    byName('copy-loc-entry-tbody', clsGrid.editPane.domNode).appendChild(row);
+}
+
 function addLimitGroup(tmpl, limit_group_entry) {
     var row = tmpl.cloneNode(true);
     var group = limit_group_entry.limit_group();
@@ -170,6 +226,30 @@ function updateLinked(fmObject, rowindex) {
         }
     );
 
+    // Next, copy locations
+    var copy_locs = [];
+    dojo.query('[name=copy-loc-entry-row]', this.editPane.domNode).forEach(
+        function(row) {
+            var loc_id = row.getAttribute('loc_id');
+            copy_locs.push(loc_id);
+            if(!copyLocEntryCache.filter(function(i) { return (i.copy_loc() == loc_id); })[0]) {
+                var entry = new fieldmapper.cclsacpl();
+                entry.isnew(true);
+                entry.limit_set(id);
+                entry.copy_loc(loc_id);
+                add.push(entry);
+            }
+        }
+    );
+    dojo.forEach(copyLocEntryCache, function(eLoc) {
+            if(!copy_locs.filter(function(i) { return (i == eLoc.copy_loc()); })[0]) {
+                eLoc.isdeleted(true);
+                remove.push(eLoc);
+            }
+        }
+    );
+
+
     // Next, limit groups
     var limit_groups = [];
     dojo.query('[name=limit-group-entry-row]', this.editPane.domNode).forEach(

commit f60b9829c0a970ac94e001270e2d8bb57c5e1584
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Apr 13 15:39:18 2012 -0400

    Copy Location Circ Limit Sets : DB and IDL
    
    Support for copy location-based circulation limit sets.  Similar to circ
    mod limit sets, this allows staff to configure a maximum number of items
    allowed checked out based on copy location(s) and link that rule to
    circulation policies to control who the rule affects.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 9c0ce4a..d51fc02 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -1659,6 +1659,31 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             </actions>
         </permacrud>
     </class>
+    <class id="cclsacpl" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_limit_set_copy_loc_map" oils_persist:tablename="config.circ_limit_set_copy_loc_map" reporter:label="Circulation Limit Set Copy Location Map">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.circ_limit_set_copy_loc_map_id_seq">
+            <field reporter:label="ID" name="id" reporter:datatype="id"/>
+            <field reporter:label="Limit Set" name="limit_set" reporter:datatype="link"/>
+            <field reporter:label="Copy Location" name="copy_loc" reporter:datatype="link"/>
+        </fields>
+        <links>
+            <link field="limit_set" reltype="has_a" key="id" map="" class="ccls"/>
+            <link field="copy_loc" reltype="has_a" key="id" map="" class="acpl"/>
+        </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
+                    <context link="limit_set" field="owning_lib"/>
+                </create>
+                <retrieve/>
+                <update permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
+                    <context link="limit_set" field="owning_lib"/>
+                </update>
+                <delete permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
+                    <context link="limit_set" field="owning_lib"/>
+                </delete>
+            </actions>
+        </permacrud>
+    </class>
 
     <class id="cclsgm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_limit_set_group_map" oils_persist:tablename="config.circ_limit_set_group_map" reporter:label="Circulation Limit Set Group Map">
         <fields oils_persist:primary="id" oils_persist:sequence="config.circ_limit_set_group_map_id_seq">
diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
index 7b9e2eb..94bc7e3 100644
--- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql
+++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
@@ -122,6 +122,14 @@ CREATE TABLE config.circ_limit_set_circ_mod_map (
     CONSTRAINT cm_once_per_set UNIQUE (limit_set, circ_mod)
 );
 
+-- Linkage between limit sets and copy locations
+CREATE TABLE config.circ_limit_set_copy_loc_map (
+    id          SERIAL  PRIMARY KEY,
+    limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    copy_loc    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT cl_once_per_set UNIQUE (limit_set, copy_loc)
+);
+
 -- Linkage between limit sets and limit groups
 CREATE TABLE config.circ_limit_set_group_map (
     id          SERIAL  PRIMARY KEY,
@@ -596,6 +604,7 @@ BEGIN
                     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
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
new file mode 100644
index 0000000..7bd883d
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
@@ -0,0 +1,230 @@
+
+-- Linkage between limit sets and circ mods
+CREATE TABLE config.circ_limit_set_copy_loc_map (
+    id          SERIAL  PRIMARY KEY,
+    limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    copy_loc    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT cl_once_per_set UNIQUE (limit_set, copy_loc)
+);
+
+-- Add support for checking config.circ_limit_set_copy_loc_map's
+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;
+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 );
+
+    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.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;
+

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   28 ++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/099.matrix_weights.sql         |    1 +
 Open-ILS/src/sql/Pg/100.circ_matrix.sql            |   15 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   10 +-
 ...ds.sql => 0720.schema.copy_loc_circ_limits.sql} |  509 +++++++++-----------
 .../conify/global/config/circ_limit_set.tt2        |   21 +
 .../global/config/circ_matrix_matchpoint.tt2       |    2 +-
 .../default/conify/global/config/circ_limit_set.js |   80 +++
 9 files changed, 391 insertions(+), 277 deletions(-)
 copy Open-ILS/src/sql/Pg/upgrade/{0503.schema.grace_periods.sql => 0720.schema.copy_loc_circ_limits.sql} (70%)


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list