]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/0527.schema.matrix-bib_level.sql
Lazy Circ (AKA partial barcode lookup)
[working/Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / 0527.schema.matrix-bib_level.sql
1 BEGIN;
2
3 ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
4
5 UPDATE config.circ_matrix_weights
6 SET marc_bib_level = marc_vr_format;
7
8 ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
9
10 UPDATE config.hold_matrix_weights
11 SET marc_bib_level = marc_vr_format;
12
13 ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
14
15 ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
16
17 ALTER TABLE config.circ_matrix_matchpoint ADD COLUMN marc_bib_level text;
18
19 ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN marc_bib_level text;
20
21 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$
22 DECLARE
23     cn_object       asset.call_number%ROWTYPE;
24     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
25     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
26     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
27     weights         config.circ_matrix_weights%ROWTYPE;
28     user_age        INTERVAL;
29     denominator     NUMERIC(6,2);
30     row_list        INT[];
31     result          action.found_circ_matrix_matchpoint;
32 BEGIN
33     -- Assume failure
34     result.success = false;
35
36     -- Fetch useful data
37     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
38     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
39
40     -- Pre-generate this so we only calc it once
41     IF user_object.dob IS NOT NULL THEN
42         SELECT INTO user_age age(user_object.dob);
43     END IF;
44
45     -- Grab the closest set circ weight setting.
46     SELECT INTO weights cw.*
47       FROM config.weight_assoc wa
48            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
49            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
50       WHERE active
51       ORDER BY d.distance
52       LIMIT 1;
53
54     -- No weights? Bad admin! Defaults to handle that anyway.
55     IF weights.id IS NULL THEN
56         weights.grp                 := 11.0;
57         weights.org_unit            := 10.0;
58         weights.circ_modifier       := 5.0;
59         weights.marc_type           := 4.0;
60         weights.marc_form           := 3.0;
61         weights.marc_bib_level      := 2.0;
62         weights.marc_vr_format      := 2.0;
63         weights.copy_circ_lib       := 8.0;
64         weights.copy_owning_lib     := 8.0;
65         weights.user_home_ou        := 8.0;
66         weights.ref_flag            := 1.0;
67         weights.juvenile_flag       := 6.0;
68         weights.is_renewal          := 7.0;
69         weights.usr_age_lower_bound := 0.0;
70         weights.usr_age_upper_bound := 0.0;
71     END IF;
72
73     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
74     -- If you break your org tree with funky parenting this may be wrong
75     -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
76     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
77     WITH all_distance(distance) AS (
78             SELECT depth AS distance FROM actor.org_unit_type
79         UNION
80             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
81         )
82     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
83
84     -- Loop over all the potential matchpoints
85     FOR cur_matchpoint IN
86         SELECT m.*
87           FROM  config.circ_matrix_matchpoint m
88                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
89                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
90                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
91                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
92                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
93           WHERE m.active
94                 -- Permission Groups
95              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
96                 -- Org Units
97              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
98                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
99                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
100                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
101                 -- Circ Type
102                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
103                 -- Static User Checks
104                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
105                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
106                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
107                 -- Static Item Checks
108                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
109                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
110                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
111                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
112                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
113                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
114           ORDER BY
115                 -- Permission Groups
116                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
117                 -- Org Units
118                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
119                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
120                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
121                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
122                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
123                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
124                 -- Static User Checks
125                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
126                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
127                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
128                 -- Static Item Checks
129                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
130                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
131                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
132                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
133                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
134                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
135                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
136                 m.id LOOP
137
138         -- Record the full matching row list
139         row_list := row_list || cur_matchpoint.id;
140
141         -- No matchpoint yet?
142         IF matchpoint.id IS NULL THEN
143             -- Take the entire matchpoint as a starting point
144             matchpoint := cur_matchpoint;
145             CONTINUE; -- No need to look at this row any more.
146         END IF;
147
148         -- Incomplete matchpoint?
149         IF matchpoint.circulate IS NULL THEN
150             matchpoint.circulate := cur_matchpoint.circulate;
151         END IF;
152         IF matchpoint.duration_rule IS NULL THEN
153             matchpoint.duration_rule := cur_matchpoint.duration_rule;
154         END IF;
155         IF matchpoint.recurring_fine_rule IS NULL THEN
156             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
157         END IF;
158         IF matchpoint.max_fine_rule IS NULL THEN
159             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
160         END IF;
161         IF matchpoint.hard_due_date IS NULL THEN
162             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
163         END IF;
164         IF matchpoint.total_copy_hold_ratio IS NULL THEN
165             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
166         END IF;
167         IF matchpoint.available_copy_hold_ratio IS NULL THEN
168             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
169         END IF;
170         IF matchpoint.renewals IS NULL THEN
171             matchpoint.renewals := cur_matchpoint.renewals;
172         END IF;
173         IF matchpoint.grace_period IS NULL THEN
174             matchpoint.grace_period := cur_matchpoint.grace_period;
175         END IF;
176     END LOOP;
177
178     -- Check required fields
179     IF matchpoint.circulate             IS NOT NULL AND
180        matchpoint.duration_rule         IS NOT NULL AND
181        matchpoint.recurring_fine_rule   IS NOT NULL AND
182        matchpoint.max_fine_rule         IS NOT NULL THEN
183         -- All there? We have a completed match.
184         result.success := true;
185     END IF;
186
187     -- Include the assembled matchpoint, even if it isn't complete
188     result.matchpoint := matchpoint;
189
190     -- Include (for debugging) the full list of matching rows
191     result.buildrows := row_list;
192
193     -- Hand the result back to caller
194     RETURN result;
195 END;
196 $func$ LANGUAGE plpgsql;
197
198 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
199   RETURNS integer AS
200 $func$
201 DECLARE
202     requestor_object    actor.usr%ROWTYPE;
203     user_object         actor.usr%ROWTYPE;
204     item_object         asset.copy%ROWTYPE;
205     item_cn_object      asset.call_number%ROWTYPE;
206     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
207     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
208     weights             config.hold_matrix_weights%ROWTYPE;
209     denominator         NUMERIC(6,2);
210 BEGIN
211     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
212     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
213     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
214     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
215     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
216
217     -- The item's owner should probably be the one determining if the item is holdable
218     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
219     -- This flag will allow for setting it to the owning library (where the call number "lives")
220     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
221
222     -- Grab the closest set circ weight setting.
223     IF NOT FOUND THEN
224         -- Default to circ library
225         SELECT INTO weights hw.*
226           FROM config.weight_assoc wa
227                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
228                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
229           WHERE active
230           ORDER BY d.distance
231           LIMIT 1;
232     ELSE
233         -- Flag is set, use owning library
234         SELECT INTO weights hw.*
235           FROM config.weight_assoc wa
236                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
237                JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
238           WHERE active
239           ORDER BY d.distance
240           LIMIT 1;
241     END IF;
242
243     -- No weights? Bad admin! Defaults to handle that anyway.
244     IF weights.id IS NULL THEN
245         weights.user_home_ou    := 5.0;
246         weights.request_ou      := 5.0;
247         weights.pickup_ou       := 5.0;
248         weights.item_owning_ou  := 5.0;
249         weights.item_circ_ou    := 5.0;
250         weights.usr_grp         := 7.0;
251         weights.requestor_grp   := 8.0;
252         weights.circ_modifier   := 4.0;
253         weights.marc_type       := 3.0;
254         weights.marc_form       := 2.0;
255         weights.marc_bib_level  := 1.0;
256         weights.marc_vr_format  := 1.0;
257         weights.juvenile_flag   := 4.0;
258         weights.ref_flag        := 0.0;
259     END IF;
260
261     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
262     -- If you break your org tree with funky parenting this may be wrong
263     -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
264     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
265     WITH all_distance(distance) AS (
266             SELECT depth AS distance FROM actor.org_unit_type
267         UNION
268             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
269         )
270     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
271
272     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
273     -- This may be better implemented as part of the upgrade script?
274     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
275     -- Then remove this flag, of course.
276     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
277
278     IF FOUND THEN
279         -- Note: This, to me, is REALLY hacky. I put it in anyway.
280         -- If you can't tell, this is a single call swap on two variables.
281         SELECT INTO user_object.profile, requestor_object.profile
282                     requestor_object.profile, user_object.profile;
283     END IF;
284
285     -- Select the winning matchpoint into the matchpoint variable for returning
286     SELECT INTO matchpoint m.*
287       FROM  config.hold_matrix_matchpoint m
288             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
289             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
290             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
291             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
292             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
293             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
294             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
295       WHERE m.active
296             -- Permission Groups
297          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
298             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
299             -- Org Units
300             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
301             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
302             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
303             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
304             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
305             -- Static User Checks
306             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
307             -- Static Item Checks
308             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
309             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
310             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
311             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
312             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
313             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
314       ORDER BY
315             -- Permission Groups
316             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
317             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
318             -- Org Units
319             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
320             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
321             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
322             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
323             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
324             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
325             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
326             -- Static Item Checks
327             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
328             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
329             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
330             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
331             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
332             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
333             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
334             m.id;
335
336     -- Return just the ID for now
337     RETURN matchpoint.id;
338 END;
339 $func$ LANGUAGE 'plpgsql';
340
341 COMMIT;