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

Evergreen Git git at git.evergreen-ils.org
Tue Feb 28 14:32:16 EST 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  4d5c6ada5a3dbca18e3abfc6dbbafc62df1d09ef (commit)
       via  eaf8b8840a6267aef294bb6b137ed64439b59a93 (commit)
       via  5e2bad24b7743d5129999d0dfb6cf4352e2ef91d (commit)
      from  3ace401ed850aa71eb555d7e30e0fe8c47dd6d0a (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 4d5c6ada5a3dbca18e3abfc6dbbafc62df1d09ef
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Feb 28 14:36:37 2012 -0500

    Stamping circ limits upgrade script
    
    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 9e56f3d..739754f 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -86,7 +86,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 ('0676', :eg_version); -- senator/miker
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0677', :eg_version); -- tsbere/miker
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql b/Open-ILS/src/sql/Pg/upgrade/0677.schema.circ_limits.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql
rename to Open-ILS/src/sql/Pg/upgrade/0677.schema.circ_limits.sql
index 59b38f6..c692549 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0677.schema.circ_limits.sql
@@ -1,3 +1,14 @@
+-- Evergreen DB patch 0677.schema.circ_limits.sql
+--
+-- FIXME: insert description of change, if needed
+--
+BEGIN;
+
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0677', :eg_version);
+
+-- FIXME: add/check SQL statements to perform the upgrade
 -- Limit groups for circ counting
 CREATE TABLE config.circ_limit_group (
     id          SERIAL  PRIMARY KEY,
@@ -315,3 +326,6 @@ DROP FUNCTION evergreen.temp_migrate_circ_mod_limits();
 --Drop the old tables
 --Not sure we want to do this. Keeping them may help "something went wrong" correction.
 --DROP TABLE IF EXISTS config.circ_matrix_circ_mod_test_map, config.circ_matrix_circ_mod_test;
+
+
+COMMIT;

commit eaf8b8840a6267aef294bb6b137ed64439b59a93
Author: Thomas Berezansky <tsbere at mvlc.org>
Date:   Tue Feb 7 17:27:00 2012 -0500

    Asciidoc documentation for circ limits
    
    Because crappy documentation is better than no documentation ;)
    
    Signed-off-by: Thomas Berezansky <tsbere at mvlc.org>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/docs/circ_limits.txt b/docs/circ_limits.txt
new file mode 100644
index 0000000..32c233d
--- /dev/null
+++ b/docs/circ_limits.txt
@@ -0,0 +1,138 @@
+Circulation Limits
+==================
+Thomas Berezansky <tsbere at mvlc.org>
+:Date: 2011-10-14
+
+== Limit Groups
+
+Limit Groups can be thought of as "Tags" the system places on circulations so
+that it can find them later. They are placed on circulations based on
+association with Limit Sets.
+
+Limit Groups are global, like Circulation Modifiers, and are edited via the
+Admin->Server Administration->Circulation Limit Groups menu.
+
+Limit Groups are selected by their Name, but support an optional description.
+This description may be useful for storing why a given group was created, or
+what it is intended for.
+
+== Limit Sets
+
+Limit Sets define rules for limiting the number of active circulations a patron
+may have based on Circulation Modifiers and Limit Groups. They support a number
+of options:
+
+Owning Library:: The library that that "owns" the limit set and can edit the
+parameters it contains.
+Name:: The name used to select the limit set when attaching it to Circulation
+Matchpoints.
+Items Out:: The maximum number of items from the associated Circulation
+Modifiers and Limit Groups allowed to be checked out. If set to zero (0) then no
+limiting will be done, but Limit Groups may still be tagged.
+Min Depth:: The minimum depth in the org tree to consider as valid circulation
+libraries for counting, based on Org Unit Type depths.
+Global:: If enabled then treat the Min Depth setting as defining the "Root" of
+the tree, and then include the entire tree below the "Root". Otherwise only
+direct ancestors and descendants of the circulation library will be checked.
+Description:: A description that may be used to describe what purpose the limit
+set is supposed to be serving.
+
+Limit Sets support linking Circulation Modifiers and Limit Groups, and count
+circulations that match any of them. Care has been taken to ensure that even if
+a circulation matches multiple criteria it will only be counted once per Limit
+Set, however.
+
+Limit Sets are configured via the Admin->Local Administration->Circulation Limit
+Sets menu.
+
+=== A Min Depth/Global Example
+
+The Min Depth and Global options are fairly hard to just describe, so lets look
+at a quick example using the default org tree:
+
+* CONS
+** SYS1
+*** BR1
+**** SL1
+*** BR2
+** SYS2
+*** BR3
+**** BM1
+*** BR4
+
+The tree has depths of 0 (CONS) through 3 (SL1/BM1). Using BR1 as a consistent
+circulation library we can build a table of Depth/Global combinations and
+expected included libraries:
+
+.BR1 Depth/Global comparisons
+[options="header"]
+|===============================
+|Depth|Global|Included Libraries
+|0    |False |CONS,SYS1,BR1,SL1
+|0    |True  |ALL
+|1    |False |SYS1,BR1,SL1
+|1    |True  |SYS1,BR1,BR2,SL1
+|2    |False |BR1,SL1
+|2    |True  |BR1,SL1
+|3    |False |SL1
+|3    |True  |SL1
+|===============================
+
+== "Tagging" Circulations
+
+In order to count circulations that have no circulation modifier the system has
+to know how to find them. When using circulation modifiers this is easy, just
+look for circulations with items assigned the modifier. But say you want to be
+able to count every video, regardless of circulation modifiers? That is where
+Limit Groups come in.
+
+Limit Groups are automatically recorded from the circulation policies as the
+Limit Sets are looked over. Those Limit Groups assigned to the Limit Sets that
+are not flagged as "Check Only" will be attached to the circulation so that
+later circulations can find them.
+
+It is possible to make Limit Sets that do not check, but only tag. This is
+accomplished by setting "Items Out" to zero (0).
+
+== Associating Limit Sets with Matchpoints
+
+Limit Sets alone are useless if they are not associated with circulation
+matchpoints. When creating or editing matchpoints you can add, remove, or adjust
+settings on linked Limit Sets.
+
+The options available for an attached limit set are:
+
+Fallthrough:: If enabled the Limit Set will be applied whenever the matchpoint
+matches a potential circulation. If not enabled the limit set will only be
+applied when the matchpoint is the first match for a potential circulation.
+Active:: An inactive Limit Set link will never be checked, for tagging or
+counting.
+
+== Limit Sets on Empty Rules
+
+For limiting without otherwise changing circulation rules you can create an
+"empty" rule and attach one or more Limit Sets to it. In this case an empty rule
+is one that does not set any of the "result" fields, but instead lets everything
+fall through to less specific rules. Top level rules based on Marc Type could be
+created for the sole purpose of attaching Limit Sets that allow 0 "Items Out".
+This would allow for a quick top-level tagging of all circulations by Marc Type,
+without otherwise introducing limits, in the event you have reason to limit
+based on that information later.
+
+== Limit a single library's items, regardless of checkout library
+
+For example, videos from BR2 limited to 5 anywhere:
+
+. Create a Limit Group, say "BR2 Videos"
+. Create a Limit Set:
+* Items Out: 5
+* Min Depth: 0
+* Global: True
+* Limit Groups: "BR2 Videos"
+. Create an "empty" matchpoint that is at the top of the org/permission trees
+with Marc Type g and circ library BR2.
+. Attach your limit set to the matchpoint with Fallthrough enabled.
+
+This should limit BR2's videos to 5 out, no matter where they are being checked
+out. Videos owned by others should be unaffected, even if circulating out of
+BR2.

commit 5e2bad24b7743d5129999d0dfb6cf4352e2ef91d
Author: Thomas Berezansky <tsbere at mvlc.org>
Date:   Tue Feb 7 17:26:03 2012 -0500

    New Circ Limits
    
    Replace the old "Circ Mod Test" limit system with a more flexible system.
    
    In addition to circ modifiers this system supports "Limit Groups" that are
    automatically applied (by default) to any circulation checking them. This
    can be overidden by setting the "Check Only" flag when linking a Limit
    Group to a Limit Set.
    
    Both the limit groups and circ modifiers are linked to "Limit Sets" that
    act similarly to rules. Each Set can be attached to 0 or more circulation
    matchpoints.
    
    Each Limit set supports a number of items out (0 replaces infinite), depth
    in the org tree to start counting at (0 for up to the top, 1 for 1 below,
    etc), and a global flag (to check everywhere below the depth point, rather
    than just those circulations that happend at ancestors/descendants).
    
    When a Limit Set is linked to a Circulation Matchpoint it can be made
    inactive and has a fallthrough flag. When the fallthrough flag is enabled
    the Limit Set will be used whenever the matchpoint is involved with making
    a decision. When it is disabled the Limit Set will only be used when the
    matchpoint is the most specific matchpoint used in making the decision.
    
    Limit Groups management can be found on the server administration menu.
    
    Limit Sets management can be found on the local administration menu.
    
    Limit Set -> Matchpoint linking is done via editing Circulation Policies.
    
    The upgrade script does not remove the old tables in case something goes
    wrong with migrating the information contained within them.
    
    Signed-off-by: Thomas Berezansky <tsbere at mvlc.org>
    
    Conflicts:
    
    	Open-ILS/web/opac/locale/en-US/lang.dtd
    	Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
    
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>
    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 1894f77..f53638e 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -1470,15 +1470,58 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
         </permacrud>
 	</class>
 
-	<class id="ccmcmt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_matrix_circ_mod_test" oils_persist:tablename="config.circ_matrix_circ_mod_test" reporter:label="Circulation Matrix Circulation Modifier Subtest">
-		<fields oils_persist:primary="id" oils_persist:sequence="config.circ_matrix_circ_mod_test_id_seq">
-			<field reporter:label="Test ID" name="id" reporter:datatype="id"/>
-			<field reporter:label="Matchpoint ID" name="matchpoint" reporter:datatype="link"/>
-			<field reporter:label="Items Out" name="items_out" reporter:datatype="int"/>
-		</fields>
-		<links>
-			<link field="matchpoint" reltype="has_a" key="id" map="" class="ccmm"/>
-		</links>
+    <class id="cclg" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_limit_group" oils_persist:tablename="config.circ_limit_group" reporter:label="Circulation Limit Group">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.circ_limit_group_id_seq">
+            <field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+            <field reporter:label="Name" name="name" reporter:datatype="text"/>
+            <field reporter:label="Description" name="description" reporter:datatype="text"/>
+        </fields>
+        <links/>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_CIRC_MATRIX_MATCHPOINT" global_required="true"/>
+                <delete permission="ADMIN_CIRC_MATRIX_MATCHPOINT" global_required="true"/>
+            </actions>
+        </permacrud>
+    </class>
+
+    <class id="ccls" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_limit_set" oils_persist:tablename="config.circ_limit_set" reporter:label="Circulation Limit Set">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.circ_limit_set_id_seq">
+            <field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+            <field reporter:label="Name" name="name" reporter:datatype="text"/>
+            <field reporter:label="Owning Library" name="owning_lib"  reporter:datatype="org_unit"/>
+            <field reporter:label="Items Out" name="items_out" reporter:datatype="int"/>
+            <field reporter:label="Min Depth" name="depth" reporter:datatype="int"/>
+            <field reporter:label="Global" name="global" reporter:datatype="bool"/>
+            <field reporter:label="Description" name="description" reporter:datatype="text"/>
+        </fields>
+        <links>
+            <link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
+        </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT" context_field="owning_lib"/>
+                <retrieve/>
+                <update permission="ADMIN_CIRC_MATRIX_MATCHPOINT" context_field="owning_lib"/>
+                <delete permission="ADMIN_CIRC_MATRIX_MATCHPOINT" context_field="owning_lib"/>
+            </actions>
+        </permacrud>
+    </class>
+
+    <class id="ccmlsm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_matrix_limit_set_map" oils_persist:tablename="config.circ_matrix_limit_set_map" reporter:label="Circulation Matrix Limit Set Map">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.circ_matrix_limit_set_map_id_seq">
+            <field reporter:label="ID" name="id" reporter:datatype="id"/>
+            <field reporter:label="Matchpoint" name="matchpoint" reporter:datatype="link"/>
+            <field reporter:label="Limit Set" name="limit_set" reporter:datatype="link"/>
+            <field reporter:label="Fallthrough" name="fallthrough" reporter:datatype="bool"/>
+            <field reporter:label="Active" name="active" reporter:datatype="bool"/>
+        </fields>
+        <links>
+            <link field="matchpoint" reltype="has_a" key="id" map="" class="ccmm"/>
+            <link field="limit_set" reltype="has_a" key="id" map="" class="ccls"/>
+        </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
                 <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
@@ -1493,33 +1536,60 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                 </delete>
             </actions>
         </permacrud>
-	</class>
+    </class>
 
-	<class id="ccmcmtm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_matrix_circ_mod_test_map" oils_persist:tablename="config.circ_matrix_circ_mod_test_map" reporter:label="Circulation Matrix Circulation Modifier Subtest Circulation Modifier Set">
-		<fields oils_persist:primary="id" oils_persist:sequence="config.circ_matrix_circ_mod_test_map_id_seq">
-			<field reporter:label="Entry ID" name="id" reporter:datatype="id"/>
-			<field reporter:label="Circulation Modifier Subtest ID" name="circ_mod_test" reporter:datatype="link"/>
-			<field reporter:label="Circulation Modifier" name="circ_mod" reporter:datatype="link"/>
-		</fields>
-		<links>
-			<link field="circ_mod_test" reltype="has_a" key="id" map="" class="ccmcmt"/>
-			<link field="circ_mod" reltype="has_a" key="code" map="" class="ccm"/>
-		</links>
+    <class id="cclscmm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_limit_set_circ_mod_map" oils_persist:tablename="config.circ_limit_set_circ_mod_map" reporter:label="Circulation Limit Set Circ Mod Map">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.circ_limit_set_circ_mod_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="Circulation Modifier" name="circ_mod" reporter:datatype="link"/>
+        </fields>
+        <links>
+            <link field="limit_set" reltype="has_a" key="id" map="" class="ccls"/>
+            <link field="circ_mod" reltype="has_a" key="code" map="" class="ccm"/>
+        </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
                 <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
-                    <context link="circ_mod_test" jump="matchpoint" field="org_unit"/>
+                    <context link="limit_set" field="owning_lib"/>
                 </create>
                 <retrieve/>
                 <update permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
-                    <context link="circ_mod_test" jump="matchpoint" field="org_unit"/>
+                    <context link="limit_set" field="owning_lib"/>
                 </update>
                 <delete permission="ADMIN_CIRC_MATRIX_MATCHPOINT">
-                    <context link="circ_mod_test" jump="matchpoint" field="org_unit"/>
+                    <context link="limit_set" field="owning_lib"/>
                 </delete>
             </actions>
         </permacrud>
-	</class>
+    </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">
+            <field reporter:label="ID" name="id" reporter:datatype="id"/>
+            <field reporter:label="Limit Set" name="limit_set" reporter:datatype="link"/>
+            <field reporter:label="Limit Group" name="limit_group" reporter:datatype="link"/>
+            <field reporter:label="Check Only" name="check_only" reporter:datatype="bool"/>
+        </fields>
+        <links>
+            <link field="limit_set" reltype="has_a" key="id" map="" class="ccls"/>
+            <link field="limit_group" reltype="has_a" key="id" map="" class="cclg"/>
+        </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="cit" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::identification_type" oils_persist:tablename="config.identification_type" reporter:label="Identification Type">
 		<fields oils_persist:primary="id" oils_persist:sequence="config.identification_type_id_seq">
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 e5d572c..731131d 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
@@ -539,6 +539,7 @@ my @AUTOLOAD_FIELDS = qw/
     retarget_mode
     hold_as_transit
     fake_hold_dest
+    limit_groups
 /;
 
 
@@ -1188,6 +1189,8 @@ sub run_indb_circ_test {
         }
         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
+        # Grab the *last* response for limit_groups, where it is more likely to be filled
+        $self->limit_groups($results->[-1]->{limit_groups});
     }
 
     return $self->matrix_test_result($results);
