]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
Copy Location Circ Limit Sets : DB and IDL
[Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / XXXX.schema.copy_loc_circ_limits.sql
1
2 -- Linkage between limit sets and circ mods
3 CREATE TABLE config.circ_limit_set_copy_loc_map (
4     id          SERIAL  PRIMARY KEY,
5     limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
6     copy_loc    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
7     CONSTRAINT cl_once_per_set UNIQUE (limit_set, copy_loc)
8 );
9
10 -- Add support for checking config.circ_limit_set_copy_loc_map's
11 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) 
12     RETURNS SETOF action.circ_matrix_test_result AS $func$
13 DECLARE
14     user_object             actor.usr%ROWTYPE;
15     standing_penalty        config.standing_penalty%ROWTYPE;
16     item_object             asset.copy%ROWTYPE;
17     item_status_object      config.copy_status%ROWTYPE;
18     item_location_object    asset.copy_location%ROWTYPE;
19     result                  action.circ_matrix_test_result;
20     circ_test               action.found_circ_matrix_matchpoint;
21     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
22     circ_limit_set          config.circ_limit_set%ROWTYPE;
23     hold_ratio              action.hold_stats%ROWTYPE;
24     penalty_type            TEXT;
25     items_out               INT;
26     context_org_list        INT[];
27     done                    BOOL := FALSE;
28 BEGIN
29     -- Assume success unless we hit a failure condition
30     result.success := TRUE;
31
32     -- Need user info to look up matchpoints
33     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
34
35     -- (Insta)Fail if we couldn't find the user
36     IF user_object.id IS NULL THEN
37         result.fail_part := 'no_user';
38         result.success := FALSE;
39         done := TRUE;
40         RETURN NEXT result;
41         RETURN;
42     END IF;
43
44     -- Need item info to look up matchpoints
45     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
46
47     -- (Insta)Fail if we couldn't find the item 
48     IF item_object.id IS NULL THEN
49         result.fail_part := 'no_item';
50         result.success := FALSE;
51         done := TRUE;
52         RETURN NEXT result;
53         RETURN;
54     END IF;
55
56     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
57
58     circ_matchpoint             := circ_test.matchpoint;
59     result.matchpoint           := circ_matchpoint.id;
60     result.circulate            := circ_matchpoint.circulate;
61     result.duration_rule        := circ_matchpoint.duration_rule;
62     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
63     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
64     result.hard_due_date        := circ_matchpoint.hard_due_date;
65     result.renewals             := circ_matchpoint.renewals;
66     result.grace_period         := circ_matchpoint.grace_period;
67     result.buildrows            := circ_test.buildrows;
68
69     -- (Insta)Fail if we couldn't find a matchpoint
70     IF circ_test.success = false THEN
71         result.fail_part := 'no_matchpoint';
72         result.success := FALSE;
73         done := TRUE;
74         RETURN NEXT result;
75         RETURN;
76     END IF;
77
78     -- All failures before this point are non-recoverable
79     -- Below this point are possibly overridable failures
80
81     -- Fail if the user is barred
82     IF user_object.barred IS TRUE THEN
83         result.fail_part := 'actor.usr.barred';
84         result.success := FALSE;
85         done := TRUE;
86         RETURN NEXT result;
87     END IF;
88
89     -- Fail if the item can't circulate
90     IF item_object.circulate IS FALSE THEN
91         result.fail_part := 'asset.copy.circulate';
92         result.success := FALSE;
93         done := TRUE;
94         RETURN NEXT result;
95     END IF;
96
97     -- Fail if the item isn't in a circulateable status on a non-renewal
98     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
99         result.fail_part := 'asset.copy.status';
100         result.success := FALSE;
101         done := TRUE;
102         RETURN NEXT result;
103     -- Alternately, fail if the item isn't checked out on a renewal
104     ELSIF renewal AND item_object.status <> 1 THEN
105         result.fail_part := 'asset.copy.status';
106         result.success := FALSE;
107         done := TRUE;
108         RETURN NEXT result;
109     END IF;
110
111     -- Fail if the item can't circulate because of the shelving location
112     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
113     IF item_location_object.circulate IS FALSE THEN
114         result.fail_part := 'asset.copy_location.circulate';
115         result.success := FALSE;
116         done := TRUE;
117         RETURN NEXT result;
118     END IF;
119
120     -- Use Circ OU for penalties and such
121     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
122
123     IF renewal THEN
124         penalty_type = '%RENEW%';
125     ELSE
126         penalty_type = '%CIRC%';
127     END IF;
128
129     FOR standing_penalty IN
130         SELECT  DISTINCT csp.*
131           FROM  actor.usr_standing_penalty usp
132                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
133           WHERE usr = match_user
134                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
135                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
136                 AND csp.block_list LIKE penalty_type LOOP
137
138         result.fail_part := standing_penalty.name;
139         result.success := FALSE;
140         done := TRUE;
141         RETURN NEXT result;
142     END LOOP;
143
144     -- Fail if the test is set to hard non-circulating
145     IF circ_matchpoint.circulate IS FALSE THEN
146         result.fail_part := 'config.circ_matrix_test.circulate';
147         result.success := FALSE;
148         done := TRUE;
149         RETURN NEXT result;
150     END IF;
151
152     -- Fail if the total copy-hold ratio is too low
153     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
154         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
155         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
156             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
157             result.success := FALSE;
158             done := TRUE;
159             RETURN NEXT result;
160         END IF;
161     END IF;
162
163     -- Fail if the available copy-hold ratio is too low
164     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
165         IF hold_ratio.hold_count IS NULL THEN
166             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
167         END IF;
168         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
169             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
170             result.success := FALSE;
171             done := TRUE;
172             RETURN NEXT result;
173         END IF;
174     END IF;
175
176     -- Fail if the user has too many items out by defined limit sets
177     FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
178       JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
179       WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
180         ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
181         ) LOOP
182             IF circ_limit_set.items_out > 0 AND NOT renewal THEN
183                 SELECT INTO context_org_list ARRAY_AGG(aou.id)
184                   FROM actor.org_unit_full_path( circ_ou ) aou
185                     JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
186                   WHERE aout.depth >= circ_limit_set.depth;
187                 IF circ_limit_set.global THEN
188                     WITH RECURSIVE descendant_depth AS (
189                         SELECT  ou.id,
190                             ou.parent_ou
191                         FROM  actor.org_unit ou
192                         WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
193                             UNION
194                         SELECT  ou.id,
195                             ou.parent_ou
196                         FROM  actor.org_unit ou
197                             JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
198                     ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
199                 END IF;
200                 SELECT INTO items_out COUNT(DISTINCT circ.id)
201                   FROM action.circulation circ
202                     JOIN asset.copy copy ON (copy.id = circ.target_copy)
203                     LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
204                   WHERE circ.usr = match_user
205                     AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
206                     AND circ.checkin_time IS NULL
207                     AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
208                     AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
209                         OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
210                         OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
211                     );
212                 IF items_out >= circ_limit_set.items_out THEN
213                     result.fail_part := 'config.circ_matrix_circ_mod_test';
214                     result.success := FALSE;
215                     done := TRUE;
216                     RETURN NEXT result;
217                 END IF;
218             END IF;
219             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;
220     END LOOP;
221
222     -- If we passed everything, return the successful matchpoint
223     IF NOT done THEN
224         RETURN NEXT result;
225     END IF;
226
227     RETURN;
228 END;
229 $func$ LANGUAGE plpgsql;
230