SCPL: fix glitch in 2.3.0 schema upgrade script
[transitory.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.2-2.3.0-upgrade-db.sql
1 --Upgrade Script for 2.2 to 2.3.0
2 BEGIN;
3
4 INSERT INTO config.settings_group (name, label) VALUES
5 ('acq', oils_i18n_gettext('config.settings_group.system', 'Acquisitions', 'coust', 'label'));
6
7
8 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.3.0', :eg_version);
9 -- Evergreen DB patch 0703.tpac_value_maps.sql
10
11 -- check whether patch can be applied
12 SELECT evergreen.upgrade_deps_block_check('0703', :eg_version);
13
14 ALTER TABLE config.coded_value_map
15     ADD COLUMN opac_visible BOOL NOT NULL DEFAULT TRUE,
16     ADD COLUMN search_label TEXT,
17     ADD COLUMN is_simple BOOL NOT NULL DEFAULT FALSE;
18
19
20
21 SELECT evergreen.upgrade_deps_block_check('0712', :eg_version);
22
23 -- General purpose query container.  Any table the needs to store
24 -- a QueryParser query should store it here.  This will be the 
25 -- source for top-level and QP sub-search inclusion queries.
26 CREATE TABLE actor.search_query (
27     id          SERIAL PRIMARY KEY, 
28     label       TEXT NOT NULL, -- i18n
29     query_text  TEXT NOT NULL -- QP text
30 );
31
32 -- e.g. "Reading Level"
33 CREATE TABLE actor.search_filter_group (
34     id          SERIAL      PRIMARY KEY,
35     owner       INT         NOT NULL REFERENCES actor.org_unit (id) 
36                             ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
37     code        TEXT        NOT NULL, -- for CGI, etc.
38     label       TEXT        NOT NULL, -- i18n
39     create_date TIMESTAMPTZ NOT NULL DEFAULT now(),
40     CONSTRAINT  asfg_label_once_per_org UNIQUE (owner, label),
41     CONSTRAINT  asfg_code_once_per_org UNIQUE (owner, code)
42 );
43
44 -- e.g. "Adult", "Teen", etc.
45 CREATE TABLE actor.search_filter_group_entry (
46     id          SERIAL  PRIMARY KEY,
47     grp         INT     NOT NULL REFERENCES actor.search_filter_group(id) 
48                         ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
49     pos         INT     NOT NULL DEFAULT 0,
50     query       INT     NOT NULL REFERENCES actor.search_query(id) 
51                         ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
52     CONSTRAINT asfge_query_once_per_group UNIQUE (grp, query)
53 );
54
55
56 SELECT evergreen.upgrade_deps_block_check('0713', :eg_version);
57
58 INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES (
59     'ui.grid_columns.circ.hold_pull_list',
60     'gui',
61     FALSE,
62     oils_i18n_gettext(
63         'ui.grid_columns.circ.hold_pull_list',
64         'Hold Pull List',
65         'cust',
66         'label'
67     ),
68     oils_i18n_gettext(
69         'ui.grid_columns.circ.hold_pull_list',
70         'Hold Pull List Saved Column Settings',
71         'cust',
72         'description'
73     ),
74     'string'
75 );
76
77
78
79 SELECT evergreen.upgrade_deps_block_check('0714', :eg_version);
80
81 INSERT into config.org_unit_setting_type 
82     (name, grp, label, description, datatype) 
83     VALUES ( 
84         'opac.patron.auto_overide_hold_events', 
85         'opac',
86         oils_i18n_gettext(
87             'opac.patron.auto_overide_hold_events',
88             'Auto-Override Permitted Hold Blocks (Patrons)',
89             'coust', 
90             'label'
91         ),
92         oils_i18n_gettext(
93             'opac.patron.auto_overide_hold_events',
94             'When a patron places a hold that fails and the patron has the correct permission ' ||
95             'to override the hold, automatically override the hold without presenting a message ' ||
96             'to the patron and requiring that the patron make a decision to override',
97             'coust', 
98             'description'
99         ),
100         'bool'
101     );
102
103 -- Evergreen DB patch 0718.data.add-to-permanent-bookbag.sql
104
105 -- check whether patch can be applied
106 SELECT evergreen.upgrade_deps_block_check('0718', :eg_version);
107
108 INSERT into config.org_unit_setting_type
109     (name, grp, label, description, datatype)
110     VALUES (
111         'opac.patron.temporary_list_warn',
112         'opac',
113         oils_i18n_gettext(
114             'opac.patron.temporary_list_warn',
115             'Warn patrons when adding to a temporary book list',
116             'coust',
117             'label'
118         ),
119         oils_i18n_gettext(
120             'opac.patron.temporary_list_warn',
121             'Present a warning dialog to the patron when a patron adds a book to a temporary book bag.',
122             'coust',
123             'description'
124         ),
125         'bool'
126     );
127
128 INSERT INTO config.usr_setting_type
129     (name,grp,opac_visible,label,description,datatype)
130 VALUES (
131     'opac.temporary_list_no_warn',
132     'opac',
133     TRUE,
134     oils_i18n_gettext(
135         'opac.temporary_list_no_warn',
136         'Opt out of warning when adding a book to a temporary book list',
137         'cust',
138         'label'
139     ),
140     oils_i18n_gettext(
141         'opac.temporary_list_no_warn',
142         'Opt out of warning when adding a book to a temporary book list',
143         'cust',
144         'description'
145     ),
146     'bool'
147 );
148
149 INSERT INTO config.usr_setting_type
150     (name,grp,opac_visible,label,description,datatype)
151 VALUES (
152     'opac.default_list',
153     'opac',
154     FALSE,
155     oils_i18n_gettext(
156         'opac.default_list',
157         'Default list to use when adding to a bookbag',
158         'cust',
159         'label'
160     ),
161     oils_i18n_gettext(
162         'opac.default_list',
163         'Default list to use when adding to a bookbag',
164         'cust',
165         'description'
166     ),
167     'integer'
168 );
169
170
171 SELECT evergreen.upgrade_deps_block_check('0719', :eg_version);
172
173 INSERT INTO config.org_unit_setting_type (
174     name, label, grp, description, datatype
175 ) VALUES (
176     'circ.staff.max_visible_event_age',
177     'Maximum visible age of User Trigger Events in Staff Interfaces',
178     'circ',
179     'If this is unset, staff can view User Trigger Events regardless of age. When this is set to an interval, it represents the age of the oldest possible User Trigger Event that can be viewed.',
180     'interval'
181 );
182
183 INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES (
184     'ui.grid_columns.actor.user.event_log',
185     'gui',
186     FALSE,
187     oils_i18n_gettext(
188         'ui.grid_columns.actor.user.event_log',
189         'User Event Log',
190         'cust',
191         'label'
192     ),
193     oils_i18n_gettext(
194         'ui.grid_columns.actor.user.event_log',
195         'User Event Log Saved Column Settings',
196         'cust',
197         'description'
198     ),
199     'string'
200 );
201
202 INSERT INTO permission.perm_list ( id, code, description )
203     VALUES (
204         535,
205         'VIEW_TRIGGER_EVENT',
206         oils_i18n_gettext(
207             535,
208             'Allows a user to view circ- and hold-related action/trigger events',
209             'ppl',
210             'description'
211         )
212     );
213
214
215 SELECT evergreen.upgrade_deps_block_check('0720', :eg_version);
216
217 ALTER TABLE config.circ_matrix_weights 
218     ADD COLUMN copy_location NUMERIC(6,2) NOT NULL DEFAULT 5.0;
219 UPDATE config.circ_matrix_weights 
220     SET copy_location = 0.0 WHERE name = 'All_Equal';
221 ALTER TABLE config.circ_matrix_weights 
222     ALTER COLUMN copy_location DROP DEFAULT; -- for consistency w/ baseline schema
223
224 ALTER TABLE config.circ_matrix_matchpoint
225     ADD COLUMN copy_location INTEGER REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED;
226
227 DROP INDEX config.ccmm_once_per_paramset;
228
229 CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(copy_location::TEXT, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
230
231 -- Linkage between limit sets and circ mods
232 CREATE TABLE config.circ_limit_set_copy_loc_map (
233     id          SERIAL  PRIMARY KEY,
234     limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
235     copy_loc    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
236     CONSTRAINT cl_once_per_set UNIQUE (limit_set, copy_loc)
237 );
238
239 -- Add support for checking config.circ_limit_set_copy_loc_map's
240 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) 
241     RETURNS SETOF action.circ_matrix_test_result AS $func$
242 DECLARE
243     user_object             actor.usr%ROWTYPE;
244     standing_penalty        config.standing_penalty%ROWTYPE;
245     item_object             asset.copy%ROWTYPE;
246     item_status_object      config.copy_status%ROWTYPE;
247     item_location_object    asset.copy_location%ROWTYPE;
248     result                  action.circ_matrix_test_result;
249     circ_test               action.found_circ_matrix_matchpoint;
250     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
251     circ_limit_set          config.circ_limit_set%ROWTYPE;
252     hold_ratio              action.hold_stats%ROWTYPE;
253     penalty_type            TEXT;
254     items_out               INT;
255     context_org_list        INT[];
256     done                    BOOL := FALSE;
257 BEGIN
258     -- Assume success unless we hit a failure condition
259     result.success := TRUE;
260
261     -- Need user info to look up matchpoints
262     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
263
264     -- (Insta)Fail if we couldn't find the user
265     IF user_object.id IS NULL THEN
266         result.fail_part := 'no_user';
267         result.success := FALSE;
268         done := TRUE;
269         RETURN NEXT result;
270         RETURN;
271     END IF;
272
273     -- Need item info to look up matchpoints
274     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
275
276     -- (Insta)Fail if we couldn't find the item 
277     IF item_object.id IS NULL THEN
278         result.fail_part := 'no_item';
279         result.success := FALSE;
280         done := TRUE;
281         RETURN NEXT result;
282         RETURN;
283     END IF;
284
285     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
286
287     circ_matchpoint             := circ_test.matchpoint;
288     result.matchpoint           := circ_matchpoint.id;
289     result.circulate            := circ_matchpoint.circulate;
290     result.duration_rule        := circ_matchpoint.duration_rule;
291     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
292     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
293     result.hard_due_date        := circ_matchpoint.hard_due_date;
294     result.renewals             := circ_matchpoint.renewals;
295     result.grace_period         := circ_matchpoint.grace_period;
296     result.buildrows            := circ_test.buildrows;
297
298     -- (Insta)Fail if we couldn't find a matchpoint
299     IF circ_test.success = false THEN
300         result.fail_part := 'no_matchpoint';
301         result.success := FALSE;
302         done := TRUE;
303         RETURN NEXT result;
304         RETURN;
305     END IF;
306
307     -- All failures before this point are non-recoverable
308     -- Below this point are possibly overridable failures
309
310     -- Fail if the user is barred
311     IF user_object.barred IS TRUE THEN
312         result.fail_part := 'actor.usr.barred';
313         result.success := FALSE;
314         done := TRUE;
315         RETURN NEXT result;
316     END IF;
317
318     -- Fail if the item can't circulate
319     IF item_object.circulate IS FALSE THEN
320         result.fail_part := 'asset.copy.circulate';
321         result.success := FALSE;
322         done := TRUE;
323         RETURN NEXT result;
324     END IF;
325
326     -- Fail if the item isn't in a circulateable status on a non-renewal
327     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
328         result.fail_part := 'asset.copy.status';
329         result.success := FALSE;
330         done := TRUE;
331         RETURN NEXT result;
332     -- Alternately, fail if the item isn't checked out on a renewal
333     ELSIF renewal AND item_object.status <> 1 THEN
334         result.fail_part := 'asset.copy.status';
335         result.success := FALSE;
336         done := TRUE;
337         RETURN NEXT result;
338     END IF;
339
340     -- Fail if the item can't circulate because of the shelving location
341     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
342     IF item_location_object.circulate IS FALSE THEN
343         result.fail_part := 'asset.copy_location.circulate';
344         result.success := FALSE;
345         done := TRUE;
346         RETURN NEXT result;
347     END IF;
348
349     -- Use Circ OU for penalties and such
350     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
351
352     IF renewal THEN
353         penalty_type = '%RENEW%';
354     ELSE
355         penalty_type = '%CIRC%';
356     END IF;
357
358     FOR standing_penalty IN
359         SELECT  DISTINCT csp.*
360           FROM  actor.usr_standing_penalty usp
361                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
362           WHERE usr = match_user
363                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
364                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
365                 AND csp.block_list LIKE penalty_type LOOP
366
367         result.fail_part := standing_penalty.name;
368         result.success := FALSE;
369         done := TRUE;
370         RETURN NEXT result;
371     END LOOP;
372
373     -- Fail if the test is set to hard non-circulating
374     IF circ_matchpoint.circulate IS FALSE THEN
375         result.fail_part := 'config.circ_matrix_test.circulate';
376         result.success := FALSE;
377         done := TRUE;
378         RETURN NEXT result;
379     END IF;
380
381     -- Fail if the total copy-hold ratio is too low
382     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
383         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
384         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
385             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
386             result.success := FALSE;
387             done := TRUE;
388             RETURN NEXT result;
389         END IF;
390     END IF;
391
392     -- Fail if the available copy-hold ratio is too low
393     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
394         IF hold_ratio.hold_count IS NULL THEN
395             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
396         END IF;
397         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
398             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
399             result.success := FALSE;
400             done := TRUE;
401             RETURN NEXT result;
402         END IF;
403     END IF;
404
405     -- Fail if the user has too many items out by defined limit sets
406     FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
407       JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
408       WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
409         ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
410         ) LOOP
411             IF circ_limit_set.items_out > 0 AND NOT renewal THEN
412                 SELECT INTO context_org_list ARRAY_AGG(aou.id)
413                   FROM actor.org_unit_full_path( circ_ou ) aou
414                     JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
415                   WHERE aout.depth >= circ_limit_set.depth;
416                 IF circ_limit_set.global THEN
417                     WITH RECURSIVE descendant_depth AS (
418                         SELECT  ou.id,
419                             ou.parent_ou
420                         FROM  actor.org_unit ou
421                         WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
422                             UNION
423                         SELECT  ou.id,
424                             ou.parent_ou
425                         FROM  actor.org_unit ou
426                             JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
427                     ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
428                 END IF;
429                 SELECT INTO items_out COUNT(DISTINCT circ.id)
430                   FROM action.circulation circ
431                     JOIN asset.copy copy ON (copy.id = circ.target_copy)
432                     LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
433                   WHERE circ.usr = match_user
434                     AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
435                     AND circ.checkin_time IS NULL
436                     AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
437                     AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
438                         OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
439                         OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
440                     );
441                 IF items_out >= circ_limit_set.items_out THEN
442                     result.fail_part := 'config.circ_matrix_circ_mod_test';
443                     result.success := FALSE;
444                     done := TRUE;
445                     RETURN NEXT result;
446                 END IF;
447             END IF;
448             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;
449     END LOOP;
450
451     -- If we passed everything, return the successful matchpoint
452     IF NOT done THEN
453         RETURN NEXT result;
454     END IF;
455
456     RETURN;
457 END;
458 $func$ LANGUAGE plpgsql;
459
460
461 -- adding copy_loc to circ_matrix_matchpoint
462 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$
463 DECLARE
464     cn_object       asset.call_number%ROWTYPE;
465     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
466     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
467     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
468     weights         config.circ_matrix_weights%ROWTYPE;
469     user_age        INTERVAL;
470     my_item_age     INTERVAL;
471     denominator     NUMERIC(6,2);
472     row_list        INT[];
473     result          action.found_circ_matrix_matchpoint;
474 BEGIN
475     -- Assume failure
476     result.success = false;
477
478     -- Fetch useful data
479     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
480     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
481
482     -- Pre-generate this so we only calc it once
483     IF user_object.dob IS NOT NULL THEN
484         SELECT INTO user_age age(user_object.dob);
485     END IF;
486
487     -- Ditto
488     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
489
490     -- Grab the closest set circ weight setting.
491     SELECT INTO weights cw.*
492       FROM config.weight_assoc wa
493            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
494            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
495       WHERE active
496       ORDER BY d.distance
497       LIMIT 1;
498
499     -- No weights? Bad admin! Defaults to handle that anyway.
500     IF weights.id IS NULL THEN
501         weights.grp                 := 11.0;
502         weights.org_unit            := 10.0;
503         weights.circ_modifier       := 5.0;
504         weights.copy_location       := 5.0;
505         weights.marc_type           := 4.0;
506         weights.marc_form           := 3.0;
507         weights.marc_bib_level      := 2.0;
508         weights.marc_vr_format      := 2.0;
509         weights.copy_circ_lib       := 8.0;
510         weights.copy_owning_lib     := 8.0;
511         weights.user_home_ou        := 8.0;
512         weights.ref_flag            := 1.0;
513         weights.juvenile_flag       := 6.0;
514         weights.is_renewal          := 7.0;
515         weights.usr_age_lower_bound := 0.0;
516         weights.usr_age_upper_bound := 0.0;
517         weights.item_age            := 0.0;
518     END IF;
519
520     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
521     -- If you break your org tree with funky parenting this may be wrong
522     -- 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
523     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
524     WITH all_distance(distance) AS (
525             SELECT depth AS distance FROM actor.org_unit_type
526         UNION
527             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
528         )
529     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
530
531     -- Loop over all the potential matchpoints
532     FOR cur_matchpoint IN
533         SELECT m.*
534           FROM  config.circ_matrix_matchpoint m
535                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
536                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
537                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
538                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
539                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
540           WHERE m.active
541                 -- Permission Groups
542              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
543                 -- Org Units
544              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
545                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
546                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
547                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
548                 -- Circ Type
549                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
550                 -- Static User Checks
551                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
552                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
553                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
554                 -- Static Item Checks
555                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
556                 AND (m.copy_location            IS NULL OR m.copy_location = item_object.location)
557                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
558                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
559                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
560                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
561                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
562                 AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
563           ORDER BY
564                 -- Permission Groups
565                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
566                 -- Org Units
567                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
568                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
569                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
570                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
571                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
572                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
573                 -- Static User Checks
574                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
575                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
576                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
577                 -- Static Item Checks
578                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
579                 CASE WHEN m.copy_location       IS NOT NULL THEN 4^weights.copy_location ELSE 0.0 END +
580                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
581                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
582                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
583                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
584                 -- Item age has a slight adjustment to weight based on value.
585                 -- This should ensure that a shorter age limit comes first when all else is equal.
586                 -- NOTE: This assumes that intervals will normally be in days.
587                 CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
588                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
589                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
590                 m.id LOOP
591
592         -- Record the full matching row list
593         row_list := row_list || cur_matchpoint.id;
594
595         -- No matchpoint yet?
596         IF matchpoint.id IS NULL THEN
597             -- Take the entire matchpoint as a starting point
598             matchpoint := cur_matchpoint;
599             CONTINUE; -- No need to look at this row any more.
600         END IF;
601
602         -- Incomplete matchpoint?
603         IF matchpoint.circulate IS NULL THEN
604             matchpoint.circulate := cur_matchpoint.circulate;
605         END IF;
606         IF matchpoint.duration_rule IS NULL THEN
607             matchpoint.duration_rule := cur_matchpoint.duration_rule;
608         END IF;
609         IF matchpoint.recurring_fine_rule IS NULL THEN
610             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
611         END IF;
612         IF matchpoint.max_fine_rule IS NULL THEN
613             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
614         END IF;
615         IF matchpoint.hard_due_date IS NULL THEN
616             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
617         END IF;
618         IF matchpoint.total_copy_hold_ratio IS NULL THEN
619             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
620         END IF;
621         IF matchpoint.available_copy_hold_ratio IS NULL THEN
622             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
623         END IF;
624         IF matchpoint.renewals IS NULL THEN
625             matchpoint.renewals := cur_matchpoint.renewals;
626         END IF;
627         IF matchpoint.grace_period IS NULL THEN
628             matchpoint.grace_period := cur_matchpoint.grace_period;
629         END IF;
630     END LOOP;
631
632     -- Check required fields
633     IF matchpoint.circulate             IS NOT NULL AND
634        matchpoint.duration_rule         IS NOT NULL AND
635        matchpoint.recurring_fine_rule   IS NOT NULL AND
636        matchpoint.max_fine_rule         IS NOT NULL THEN
637         -- All there? We have a completed match.
638         result.success := true;
639     END IF;
640
641     -- Include the assembled matchpoint, even if it isn't complete
642     result.matchpoint := matchpoint;
643
644     -- Include (for debugging) the full list of matching rows
645     result.buildrows := row_list;
646
647     -- Hand the result back to caller
648     RETURN result;
649 END;
650 $func$ LANGUAGE plpgsql;
651
652
653
654
655 SELECT evergreen.upgrade_deps_block_check('0721', :eg_version);
656
657 UPDATE config.standing_penalty 
658     SET block_list = REPLACE(block_list, 'HOLD', 'HOLD|CAPTURE') 
659     WHERE   
660         -- STAFF_ penalties have names that match their block list
661         name NOT LIKE 'STAFF_%' 
662         -- belt & suspenders, also good for testing
663         AND block_list NOT LIKE '%CAPTURE%'; 
664
665  -- CIRC|FULFILL is now the same as CIRC previously was by itself
666 UPDATE config.standing_penalty 
667     SET block_list = REPLACE(block_list, 'CIRC', 'CIRC|FULFILL') 
668     WHERE   
669         -- STAFF_ penalties have names that match their block list
670         name NOT LIKE 'STAFF_%' 
671         -- belt & suspenders, also good for testing
672         AND block_list NOT LIKE '%FULFILL%'; 
673
674
675 -- apply the HOLD vs CAPTURE block logic
676 CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
677 DECLARE
678     matchpoint_id        INT;
679     user_object        actor.usr%ROWTYPE;
680     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
681     standing_penalty    config.standing_penalty%ROWTYPE;
682     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
683     transit_source        actor.org_unit%ROWTYPE;
684     item_object        asset.copy%ROWTYPE;
685     item_cn_object     asset.call_number%ROWTYPE;
686     item_status_object  config.copy_status%ROWTYPE;
687     item_location_object    asset.copy_location%ROWTYPE;
688     ou_skip              actor.org_unit_setting%ROWTYPE;
689     result            action.matrix_test_result;
690     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
691     use_active_date   TEXT;
692     age_protect_date  TIMESTAMP WITH TIME ZONE;
693     hold_count        INT;
694     hold_transit_prox    INT;
695     frozen_hold_count    INT;
696     context_org_list    INT[];
697     done            BOOL := FALSE;
698     hold_penalty TEXT;
699 BEGIN
700     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
701     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
702
703     result.success := TRUE;
704
705     -- The HOLD penalty block only applies to new holds.
706     -- The CAPTURE penalty block applies to existing holds.
707     hold_penalty := 'HOLD';
708     IF retargetting THEN
709         hold_penalty := 'CAPTURE';
710     END IF;
711
712     -- Fail if we couldn't find a user
713     IF user_object.id IS NULL THEN
714         result.fail_part := 'no_user';
715         result.success := FALSE;
716         done := TRUE;
717         RETURN NEXT result;
718         RETURN;
719     END IF;
720
721     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
722
723     -- Fail if we couldn't find a copy
724     IF item_object.id IS NULL THEN
725         result.fail_part := 'no_item';
726         result.success := FALSE;
727         done := TRUE;
728         RETURN NEXT result;
729         RETURN;
730     END IF;
731
732     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
733     result.matchpoint := matchpoint_id;
734
735     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
736
737     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
738     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
739         result.fail_part := 'circ.holds.target_skip_me';
740         result.success := FALSE;
741         done := TRUE;
742         RETURN NEXT result;
743         RETURN;
744     END IF;
745
746     -- Fail if user is barred
747     IF user_object.barred IS TRUE THEN
748         result.fail_part := 'actor.usr.barred';
749         result.success := FALSE;
750         done := TRUE;
751         RETURN NEXT result;
752         RETURN;
753     END IF;
754
755     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
756     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
757     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
758
759     -- Fail if we couldn't find any matchpoint (requires a default)
760     IF matchpoint_id IS NULL THEN
761         result.fail_part := 'no_matchpoint';
762         result.success := FALSE;
763         done := TRUE;
764         RETURN NEXT result;
765         RETURN;
766     END IF;
767
768     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
769
770     IF hold_test.holdable IS FALSE THEN
771         result.fail_part := 'config.hold_matrix_test.holdable';
772         result.success := FALSE;
773         done := TRUE;
774         RETURN NEXT result;
775     END IF;
776
777     IF item_object.holdable IS FALSE THEN
778         result.fail_part := 'item.holdable';
779         result.success := FALSE;
780         done := TRUE;
781         RETURN NEXT result;
782     END IF;
783
784     IF item_status_object.holdable IS FALSE THEN
785         result.fail_part := 'status.holdable';
786         result.success := FALSE;
787         done := TRUE;
788         RETURN NEXT result;
789     END IF;
790
791     IF item_location_object.holdable IS FALSE THEN
792         result.fail_part := 'location.holdable';
793         result.success := FALSE;
794         done := TRUE;
795         RETURN NEXT result;
796     END IF;
797
798     IF hold_test.transit_range IS NOT NULL THEN
799         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
800         IF hold_test.distance_is_from_owner THEN
801             SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
802         ELSE
803             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
804         END IF;
805
806         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
807
808         IF NOT FOUND THEN
809             result.fail_part := 'transit_range';
810             result.success := FALSE;
811             done := TRUE;
812             RETURN NEXT result;
813         END IF;
814     END IF;
815  
816     FOR standing_penalty IN
817         SELECT  DISTINCT csp.*
818           FROM  actor.usr_standing_penalty usp
819                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
820           WHERE usr = match_user
821                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
822                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
823                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
824
825         result.fail_part := standing_penalty.name;
826         result.success := FALSE;
827         done := TRUE;
828         RETURN NEXT result;
829     END LOOP;
830
831     IF hold_test.stop_blocked_user IS TRUE THEN
832         FOR standing_penalty IN
833             SELECT  DISTINCT csp.*
834               FROM  actor.usr_standing_penalty usp
835                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
836               WHERE usr = match_user
837                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
838                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
839                     AND csp.block_list LIKE '%CIRC%' LOOP
840     
841             result.fail_part := standing_penalty.name;
842             result.success := FALSE;
843             done := TRUE;
844             RETURN NEXT result;
845         END LOOP;
846     END IF;
847
848     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
849         SELECT    INTO hold_count COUNT(*)
850           FROM    action.hold_request
851           WHERE    usr = match_user
852             AND fulfillment_time IS NULL
853             AND cancel_time IS NULL
854             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
855
856         IF hold_count >= hold_test.max_holds THEN
857             result.fail_part := 'config.hold_matrix_test.max_holds';
858             result.success := FALSE;
859             done := TRUE;
860             RETURN NEXT result;
861         END IF;
862     END IF;
863
864     IF item_object.age_protect IS NOT NULL THEN
865         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
866         IF hold_test.distance_is_from_owner THEN
867             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
868         ELSE
869             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
870         END IF;
871         IF use_active_date = 'true' THEN
872             age_protect_date := COALESCE(item_object.active_date, NOW());
873         ELSE
874             age_protect_date := item_object.create_date;
875         END IF;
876         IF age_protect_date + age_protect_object.age > NOW() THEN
877             IF hold_test.distance_is_from_owner THEN
878                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
879                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
880             ELSE
881                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
882             END IF;
883
884             IF hold_transit_prox > age_protect_object.prox THEN
885                 result.fail_part := 'config.rule_age_hold_protect.prox';
886                 result.success := FALSE;
887                 done := TRUE;
888                 RETURN NEXT result;
889             END IF;
890         END IF;
891     END IF;
892
893     IF NOT done THEN
894         RETURN NEXT result;
895     END IF;
896
897     RETURN;
898 END;
899 $func$ LANGUAGE plpgsql;
900
901
902 -- Evergreen DB patch 0727.function.xml_pretty_print.sql
903 --
904 -- A simple pretty printer for XML.
905 -- Particularly useful for debugging the biblio.record_entry.marc field.
906 --
907
908 -- check whether patch can be applied
909 SELECT evergreen.upgrade_deps_block_check('0727', :eg_version);
910
911 CREATE OR REPLACE FUNCTION evergreen.xml_pretty_print(input XML) 
912     RETURNS XML
913     LANGUAGE SQL AS
914 $func$
915 SELECT xslt_process($1::text,
916 $$<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
917     version="1.0">
918    <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
919    <xsl:strip-space elements="*"/>
920    <xsl:template match="@*|node()">
921      <xsl:copy>
922        <xsl:apply-templates select="@*|node()"/>
923      </xsl:copy>
924    </xsl:template>
925  </xsl:stylesheet>
926 $$::text)::XML
927 $func$;
928
929 COMMENT ON FUNCTION evergreen.xml_pretty_print(input XML) IS
930 'Simple pretty printer for XML, as written by Andrew Dunstan at http://goo.gl/zBHIk';
931
932
933 SELECT evergreen.upgrade_deps_block_check('0728', :eg_version);
934
935 INSERT INTO actor.search_filter_group (owner, code, label) 
936     VALUES (1, 'kpac_main', 'Kid''s OPAC Search Filter');
937
938 INSERT INTO actor.search_query (label, query_text) 
939     VALUES ('Children''s Materials', 'audience(a,b,c)');
940 INSERT INTO actor.search_query (label, query_text) 
941     VALUES ('Young Adult Materials', 'audience(j,d)');
942 INSERT INTO actor.search_query (label, query_text) 
943     VALUES ('General/Adult Materials',  'audience(e,f,g, )');
944
945 INSERT INTO actor.search_filter_group_entry (grp, query, pos)
946     VALUES (
947         (SELECT id FROM actor.search_filter_group WHERE code = 'kpac_main'),
948         (SELECT id FROM actor.search_query WHERE label = 'Children''s Materials'),
949         0
950     );
951 INSERT INTO actor.search_filter_group_entry (grp, query, pos) 
952     VALUES (
953         (SELECT id FROM actor.search_filter_group WHERE code = 'kpac_main'),
954         (SELECT id FROM actor.search_query WHERE label = 'Young Adult Materials'),
955         1
956     );
957 INSERT INTO actor.search_filter_group_entry (grp, query, pos) 
958     VALUES (
959         (SELECT id FROM actor.search_filter_group WHERE code = 'kpac_main'),
960         (SELECT id FROM actor.search_query WHERE label = 'General/Adult Materials'),
961         2
962     );
963
964
965 -- Evergreen DB patch 0729.vr_format_value_maps.sql
966 --
967
968 -- check whether patch can be applied
969 SELECT evergreen.upgrade_deps_block_check('0729', :eg_version);
970
971 CREATE OR REPLACE FUNCTION config.update_coded_value_map(in_ctype TEXT, in_code TEXT, in_value TEXT, in_description TEXT DEFAULT NULL, in_opac_visible BOOL DEFAULT NULL, in_search_label TEXT DEFAULT NULL, in_is_simple BOOL DEFAULT NULL, add_only BOOL DEFAULT FALSE) RETURNS VOID AS $f$
972 DECLARE
973     current_row config.coded_value_map%ROWTYPE;
974 BEGIN
975     -- Look for a current value
976     SELECT INTO current_row * FROM config.coded_value_map WHERE ctype = in_ctype AND code = in_code;
977     -- If we have one..
978     IF FOUND AND NOT add_only THEN
979         -- Update anything we were handed
980         current_row.value := COALESCE(current_row.value, in_value);
981         current_row.description := COALESCE(current_row.description, in_description);
982         current_row.opac_visible := COALESCE(current_row.opac_visible, in_opac_visible);
983         current_row.search_label := COALESCE(current_row.search_label, in_search_label);
984         current_row.is_simple := COALESCE(current_row.is_simple, in_is_simple);
985         UPDATE config.coded_value_map
986             SET
987                 value = current_row.value,
988                 description = current_row.description,
989                 opac_visible = current_row.opac_visible,
990                 search_label = current_row.search_label,
991                 is_simple = current_row.is_simple
992             WHERE id = current_row.id;
993     ELSE
994         INSERT INTO config.coded_value_map(ctype, code, value, description, opac_visible, search_label, is_simple) VALUES
995             (in_ctype, in_code, in_value, in_description, COALESCE(in_opac_visible, TRUE), in_search_label, COALESCE(in_is_simple, FALSE));
996     END IF;
997 END;
998 $f$ LANGUAGE PLPGSQL;
999
1000 SELECT config.update_coded_value_map('vr_format', 'a', 'Beta', add_only := TRUE);
1001 SELECT config.update_coded_value_map('vr_format', 'b', 'VHS', add_only := TRUE);
1002 SELECT config.update_coded_value_map('vr_format', 'c', 'U-matic', add_only := TRUE);
1003 SELECT config.update_coded_value_map('vr_format', 'd', 'EIAJ', add_only := TRUE);
1004 SELECT config.update_coded_value_map('vr_format', 'e', 'Type C', add_only := TRUE);
1005 SELECT config.update_coded_value_map('vr_format', 'f', 'Quadruplex', add_only := TRUE);
1006 SELECT config.update_coded_value_map('vr_format', 'g', 'Laserdisc', add_only := TRUE);
1007 SELECT config.update_coded_value_map('vr_format', 'h', 'CED videodisc', add_only := TRUE);
1008 SELECT config.update_coded_value_map('vr_format', 'i', 'Betacam', add_only := TRUE);
1009 SELECT config.update_coded_value_map('vr_format', 'j', 'Betacam SP', add_only := TRUE);
1010 SELECT config.update_coded_value_map('vr_format', 'k', 'Super-VHS', add_only := TRUE);
1011 SELECT config.update_coded_value_map('vr_format', 'm', 'M-II', add_only := TRUE);
1012 SELECT config.update_coded_value_map('vr_format', 'o', 'D-2', add_only := TRUE);
1013 SELECT config.update_coded_value_map('vr_format', 'p', '8 mm.', add_only := TRUE);
1014 SELECT config.update_coded_value_map('vr_format', 'q', 'Hi-8 mm.', add_only := TRUE);
1015 SELECT config.update_coded_value_map('vr_format', 's', 'Blu-ray disc', add_only := TRUE);
1016 SELECT config.update_coded_value_map('vr_format', 'u', 'Unknown', add_only := TRUE);
1017 SELECT config.update_coded_value_map('vr_format', 'v', 'DVD', add_only := TRUE);
1018 SELECT config.update_coded_value_map('vr_format', 'z', 'Other', add_only := TRUE);
1019 SELECT config.update_coded_value_map('vr_format', ' ', 'Unspecified', add_only := TRUE);
1020
1021
1022
1023 SELECT evergreen.upgrade_deps_block_check('0730', :eg_version);
1024
1025 DROP FUNCTION acq.propagate_funds_by_org_tree (INT, INT, INT);
1026 DROP FUNCTION acq.propagate_funds_by_org_unit (INT, INT, INT);
1027
1028 CREATE OR REPLACE FUNCTION acq.propagate_funds_by_org_tree(
1029         old_year INTEGER,
1030         user_id INTEGER,
1031         org_unit_id INTEGER,
1032     include_desc BOOL DEFAULT TRUE
1033 ) RETURNS VOID AS $$
1034 DECLARE
1035 --
1036 new_id      INT;
1037 old_fund    RECORD;
1038 org_found   BOOLEAN;
1039 --
1040 BEGIN
1041         --
1042         -- Sanity checks
1043         --
1044         IF old_year IS NULL THEN
1045                 RAISE EXCEPTION 'Input year argument is NULL';
1046         ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
1047                 RAISE EXCEPTION 'Input year is out of range';
1048         END IF;
1049         --
1050         IF user_id IS NULL THEN
1051                 RAISE EXCEPTION 'Input user id argument is NULL';
1052         END IF;
1053         --
1054         IF org_unit_id IS NULL THEN
1055                 RAISE EXCEPTION 'Org unit id argument is NULL';
1056         ELSE
1057                 SELECT TRUE INTO org_found
1058                 FROM actor.org_unit
1059                 WHERE id = org_unit_id;
1060                 --
1061                 IF org_found IS NULL THEN
1062                         RAISE EXCEPTION 'Org unit id is invalid';
1063                 END IF;
1064         END IF;
1065         --
1066         -- Loop over the applicable funds
1067         --
1068         FOR old_fund in SELECT * FROM acq.fund
1069         WHERE
1070                 year = old_year
1071                 AND propagate
1072                 AND ( ( include_desc AND org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
1073                 OR (NOT include_desc AND org = org_unit_id ) )
1074     
1075         LOOP
1076                 BEGIN
1077                         INSERT INTO acq.fund (
1078                                 org,
1079                                 name,
1080                                 year,
1081                                 currency_type,
1082                                 code,
1083                                 rollover,
1084                                 propagate,
1085                                 balance_warning_percent,
1086                                 balance_stop_percent
1087                         ) VALUES (
1088                                 old_fund.org,
1089                                 old_fund.name,
1090                                 old_year + 1,
1091                                 old_fund.currency_type,
1092                                 old_fund.code,
1093                                 old_fund.rollover,
1094                                 true,
1095                                 old_fund.balance_warning_percent,
1096                                 old_fund.balance_stop_percent
1097                         )
1098                         RETURNING id INTO new_id;
1099                 EXCEPTION
1100                         WHEN unique_violation THEN
1101                                 --RAISE NOTICE 'Fund % already propagated', old_fund.id;
1102                                 CONTINUE;
1103                 END;
1104                 --RAISE NOTICE 'Propagating fund % to fund %',
1105                 --      old_fund.code, new_id;
1106         END LOOP;
1107 END;
1108 $$ LANGUAGE plpgsql;
1109
1110 CREATE OR REPLACE FUNCTION acq.propagate_funds_by_org_unit( old_year INTEGER, user_id INTEGER, org_unit_id INTEGER ) RETURNS VOID AS $$
1111     SELECT acq.propagate_funds_by_org_tree( $1, $2, $3, FALSE );
1112 $$ LANGUAGE SQL;
1113
1114
1115 DROP FUNCTION acq.rollover_funds_by_org_tree (INT, INT, INT);
1116 DROP FUNCTION acq.rollover_funds_by_org_unit (INT, INT, INT);
1117
1118
1119 CREATE OR REPLACE FUNCTION acq.rollover_funds_by_org_tree(
1120         old_year INTEGER,
1121         user_id INTEGER,
1122         org_unit_id INTEGER,
1123     encumb_only BOOL DEFAULT FALSE,
1124     include_desc BOOL DEFAULT TRUE
1125 ) RETURNS VOID AS $$
1126 DECLARE
1127 --
1128 new_fund    INT;
1129 new_year    INT := old_year + 1;
1130 org_found   BOOL;
1131 perm_ous    BOOL;
1132 xfer_amount NUMERIC := 0;
1133 roll_fund   RECORD;
1134 deb         RECORD;
1135 detail      RECORD;
1136 --
1137 BEGIN
1138         --
1139         -- Sanity checks
1140         --
1141         IF old_year IS NULL THEN
1142                 RAISE EXCEPTION 'Input year argument is NULL';
1143     ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
1144         RAISE EXCEPTION 'Input year is out of range';
1145         END IF;
1146         --
1147         IF user_id IS NULL THEN
1148                 RAISE EXCEPTION 'Input user id argument is NULL';
1149         END IF;
1150         --
1151         IF org_unit_id IS NULL THEN
1152                 RAISE EXCEPTION 'Org unit id argument is NULL';
1153         ELSE
1154                 --
1155                 -- Validate the org unit
1156                 --
1157                 SELECT TRUE
1158                 INTO org_found
1159                 FROM actor.org_unit
1160                 WHERE id = org_unit_id;
1161                 --
1162                 IF org_found IS NULL THEN
1163                         RAISE EXCEPTION 'Org unit id % is invalid', org_unit_id;
1164                 ELSIF encumb_only THEN
1165                         SELECT INTO perm_ous value::BOOL FROM
1166                         actor.org_unit_ancestor_setting(
1167                                 'acq.fund.allow_rollover_without_money', org_unit_id
1168                         );
1169                         IF NOT FOUND OR NOT perm_ous THEN
1170                                 RAISE EXCEPTION 'Encumbrance-only rollover not permitted at org %', org_unit_id;
1171                         END IF;
1172                 END IF;
1173         END IF;
1174         --
1175         -- Loop over the propagable funds to identify the details
1176         -- from the old fund plus the id of the new one, if it exists.
1177         --
1178         FOR roll_fund in
1179         SELECT
1180             oldf.id AS old_fund,
1181             oldf.org,
1182             oldf.name,
1183             oldf.currency_type,
1184             oldf.code,
1185                 oldf.rollover,
1186             newf.id AS new_fund_id
1187         FROM
1188         acq.fund AS oldf
1189         LEFT JOIN acq.fund AS newf
1190                 ON ( oldf.code = newf.code )
1191         WHERE
1192                     oldf.year = old_year
1193                 AND oldf.propagate
1194         AND newf.year = new_year
1195                 AND ( ( include_desc AND oldf.org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
1196                 OR (NOT include_desc AND oldf.org = org_unit_id ) )
1197         LOOP
1198                 --RAISE NOTICE 'Processing fund %', roll_fund.old_fund;
1199                 --
1200                 IF roll_fund.new_fund_id IS NULL THEN
1201                         --
1202                         -- The old fund hasn't been propagated yet.  Propagate it now.
1203                         --
1204                         INSERT INTO acq.fund (
1205                                 org,
1206                                 name,
1207                                 year,
1208                                 currency_type,
1209                                 code,
1210                                 rollover,
1211                                 propagate,
1212                                 balance_warning_percent,
1213                                 balance_stop_percent
1214                         ) VALUES (
1215                                 roll_fund.org,
1216                                 roll_fund.name,
1217                                 new_year,
1218                                 roll_fund.currency_type,
1219                                 roll_fund.code,
1220                                 true,
1221                                 true,
1222                                 roll_fund.balance_warning_percent,
1223                                 roll_fund.balance_stop_percent
1224                         )
1225                         RETURNING id INTO new_fund;
1226                 ELSE
1227                         new_fund = roll_fund.new_fund_id;
1228                 END IF;
1229                 --
1230                 -- Determine the amount to transfer
1231                 --
1232                 SELECT amount
1233                 INTO xfer_amount
1234                 FROM acq.fund_spent_balance
1235                 WHERE fund = roll_fund.old_fund;
1236                 --
1237                 IF xfer_amount <> 0 THEN
1238                         IF NOT encumb_only AND roll_fund.rollover THEN
1239                                 --
1240                                 -- Transfer balance from old fund to new
1241                                 --
1242                                 --RAISE NOTICE 'Transferring % from fund % to %', xfer_amount, roll_fund.old_fund, new_fund;
1243                                 --
1244                                 PERFORM acq.transfer_fund(
1245                                         roll_fund.old_fund,
1246                                         xfer_amount,
1247                                         new_fund,
1248                                         xfer_amount,
1249                                         user_id,
1250                                         'Rollover'
1251                                 );
1252                         ELSE
1253                                 --
1254                                 -- Transfer balance from old fund to the void
1255                                 --
1256                                 -- RAISE NOTICE 'Transferring % from fund % to the void', xfer_amount, roll_fund.old_fund;
1257                                 --
1258                                 PERFORM acq.transfer_fund(
1259                                         roll_fund.old_fund,
1260                                         xfer_amount,
1261                                         NULL,
1262                                         NULL,
1263                                         user_id,
1264                                         'Rollover into the void'
1265                                 );
1266                         END IF;
1267                 END IF;
1268                 --
1269                 IF roll_fund.rollover THEN
1270                         --
1271                         -- Move any lineitems from the old fund to the new one
1272                         -- where the associated debit is an encumbrance.
1273                         --
1274                         -- Any other tables tying expenditure details to funds should
1275                         -- receive similar treatment.  At this writing there are none.
1276                         --
1277                         UPDATE acq.lineitem_detail
1278                         SET fund = new_fund
1279                         WHERE
1280                         fund = roll_fund.old_fund -- this condition may be redundant
1281                         AND fund_debit in
1282                         (
1283                                 SELECT id
1284                                 FROM acq.fund_debit
1285                                 WHERE
1286                                 fund = roll_fund.old_fund
1287                                 AND encumbrance
1288                         );
1289                         --
1290                         -- Move encumbrance debits from the old fund to the new fund
1291                         --
1292                         UPDATE acq.fund_debit
1293                         SET fund = new_fund
1294                         wHERE
1295                                 fund = roll_fund.old_fund
1296                                 AND encumbrance;
1297                 END IF;
1298                 --
1299                 -- Mark old fund as inactive, now that we've closed it
1300                 --
1301                 UPDATE acq.fund
1302                 SET active = FALSE
1303                 WHERE id = roll_fund.old_fund;
1304         END LOOP;
1305 END;
1306 $$ LANGUAGE plpgsql;
1307
1308 CREATE OR REPLACE FUNCTION acq.rollover_funds_by_org_unit( old_year INTEGER, user_id INTEGER, org_unit_id INTEGER, encumb_only BOOL DEFAULT FALSE ) RETURNS VOID AS $$
1309     SELECT acq.rollover_funds_by_org_tree( $1, $2, $3, $4, FALSE );
1310 $$ LANGUAGE SQL;
1311
1312 INSERT into config.org_unit_setting_type
1313     (name, grp, label, description, datatype)
1314     VALUES (
1315         'acq.fund.allow_rollover_without_money',
1316         'acq',
1317         oils_i18n_gettext(
1318             'acq.fund.allow_rollover_without_money',
1319             'Allow funds to be rolled over without bringing the money along',
1320             'coust',
1321             'label'
1322         ),
1323         oils_i18n_gettext(
1324             'acq.fund.allow_rollover_without_money',
1325             'Allow funds to be rolled over without bringing the money along.  This makes money left in the old fund disappear, modeling its return to some outside entity.',
1326             'coust',
1327             'description'
1328         ),
1329         'bool'
1330     );
1331
1332 -- 0731.schema.vandelay_item_overlay.sql
1333
1334 SELECT evergreen.upgrade_deps_block_check('0731', :eg_version);
1335
1336 ALTER TABLE vandelay.import_item_attr_definition 
1337     ADD COLUMN internal_id TEXT; 
1338
1339 ALTER TABLE vandelay.import_item 
1340     ADD COLUMN internal_id BIGINT;
1341
1342 INSERT INTO permission.perm_list ( id, code, description ) VALUES
1343 ( 536, 'IMPORT_OVERLAY_COPY', oils_i18n_gettext( 536,
1344     'Allows a user to overlay copy data in MARC import', 'ppl', 'description'));
1345
1346 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
1347 DECLARE
1348     attr_def    BIGINT;
1349     item_data   vandelay.import_item%ROWTYPE;
1350 BEGIN
1351
1352     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1353         RETURN NEW;
1354     END IF;
1355
1356     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
1357
1358     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
1359         INSERT INTO vandelay.import_item (
1360             record,
1361             definition,
1362             owning_lib,
1363             circ_lib,
1364             call_number,
1365             copy_number,
1366             status,
1367             location,
1368             circulate,
1369             deposit,
1370             deposit_amount,
1371             ref,
1372             holdable,
1373             price,
1374             barcode,
1375             circ_modifier,
1376             circ_as_type,
1377             alert_message,
1378             pub_note,
1379             priv_note,
1380             internal_id,
1381             opac_visible,
1382             import_error,
1383             error_detail
1384         ) VALUES (
1385             NEW.id,
1386             item_data.definition,
1387             item_data.owning_lib,
1388             item_data.circ_lib,
1389             item_data.call_number,
1390             item_data.copy_number,
1391             item_data.status,
1392             item_data.location,
1393             item_data.circulate,
1394             item_data.deposit,
1395             item_data.deposit_amount,
1396             item_data.ref,
1397             item_data.holdable,
1398             item_data.price,
1399             item_data.barcode,
1400             item_data.circ_modifier,
1401             item_data.circ_as_type,
1402             item_data.alert_message,
1403             item_data.pub_note,
1404             item_data.priv_note,
1405             item_data.internal_id,
1406             item_data.opac_visible,
1407             item_data.import_error,
1408             item_data.error_detail
1409         );
1410     END LOOP;
1411
1412     RETURN NULL;
1413 END;
1414 $func$ LANGUAGE PLPGSQL;
1415
1416
1417 CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
1418 DECLARE
1419
1420     owning_lib      TEXT;
1421     circ_lib        TEXT;
1422     call_number     TEXT;
1423     copy_number     TEXT;
1424     status          TEXT;
1425     location        TEXT;
1426     circulate       TEXT;
1427     deposit         TEXT;
1428     deposit_amount  TEXT;
1429     ref             TEXT;
1430     holdable        TEXT;
1431     price           TEXT;
1432     barcode         TEXT;
1433     circ_modifier   TEXT;
1434     circ_as_type    TEXT;
1435     alert_message   TEXT;
1436     opac_visible    TEXT;
1437     pub_note        TEXT;
1438     priv_note       TEXT;
1439     internal_id     TEXT;
1440
1441     attr_def        RECORD;
1442     tmp_attr_set    RECORD;
1443     attr_set        vandelay.import_item%ROWTYPE;
1444
1445     xpath           TEXT;
1446     tmp_str         TEXT;
1447
1448 BEGIN
1449
1450     SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
1451
1452     IF FOUND THEN
1453
1454         attr_set.definition := attr_def.id;
1455
1456         -- Build the combined XPath
1457
1458         owning_lib :=
1459             CASE
1460                 WHEN attr_def.owning_lib IS NULL THEN 'null()'
1461                 WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.owning_lib || '"]'
1462                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.owning_lib
1463             END;
1464
1465         circ_lib :=
1466             CASE
1467                 WHEN attr_def.circ_lib IS NULL THEN 'null()'
1468                 WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_lib || '"]'
1469                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_lib
1470             END;
1471
1472         call_number :=
1473             CASE
1474                 WHEN attr_def.call_number IS NULL THEN 'null()'
1475                 WHEN LENGTH( attr_def.call_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.call_number || '"]'
1476                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.call_number
1477             END;
1478
1479         copy_number :=
1480             CASE
1481                 WHEN attr_def.copy_number IS NULL THEN 'null()'
1482                 WHEN LENGTH( attr_def.copy_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.copy_number || '"]'
1483                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.copy_number
1484             END;
1485
1486         status :=
1487             CASE
1488                 WHEN attr_def.status IS NULL THEN 'null()'
1489                 WHEN LENGTH( attr_def.status ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.status || '"]'
1490                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.status
1491             END;
1492
1493         location :=
1494             CASE
1495                 WHEN attr_def.location IS NULL THEN 'null()'
1496                 WHEN LENGTH( attr_def.location ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.location || '"]'
1497                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.location
1498             END;
1499
1500         circulate :=
1501             CASE
1502                 WHEN attr_def.circulate IS NULL THEN 'null()'
1503                 WHEN LENGTH( attr_def.circulate ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circulate || '"]'
1504                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circulate
1505             END;
1506
1507         deposit :=
1508             CASE
1509                 WHEN attr_def.deposit IS NULL THEN 'null()'
1510                 WHEN LENGTH( attr_def.deposit ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit || '"]'
1511                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit
1512             END;
1513
1514         deposit_amount :=
1515             CASE
1516                 WHEN attr_def.deposit_amount IS NULL THEN 'null()'
1517                 WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit_amount || '"]'
1518                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit_amount
1519             END;
1520
1521         ref :=
1522             CASE
1523                 WHEN attr_def.ref IS NULL THEN 'null()'
1524                 WHEN LENGTH( attr_def.ref ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.ref || '"]'
1525                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.ref
1526             END;
1527
1528         holdable :=
1529             CASE
1530                 WHEN attr_def.holdable IS NULL THEN 'null()'
1531                 WHEN LENGTH( attr_def.holdable ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.holdable || '"]'
1532                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.holdable
1533             END;
1534
1535         price :=
1536             CASE
1537                 WHEN attr_def.price IS NULL THEN 'null()'
1538                 WHEN LENGTH( attr_def.price ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.price || '"]'
1539                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.price
1540             END;
1541
1542         barcode :=
1543             CASE
1544                 WHEN attr_def.barcode IS NULL THEN 'null()'
1545                 WHEN LENGTH( attr_def.barcode ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.barcode || '"]'
1546                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.barcode
1547             END;
1548
1549         circ_modifier :=
1550             CASE
1551                 WHEN attr_def.circ_modifier IS NULL THEN 'null()'
1552                 WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_modifier || '"]'
1553                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_modifier
1554             END;
1555
1556         circ_as_type :=
1557             CASE
1558                 WHEN attr_def.circ_as_type IS NULL THEN 'null()'
1559                 WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_as_type || '"]'
1560                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_as_type
1561             END;
1562
1563         alert_message :=
1564             CASE
1565                 WHEN attr_def.alert_message IS NULL THEN 'null()'
1566                 WHEN LENGTH( attr_def.alert_message ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.alert_message || '"]'
1567                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.alert_message
1568             END;
1569
1570         opac_visible :=
1571             CASE
1572                 WHEN attr_def.opac_visible IS NULL THEN 'null()'
1573                 WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.opac_visible || '"]'
1574                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.opac_visible
1575             END;
1576
1577         pub_note :=
1578             CASE
1579                 WHEN attr_def.pub_note IS NULL THEN 'null()'
1580                 WHEN LENGTH( attr_def.pub_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.pub_note || '"]'
1581                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.pub_note
1582             END;
1583         priv_note :=
1584             CASE
1585                 WHEN attr_def.priv_note IS NULL THEN 'null()'
1586                 WHEN LENGTH( attr_def.priv_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.priv_note || '"]'
1587                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.priv_note
1588             END;
1589
1590         internal_id :=
1591             CASE
1592                 WHEN attr_def.internal_id IS NULL THEN 'null()'
1593                 WHEN LENGTH( attr_def.internal_id ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.internal_id || '"]'
1594                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.internal_id
1595             END;
1596
1597
1598
1599         xpath :=
1600             owning_lib      || '|' ||
1601             circ_lib        || '|' ||
1602             call_number     || '|' ||
1603             copy_number     || '|' ||
1604             status          || '|' ||
1605             location        || '|' ||
1606             circulate       || '|' ||
1607             deposit         || '|' ||
1608             deposit_amount  || '|' ||
1609             ref             || '|' ||
1610             holdable        || '|' ||
1611             price           || '|' ||
1612             barcode         || '|' ||
1613             circ_modifier   || '|' ||
1614             circ_as_type    || '|' ||
1615             alert_message   || '|' ||
1616             pub_note        || '|' ||
1617             priv_note       || '|' ||
1618             internal_id     || '|' ||
1619             opac_visible;
1620
1621         FOR tmp_attr_set IN
1622                 SELECT  *
1623                   FROM  oils_xpath_table( 'id', 'marc', 'vandelay.queued_bib_record', xpath, 'id = ' || import_id )
1624                             AS t( id INT, ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
1625                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
1626                                   circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, internal_id TEXT, opac_vis TEXT )
1627         LOOP
1628
1629             attr_set.import_error := NULL;
1630             attr_set.error_detail := NULL;
1631             attr_set.deposit_amount := NULL;
1632             attr_set.copy_number := NULL;
1633             attr_set.price := NULL;
1634
1635             IF tmp_attr_set.pr != '' THEN
1636                 tmp_str = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
1637                 IF tmp_str = '' THEN 
1638                     attr_set.import_error := 'import.item.invalid.price';
1639                     attr_set.error_detail := tmp_attr_set.pr; -- original value
1640                     RETURN NEXT attr_set; CONTINUE; 
1641                 END IF;
1642                 attr_set.price := tmp_str::NUMERIC(8,2); 
1643             END IF;
1644
1645             IF tmp_attr_set.dep_amount != '' THEN
1646                 tmp_str = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
1647                 IF tmp_str = '' THEN 
1648                     attr_set.import_error := 'import.item.invalid.deposit_amount';
1649                     attr_set.error_detail := tmp_attr_set.dep_amount; 
1650                     RETURN NEXT attr_set; CONTINUE; 
1651                 END IF;
1652                 attr_set.deposit_amount := tmp_str::NUMERIC(8,2); 
1653             END IF;
1654
1655             IF tmp_attr_set.cnum != '' THEN
1656                 tmp_str = REGEXP_REPLACE(tmp_attr_set.cnum, E'[^0-9]', '', 'g');
1657                 IF tmp_str = '' THEN 
1658                     attr_set.import_error := 'import.item.invalid.copy_number';
1659                     attr_set.error_detail := tmp_attr_set.cnum; 
1660                     RETURN NEXT attr_set; CONTINUE; 
1661                 END IF;
1662                 attr_set.copy_number := tmp_str::INT; 
1663             END IF;
1664
1665             IF tmp_attr_set.ol != '' THEN
1666                 SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
1667                 IF NOT FOUND THEN
1668                     attr_set.import_error := 'import.item.invalid.owning_lib';
1669                     attr_set.error_detail := tmp_attr_set.ol;
1670                     RETURN NEXT attr_set; CONTINUE; 
1671                 END IF;
1672             END IF;
1673
1674             IF tmp_attr_set.clib != '' THEN
1675                 SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
1676                 IF NOT FOUND THEN
1677                     attr_set.import_error := 'import.item.invalid.circ_lib';
1678                     attr_set.error_detail := tmp_attr_set.clib;
1679                     RETURN NEXT attr_set; CONTINUE; 
1680                 END IF;
1681             END IF;
1682
1683             IF tmp_attr_set.cs != '' THEN
1684                 SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
1685                 IF NOT FOUND THEN
1686                     attr_set.import_error := 'import.item.invalid.status';
1687                     attr_set.error_detail := tmp_attr_set.cs;
1688                     RETURN NEXT attr_set; CONTINUE; 
1689                 END IF;
1690             END IF;
1691
1692             IF tmp_attr_set.circ_mod != '' THEN
1693                 SELECT code INTO attr_set.circ_modifier FROM config.circ_modifier WHERE code = tmp_attr_set.circ_mod;
1694                 IF NOT FOUND THEN
1695                     attr_set.import_error := 'import.item.invalid.circ_modifier';
1696                     attr_set.error_detail := tmp_attr_set.circ_mod;
1697                     RETURN NEXT attr_set; CONTINUE; 
1698                 END IF;
1699             END IF;
1700
1701             IF tmp_attr_set.circ_as != '' THEN
1702                 SELECT code INTO attr_set.circ_as_type FROM config.coded_value_map WHERE ctype = 'item_type' AND code = tmp_attr_set.circ_as;
1703                 IF NOT FOUND THEN
1704                     attr_set.import_error := 'import.item.invalid.circ_as_type';
1705                     attr_set.error_detail := tmp_attr_set.circ_as;
1706                     RETURN NEXT attr_set; CONTINUE; 
1707                 END IF;
1708             END IF;
1709
1710             IF tmp_attr_set.cl != '' THEN
1711
1712                 -- search up the org unit tree for a matching copy location
1713                 WITH RECURSIVE anscestor_depth AS (
1714                     SELECT  ou.id,
1715                         out.depth AS depth,
1716                         ou.parent_ou
1717                     FROM  actor.org_unit ou
1718                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1719                     WHERE ou.id = COALESCE(attr_set.owning_lib, attr_set.circ_lib)
1720                         UNION ALL
1721                     SELECT  ou.id,
1722                         out.depth,
1723                         ou.parent_ou
1724                     FROM  actor.org_unit ou
1725                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1726                         JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
1727                 ) SELECT  cpl.id INTO attr_set.location
1728                     FROM  anscestor_depth a
1729                         JOIN asset.copy_location cpl ON (cpl.owning_lib = a.id)
1730                     WHERE LOWER(cpl.name) = LOWER(tmp_attr_set.cl)
1731                     ORDER BY a.depth DESC
1732                     LIMIT 1; 
1733
1734                 IF NOT FOUND THEN
1735                     attr_set.import_error := 'import.item.invalid.location';
1736                     attr_set.error_detail := tmp_attr_set.cs;
1737                     RETURN NEXT attr_set; CONTINUE; 
1738                 END IF;
1739             END IF;
1740
1741             attr_set.circulate      :=
1742                 LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
1743                 OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
1744
1745             attr_set.deposit        :=
1746                 LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
1747                 OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
1748
1749             attr_set.holdable       :=
1750                 LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
1751                 OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
1752
1753             attr_set.opac_visible   :=
1754                 LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
1755                 OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
1756
1757             attr_set.ref            :=
1758                 LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
1759                 OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
1760
1761             attr_set.call_number    := tmp_attr_set.cn; -- TEXT
1762             attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
1763             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
1764             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
1765             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
1766             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
1767             attr_set.internal_id    := tmp_attr_set.internal_id::BIGINT;
1768
1769             RETURN NEXT attr_set;
1770
1771         END LOOP;
1772
1773     END IF;
1774
1775     RETURN;
1776
1777 END;
1778 $$ LANGUAGE PLPGSQL;
1779
1780
1781
1782 -- 0732.schema.acq-lineitem-summary.sql
1783
1784 SELECT evergreen.upgrade_deps_block_check('0732', :eg_version);
1785
1786 CREATE OR REPLACE VIEW acq.lineitem_summary AS
1787     SELECT 
1788         li.id AS lineitem, 
1789         (
1790             SELECT COUNT(lid.id) 
1791             FROM acq.lineitem_detail lid
1792             WHERE lineitem = li.id
1793         ) AS item_count,
1794         (
1795             SELECT COUNT(lid.id) 
1796             FROM acq.lineitem_detail lid
1797             WHERE recv_time IS NOT NULL AND lineitem = li.id
1798         ) AS recv_count,
1799         (
1800             SELECT COUNT(lid.id) 
1801             FROM acq.lineitem_detail lid
1802             WHERE cancel_reason IS NOT NULL AND lineitem = li.id
1803         ) AS cancel_count,
1804         (
1805             SELECT COUNT(lid.id) 
1806             FROM acq.lineitem_detail lid
1807                 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
1808             WHERE NOT debit.encumbrance AND lineitem = li.id
1809         ) AS invoice_count,
1810         (
1811             SELECT COUNT(DISTINCT(lid.id)) 
1812             FROM acq.lineitem_detail lid
1813                 JOIN acq.claim claim ON (claim.lineitem_detail = lid.id)
1814             WHERE lineitem = li.id
1815         ) AS claim_count,
1816         (
1817             SELECT (COUNT(lid.id) * li.estimated_unit_price)::NUMERIC(8,2)
1818             FROM acq.lineitem_detail lid
1819             WHERE lid.cancel_reason IS NULL AND lineitem = li.id
1820         ) AS estimated_amount,
1821         (
1822             SELECT SUM(debit.amount)::NUMERIC(8,2)
1823             FROM acq.lineitem_detail lid
1824                 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
1825             WHERE debit.encumbrance AND lineitem = li.id
1826         ) AS encumbrance_amount,
1827         (
1828             SELECT SUM(debit.amount)::NUMERIC(8,2)
1829             FROM acq.lineitem_detail lid
1830                 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
1831             WHERE NOT debit.encumbrance AND lineitem = li.id
1832         ) AS paid_amount
1833
1834         FROM acq.lineitem AS li;
1835
1836
1837
1838 -- XXX
1839 -- Template update included here for reference only.
1840 -- The stock JEDI template is not updated here (see WHERE clause)
1841 -- We do update the environment, though, for easier local template 
1842 -- updating.  No env fields are removed (that aren't otherwise replaced).
1843 --
1844
1845
1846 SELECT evergreen.upgrade_deps_block_check('0733', :eg_version);
1847
1848 UPDATE action_trigger.event_definition SET template =
1849 $$[%- USE date -%]
1850 [%# start JEDI document 
1851   # Vendor specific kludges:
1852   # BT      - vendcode goes to NAD/BY *suffix*  w/ 91 qualifier
1853   # INGRAM  - vendcode goes to NAD/BY *segment* w/ 91 qualifier (separately)
1854   # BRODART - vendcode goes to FTX segment (lineitem level)
1855 -%]
1856 [%- 
1857 IF target.provider.edi_default.vendcode && target.provider.code == 'BRODART';
1858     xtra_ftx = target.provider.edi_default.vendcode;
1859 END;
1860 -%]
1861 [%- BLOCK big_block -%]
1862 {
1863    "recipient":"[% target.provider.san %]",
1864    "sender":"[% target.ordering_agency.mailing_address.san %]",
1865    "body": [{
1866      "ORDERS":[ "order", {
1867         "po_number":[% target.id %],
1868         "date":"[% date.format(date.now, '%Y%m%d') %]",
1869         "buyer":[
1870             [%   IF   target.provider.edi_default.vendcode && (target.provider.code == 'BT' || target.provider.name.match('(?i)^BAKER & TAYLOR'))  -%]
1871                 {"id-qualifier": 91, "id":"[% target.ordering_agency.mailing_address.san _ ' ' _ target.provider.edi_default.vendcode %]"}
1872             [%- ELSIF target.provider.edi_default.vendcode && target.provider.code == 'INGRAM' -%]
1873                 {"id":"[% target.ordering_agency.mailing_address.san %]"},
1874                 {"id-qualifier": 91, "id":"[% target.provider.edi_default.vendcode %]"}
1875             [%- ELSE -%]
1876                 {"id":"[% target.ordering_agency.mailing_address.san %]"}
1877             [%- END -%]
1878         ],
1879         "vendor":[
1880             [%- # target.provider.name (target.provider.id) -%]
1881             "[% target.provider.san %]",
1882             {"id-qualifier": 92, "id":"[% target.provider.id %]"}
1883         ],
1884         "currency":"[% target.provider.currency_type %]",
1885                 
1886         "items":[
1887         [%- FOR li IN target.lineitems %]
1888         {
1889             "line_index":"[% li.id %]",
1890             "identifiers":[   [%-# li.isbns = helpers.get_li_isbns(li.attributes) %]
1891             [% FOR isbn IN helpers.get_li_isbns(li.attributes) -%]
1892                 [% IF isbn.length == 13 -%]
1893                 {"id-qualifier":"EN","id":"[% isbn %]"},
1894                 [% ELSE -%]
1895                 {"id-qualifier":"IB","id":"[% isbn %]"},
1896                 [%- END %]
1897             [% END %]
1898                 {"id-qualifier":"IN","id":"[% li.id %]"}
1899             ],
1900             "price":[% li.estimated_unit_price || '0.00' %],
1901             "desc":[
1902                 {"BTI":"[% helpers.get_li_attr_jedi('title',     '', li.attributes) %]"},
1903                 {"BPU":"[% helpers.get_li_attr_jedi('publisher', '', li.attributes) %]"},
1904                 {"BPD":"[% helpers.get_li_attr_jedi('pubdate',   '', li.attributes) %]"},
1905                 {"BPH":"[% helpers.get_li_attr_jedi('pagination','', li.attributes) %]"}
1906             ],
1907             [%- ftx_vals = []; 
1908                 FOR note IN li.lineitem_notes; 
1909                     NEXT UNLESS note.vendor_public == 't'; 
1910                     ftx_vals.push(note.value); 
1911                 END; 
1912                 IF xtra_ftx;           ftx_vals.unshift(xtra_ftx); END; 
1913                 IF ftx_vals.size == 0; ftx_vals.unshift('');       END;  # BT needs FTX+LIN for every LI, even if it is an empty one
1914             -%]
1915
1916             "free-text":[ 
1917                 [% FOR note IN ftx_vals -%] "[% note %]"[% UNLESS loop.last %], [% END %][% END %] 
1918             ],            
1919             "quantity":[% li.lineitem_details.size %],
1920             "copies" : [
1921                 [%- IF 1 -%]
1922                 [%- FOR lid IN li.lineitem_details;
1923                         fund = lid.fund.code;
1924                         item_type = lid.circ_modifier;
1925                         callnumber = lid.cn_label;
1926                         owning_lib = lid.owning_lib.shortname;
1927                         location = lid.location;
1928     
1929                         # when we have real copy data, treat it as authoritative
1930                         acp = lid.eg_copy_id;
1931                         IF acp;
1932                             item_type = acp.circ_modifier;
1933                             callnumber = acp.call_number.label;
1934                             location = acp.location.name;
1935                         END -%]
1936                 {   [%- IF fund %] "fund" : "[% fund %]",[% END -%]
1937                     [%- IF callnumber %] "call_number" : "[% callnumber %]", [% END -%]
1938                     [%- IF item_type %] "item_type" : "[% item_type %]", [% END -%]
1939                     [%- IF location %] "copy_location" : "[% location %]", [% END -%]
1940                     [%- IF owning_lib %] "owning_lib" : "[% owning_lib %]", [% END -%]
1941                     [%- #chomp %]"copy_id" : "[% lid.id %]" }[% ',' UNLESS loop.last %]
1942                 [% END -%]
1943                 [%- END -%]
1944              ]
1945         }[% UNLESS loop.last %],[% END %]
1946         [%-# TODO: lineitem details (later) -%]
1947         [% END %]
1948         ],
1949         "line_items":[% target.lineitems.size %]
1950      }]  [%# close ORDERS array %]
1951    }]    [%# close  body  array %]
1952 }
1953 [% END %]
1954 [% tempo = PROCESS big_block; helpers.escape_json(tempo) %]
1955 $$
1956 WHERE id = 23 AND FALSE; -- DON'T PERFORM THE UPDATE
1957
1958
1959 -- add copy-related fields to the environment if they're not already there.
1960 DO $$
1961 BEGIN
1962     PERFORM 1 
1963         FROM action_trigger.environment 
1964         WHERE 
1965             event_def = 23 AND 
1966             path = 'lineitems.lineitem_details.owning_lib';
1967     IF NOT FOUND THEN
1968         INSERT INTO action_trigger.environment (event_def, path) 
1969             VALUES (23, 'lineitems.lineitem_details.owning_lib'); 
1970     END IF;
1971
1972     PERFORM 1 
1973         FROM action_trigger.environment 
1974         WHERE 
1975             event_def = 23 AND 
1976             path = 'lineitems.lineitem_details.fund';
1977     IF NOT FOUND THEN
1978         INSERT INTO action_trigger.environment (event_def, path) 
1979             VALUES (23, 'lineitems.lineitem_details.fund'); 
1980     END IF;
1981
1982     PERFORM 1 
1983         FROM action_trigger.environment 
1984         WHERE 
1985             event_def = 23 AND 
1986             path = 'lineitems.lineitem_details.location';
1987     IF NOT FOUND THEN
1988         INSERT INTO action_trigger.environment (event_def, path) 
1989             VALUES (23, 'lineitems.lineitem_details.location'); 
1990     END IF;
1991
1992     PERFORM 1 
1993         FROM action_trigger.environment 
1994         WHERE 
1995             event_def = 23 AND 
1996             path = 'lineitems.lineitem_details.eg_copy_id.location';
1997     IF NOT FOUND THEN
1998         INSERT INTO action_trigger.environment (event_def, path) 
1999             VALUES (23, 'lineitems.lineitem_details.eg_copy_id.location'); 
2000     END IF;
2001
2002     PERFORM 1 
2003         FROM action_trigger.environment 
2004         WHERE 
2005             event_def = 23 AND 
2006             path = 'lineitems.lineitem_details.eg_copy_id.call_number';
2007     IF NOT FOUND THEN
2008         INSERT INTO action_trigger.environment (event_def, path) 
2009             VALUES (23, 'lineitems.lineitem_details.eg_copy_id.call_number'); 
2010     END IF;
2011
2012
2013
2014     -- remove redundant entry
2015     DELETE FROM action_trigger.environment 
2016         WHERE event_def = 23 AND path = 'lineitems.lineitem_details'; 
2017
2018 END $$;
2019
2020
2021 -- Evergreen DB patch 0734.tpac_holdable_check.sql
2022 --
2023
2024 -- check whether patch can be applied
2025 SELECT evergreen.upgrade_deps_block_check('0734', :eg_version);
2026
2027 CREATE OR REPLACE FUNCTION asset.record_has_holdable_copy ( rid BIGINT ) RETURNS BOOL AS $f$
2028 BEGIN
2029     PERFORM 1
2030         FROM
2031             asset.copy acp
2032             JOIN asset.call_number acn ON acp.call_number = acn.id
2033             JOIN asset.copy_location acpl ON acp.location = acpl.id
2034             JOIN config.copy_status ccs ON acp.status = ccs.id
2035         WHERE
2036             acn.record = rid
2037             AND acp.holdable = true
2038             AND acpl.holdable = true
2039             AND ccs.holdable = true
2040             AND acp.deleted = false
2041         LIMIT 1;
2042     IF FOUND THEN
2043         RETURN true;
2044     END IF;
2045     RETURN FALSE;
2046 END;
2047 $f$ LANGUAGE PLPGSQL;
2048
2049 CREATE OR REPLACE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT ) RETURNS BOOL AS $f$
2050 BEGIN
2051     PERFORM 1
2052         FROM
2053             asset.copy acp
2054             JOIN asset.call_number acn ON acp.call_number = acn.id
2055             JOIN asset.copy_location acpl ON acp.location = acpl.id
2056             JOIN config.copy_status ccs ON acp.status = ccs.id
2057             JOIN metabib.metarecord_source_map mmsm ON acn.record = mmsm.source
2058         WHERE
2059             mmsm.metarecord = rid
2060             AND acp.holdable = true
2061             AND acpl.holdable = true
2062             AND ccs.holdable = true
2063             AND acp.deleted = false
2064         LIMIT 1;
2065     IF FOUND THEN
2066         RETURN true;
2067     END IF;
2068     RETURN FALSE;
2069 END;
2070 $f$ LANGUAGE PLPGSQL;
2071
2072 CREATE OR REPLACE FUNCTION unapi.holdings_xml (
2073     bid BIGINT,
2074     ouid INT,
2075     org TEXT,
2076     depth INT DEFAULT NULL,
2077     includes TEXT[] DEFAULT NULL::TEXT[],
2078     slimit HSTORE DEFAULT NULL,
2079     soffset HSTORE DEFAULT NULL,
2080     include_xmlns BOOL DEFAULT TRUE,
2081     pref_lib INT DEFAULT NULL
2082 )
2083 RETURNS XML AS $F$
2084      SELECT  XMLELEMENT(
2085                  name holdings,
2086                  XMLATTRIBUTES(
2087                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2088                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
2089                     (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
2090                  ),
2091                  XMLELEMENT(
2092                      name counts,
2093                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2094                          SELECT  XMLELEMENT(
2095                                      name count,
2096                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2097                                  )::text
2098                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2099                                      UNION
2100                          SELECT  XMLELEMENT(
2101                                      name count,
2102                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2103                                  )::text
2104                            FROM  asset.staff_ou_record_copy_count($2, $1)
2105                                      UNION
2106                          SELECT  XMLELEMENT(
2107                                      name count,
2108                                      XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2109                                  )::text
2110                            FROM  asset.opac_ou_record_copy_count($9,  $1)
2111                                      ORDER BY 1
2112                      )x)
2113                  ),
2114                  CASE 
2115                      WHEN ('bmp' = ANY ($5)) THEN
2116                         XMLELEMENT(
2117                             name monograph_parts,
2118                             (SELECT XMLAGG(bmp) FROM (
2119                                 SELECT  unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
2120                                   FROM  biblio.monograph_part
2121                                   WHERE record = $1
2122                             )x)
2123                         )
2124                      ELSE NULL
2125                  END,
2126                  XMLELEMENT(
2127                      name volumes,
2128                      (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
2129                         -- Physical copies
2130                         SELECT  unapi.acn(y.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
2131                         FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9) AS y
2132                         UNION ALL
2133                         -- Located URIs
2134                         SELECT unapi.acn(uris.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), 0, name, label_sortkey
2135                         FROM evergreen.located_uris($1, $2, $9) AS uris
2136                      )x)
2137                  ),
2138                  CASE WHEN ('ssub' = ANY ($5)) THEN 
2139                      XMLELEMENT(
2140                          name subscriptions,
2141                          (SELECT XMLAGG(ssub) FROM (
2142                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2143                               FROM  serial.subscription
2144                               WHERE record_entry = $1
2145                         )x)
2146                      )
2147                  ELSE NULL END,
2148                  CASE WHEN ('acp' = ANY ($5)) THEN 
2149                      XMLELEMENT(
2150                          name foreign_copies,
2151                          (SELECT XMLAGG(acp) FROM (
2152                             SELECT  unapi.acp(p.target_copy,'xml','copy',evergreen.array_remove_item_by_value($5,'acp'), $3, $4, $6, $7, FALSE)
2153                               FROM  biblio.peer_bib_copy_map p
2154                                     JOIN asset.copy c ON (p.target_copy = c.id)
2155                               WHERE NOT c.deleted AND p.peer_record = $1
2156                             LIMIT ($6 -> 'acp')::INT
2157                             OFFSET ($7 -> 'acp')::INT
2158                         )x)
2159                      )
2160                  ELSE NULL END
2161              );
2162 $F$ LANGUAGE SQL STABLE;
2163
2164 -- Evergreen DB patch 0735.data.search_filter_group_perms.sql
2165 --
2166
2167 -- check whether patch can be applied
2168 SELECT evergreen.upgrade_deps_block_check('0735', :eg_version);
2169
2170 INSERT INTO permission.perm_list ( id, code, description ) 
2171     VALUES ( 
2172         537, 
2173         'ADMIN_SEARCH_FILTER_GROUP',
2174         oils_i18n_gettext( 
2175             537,
2176             'Allows staff to manage search filter groups and entries',
2177             'ppl', 
2178             'description' 
2179         )
2180     ),
2181     (
2182         538, 
2183         'VIEW_SEARCH_FILTER_GROUP',
2184         oils_i18n_gettext( 
2185             538,
2186             'Allows staff to view search filter groups and entries',
2187             'ppl', 
2188             'description' 
2189         )
2190
2191     );
2192
2193 -- check whether patch can be applied
2194 SELECT evergreen.upgrade_deps_block_check('0737', :eg_version);
2195
2196 UPDATE action_trigger.event_definition
2197 SET template =
2198 $$
2199 [%-
2200 # target is the bookbag itself. The 'items' variable does not need to be in
2201 # the environment because a special reactor will take care of filling it in.
2202
2203 FOR item IN items;
2204     bibxml = helpers.unapi_bre(item.target_biblio_record_entry, {flesh => '{mra}'});
2205     title = "";
2206     FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
2207         title = title _ part.textContent;
2208     END;
2209     author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
2210     item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
2211     pub_date = "";
2212     FOR pdatum IN bibxml.findnodes('//*[@tag="260"]/*[@code="c"]');
2213         IF pub_date ;
2214             pub_date = pub_date _ ", " _ pdatum.textContent;
2215         ELSE ;
2216             pub_date = pdatum.textContent;
2217         END;
2218     END;
2219     helpers.csv_datum(title) %],[% helpers.csv_datum(author) %],[% helpers.csv_datum(pub_date) %],[% helpers.csv_datum(item_type) %],[% FOR note IN item.notes; helpers.csv_datum(note.note); ","; END; "\n";
2220 END -%]
2221 $$
2222 WHERE name = 'Bookbag CSV';
2223
2224 COMMIT;