@@ -1487,6 +1490,10 @@ sub do_checkout {
     # refresh the circ to force local time zone for now
     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
 
+    if($self->limit_groups) {
+        $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
+    }
+
     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
     $self->update_copy;
     return if $self->bail_out;
diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
index 6d82e73..7b9e2eb 100644
--- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql
+++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql
@@ -86,20 +86,63 @@ 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;
 
--- Tests for max items out by circ_modifier
-CREATE TABLE config.circ_matrix_circ_mod_test (
-    id          SERIAL     PRIMARY KEY,
+-- Limit groups for circ counting
+CREATE TABLE config.circ_limit_group (
+    id          SERIAL  PRIMARY KEY,
+    name        TEXT    UNIQUE NOT NULL,
+    description TEXT
+);
+
+-- Limit sets
+CREATE TABLE config.circ_limit_set (
+    id          SERIAL  PRIMARY KEY,
+    name        TEXT    UNIQUE NOT NULL,
+    owning_lib  INT     NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
+    items_out   INT     NOT NULL, -- Total current active circulations must be less than this. 0 means skip counting (always pass)
+    depth       INT     NOT NULL DEFAULT 0, -- Depth count starts at
+    global      BOOL    NOT NULL DEFAULT FALSE, -- If enabled, include everything below depth, otherwise ancestors/descendants only
+    description TEXT
+);
+
+-- Linkage between matchpoints and limit sets
+CREATE TABLE config.circ_matrix_limit_set_map (
+    id          SERIAL  PRIMARY KEY,
     matchpoint  INT     NOT NULL REFERENCES config.circ_matrix_matchpoint (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
-    items_out   INT     NOT NULL -- Total current active circulations must be less than this, NULL means skip (always pass)
+    limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    fallthrough BOOL    NOT NULL DEFAULT FALSE, -- If true fallthrough will grab this rule as it goes along
+    active      BOOL    NOT NULL DEFAULT TRUE,
+    CONSTRAINT circ_limit_set_once_per_matchpoint UNIQUE (matchpoint, limit_set)
+);
+
+-- Linkage between limit sets and circ mods
+CREATE TABLE config.circ_limit_set_circ_mod_map (
+    id          SERIAL  PRIMARY KEY,
+    limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    circ_mod    TEXT    NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT cm_once_per_set UNIQUE (limit_set, circ_mod)
+);
+
+-- Linkage between limit sets and limit groups
+CREATE TABLE config.circ_limit_set_group_map (
+    id          SERIAL  PRIMARY KEY,
+    limit_set    INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    limit_group INT     NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    check_only  BOOL    NOT NULL DEFAULT FALSE, -- If true, don't accumulate this limit_group for storing with the circulation
+    CONSTRAINT clg_once_per_set UNIQUE (limit_set, limit_group)
 );
 
-CREATE TABLE config.circ_matrix_circ_mod_test_map (
-    id      SERIAL  PRIMARY KEY,
-    circ_mod_test   INT NOT NULL REFERENCES config.circ_matrix_circ_mod_test (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
-    circ_mod        TEXT    NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE  DEFERRABLE INITIALLY DEFERRED,
-    CONSTRAINT cm_once_per_test UNIQUE (circ_mod_test, circ_mod)
+-- Linkage between limit groups and circulations
+CREATE TABLE action.circulation_limit_group_map (
+    circ        BIGINT      NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    limit_group INT         NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    PRIMARY KEY (circ, limit_group)
 );
 
+-- Function for populating the circ/limit group mappings
+CREATE OR REPLACE FUNCTION action.link_circ_limit_groups ( BIGINT, INT[] ) RETURNS VOID AS $func$
+    INSERT INTO action.circulation_limit_group_map(circ, limit_group) SELECT $1, id FROM config.circ_limit_group WHERE id IN (SELECT * FROM UNNEST($2));
+$func$ LANGUAGE SQL;
+
 CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] );
 
 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$
@@ -355,7 +398,7 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
-CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
+CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL, limit_groups INT[] );
 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;
@@ -366,8 +409,7 @@ DECLARE
     result                  action.circ_matrix_test_result;
     circ_test               action.found_circ_matrix_matchpoint;
     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
-    out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
-    circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
+    circ_limit_set          config.circ_limit_set%ROWTYPE;
     hold_ratio              action.hold_stats%ROWTYPE;
     penalty_type            TEXT;
     items_out               INT;
@@ -466,7 +508,7 @@ BEGIN
     END IF;
 
     -- Use Circ OU for penalties and such
-    SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_ou );
+    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
 
     IF renewal THEN
         penalty_type = '%RENEW%';
@@ -521,25 +563,50 @@ BEGIN
         END IF;
     END IF;
 
-    -- Fail if the user has too many items with specific circ_modifiers checked out
-    IF NOT renewal THEN
-        FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
-            SELECT  INTO items_out COUNT(*)
-              FROM  action.circulation circ
-                JOIN asset.copy cp ON (cp.id = circ.target_copy)
-              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 cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
-            IF items_out >= out_by_circ_mod.items_out THEN
-                result.fail_part := 'config.circ_matrix_circ_mod_test';
-                result.success := FALSE;
-                done := TRUE;
-                RETURN NEXT result;
+    -- 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 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;
-        END LOOP;
-    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
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql
new file mode 100644
index 0000000..59b38f6
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql
@@ -0,0 +1,317 @@
+-- Limit groups for circ counting
+CREATE TABLE config.circ_limit_group (
+    id          SERIAL  PRIMARY KEY,
+    name        TEXT    UNIQUE NOT NULL,
+    description TEXT
+);
+
+-- Limit sets
+CREATE TABLE config.circ_limit_set (
+    id          SERIAL  PRIMARY KEY,
+    name        TEXT    UNIQUE NOT NULL,
+    owning_lib  INT     NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
+    items_out   INT     NOT NULL, -- Total current active circulations must be less than this. 0 means skip counting (always pass)
+    depth       INT     NOT NULL DEFAULT 0, -- Depth count starts at
+    global      BOOL    NOT NULL DEFAULT FALSE, -- If enabled, include everything below depth, otherwise ancestors/descendants only
+    description TEXT
+);
+
+-- Linkage between matchpoints and limit sets
+CREATE TABLE config.circ_matrix_limit_set_map (
+    id          SERIAL  PRIMARY KEY,
+    matchpoint  INT     NOT NULL REFERENCES config.circ_matrix_matchpoint (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    fallthrough BOOL    NOT NULL DEFAULT FALSE, -- If true fallthrough will grab this rule as it goes along
+    active      BOOL    NOT NULL DEFAULT TRUE,
+    CONSTRAINT circ_limit_set_once_per_matchpoint UNIQUE (matchpoint, limit_set)
+);
+
+-- Linkage between limit sets and circ mods
+CREATE TABLE config.circ_limit_set_circ_mod_map (
+    id          SERIAL  PRIMARY KEY,
+    limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    circ_mod    TEXT    NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT cm_once_per_set UNIQUE (limit_set, circ_mod)
+);
+
+-- Linkage between limit sets and limit groups
+CREATE TABLE config.circ_limit_set_group_map (
+    id          SERIAL  PRIMARY KEY,
+    limit_set    INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    limit_group INT     NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    check_only  BOOL    NOT NULL DEFAULT FALSE, -- If true, don't accumulate this limit_group for storing with the circulation
+    CONSTRAINT clg_once_per_set UNIQUE (limit_set, limit_group)
+);
+
+-- Linkage between limit groups and circulations
+CREATE TABLE action.circulation_limit_group_map (
+    circ        BIGINT      NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    limit_group INT         NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    PRIMARY KEY (circ, limit_group)
+);
+
+-- Function for populating the circ/limit group mappings
+CREATE OR REPLACE FUNCTION action.link_circ_limit_groups ( BIGINT, INT[] ) RETURNS VOID AS $func$
+    INSERT INTO action.circulation_limit_group_map(circ, limit_group) SELECT $1, id FROM config.circ_limit_group WHERE id IN (SELECT * FROM UNNEST($2));
+$func$ LANGUAGE SQL;
+
+DROP TYPE IF EXISTS action.circ_matrix_test_result CASCADE;
+CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL, limit_groups INT[] );
+
+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 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;
+
+-- We need to re-create these, as they got dropped with the type above.
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
+$func$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
+$func$ LANGUAGE SQL;
+
+-- Temp function for migrating circ mod limits.
+CREATE OR REPLACE FUNCTION evergreen.temp_migrate_circ_mod_limits() RETURNS VOID AS $func$
+DECLARE
+    circ_mod_group config.circ_matrix_circ_mod_test%ROWTYPE;
+    current_set INT;
+    circ_mod_count INT;
+BEGIN
+    FOR circ_mod_group IN SELECT * FROM config.circ_matrix_circ_mod_test LOOP
+        INSERT INTO config.circ_limit_set(name, owning_lib, items_out, depth, global, description)
+            SELECT org_unit || ' : Matchpoint ' || circ_mod_group.matchpoint || ' : Circ Mod Test ' || circ_mod_group.id, org_unit, circ_mod_group.items_out, 0, false, 'Migrated from Circ Mod Test System'
+                FROM config.circ_matrix_matchpoint WHERE id = circ_mod_group.matchpoint
+            RETURNING id INTO current_set;
+        INSERT INTO config.circ_matrix_limit_set_map(matchpoint, limit_set, fallthrough, active) VALUES (circ_mod_group.matchpoint, current_set, false, true);
+        INSERT INTO config.circ_limit_set_circ_mod_map(limit_set, circ_mod)
+            SELECT current_set, circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = circ_mod_group.id;
+        SELECT INTO circ_mod_count count(id) FROM config.circ_limit_set_circ_mod_map WHERE limit_set = current_set;
+        RAISE NOTICE 'Created limit set with id % and % circ modifiers attached to matchpoint %', current_set, circ_mod_count, circ_mod_group.matchpoint;
+    END LOOP;
+END;
+$func$ LANGUAGE plpgsql;
+
+-- Run the temp function
+SELECT * FROM evergreen.temp_migrate_circ_mod_limits();
+
+-- Drop the temp function
+DROP FUNCTION evergreen.temp_migrate_circ_mod_limits();
+
+--Drop the old tables
+--Not sure we want to do this. Keeping them may help "something went wrong" correction.
+--DROP TABLE IF EXISTS config.circ_matrix_circ_mod_test_map, config.circ_matrix_circ_mod_test;
diff --git a/Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2 b/Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2
new file mode 100644
index 0000000..4f36e9c
--- /dev/null
+++ b/Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2
@@ -0,0 +1,36 @@
+[% WRAPPER base.tt2 %]
+<h1>Circulation Limit Group</h1> <br/>
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Circulation Limit Group</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='clgGrid.showCreateDialog()'>New Limit Group</button>
+        <button dojoType='dijit.form.Button' onClick='clgGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="clgGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['id','name','description']"
+            query="{id: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='cclg'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+<script type ="text/javascript">
+
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            clgGrid.loadAll({order_by:{cclg : 'name'}});
+        }
+    );
+
+</script>
+
+[% END %]
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
new file mode 100644
index 0000000..2138706
--- /dev/null
+++ b/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2
@@ -0,0 +1,78 @@
+[% ctx.page_title = 'Circulation Limit Set' %]
+[% WRAPPER base.tt2 %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/conify/global/config/circ_limit_set.js'> </script>
+<div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+    <div>Circulation Limit Set</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='clsGrid.showCreatePane()'>New</button>
+        <button dojoType='dijit.form.Button' onClick='clsGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="clsGrid"
+            style="height: 600px;"
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['id', 'owning_lib', 'name', 'items_out', 'depth', 'global', 'description']"
+            defaultCellWidth='"auto"'
+            query="{id: '*'}"
+            fmClass='ccls'
+            editStyle='pane'
+            editOnEnter='true'
+            showColumnPicker='true'
+            columnPickerPrefix='"conify.config.circ_limit_set"'>
+    </table>
+</div>
+
+<div class='hidden'>
+    <div id='linked-editor' style='border:1px solid #aaa'>
+        <h3>Linked Circ Modifiers</h3>
+        <table class='oils-generic-table'>
+            <tbody>
+                <tr>
+                    <th>Name</th>
+                    <th>Remove</th>
+                </tr>
+            </tbody>
+            <tbody name='circ-mod-entry-tbody'>
+                <tr name='circ-mod-entry-row'>
+                    <td name='circ-mod'></td>
+                    <td><a name='remove-circ-mod' href='javascript:void(0);'>Remove</a></td>
+                </tr>
+            </tbody>
+            <tbody name='circ-mod-entry-new'>
+                <tr>
+                    <td><div name='circ-mod-selector'></div></td>
+                    <td><a href='javascript:void(0);' name='add-circ-mod'>Add</a></td>
+                </tr>
+            </tbody>
+        </table>
+        <h3>Linked Limit Groups</h3>
+        <table class='oils-generic-table'>
+            <tbody>
+                <tr>
+                    <th>Name</th>
+                    <th>Check Only?</th>
+                    <th>Remove</th>
+                </tr>
+            </tbody>
+            <tbody name='limit-group-entry-tbody'>
+                <tr name='limit-group-entry-row'>
+                    <td name='limit-group'></td>
+                    <td><input type="checkbox" name="limit-group-check-only"/></td>
+                    <td><a name='remove-limit-group' href='javascript:void(0);'>Remove</a></td>
+                </tr>
+            </tbody>
+            <tbody name='limit-group-entry-new'>
+                <tr>
+                    <td><div name='limit-group-selector'></div></td>
+                    <td><a href='javascript:void(0);' name='add-limit-group'>Add</a></td>
+                    <td></td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<div class='hidden'><div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div></div>
+[% END %]
+
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 b1d7150..67386f0 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
@@ -27,51 +27,37 @@
 </div>
 
 <div class='hidden'>
-    <div id='circ-mod-editor' style='border:1px solid #aaa'>
-        <h3>Circ Modifier Count Groups</h3>
-        <table class='oils-generic-table' name='circ-mod-group-table'>
-            <tbody><tr>
-                <td>Total items out</td>
-                <td><input type='text' size='3' name='circ-mod-count'></div></td>
-            </tr><tbody>
-            <tbody name='circ-mod-entry-tbody'>
-                <tr name='circ-mod-entry-row'>
-                    <td name='circ-mod'/>
-                    <td><a name='remove-circ-mod' href='javascript:void(0);'>Remove</a></td>
+    <div id='limit-set-editor' style='border:1px solid #aaa'>
+        <h3>Linked Limit Sets</h3>
+        <table class='oils-generic-table'>
+            <tbody>
+                <tr>
+                    <th>Name</th>
+                    <th>Fallthrough</th>
+                    <th>Active</th>
+                    <th>Remove</th>
                 </tr>
             </tbody>
-            <tbody>
+            <tbody name='limit-set-entry-tbody'>
+                <tr name='limit-set-entry-row'>
+                    <td name='limit-set'></td>
+                    <td><input type="checkbox" name="limit-set-fallthrough"/></td>
+                    <td><input type="checkbox" name="limit-set-active"/></td>
+                    <td><a name='remove-limit-set' href='javascript:void(0);'>Remove</a></td>
+                </tr>
+            </tbody>
+            <tbody name='limit-set-entry-new'>
                 <tr>
-                    <td><div name='circ-mod-selector'></div></td>
-                    <td><a href='javascript:void(0);' name='add-circ-mod'>Add</a></td>
+                    <td><div name='limit-set-selector'></div></td>
+                    <td colspan="3"><a href='javascript:void(0);' name='add-limit-set'>Add</a></td>
                 </tr>
             </tbody>
         </table>
-        <span name='add-circ-mod-group-span'>
-        <a href='javascript:void(0);' name='add-circ-mod-group'>Create New Group</a>
-        </span>&nbsp;&nbsp;<span>
-        <a href='javascript:void(0);' onclick='applyCircModChanges()'>Apply Circ Modifier Changes</a>
-        </span>
     </div>
 </div>
 
+
 <div class='hidden'><div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div></div>
 
-<script type="text/javascript">
-    function format_hard_due_date(name, id) {
-        var item=this.grid.getItem(id);
-        if(!item) return name;
-        switch (this.grid.store.getValue(this.grid.getItem(id), 'hard_due_date')) {
-            case null :
-            case undefined :
-            case 'unset' :
-                return name;
-            default:
-                return "<a href='" + oilsBasePath +
-                    "/conify/global/config/hard_due_date?name=" +
-                    encodeURIComponent(name) + "'>" + name + "</a>";
-        }
-    }
-</script>
 [% END %]
 
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
new file mode 100644
index 0000000..49d6aa8
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js
@@ -0,0 +1,250 @@
+dojo.require('dijit.layout.ContentPane');
+dojo.require('dijit.form.Button');
+dojo.require('openils.widget.AutoGrid');
+dojo.require('openils.widget.AutoFieldWidget');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.ProgressDialog');
+
+var linkedEditor = null;
+var circModEntryCache = [];
+var limitGroupEntryCache = [];
+var circModCache = {};
+var limitGroupCache = {};
+var curLinkedEditor;
+
+function load(){
+    clsGrid.loadAll({order_by:{ccls:'name'}});
+    clsGrid.onEditPane = buildEditPaneAdditions;
+    clsGrid.onPostUpdate = updateLinked;
+    clsGrid.onPostCreate = updateLinked;
+    linkedEditor = dojo.byId('linked-editor').parentNode.removeChild(dojo.byId('linked-editor'));
+
+    // Cache circ mod/limit group info for later display
+    var pcrud = new openils.PermaCrud();
+    var temp = pcrud.retrieveAll('ccm');
+    dojo.forEach(temp, function(g) { circModCache[g.code()] = g; } );
+    temp = pcrud.retrieveAll('cclg');
+    dojo.forEach(temp, function(g) { limitGroupCache[g.id()] = g; } );
+}
+
+function byName(name, ctxt) {
+    return dojo.query('[name=' + name + ']', ctxt)[0];
+}
+
+function buildEditPaneAdditions(editPane) {
+    circModEntryCache = [];
+    limitGroupEntryCache = [];
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.setAttribute('colspan','2');
+    // Explanation....
+    // editPane.domNode.lastChild = Table
+    // .lastChild = Table Body
+    // .lastChild = Table Row containing Action Buttons
+    editPane.domNode.lastChild.lastChild.insertBefore(tr, editPane.domNode.lastChild.lastChild.lastChild);
+    tr.appendChild(td);
+    curLinkedEditor = linkedEditor.cloneNode(true);
+    td.appendChild(curLinkedEditor);
+    var circModTmpl = byName('circ-mod-entry-tbody', curLinkedEditor).removeChild(byName('circ-mod-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({
+        fmClass : 'cclscmm',
+        fmField : 'circ_mod',
+        parentNode : byName('circ-mod-selector', curLinkedEditor)
+    });
+    cm_selector.build();
+
+    var lg_selector = new openils.widget.AutoFieldWidget({
+        fmClass : 'cclsgm',
+        fmField : 'limit_group',
+        parentNode : byName('limit-group-selector', curLinkedEditor)
+    });
+    lg_selector.build();
+
+    function addMod(code) {
+        var row = circModTmpl.cloneNode(true);
+        row.setAttribute('code', code);
+        byName('circ-mod', row).innerHTML = code + ' : ' + circModCache[code].name();
+        byName('remove-circ-mod', row).onclick = function() {
+            byName('circ-mod-entry-tbody', clsGrid.editPane.domNode).removeChild(row);
+        }
+        byName('circ-mod-entry-tbody', editPane.domNode).appendChild(row);
+    }
+
+    function addGroup(group) {
+        var row = limitGroupTmpl.cloneNode(true);
+        row.setAttribute('limit_group', group);
+        byName('limit-group', row).innerHTML = limitGroupCache[group].name();
+        byName('remove-limit-group', row).onclick = function() {
+            byName('limit-group-entry-tbody', clsGrid.editPane.domNode).removeChild(row);
+        }
+        byName('limit-group-entry-tbody', editPane.domNode).appendChild(row);
+    }
+
+    byName('add-circ-mod', editPane.domNode).onclick = function() {
+        addMod(cm_selector.widget.attr('value'));
+    }
+
+    byName('add-limit-group', editPane.domNode).onclick = function() {
+        addGroup(lg_selector.widget.attr('value'));
+    }
+
+    // On edit we need to load existing entries.
+    // On create, not so much.
+    if(!editPane.fmObject) return; 
+    var limitSet = editPane.fmObject.id();
+
+    if(editPane.mode == 'update') {
+        var pcrud = new openils.PermaCrud();
+        circModEntryCache = pcrud.search('cclscmm', {limit_set: limitSet});
+        limitGroupEntryCache = pcrud.search('cclsgm', {limit_set: limitSet});
+        dojo.forEach(circModEntryCache, function(g) { addCircMod(circModTmpl, g); } );
+        dojo.forEach(limitGroupEntryCache, function(g) { addLimitGroup(limitGroupTmpl, g); } );
+    } 
+}
+
+function addCircMod(tmpl, circ_mod_entry) {
+    var row = tmpl.cloneNode(true);
+    var code = circ_mod_entry.circ_mod();
+    row.setAttribute('code', code);
+    byName('circ-mod', row).innerHTML = code + ' : ' + circModCache[code].name();
+    byName('remove-circ-mod', row).onclick = function() {
+        byName('circ-mod-entry-tbody', clsGrid.editPane.domNode).removeChild(row);
+    }
+    byName('circ-mod-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();
+    row.setAttribute('limit_group', group);
+    byName('limit-group', row).innerHTML = limitGroupCache[group].name();
+    if(limit_group_entry.check_only() == 't') {
+        byName('limit-group-check-only', row).setAttribute('checked', 'true');
+    }
+    byName('remove-limit-group', row).onclick = function() {
+        byName('limit-group-entry-tbody', clsGrid.editPane.domNode).removeChild(row);
+    }
+    byName('limit-group-entry-tbody', clsGrid.editPane.domNode).appendChild(row);
+}
+
+function updateLinked(fmObject, rowindex) {
+    var id = null;
+    if(rowindex != undefined && this.editPane && this.editPane.fmObject) {
+        // Edit, grab existing ID
+        id = this.editPane.fmObject.id();
+    } else if(fmObject.id) {
+        // Create, grab new ID
+        id = fmObject.id();
+    }
+    // If we don't have an ID, drop out.
+    if(id == null) return;
+    var pcrud = new openils.PermaCrud();
+    progressDialog.show(true);
+
+    var add = [];
+    var remove = [];
+    var update = [];
+
+    // First up, circ mods.
+    var circ_mods = [];
+    dojo.query('[name=circ-mod-entry-row]', this.editPane.domNode).forEach(
+        function(row) {
+            var mod = row.getAttribute('code');
+            circ_mods.push(mod);
+            if(!circModEntryCache.filter(function(i) { return (i.circ_mod() == mod); })[0]) {
+                var entry = new fieldmapper.cclscmm();
+                entry.isnew(true);
+                entry.limit_set(id);
+                entry.circ_mod(mod);
+                add.push(entry);
+            }
+        }
+    );
+    dojo.forEach(circModEntryCache, function(eMod) {
+            if(!circ_mods.filter(function(i) { return (i == eMod.circ_mod()); })[0]) {
+                eMod.isdeleted(true);
+                remove.push(eMod);
+            }
+        }
+    );
+
+    // Next, limit groups
+    var limit_groups = [];
+    dojo.query('[name=limit-group-entry-row]', this.editPane.domNode).forEach(
+        function(row) {
+            var group = row.getAttribute('limit_group');
+            limit_groups.push(group);
+            var cached = limitGroupEntryCache.filter(function(i) { return (i.limit_group() == group); })[0];
+            if(!cached) {
+                var entry = new fieldmapper.cclsgm();
+                entry.isnew(true);
+                entry.limit_set(id);
+                entry.limit_group(group);
+                entry.check_only(byName('limit-group-check-only', row).checked ? 't' : 'f');
+                add.push(entry);
+            } else {
+                var check_only = byName('limit-group-check-only', row).checked;
+                if(check_only != (cached.check_only() == 't')) {
+                    cached.check_only(check_only ? 't' : 'f');
+                    cached.ischanged(true);
+                    update.push(cached);
+                }
+            }
+        }
+    );
+    dojo.forEach(limitGroupEntryCache, function(eGroup) {
+            if(!limit_groups.filter(function(i) { return (i == eGroup.limit_group()); })[0]) {
+                eGroup.isdeleted(true);
+                remove.push(eGroup);
+            }
+        }
+    );
+
+    function updateEntries() {
+        pcrud.update(update, {
+            oncomplete : function () {
+                progressDialog.hide();
+            }
+        });
+    }
+
+    function removeEntries() {
+        pcrud.eliminate(remove, {
+            oncomplete : function () {
+                if(update.length) {
+                    updateEntries();
+                } else {
+                    progressDialog.hide();
+                }
+            }
+        });
+    }
+
+    function addEntries() {
+        pcrud.create(add, {
+            oncomplete : function () {
+                if(remove.length) {
+                    removeEntries();
+                } else if (update.length) {
+                    updateEntries();
+                } else {
+                    progressDialog.hide();
+                }
+            }
+        });
+    }
+
+    if(add.length)
+        addEntries();
+    else if (remove.length)
+        removeEntries();
+    else if (update.length)
+        updateEntries();
+    else
+        progressDialog.hide();
+}
+
+openils.Util.addOnLoad(load);
+
diff --git a/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js b/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js
index 6e8dd50..175a15d 100644
--- a/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js
+++ b/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js
@@ -5,11 +5,9 @@ dojo.require('openils.widget.AutoFieldWidget');
 dojo.require('openils.PermaCrud');
 dojo.require('openils.widget.ProgressDialog');
 
-var circModEditor = null;
-var circModGroupTables = [];
-var circModGroupCache = {};
-var circModEntryCache = {};
-var matchPoint;
+var limitSetEditor = null;
+var limitSetEntryCache = [];
+var limitSetCache = {};
 
 function load(){
     cmGrid.overrideWidgetArgs.grp = {hrbefore : true};
@@ -27,7 +25,14 @@ function load(){
     cmGrid.overrideWidgetArgs.hard_due_date = {inherits : true};
     cmGrid.loadAll({order_by:{ccmm:'circ_modifier'}});
     cmGrid.onEditPane = buildEditPaneAdditions;
-    circModEditor = dojo.byId('circ-mod-editor').parentNode.removeChild(dojo.byId('circ-mod-editor'));
+    cmGrid.onPostUpdate = updateLinked;
+    cmGrid.onPostCreate = updateLinked;
+    limitSetEditor = dojo.byId('limit-set-editor').parentNode.removeChild(dojo.byId('limit-set-editor'));
+
+    // Cache limit set info for later display
+    var pcrud = new openils.PermaCrud();
+    var temp = pcrud.retrieveAll('ccls');
+    dojo.forEach(temp, function(g) { limitSetCache[g.id()] = g; } );
 }
 
 function byName(name, ctxt) {
@@ -35,174 +40,180 @@ function byName(name, ctxt) {
 }
 
 function buildEditPaneAdditions(editPane) {
-    if(!editPane.fmObject) return; 
-    var node = circModEditor.cloneNode(true);
-    var tableTmpl = node.removeChild(byName('circ-mod-group-table', node));
-    circModGroupTables = [];
-    matchPoint = editPane.fmObject.id();
+    limitSetEntryCache = [];
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.setAttribute('colspan','2');
+    // Explanation....
+    // editPane.domNode.lastChild = Table
+    // .lastChild = Table Body
+    // .lastChild = Table Row containing Action Buttons
+    editPane.domNode.lastChild.lastChild.insertBefore(tr, editPane.domNode.lastChild.lastChild.lastChild);
+    tr.appendChild(td);
+    curLimitSetEditor = limitSetEditor.cloneNode(true);
+    td.appendChild(curLimitSetEditor);
+    var limitSetTmpl = byName('limit-set-entry-tbody', curLimitSetEditor).removeChild(byName('limit-set-entry-row', curLimitSetEditor));
+
+    var selector = new openils.widget.AutoFieldWidget({
+        fmClass : 'ccmlsm',
+        fmField : 'limit_set',
+        parentNode : byName('limit-set-selector', curLimitSetEditor)
+    });
+    selector.build();
+
+    function addSet(lset) {
+        var row = limitSetTmpl.cloneNode(true);
+        row.setAttribute('limit_set', lset);
+        byName('limit-set', row).innerHTML = limitSetCache[lset].name();
+        byName('remove-limit-set', row).onclick = function() {
+            byName('limit-set-entry-tbody', cmGrid.editPane.domNode).removeChild(row);
+        }
+        byName('limit-set-active', row).setAttribute('checked', 'true');
+        byName('limit-set-entry-tbody', editPane.domNode).appendChild(row);
+    }
 
-    byName('add-circ-mod-group', node).onclick = function() {
-        addCircModGroup(node, tableTmpl)
+    byName('add-limit-set', editPane.domNode).onclick = function() {
+        addSet(selector.widget.attr('value'));
     }
 
+    // On edit we need to load existing entries.
+    // On create, not so much.
+    if(!editPane.fmObject) return; 
+    var matchpoint = editPane.fmObject.id();
+
     if(editPane.mode == 'update') {
-        var groups = new openils.PermaCrud().search('ccmcmt', {matchpoint: editPane.fmObject.id()});
-        dojo.forEach(groups, function(g) { addCircModGroup(node, tableTmpl, g); } );
+        var pcrud = new openils.PermaCrud();
+        limitSetEntryCache = pcrud.search('ccmlsm', {matchpoint: editPane.fmObject.id()});
+        dojo.forEach(limitSetEntryCache, function(g) { addLimitSet(limitSetTmpl, g); } );
     } 
-
-    editPane.domNode.appendChild(node);
 }
 
-
-function addCircModGroup(node, tableTmpl, group) {
-
-    var table = tableTmpl.cloneNode(true);
-    var circModRowTmpl = byName('circ-mod-entry-tbody', table).removeChild(byName('circ-mod-entry-row', table));
-    circModGroupTables.push(table);
-
-    var entries = [];
-    if(group) {
-        entries = new openils.PermaCrud().search('ccmcmtm', {circ_mod_test : group.id()});
-        table.setAttribute('group', group.id());
-        circModGroupCache[group.id()] = group;
-        circModEntryCache[group.id()] = entries;
+function addLimitSet(tmpl, limit_set_entry) {
+    var row = tmpl.cloneNode(true);
+    var lset = limit_set_entry.limit_set();
+    row.setAttribute('limit_set', lset);
+    byName('limit-set', row).innerHTML = limitSetCache[lset].name();
+    if(limit_set_entry.active() == 't') {
+        byName('limit-set-active', row).setAttribute('checked', 'true');
     }
-
-    function addMod(code, name) {
-        name = name || code; // XXX
-        var row = circModRowTmpl.cloneNode(true);
-        byName('circ-mod', row).innerHTML = name;
-        byName('circ-mod', row).setAttribute('code', code);
-        byName('circ-mod-entry-tbody', table).appendChild(row);
-        byName('remove-circ-mod', row).onclick = function() {
-            byName('circ-mod-entry-tbody', table).removeChild(row);
-        }
+    if(limit_set_entry.fallthrough() == 't') {
+        byName('limit-set-fallthrough', row).setAttribute('checked', 'true');
     }
-
-    dojo.forEach(entries, function(e) { addMod(e.circ_mod()); });
-
-    byName('circ-mod-count', table).value = (group) ? group.items_out() : 0;
-
-    var selector = new openils.widget.AutoFieldWidget({
-        fmClass : 'ccmcmtm',
-        fmField : 'circ_mod',
-        parentNode : byName('circ-mod-selector', table)
-    });
-    selector.build();
-
-    byName('add-circ-mod', table).onclick = function() {
-        addMod(selector.widget.attr('value'), selector.widget.attr('displayedValue'));
+    byName('remove-limit-set', row).onclick = function() {
+        byName('limit-set-entry-tbody', cmGrid.editPane.domNode).removeChild(row);
     }
+    byName('limit-set-entry-tbody', cmGrid.editPane.domNode).appendChild(row);
+}
 
-    node.insertBefore(table, byName('add-circ-mod-group-span', node));
-    node.insertBefore(dojo.create('hr'), byName('add-circ-mod-group-span', node));
+function format_hard_due_date(name, id) {
+    var item=this.grid.getItem(id);
+    if(!item) return name;
+    switch (this.grid.store.getValue(this.grid.getItem(id), 'hard_due_date')) {
+        case null :
+        case undefined :
+        case 'unset' :
+            return name;
+        default:
+            return "<a href='" + oilsBasePath +
+                "/conify/global/config/hard_due_date?name=" +
+                encodeURIComponent(name) + "'>" + name + "</a>";
+    }
 }
 
-function applyCircModChanges() {
+function updateLinked(fmObject, rowindex) {
+    var id = null;
+    if(rowindex != undefined && this.editPane && this.editPane.fmObject) {
+        // Edit, grab existing ID
+        id = this.editPane.fmObject.id();
+    } else if(fmObject.id) {
+        // Create, grab new ID
+        id = fmObject.id();
+    }
+    // If we don't have an ID, drop out.
+    if(id == null) return;
     var pcrud = new openils.PermaCrud();
     progressDialog.show(true);
 
-    for(var idx in circModGroupTables) {
-        var table = circModGroupTables[idx];
-        var gp = table.getAttribute('group');
-
-        var count = byName('circ-mod-count', table).value;
-        var mods = [];
-        var entries = [];
-
-        dojo.forEach(dojo.query('[name=circ-mod]', table), function(td) { 
-            mods.push(td.getAttribute('code'));
-        });
-
-        var group = circModGroupCache[gp];
-
-        if(!group) {
-
-            group = new fieldmapper.ccmcmt();
-            group.isnew(true);
-            dojo.forEach(mods, function(mod) {
-                var entry = new fieldmapper.ccmcmtm();
+    var add = [];
+    var remove = [];
+    var update = [];
+
+    var limit_sets = [];
+    dojo.query('[name=limit-set-entry-row]', this.editPane.domNode).forEach(
+        function(row) {
+            var lset = row.getAttribute('limit_set');
+            limit_sets.push(lset);
+            var cached = limitSetEntryCache.filter(function(i) { return (i.limit_set() == lset); })[0];
+            if(!cached) {
+                var entry = new fieldmapper.ccmlsm();
                 entry.isnew(true);
-                entry.circ_mod(mod);
-                entries.push(entry);
-            });
-
-
-        } else {
-
-            var existing = circModEntryCache[group.id()];
-            dojo.forEach(mods, function(mod) {
-                
-                // new circ mod for this group
-                if(!existing.filter(function(i){ return (i.circ_mod() == mod)})[0]) {
-                    var entry = new fieldmapper.ccmcmtm();
-                    entry.isnew(true);
-                    entry.circ_mod(mod);
-                    entries.push(entry);
-                    entry.circ_mod_test(group.id());
+                entry.matchpoint(id);
+                entry.limit_set(lset);
+                entry.active(byName('limit-set-active', row).checked ? 't' : 'f');
+                entry.fallthrough(byName('limit-set-fallthrough', row).checked ? 't' : 'f');
+                add.push(entry);
+            } else {
+                var active = byName('limit-set-active', row).checked;
+                var fallthrough = byName('limit-set-fallthrough', row).checked;
+                if((active != (cached.active() == 't')) || (fallthrough != (cached.fallthrough() == 't'))) {
+                    cached.active(active ? 't' : 'f');
+                    cached.fallthrough(fallthrough ? 't' : 'f');
+                    cached.ischanged(true);
+                    update.push(cached);
                 }
-            });
-
-            dojo.forEach(existing, function(eMod) {
-                if(!mods.filter(function(i){ return (i == eMod.circ_mod()) })[0]) {
-                    eMod.isdeleted(true);
-                    entries.push(eMod);
-                }
-            });
+            }
         }
+    );
+    dojo.forEach(limitSetEntryCache, function(eSet) {
+            if(!limit_sets.filter(function(i) { return (i == eSet.limit_set()); })[0]) {
+                eSet.isdeleted(true);
+                remove.push(eSet);
+            }
+        }
+    );
 
-        group.items_out(count);
-        group.matchpoint(matchPoint);
-
-        if(group.isnew()) {
+    function updateEntries() {
+        pcrud.update(update, {
+            oncomplete : function () {
+                progressDialog.hide();
+            }
+        });
+    }
 
-            pcrud.create(group, {
-                oncomplete : function(r, cudResults) {
-                    var group = cudResults[0];
-                    dojo.forEach(entries, function(e) { e.circ_mod_test(group.id()) } );
-                    pcrud.create(entries, {
-                        oncomplete : function() {
-                            progressDialog.hide();
-                        }
-                    });
+    function removeEntries() {
+        pcrud.eliminate(remove, {
+            oncomplete : function () {
+                if(update.length) {
+                    updateEntries();
+                } else {
+                    progressDialog.hide();
                 }
-            });
-
-        } else {
-
-            pcrud.update(group, {
-                oncomplete : function(r, cudResults) {
-                    var newOnes = entries.filter(function(e) { return e.isnew() });
-                    var delOnes = entries.filter(function(e) { return e.isdeleted() });
-                    if(!delOnes.length && !newOnes.length) {
-                        progressDialog.hide();
-                        return;
-                    }
-                    if(newOnes.length) {
-                        pcrud.create(newOnes, {
-                            oncomplete : function() {
-                                if(delOnes.length) {
-                                    pcrud.eliminate(delOnes, {
-                                        oncomplete : function() {
-                                            progressDialog.hide();
-                                        }
-                                    });
-                                } else {
-                                    progressDialog.hide();
-                                }
-                            }
-                        });
-                    } else {
-                        pcrud.eliminate(delOnes, {
-                            oncomplete : function() {
-                                progressDialog.hide();
-                            }
-                        });
-                    }
+            }
+        });
+    }
+
+    function addEntries() {
+        pcrud.create(add, {
+            oncomplete : function () {
+                if(remove.length) {
+                    removeEntries();
+                } else if (update.length) {
+                    updateEntries();
+                } else {
+                    progressDialog.hide();
                 }
-            });
-        }
+            }
+        });
     }
+
+    if(add.length)
+        addEntries();
+    else if (remove.length)
+        removeEntries();
+    else if (update.length)
+        updateEntries();
+    else
+        progressDialog.hide();
 }
 
 openils.Util.addOnLoad(load);
diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd
index fc327f6..2acf0c7 100644
--- a/Open-ILS/web/opac/locale/en-US/lang.dtd
+++ b/Open-ILS/web/opac/locale/en-US/lang.dtd
@@ -716,6 +716,7 @@
 <!ENTITY staff.main.menu.admin.local_admin.patrons_due_refunds.label "Patrons with Negative Balances">
 <!ENTITY staff.main.menu.admin.local_admin.patrons_due_refunds.accesskey "N">
 <!ENTITY staff.main.menu.admin.local_admin.address_alert.label "Address Alerts">
+<!ENTITY staff.main.menu.admin.local_admin.circ_limit_set.label "Circulation Limit Sets">
 
 <!ENTITY staff.main.menu.admin.server_admin.label "Server Administration">
 <!ENTITY staff.main.menu.admin.server_admin.conify.org_unit_type.label "Organization Types">
@@ -746,6 +747,7 @@
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_actor_sip_fields "Actor Stat Cat Sip Fields">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_asset_sip_fields "Asset Stat Cat Sip Fields">
 <!ENTITY staff.main.menu.admin.server_admin.conify.global_flag.label "Global Flags">
+<!ENTITY staff.main.menu.admin.server_admin.conify.circulation_limit_group.label "Circulation Limit Groups">
 
 <!ENTITY staff.main.menu.admin.server_admin.acq.label "Acquisitions">
 <!ENTITY staff.main.menu.admin.server_admin.acq.accesskey "A">
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
index 402acb0..e84ffac 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
@@ -770,6 +770,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/permission/grp_penalty_threshold', null, event); }
             ],
+            'cmd_local_admin_circ_limit_set' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/config/circ_limit_set', null, event); }
+            ],
             'cmd_server_admin_config_rule_circ_duration' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/rule_circ_duration', null, event); }
@@ -810,6 +814,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/asset_sip_fields', null, event); }
             ],
+            'cmd_server_admin_circ_limit_group' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/config/circ_limit_group', null, event); }
+            ],
             'cmd_local_admin_external_text_editor' : [
                 ['oncommand'],
                 function() {
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
index dd1f479..c12c245 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
@@ -149,7 +149,9 @@
     <command id="cmd_local_admin_patrons_due_refunds" />
     <command id="cmd_local_admin_copy_template" />
     <command id="cmd_local_admin_address_alert"
-             perm="ADMIN_ADDRESS_ALERT VIEW_ADDRESS_ALERT"
+             perm="ADMIN_ADDRESS_ALERT VIEW_ADDRESS_ALERT" />
+    <command id="cmd_local_admin_circ_limit_set"
+             perm="ADMIN_CIRC_MATRIX_MATCHPOINT VIEW_CIRC_MATRIX_MATCHPOINT"
              />
 
     <!-- server admin menu commands -->
@@ -195,6 +197,9 @@
     <command id="cmd_server_admin_circ_mod" 
              perm="CREATE_CIRC_MOD DELETE_CIRC_MOD UPDATE_CIRC_MOD ADMIN_CIRC_MOD"
              />
+    <command id="cmd_server_admin_circ_limit_group"
+             perm="ADMIN_CIRC_MATRIX_MATCHPOINT VIEW_CIRC_MATRIX_MATCHPOINT"
+             />
     <command id="cmd_server_admin_global_flag"
              perm="ADMIN_GLOBAL_FLAG"
              />
@@ -485,6 +490,7 @@
                 <menuitem label="&staff.server.admin.index.cash_reports;" command="cmd_local_admin_cash_reports"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.barcode_completion.label;" command="cmd_local_admin_barcode_completion"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.circ_matrix_matchpoint.label;" command="cmd_local_admin_circ_matrix_matchpoint"/>
+                <menuitem label="&staff.main.menu.admin.local_admin.circ_limit_set.label;" command="cmd_local_admin_circ_limit_set"/>
                 <menuitem label="&staff.server.admin.index.closed_dates;" command="cmd_local_admin_closed_dates"/>
                 <menuitem label="&staff.server.admin.index.copy_locations;" command="cmd_local_admin_copy_locations"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.copy_location_order.label;" command="cmd_local_admin_copy_location_order"/>
@@ -522,6 +528,7 @@
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.sms_carrier.label;" command="cmd_server_admin_sms_carrier"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.z3950_source.label;" command="cmd_server_admin_z39_source"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.circulation_modifier.label;" command="cmd_server_admin_circ_mod"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.circulation_limit_group.label;" command="cmd_server_admin_circ_limit_group"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.global_flag.label;" command="cmd_server_admin_global_flag"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.import_match_set;" command="cmd_server_admin_import_match_set"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.org_unit_setting_type;" command="cmd_server_admin_org_unit_setting_type"/>

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |  118 ++++++--
 .../lib/OpenILS/Application/Circ/Circulate.pm      |    7 +
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/100.circ_matrix.sql            |  129 ++++++--
 .../src/sql/Pg/upgrade/0677.schema.circ_limits.sql |  331 ++++++++++++++++++++
 .../conify/global/config/circ_limit_group.tt2      |   36 +++
 .../conify/global/config/circ_limit_set.tt2        |   78 +++++
 .../global/config/circ_matrix_matchpoint.tt2       |   56 ++--
 .../default/conify/global/config/circ_limit_set.js |  250 +++++++++++++++
 .../conify/global/config/circ_matrix_matchpoint.js |  311 ++++++++++---------
 Open-ILS/web/opac/locale/en-US/lang.dtd            |    2 +
 .../xul/staff_client/chrome/content/main/menu.js   |    8 +
 .../chrome/content/main/menu_frame_menus.xul       |    9 +-
 docs/circ_limits.txt                               |  138 ++++++++
 14 files changed, 1233 insertions(+), 242 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0677.schema.circ_limits.sql
 create mode 100644 Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2
 create mode 100644 Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2
 create mode 100644 Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js
 create mode 100644 docs/circ_limits.txt


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list