New Circ Limits
authorThomas Berezansky <tsbere@mvlc.org>
Tue, 7 Feb 2012 22:26:03 +0000 (17:26 -0500)
committerMike Rylander <mrylander@gmail.com>
Tue, 28 Feb 2012 18:33:50 +0000 (13:33 -0500)
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@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@mvlc.org>
Signed-off-by: Mike Rylander <mrylander@gmail.com>

12 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/sql/Pg/100.circ_matrix.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql [new file with mode: 0644]
Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2 [new file with mode: 0644]
Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2 [new file with mode: 0644]
Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2
Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul

index 1894f77..f53638e 100644 (file)
@@ -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">
index e5d572c..731131d 100644 (file)
@@ -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;
index 6d82e73..7b9e2eb 100644 (file)
@@ -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 (file)
index 0000000..59b38f6
--- /dev/null
@@ -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 (file)
index 0000000..4f36e9c
--- /dev/null
@@ -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 (file)
index 0000000..2138706
--- /dev/null
@@ -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 %]
+
index b1d7150..67386f0 100644 (file)
 </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 (file)
index 0000000..49d6aa8
--- /dev/null
@@ -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);
+
index 6e8dd50..175a15d 100644 (file)
@@ -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);
index fc327f6..2acf0c7 100644 (file)
 <!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">
 <!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">
index 402acb0..e84ffac 100644 (file)
@@ -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() {
index dd1f479..c12c245 100644 (file)
     <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 -->
     <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"
              />
                 <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"/>
                 <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"/>