forward-port 2.9.3-2.10.0 monolithic DB update script
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.9.3-2.10.0-upgrade-db.sql
1 --Upgrade Script for 2.9.3 to 2.10.0
2 \set eg_version '''2.10.0'''
3 BEGIN;
4
5 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.10.0', :eg_version);
6
7 SELECT evergreen.upgrade_deps_block_check('0945', :eg_version);
8
9 -- run the entire update inside a DO block for managing the logic
10 -- of whether to recreate the optional reporter views
11 DO $$
12 DECLARE
13     has_current_circ BOOLEAN;
14     has_billing_summary BOOLEAN;
15 BEGIN
16
17 SELECT INTO has_current_circ TRUE FROM pg_views 
18     WHERE schemaname = 'reporter' AND viewname = 'classic_current_circ';
19
20 SELECT INTO has_billing_summary TRUE FROM pg_views 
21     WHERE schemaname = 'reporter' AND 
22     viewname = 'classic_current_billing_summary';
23
24 DROP VIEW action.all_circulation;
25 DROP VIEW IF EXISTS reporter.classic_current_circ;
26 DROP VIEW IF EXISTS reporter.classic_current_billing_summary;
27 DROP VIEW reporter.demographic;
28 DROP VIEW auditor.actor_usr_lifecycle;
29 DROP VIEW action.all_hold_request;
30
31 ALTER TABLE actor.usr 
32     ALTER dob TYPE DATE USING (dob + '3 hours'::INTERVAL)::DATE;
33
34 -- alter the auditor table manually to apply the same
35 -- dob mangling logic as above.
36 ALTER TABLE auditor.actor_usr_history 
37     ALTER dob TYPE DATE USING (dob + '3 hours'::INTERVAL)::DATE;
38
39 -- this recreates auditor.actor_usr_lifecycle
40 PERFORM auditor.update_auditors();
41
42 CREATE VIEW reporter.demographic AS
43     SELECT u.id, u.dob,
44         CASE
45             WHEN u.dob IS NULL THEN 'Adult'::text
46             WHEN age(u.dob) > '18 years'::interval THEN 'Adult'::text
47             ELSE 'Juvenile'::text
48         END AS general_division
49     FROM actor.usr u;
50
51 CREATE VIEW action.all_circulation AS
52          SELECT aged_circulation.id, aged_circulation.usr_post_code,
53             aged_circulation.usr_home_ou, aged_circulation.usr_profile,
54             aged_circulation.usr_birth_year, aged_circulation.copy_call_number,
55             aged_circulation.copy_location, aged_circulation.copy_owning_lib,
56             aged_circulation.copy_circ_lib, aged_circulation.copy_bib_record,
57             aged_circulation.xact_start, aged_circulation.xact_finish,
58             aged_circulation.target_copy, aged_circulation.circ_lib,
59             aged_circulation.circ_staff, aged_circulation.checkin_staff,
60             aged_circulation.checkin_lib, aged_circulation.renewal_remaining,
61             aged_circulation.grace_period, aged_circulation.due_date,
62             aged_circulation.stop_fines_time, aged_circulation.checkin_time,
63             aged_circulation.create_time, aged_circulation.duration,
64             aged_circulation.fine_interval, aged_circulation.recurring_fine,
65             aged_circulation.max_fine, aged_circulation.phone_renewal,
66             aged_circulation.desk_renewal, aged_circulation.opac_renewal,
67             aged_circulation.duration_rule,
68             aged_circulation.recurring_fine_rule,
69             aged_circulation.max_fine_rule, aged_circulation.stop_fines,
70             aged_circulation.workstation, aged_circulation.checkin_workstation,
71             aged_circulation.checkin_scan_time, aged_circulation.parent_circ
72            FROM action.aged_circulation
73 UNION ALL
74          SELECT DISTINCT circ.id,
75             COALESCE(a.post_code, b.post_code) AS usr_post_code,
76             p.home_ou AS usr_home_ou, p.profile AS usr_profile,
77             date_part('year'::text, p.dob)::integer AS usr_birth_year,
78             cp.call_number AS copy_call_number, circ.copy_location,
79             cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
80             cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish,
81             circ.target_copy, circ.circ_lib, circ.circ_staff,
82             circ.checkin_staff, circ.checkin_lib, circ.renewal_remaining,
83             circ.grace_period, circ.due_date, circ.stop_fines_time,
84             circ.checkin_time, circ.create_time, circ.duration,
85             circ.fine_interval, circ.recurring_fine, circ.max_fine,
86             circ.phone_renewal, circ.desk_renewal, circ.opac_renewal,
87             circ.duration_rule, circ.recurring_fine_rule, circ.max_fine_rule,
88             circ.stop_fines, circ.workstation, circ.checkin_workstation,
89             circ.checkin_scan_time, circ.parent_circ
90            FROM action.circulation circ
91       JOIN asset.copy cp ON circ.target_copy = cp.id
92    JOIN asset.call_number cn ON cp.call_number = cn.id
93    JOIN actor.usr p ON circ.usr = p.id
94    LEFT JOIN actor.usr_address a ON p.mailing_address = a.id
95    LEFT JOIN actor.usr_address b ON p.billing_address = b.id;
96
97 CREATE OR REPLACE VIEW action.all_hold_request AS
98          SELECT DISTINCT COALESCE(a.post_code, b.post_code) AS usr_post_code,
99             p.home_ou AS usr_home_ou, p.profile AS usr_profile,
100             date_part('year'::text, p.dob)::integer AS usr_birth_year,
101             ahr.requestor <> ahr.usr AS staff_placed, ahr.id, ahr.request_time,
102             ahr.capture_time, ahr.fulfillment_time, ahr.checkin_time,
103             ahr.return_time, ahr.prev_check_time, ahr.expire_time,
104             ahr.cancel_time, ahr.cancel_cause, ahr.cancel_note, ahr.target,
105             ahr.current_copy, ahr.fulfillment_staff, ahr.fulfillment_lib,
106             ahr.request_lib, ahr.selection_ou, ahr.selection_depth,
107             ahr.pickup_lib, ahr.hold_type, ahr.holdable_formats,
108                 CASE
109                     WHEN ahr.phone_notify IS NULL THEN false
110                     WHEN ahr.phone_notify = ''::text THEN false
111                     ELSE true
112                 END AS phone_notify,
113             ahr.email_notify,
114                 CASE
115                     WHEN ahr.sms_notify IS NULL THEN false
116                     WHEN ahr.sms_notify = ''::text THEN false
117                     ELSE true
118                 END AS sms_notify,
119             ahr.frozen, ahr.thaw_date, ahr.shelf_time, ahr.cut_in_line,
120             ahr.mint_condition, ahr.shelf_expire_time, ahr.current_shelf_lib,
121             ahr.behind_desk
122            FROM action.hold_request ahr
123       JOIN actor.usr p ON ahr.usr = p.id
124    LEFT JOIN actor.usr_address a ON p.mailing_address = a.id
125    LEFT JOIN actor.usr_address b ON p.billing_address = b.id
126 UNION ALL
127          SELECT aged_hold_request.usr_post_code, aged_hold_request.usr_home_ou,
128             aged_hold_request.usr_profile, aged_hold_request.usr_birth_year,
129             aged_hold_request.staff_placed, aged_hold_request.id,
130             aged_hold_request.request_time, aged_hold_request.capture_time,
131             aged_hold_request.fulfillment_time, aged_hold_request.checkin_time,
132             aged_hold_request.return_time, aged_hold_request.prev_check_time,
133             aged_hold_request.expire_time, aged_hold_request.cancel_time,
134             aged_hold_request.cancel_cause, aged_hold_request.cancel_note,
135             aged_hold_request.target, aged_hold_request.current_copy,
136             aged_hold_request.fulfillment_staff,
137             aged_hold_request.fulfillment_lib, aged_hold_request.request_lib,
138             aged_hold_request.selection_ou, aged_hold_request.selection_depth,
139             aged_hold_request.pickup_lib, aged_hold_request.hold_type,
140             aged_hold_request.holdable_formats, aged_hold_request.phone_notify,
141             aged_hold_request.email_notify, aged_hold_request.sms_notify,
142             aged_hold_request.frozen, aged_hold_request.thaw_date,
143             aged_hold_request.shelf_time, aged_hold_request.cut_in_line,
144             aged_hold_request.mint_condition,
145             aged_hold_request.shelf_expire_time,
146             aged_hold_request.current_shelf_lib, aged_hold_request.behind_desk
147            FROM action.aged_hold_request;
148
149 IF has_current_circ THEN
150 RAISE NOTICE 'Recreating optional view reporter.classic_current_circ';
151
152 CREATE OR REPLACE VIEW reporter.classic_current_circ AS
153 SELECT  cl.shortname AS circ_lib,
154         cl.id AS circ_lib_id,
155         circ.xact_start AS xact_start,
156         circ_type.type AS circ_type,
157         cp.id AS copy_id,
158         cp.circ_modifier,
159         ol.shortname AS owning_lib_name,
160         lm.value AS language,
161         lfm.value AS lit_form,
162         ifm.value AS item_form,
163         itm.value AS item_type,
164         sl.name AS shelving_location,
165         p.id AS patron_id,
166         g.name AS profile_group,
167         dem.general_division AS demographic_general_division,
168         circ.id AS id,
169         cn.id AS call_number,
170         cn.label AS call_number_label,
171         call_number_dewey(cn.label) AS dewey,
172         CASE
173                 WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
174                         THEN
175                                 btrim(
176                                         to_char(
177                                                 10 * floor((call_number_dewey(cn.label)::float) / 10), '000'
178                                         )
179                                 )
180                 ELSE NULL
181         END AS dewey_block_tens,
182         CASE
183                 WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
184                         THEN
185                                 btrim(
186                                         to_char(
187                                                 100 * floor((call_number_dewey(cn.label)::float) / 100), '000'
188                                         )
189                                 )
190                 ELSE NULL
191         END AS dewey_block_hundreds,
192         CASE
193                 WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
194                         THEN
195                                 btrim(
196                                         to_char(
197                                                 10 * floor((call_number_dewey(cn.label)::float) / 10), '000'
198                                         )
199                                 )
200                                 || '-' ||
201                                 btrim(
202                                         to_char(
203                                                 10 * floor((call_number_dewey(cn.label)::float) / 10) + 9, '000'
204                                         )
205                                 )
206                 ELSE NULL
207         END AS dewey_range_tens,
208         CASE
209                 WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
210                         THEN
211                                 btrim(
212                                         to_char(
213                                                 100 * floor((call_number_dewey(cn.label)::float) / 100), '000'
214                                         )
215                                 )
216                                 || '-' ||
217                                 btrim(
218                                         to_char(
219                                                 100 * floor((call_number_dewey(cn.label)::float) / 100) + 99, '000'
220                                         )
221                                 )
222                 ELSE NULL
223         END AS dewey_range_hundreds,
224         hl.id AS patron_home_lib,
225         hl.shortname AS patron_home_lib_shortname,
226         paddr.county AS patron_county,
227         paddr.city AS patron_city,
228         paddr.post_code AS patron_zip,
229         sc1.stat_cat_entry AS stat_cat_1,
230         sc2.stat_cat_entry AS stat_cat_2,
231         sce1.value AS stat_cat_1_value,
232         sce2.value AS stat_cat_2_value
233   FROM  action.circulation circ
234         JOIN reporter.circ_type circ_type ON (circ.id = circ_type.id)
235         JOIN asset.copy cp ON (cp.id = circ.target_copy)
236         JOIN asset.copy_location sl ON (cp.location = sl.id)
237         JOIN asset.call_number cn ON (cp.call_number = cn.id)
238         JOIN actor.org_unit ol ON (cn.owning_lib = ol.id)
239         JOIN metabib.rec_descriptor rd ON (rd.record = cn.record)
240         JOIN actor.org_unit cl ON (circ.circ_lib = cl.id)
241         JOIN actor.usr p ON (p.id = circ.usr)
242         JOIN actor.org_unit hl ON (p.home_ou = hl.id)
243         JOIN permission.grp_tree g ON (p.profile = g.id)
244         JOIN reporter.demographic dem ON (dem.id = p.id)
245         JOIN actor.usr_address paddr ON (paddr.id = p.billing_address)
246         LEFT JOIN config.language_map lm ON (rd.item_lang = lm.code)
247         LEFT JOIN config.lit_form_map lfm ON (rd.lit_form = lfm.code)
248         LEFT JOIN config.item_form_map ifm ON (rd.item_form = ifm.code)
249         LEFT JOIN config.item_type_map itm ON (rd.item_type = itm.code)
250         LEFT JOIN asset.stat_cat_entry_copy_map sc1 ON (sc1.owning_copy = cp.id AND sc1.stat_cat = 1)
251         LEFT JOIN asset.stat_cat_entry sce1 ON (sce1.id = sc1.stat_cat_entry)
252         LEFT JOIN asset.stat_cat_entry_copy_map sc2 ON (sc2.owning_copy = cp.id AND sc2.stat_cat = 2)
253         LEFT JOIN asset.stat_cat_entry sce2 ON (sce2.id = sc2.stat_cat_entry);
254 END IF;
255
256 IF has_billing_summary THEN
257 RAISE NOTICE 'Recreating optional view reporter.classic_current_billing_summary';
258
259 CREATE OR REPLACE VIEW reporter.classic_current_billing_summary AS
260 SELECT  x.id AS id,
261         x.usr AS usr,
262         bl.shortname AS billing_location_shortname,
263         bl.name AS billing_location_name,
264         x.billing_location AS billing_location,
265         c.barcode AS barcode,
266         u.home_ou AS usr_home_ou,
267         ul.shortname AS usr_home_ou_shortname,
268         ul.name AS usr_home_ou_name,
269         x.xact_start AS xact_start,
270         x.xact_finish AS xact_finish,
271         x.xact_type AS xact_type,
272         x.total_paid AS total_paid,
273         x.total_owed AS total_owed,
274         x.balance_owed AS balance_owed,
275         x.last_payment_ts AS last_payment_ts,
276         x.last_payment_note AS last_payment_note,
277         x.last_payment_type AS last_payment_type,
278         x.last_billing_ts AS last_billing_ts,
279         x.last_billing_note AS last_billing_note,
280         x.last_billing_type AS last_billing_type,
281         paddr.county AS patron_county,
282         paddr.city AS patron_city,
283         paddr.post_code AS patron_zip,
284         g.name AS profile_group,
285         dem.general_division AS demographic_general_division
286   FROM  money.open_billable_xact_summary x
287         JOIN actor.org_unit bl ON (x.billing_location = bl.id)
288         JOIN actor.usr u ON (u.id = x.usr)
289         JOIN actor.org_unit ul ON (u.home_ou = ul.id)
290         JOIN actor.card c ON (u.card = c.id)
291         JOIN permission.grp_tree g ON (u.profile = g.id)
292         JOIN reporter.demographic dem ON (dem.id = u.id)
293         JOIN actor.usr_address paddr ON (paddr.id = u.billing_address);
294 END IF;
295
296 END $$;
297
298 SELECT evergreen.upgrade_deps_block_check('0946', :eg_version);
299
300 CREATE OR REPLACE FUNCTION actor.org_unit_ancestor_setting_batch( org_id INT, VARIADIC setting_names TEXT[] ) RETURNS SETOF actor.org_unit_setting AS $$
301 DECLARE
302     setting RECORD;
303     setting_name TEXT;
304     cur_org INT;
305 BEGIN
306     FOREACH setting_name IN ARRAY setting_names
307     LOOP
308         cur_org := org_id;
309         LOOP
310             SELECT INTO setting * FROM actor.org_unit_setting WHERE org_unit = cur_org AND name = setting_name;
311             IF FOUND THEN
312                 RETURN NEXT setting;
313                 EXIT;
314             END IF;
315             SELECT INTO cur_org parent_ou FROM actor.org_unit WHERE id = cur_org;
316             EXIT WHEN cur_org IS NULL;
317         END LOOP;
318     END LOOP;
319     RETURN;
320 END;
321 $$ LANGUAGE plpgsql STABLE;
322
323 COMMENT ON FUNCTION actor.org_unit_ancestor_setting_batch( INT, VARIADIC TEXT[] ) IS $$
324 For each setting name passed, search "up" the org_unit tree until
325 we find the first occurrence of an org_unit_setting with the given name.
326 $$;
327
328 SELECT evergreen.upgrade_deps_block_check('0947', :eg_version);
329
330 CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) RETURNS TEXT AS $$
331     my $string = shift;            # Source string
332     my $pad = shift;               # string to fill. Typically '0'. This should be a single character.
333     my $len = shift;               # length of resultant padded field
334
335     $string =~ s/([0-9]+)/$pad x ($len - length($1)) . $1/eg;
336
337     return $string;
338 $$ LANGUAGE PLPERLU;
339
340 SELECT evergreen.upgrade_deps_block_check('0951', :eg_version);
341
342 ALTER TABLE config.standing_penalty
343       ADD COLUMN ignore_proximity INTEGER;
344
345 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$
346 DECLARE
347     matchpoint_id        INT;
348     user_object        actor.usr%ROWTYPE;
349     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
350     standing_penalty    config.standing_penalty%ROWTYPE;
351     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
352     transit_source        actor.org_unit%ROWTYPE;
353     item_object        asset.copy%ROWTYPE;
354     item_cn_object     asset.call_number%ROWTYPE;
355     item_status_object  config.copy_status%ROWTYPE;
356     item_location_object    asset.copy_location%ROWTYPE;
357     ou_skip              actor.org_unit_setting%ROWTYPE;
358     result            action.matrix_test_result;
359     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
360     use_active_date   TEXT;
361     age_protect_date  TIMESTAMP WITH TIME ZONE;
362     hold_count        INT;
363     hold_transit_prox    INT;
364     frozen_hold_count    INT;
365     context_org_list    INT[];
366     done            BOOL := FALSE;
367     hold_penalty TEXT;
368     v_pickup_ou ALIAS FOR pickup_ou;
369     v_request_ou ALIAS FOR request_ou;
370     item_prox INT;
371     pickup_prox INT;
372 BEGIN
373     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
374     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
375
376     result.success := TRUE;
377
378     -- The HOLD penalty block only applies to new holds.
379     -- The CAPTURE penalty block applies to existing holds.
380     hold_penalty := 'HOLD';
381     IF retargetting THEN
382         hold_penalty := 'CAPTURE';
383     END IF;
384
385     -- Fail if we couldn't find a user
386     IF user_object.id IS NULL THEN
387         result.fail_part := 'no_user';
388         result.success := FALSE;
389         done := TRUE;
390         RETURN NEXT result;
391         RETURN;
392     END IF;
393
394     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
395
396     -- Fail if we couldn't find a copy
397     IF item_object.id IS NULL THEN
398         result.fail_part := 'no_item';
399         result.success := FALSE;
400         done := TRUE;
401         RETURN NEXT result;
402         RETURN;
403     END IF;
404
405     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
406     result.matchpoint := matchpoint_id;
407
408     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
409
410     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
411     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
412         result.fail_part := 'circ.holds.target_skip_me';
413         result.success := FALSE;
414         done := TRUE;
415         RETURN NEXT result;
416         RETURN;
417     END IF;
418
419     -- Fail if user is barred
420     IF user_object.barred IS TRUE THEN
421         result.fail_part := 'actor.usr.barred';
422         result.success := FALSE;
423         done := TRUE;
424         RETURN NEXT result;
425         RETURN;
426     END IF;
427
428     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
429     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
430     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
431
432     -- Fail if we couldn't find any matchpoint (requires a default)
433     IF matchpoint_id IS NULL THEN
434         result.fail_part := 'no_matchpoint';
435         result.success := FALSE;
436         done := TRUE;
437         RETURN NEXT result;
438         RETURN;
439     END IF;
440
441     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
442
443     IF hold_test.holdable IS FALSE THEN
444         result.fail_part := 'config.hold_matrix_test.holdable';
445         result.success := FALSE;
446         done := TRUE;
447         RETURN NEXT result;
448     END IF;
449
450     IF item_object.holdable IS FALSE THEN
451         result.fail_part := 'item.holdable';
452         result.success := FALSE;
453         done := TRUE;
454         RETURN NEXT result;
455     END IF;
456
457     IF item_status_object.holdable IS FALSE THEN
458         result.fail_part := 'status.holdable';
459         result.success := FALSE;
460         done := TRUE;
461         RETURN NEXT result;
462     END IF;
463
464     IF item_location_object.holdable IS FALSE THEN
465         result.fail_part := 'location.holdable';
466         result.success := FALSE;
467         done := TRUE;
468         RETURN NEXT result;
469     END IF;
470
471     IF hold_test.transit_range IS NOT NULL THEN
472         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
473         IF hold_test.distance_is_from_owner THEN
474             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;
475         ELSE
476             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
477         END IF;
478
479         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
480
481         IF NOT FOUND THEN
482             result.fail_part := 'transit_range';
483             result.success := FALSE;
484             done := TRUE;
485             RETURN NEXT result;
486         END IF;
487     END IF;
488  
489     -- Proximity of user's home_ou to the pickup_lib to see if penalty should be ignored.
490     SELECT INTO pickup_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = v_pickup_ou;
491     -- Proximity of user's home_ou to the items' lib to see if penalty should be ignored.
492     IF hold_test.distance_is_from_owner THEN
493         SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_cn_object.owning_lib;
494     ELSE
495         SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
496     END IF;
497
498     FOR standing_penalty IN
499         SELECT  DISTINCT csp.*
500           FROM  actor.usr_standing_penalty usp
501                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
502           WHERE usr = match_user
503                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
504                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
505                 AND (csp.ignore_proximity IS NULL OR csp.ignore_proximity < item_prox
506                      OR csp.ignore_proximity < pickup_prox)
507                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
508
509         result.fail_part := standing_penalty.name;
510         result.success := FALSE;
511         done := TRUE;
512         RETURN NEXT result;
513     END LOOP;
514
515     IF hold_test.stop_blocked_user IS TRUE THEN
516         FOR standing_penalty IN
517             SELECT  DISTINCT csp.*
518               FROM  actor.usr_standing_penalty usp
519                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
520               WHERE usr = match_user
521                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
522                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
523                     AND csp.block_list LIKE '%CIRC%' LOOP
524     
525             result.fail_part := standing_penalty.name;
526             result.success := FALSE;
527             done := TRUE;
528             RETURN NEXT result;
529         END LOOP;
530     END IF;
531
532     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
533         SELECT    INTO hold_count COUNT(*)
534           FROM    action.hold_request
535           WHERE    usr = match_user
536             AND fulfillment_time IS NULL
537             AND cancel_time IS NULL
538             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
539
540         IF hold_count >= hold_test.max_holds THEN
541             result.fail_part := 'config.hold_matrix_test.max_holds';
542             result.success := FALSE;
543             done := TRUE;
544             RETURN NEXT result;
545         END IF;
546     END IF;
547
548     IF item_object.age_protect IS NOT NULL THEN
549         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
550         IF hold_test.distance_is_from_owner THEN
551             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
552         ELSE
553             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
554         END IF;
555         IF use_active_date = 'true' THEN
556             age_protect_date := COALESCE(item_object.active_date, NOW());
557         ELSE
558             age_protect_date := item_object.create_date;
559         END IF;
560         IF age_protect_date + age_protect_object.age > NOW() THEN
561             IF hold_test.distance_is_from_owner THEN
562                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
563                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = v_pickup_ou;
564             ELSE
565                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = v_pickup_ou;
566             END IF;
567
568             IF hold_transit_prox > age_protect_object.prox THEN
569                 result.fail_part := 'config.rule_age_hold_protect.prox';
570                 result.success := FALSE;
571                 done := TRUE;
572                 RETURN NEXT result;
573             END IF;
574         END IF;
575     END IF;
576
577     IF NOT done THEN
578         RETURN NEXT result;
579     END IF;
580
581     RETURN;
582 END;
583 $func$ LANGUAGE plpgsql;
584
585 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
586 DECLARE
587     user_object             actor.usr%ROWTYPE;
588     standing_penalty        config.standing_penalty%ROWTYPE;
589     item_object             asset.copy%ROWTYPE;
590     item_status_object      config.copy_status%ROWTYPE;
591     item_location_object    asset.copy_location%ROWTYPE;
592     result                  action.circ_matrix_test_result;
593     circ_test               action.found_circ_matrix_matchpoint;
594     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
595     circ_limit_set          config.circ_limit_set%ROWTYPE;
596     hold_ratio              action.hold_stats%ROWTYPE;
597     penalty_type            TEXT;
598     items_out               INT;
599     context_org_list        INT[];
600     done                    BOOL := FALSE;
601     item_prox               INT;
602     home_prox               INT;
603 BEGIN
604     -- Assume success unless we hit a failure condition
605     result.success := TRUE;
606
607     -- Need user info to look up matchpoints
608     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
609
610     -- (Insta)Fail if we couldn't find the user
611     IF user_object.id IS NULL THEN
612         result.fail_part := 'no_user';
613         result.success := FALSE;
614         done := TRUE;
615         RETURN NEXT result;
616         RETURN;
617     END IF;
618
619     -- Need item info to look up matchpoints
620     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
621
622     -- (Insta)Fail if we couldn't find the item 
623     IF item_object.id IS NULL THEN
624         result.fail_part := 'no_item';
625         result.success := FALSE;
626         done := TRUE;
627         RETURN NEXT result;
628         RETURN;
629     END IF;
630
631     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
632
633     circ_matchpoint             := circ_test.matchpoint;
634     result.matchpoint           := circ_matchpoint.id;
635     result.circulate            := circ_matchpoint.circulate;
636     result.duration_rule        := circ_matchpoint.duration_rule;
637     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
638     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
639     result.hard_due_date        := circ_matchpoint.hard_due_date;
640     result.renewals             := circ_matchpoint.renewals;
641     result.grace_period         := circ_matchpoint.grace_period;
642     result.buildrows            := circ_test.buildrows;
643
644     -- (Insta)Fail if we couldn't find a matchpoint
645     IF circ_test.success = false THEN
646         result.fail_part := 'no_matchpoint';
647         result.success := FALSE;
648         done := TRUE;
649         RETURN NEXT result;
650         RETURN;
651     END IF;
652
653     -- All failures before this point are non-recoverable
654     -- Below this point are possibly overridable failures
655
656     -- Fail if the user is barred
657     IF user_object.barred IS TRUE THEN
658         result.fail_part := 'actor.usr.barred';
659         result.success := FALSE;
660         done := TRUE;
661         RETURN NEXT result;
662     END IF;
663
664     -- Fail if the item can't circulate
665     IF item_object.circulate IS FALSE THEN
666         result.fail_part := 'asset.copy.circulate';
667         result.success := FALSE;
668         done := TRUE;
669         RETURN NEXT result;
670     END IF;
671
672     -- Fail if the item isn't in a circulateable status on a non-renewal
673     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
674         result.fail_part := 'asset.copy.status';
675         result.success := FALSE;
676         done := TRUE;
677         RETURN NEXT result;
678     -- Alternately, fail if the item isn't checked out on a renewal
679     ELSIF renewal AND item_object.status <> 1 THEN
680         result.fail_part := 'asset.copy.status';
681         result.success := FALSE;
682         done := TRUE;
683         RETURN NEXT result;
684     END IF;
685
686     -- Fail if the item can't circulate because of the shelving location
687     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
688     IF item_location_object.circulate IS FALSE THEN
689         result.fail_part := 'asset.copy_location.circulate';
690         result.success := FALSE;
691         done := TRUE;
692         RETURN NEXT result;
693     END IF;
694
695     -- Use Circ OU for penalties and such
696     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
697
698     -- Proximity of user's home_ou to circ_ou to see if penalties should be ignored.
699     SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = circ_ou;
700
701     -- Proximity of user's home_ou to item circ_lib to see if penalties should be ignored.
702     SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
703
704     IF renewal THEN
705         penalty_type = '%RENEW%';
706     ELSE
707         penalty_type = '%CIRC%';
708     END IF;
709
710     FOR standing_penalty IN
711         SELECT  DISTINCT csp.*
712           FROM  actor.usr_standing_penalty usp
713                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
714           WHERE usr = match_user
715                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
716                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
717                 AND (csp.ignore_proximity IS NULL
718                      OR csp.ignore_proximity < home_prox
719                      OR csp.ignore_proximity < item_prox)
720                 AND csp.block_list LIKE penalty_type LOOP
721
722         result.fail_part := standing_penalty.name;
723         result.success := FALSE;
724         done := TRUE;
725         RETURN NEXT result;
726     END LOOP;
727
728     -- Fail if the test is set to hard non-circulating
729     IF circ_matchpoint.circulate IS FALSE THEN
730         result.fail_part := 'config.circ_matrix_test.circulate';
731         result.success := FALSE;
732         done := TRUE;
733         RETURN NEXT result;
734     END IF;
735
736     -- Fail if the total copy-hold ratio is too low
737     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
738         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
739         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
740             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
741             result.success := FALSE;
742             done := TRUE;
743             RETURN NEXT result;
744         END IF;
745     END IF;
746
747     -- Fail if the available copy-hold ratio is too low
748     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
749         IF hold_ratio.hold_count IS NULL THEN
750             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
751         END IF;
752         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
753             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
754             result.success := FALSE;
755             done := TRUE;
756             RETURN NEXT result;
757         END IF;
758     END IF;
759
760     -- Fail if the user has too many items out by defined limit sets
761     FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
762       JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
763       WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
764         ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
765         ) LOOP
766             IF circ_limit_set.items_out > 0 AND NOT renewal THEN
767                 SELECT INTO context_org_list ARRAY_AGG(aou.id)
768                   FROM actor.org_unit_full_path( circ_ou ) aou
769                     JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
770                   WHERE aout.depth >= circ_limit_set.depth;
771                 IF circ_limit_set.global THEN
772                     WITH RECURSIVE descendant_depth AS (
773                         SELECT  ou.id,
774                             ou.parent_ou
775                         FROM  actor.org_unit ou
776                         WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
777                             UNION
778                         SELECT  ou.id,
779                             ou.parent_ou
780                         FROM  actor.org_unit ou
781                             JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
782                     ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
783                 END IF;
784                 SELECT INTO items_out COUNT(DISTINCT circ.id)
785                   FROM action.circulation circ
786                     JOIN asset.copy copy ON (copy.id = circ.target_copy)
787                     LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
788                   WHERE circ.usr = match_user
789                     AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
790                     AND circ.checkin_time IS NULL
791                     AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
792                     AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
793                         OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
794                         OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
795                     );
796                 IF items_out >= circ_limit_set.items_out THEN
797                     result.fail_part := 'config.circ_matrix_circ_mod_test';
798                     result.success := FALSE;
799                     done := TRUE;
800                     RETURN NEXT result;
801                 END IF;
802             END IF;
803             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;
804     END LOOP;
805
806     -- If we passed everything, return the successful matchpoint
807     IF NOT done THEN
808         RETURN NEXT result;
809     END IF;
810
811     RETURN;
812 END;
813 $func$ LANGUAGE plpgsql;
814
815 SELECT evergreen.upgrade_deps_block_check('0952', :eg_version); --miker/kmlussier/gmcharlt
816
817 INSERT INTO config.metabib_field ( id, field_class, name, label, format, xpath, browse_field, facet_field, facet_xpath, joiner ) VALUES
818     (33, 'identifier', 'genre', oils_i18n_gettext(33, 'Genre', 'cmf', 'label'), 'marcxml', $$//marc:datafield[@tag='655']$$, FALSE, TRUE, $$//*[local-name()='subfield' and contains('abvxyz',@code)]$$, ' -- ' ); -- /* to fool vim */;
819
820 INSERT INTO config.metabib_field_index_norm_map (field,norm)
821     SELECT  m.id,
822             i.id
823       FROM  config.metabib_field m,
824         config.index_normalizer i
825       WHERE i.func IN ('search_normalize','split_date_range')
826             AND m.id IN (33);
827
828 SELECT evergreen.upgrade_deps_block_check('0953', :eg_version);
829
830 CREATE OR REPLACE FUNCTION unapi.bre (
831     obj_id BIGINT,
832     format TEXT,
833     ename TEXT,
834     includes TEXT[],
835     org TEXT,
836     depth INT DEFAULT NULL,
837     slimit HSTORE DEFAULT NULL,
838     soffset HSTORE DEFAULT NULL,
839     include_xmlns BOOL DEFAULT TRUE,
840     pref_lib INT DEFAULT NULL
841 )
842 RETURNS XML AS $F$
843 DECLARE
844     me      biblio.record_entry%ROWTYPE;
845     layout  unapi.bre_output_layout%ROWTYPE;
846     xfrm    config.xml_transform%ROWTYPE;
847     ouid    INT;
848     tmp_xml TEXT;
849     top_el  TEXT;
850     output  XML;
851     hxml    XML;
852     axml    XML;
853     source  XML;
854 BEGIN
855
856     IF org = '-' OR org IS NULL THEN
857         SELECT shortname INTO org FROM evergreen.org_top();
858     END IF;
859
860     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
861
862     IF ouid IS NULL THEN
863         RETURN NULL::XML;
864     END IF;
865
866     IF format = 'holdings_xml' THEN -- the special case
867         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
868         RETURN output;
869     END IF;
870
871     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
872
873     IF layout.name IS NULL THEN
874         RETURN NULL::XML;
875     END IF;
876
877     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
878
879     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
880
881     -- grab bib_source, if any
882     IF ('cbs' = ANY (includes) AND me.source IS NOT NULL) THEN
883         source := unapi.cbs(me.source,NULL,NULL,NULL,NULL);
884     ELSE
885         source := NULL::XML;
886     END IF;
887
888     -- grab SVF if we need them
889     IF ('mra' = ANY (includes)) THEN 
890         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
891     ELSE
892         axml := NULL::XML;
893     END IF;
894
895     -- grab holdings if we need them
896     IF ('holdings_xml' = ANY (includes)) THEN 
897         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib);
898     ELSE
899         hxml := NULL::XML;
900     END IF;
901
902
903     -- generate our item node
904
905
906     IF format = 'marcxml' THEN
907         tmp_xml := me.marc;
908         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
909            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
910         END IF; 
911     ELSE
912         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
913     END IF;
914
915     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
916
917     IF source IS NOT NULL THEN
918         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', source || '</' || top_el || E'>\\1');
919     END IF;
920
921     IF axml IS NOT NULL THEN 
922         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
923     END IF;
924
925     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
926         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
927     END IF;
928
929     IF ('bre.unapi' = ANY (includes)) THEN 
930         output := REGEXP_REPLACE(
931             tmp_xml,
932             '</' || top_el || '>(.*?)',
933             XMLELEMENT(
934                 name abbr,
935                 XMLATTRIBUTES(
936                     'http://www.w3.org/1999/xhtml' AS xmlns,
937                     'unapi-id' AS class,
938                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
939                 )
940             )::TEXT || '</' || top_el || E'>\\1'
941         );
942     ELSE
943         output := tmp_xml;
944     END IF;
945
946     IF ('bre.extern' = ANY (includes)) THEN 
947         output := REGEXP_REPLACE(
948             tmp_xml,
949             '</' || top_el || '>(.*?)',
950             XMLELEMENT(
951                 name extern,
952                 XMLATTRIBUTES(
953                     'http://open-ils.org/spec/biblio/v1' AS xmlns,
954                     me.creator AS creator,
955                     me.editor AS editor,
956                     me.create_date AS create_date,
957                     me.edit_date AS edit_date,
958                     me.quality AS quality,
959                     me.fingerprint AS fingerprint,
960                     me.tcn_source AS tcn_source,
961                     me.tcn_value AS tcn_value,
962                     me.owner AS owner,
963                     me.share_depth AS share_depth,
964                     me.active AS active,
965                     me.deleted AS deleted
966                 )
967             )::TEXT || '</' || top_el || E'>\\1'
968         );
969     ELSE
970         output := tmp_xml;
971     END IF;
972
973     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
974     RETURN output;
975 END;
976 $F$ LANGUAGE PLPGSQL STABLE;
977
978 SELECT evergreen.upgrade_deps_block_check('0954', :eg_version);
979
980 ALTER TABLE acq.fund_debit 
981     ADD COLUMN invoice_entry INTEGER 
982         REFERENCES acq.invoice_entry (id)
983         ON DELETE SET NULL;
984
985 CREATE INDEX fund_debit_invoice_entry_idx ON acq.fund_debit (invoice_entry);
986 CREATE INDEX lineitem_detail_fund_debit_idx ON acq.lineitem_detail (fund_debit);
987
988 SELECT evergreen.upgrade_deps_block_check('0955', :eg_version);
989
990 UPDATE config.org_unit_setting_type
991 SET description = 'Regular expression defining the password format.  Note: Be sure to update the update_password_msg.tt2 TPAC template with a user-friendly description of your password strength requirements.'
992 WHERE NAME = 'global.password_regex';
993
994 SELECT evergreen.upgrade_deps_block_check('0956', :eg_version);
995
996 ALTER TABLE money.credit_card_payment 
997     DROP COLUMN cc_type,
998     DROP COLUMN expire_month,
999     DROP COLUMN expire_year,
1000     DROP COLUMN cc_first_name,
1001     DROP COLUMN cc_last_name;
1002
1003 SELECT evergreen.upgrade_deps_block_check('0957', :eg_version);
1004
1005 -- Remove references to dropped CC payment columns in the print/email 
1006 -- payment receipt templates, but only if the in-db template matches 
1007 -- the stock template.
1008 -- The actual diff here is only about 8 lines.
1009
1010 UPDATE action_trigger.event_definition SET template = 
1011 $$
1012 [%- USE date -%]
1013 [%- SET user = target.0.xact.usr -%]
1014 To: [%- params.recipient_email || user.email %]
1015 From: [%- params.sender_email || default_sender %]
1016 Subject: Payment Receipt
1017
1018 [% date.format -%]
1019 [%- SET xact_mp_hash = {} -%]
1020 [%- FOR mp IN target %][%# Template is hooked around payments, but let us make the receipt focused on transactions -%]
1021     [%- SET xact_id = mp.xact.id -%]
1022     [%- IF ! xact_mp_hash.defined( xact_id ) -%][%- xact_mp_hash.$xact_id = { 'xact' => mp.xact, 'payments' => [] } -%][%- END -%]
1023     [%- xact_mp_hash.$xact_id.payments.push(mp) -%]
1024 [%- END -%]
1025 [%- FOR xact_id IN xact_mp_hash.keys.sort -%]
1026     [%- SET xact = xact_mp_hash.$xact_id.xact %]
1027 Transaction ID: [% xact_id %]
1028     [% IF xact.circulation %][% helpers.get_copy_bib_basics(xact.circulation.target_copy).title %]
1029     [% ELSE %]Miscellaneous
1030     [% END %]
1031     Line item billings:
1032         [%- SET mb_type_hash = {} -%]
1033         [%- FOR mb IN xact.billings %][%# Group billings by their btype -%]
1034             [%- IF mb.voided == 'f' -%]
1035                 [%- SET mb_type = mb.btype.id -%]
1036                 [%- IF ! mb_type_hash.defined( mb_type ) -%][%- mb_type_hash.$mb_type = { 'sum' => 0.00, 'billings' => [] } -%][%- END -%]
1037                 [%- IF ! mb_type_hash.$mb_type.defined( 'first_ts' ) -%][%- mb_type_hash.$mb_type.first_ts = mb.billing_ts -%][%- END -%]
1038                 [%- mb_type_hash.$mb_type.last_ts = mb.billing_ts -%]
1039                 [%- mb_type_hash.$mb_type.sum = mb_type_hash.$mb_type.sum + mb.amount -%]
1040                 [%- mb_type_hash.$mb_type.billings.push( mb ) -%]
1041             [%- END -%]
1042         [%- END -%]
1043         [%- FOR mb_type IN mb_type_hash.keys.sort -%]
1044             [%- IF mb_type == 1 %][%-# Consolidated view of overdue billings -%]
1045                 $[% mb_type_hash.$mb_type.sum %] for [% mb_type_hash.$mb_type.billings.0.btype.name %] 
1046                     on [% mb_type_hash.$mb_type.first_ts %] through [% mb_type_hash.$mb_type.last_ts %]
1047             [%- ELSE -%][%# all other billings show individually %]
1048                 [% FOR mb IN mb_type_hash.$mb_type.billings %]
1049                     $[% mb.amount %] for [% mb.btype.name %] on [% mb.billing_ts %] [% mb.note %]
1050                 [% END %]
1051             [% END %]
1052         [% END %]
1053     Line item payments:
1054         [% FOR mp IN xact_mp_hash.$xact_id.payments %]
1055             Payment ID: [% mp.id %]
1056                 Paid [% mp.amount %] via [% SWITCH mp.payment_type -%]
1057                     [% CASE "cash_payment" %]cash
1058                     [% CASE "check_payment" %]check
1059                     [% CASE "credit_card_payment" %]credit card
1060                     [%- IF mp.credit_card_payment.cc_number %] ([% mp.credit_card_payment.cc_number %])[% END %]
1061                     [% CASE "credit_payment" %]credit
1062                     [% CASE "forgive_payment" %]forgiveness
1063                     [% CASE "goods_payment" %]goods
1064                     [% CASE "work_payment" %]work
1065                 [%- END %] on [% mp.payment_ts %] [% mp.note %]
1066         [% END %]
1067 [% END %]
1068 $$
1069
1070 WHERE id = 29 AND template =
1071
1072 $$
1073 [%- USE date -%]
1074 [%- SET user = target.0.xact.usr -%]
1075 To: [%- params.recipient_email || user.email %]
1076 From: [%- params.sender_email || default_sender %]
1077 Subject: Payment Receipt
1078
1079 [% date.format -%]
1080 [%- SET xact_mp_hash = {} -%]
1081 [%- FOR mp IN target %][%# Template is hooked around payments, but let us make the receipt focused on transactions -%]
1082     [%- SET xact_id = mp.xact.id -%]
1083     [%- IF ! xact_mp_hash.defined( xact_id ) -%][%- xact_mp_hash.$xact_id = { 'xact' => mp.xact, 'payments' => [] } -%][%- END -%]
1084     [%- xact_mp_hash.$xact_id.payments.push(mp) -%]
1085 [%- END -%]
1086 [%- FOR xact_id IN xact_mp_hash.keys.sort -%]
1087     [%- SET xact = xact_mp_hash.$xact_id.xact %]
1088 Transaction ID: [% xact_id %]
1089     [% IF xact.circulation %][% helpers.get_copy_bib_basics(xact.circulation.target_copy).title %]
1090     [% ELSE %]Miscellaneous
1091     [% END %]
1092     Line item billings:
1093         [%- SET mb_type_hash = {} -%]
1094         [%- FOR mb IN xact.billings %][%# Group billings by their btype -%]
1095             [%- IF mb.voided == 'f' -%]
1096                 [%- SET mb_type = mb.btype.id -%]
1097                 [%- IF ! mb_type_hash.defined( mb_type ) -%][%- mb_type_hash.$mb_type = { 'sum' => 0.00, 'billings' => [] } -%][%- END -%]
1098                 [%- IF ! mb_type_hash.$mb_type.defined( 'first_ts' ) -%][%- mb_type_hash.$mb_type.first_ts = mb.billing_ts -%][%- END -%]
1099                 [%- mb_type_hash.$mb_type.last_ts = mb.billing_ts -%]
1100                 [%- mb_type_hash.$mb_type.sum = mb_type_hash.$mb_type.sum + mb.amount -%]
1101                 [%- mb_type_hash.$mb_type.billings.push( mb ) -%]
1102             [%- END -%]
1103         [%- END -%]
1104         [%- FOR mb_type IN mb_type_hash.keys.sort -%]
1105             [%- IF mb_type == 1 %][%-# Consolidated view of overdue billings -%]
1106                 $[% mb_type_hash.$mb_type.sum %] for [% mb_type_hash.$mb_type.billings.0.btype.name %] 
1107                     on [% mb_type_hash.$mb_type.first_ts %] through [% mb_type_hash.$mb_type.last_ts %]
1108             [%- ELSE -%][%# all other billings show individually %]
1109                 [% FOR mb IN mb_type_hash.$mb_type.billings %]
1110                     $[% mb.amount %] for [% mb.btype.name %] on [% mb.billing_ts %] [% mb.note %]
1111                 [% END %]
1112             [% END %]
1113         [% END %]
1114     Line item payments:
1115         [% FOR mp IN xact_mp_hash.$xact_id.payments %]
1116             Payment ID: [% mp.id %]
1117                 Paid [% mp.amount %] via [% SWITCH mp.payment_type -%]
1118                     [% CASE "cash_payment" %]cash
1119                     [% CASE "check_payment" %]check
1120                     [% CASE "credit_card_payment" %]credit card (
1121                         [%- SET cc_chunks = mp.credit_card_payment.cc_number.replace(' ','').chunk(4); -%]
1122                         [%- cc_chunks.slice(0, -1+cc_chunks.max).join.replace('\S','X') -%] 
1123                         [% cc_chunks.last -%]
1124                         exp [% mp.credit_card_payment.expire_month %]/[% mp.credit_card_payment.expire_year -%]
1125                     )
1126                     [% CASE "credit_payment" %]credit
1127                     [% CASE "forgive_payment" %]forgiveness
1128                     [% CASE "goods_payment" %]goods
1129                     [% CASE "work_payment" %]work
1130                 [%- END %] on [% mp.payment_ts %] [% mp.note %]
1131         [% END %]
1132 [% END %]
1133 $$;
1134
1135
1136 UPDATE action_trigger.event_definition SET template = 
1137 $$
1138 [%- USE date -%][%- SET user = target.0.xact.usr -%]
1139 <div style="li { padding: 8px; margin 5px; }">
1140     <div>[% date.format %]</div><br/>
1141     <ol>
1142     [% SET xact_mp_hash = {} %]
1143     [% FOR mp IN target %][%# Template is hooked around payments, but let us make the receipt focused on transactions %]
1144         [% SET xact_id = mp.xact.id %]
1145         [% IF ! xact_mp_hash.defined( xact_id ) %][% xact_mp_hash.$xact_id = { 'xact' => mp.xact, 'payments' => [] } %][% END %]
1146         [% xact_mp_hash.$xact_id.payments.push(mp) %]
1147     [% END %]
1148     [% FOR xact_id IN xact_mp_hash.keys.sort %]
1149         [% SET xact = xact_mp_hash.$xact_id.xact %]
1150         <li>Transaction ID: [% xact_id %]
1151             [% IF xact.circulation %][% helpers.get_copy_bib_basics(xact.circulation.target_copy).title %]
1152             [% ELSE %]Miscellaneous
1153             [% END %]
1154             Line item billings:<ol>
1155                 [% SET mb_type_hash = {} %]
1156                 [% FOR mb IN xact.billings %][%# Group billings by their btype %]
1157                     [% IF mb.voided == 'f' %]
1158                         [% SET mb_type = mb.btype.id %]
1159                         [% IF ! mb_type_hash.defined( mb_type ) %][% mb_type_hash.$mb_type = { 'sum' => 0.00, 'billings' => [] } %][% END %]
1160                         [% IF ! mb_type_hash.$mb_type.defined( 'first_ts' ) %][% mb_type_hash.$mb_type.first_ts = mb.billing_ts %][% END %]
1161                         [% mb_type_hash.$mb_type.last_ts = mb.billing_ts %]
1162                         [% mb_type_hash.$mb_type.sum = mb_type_hash.$mb_type.sum + mb.amount %]
1163                         [% mb_type_hash.$mb_type.billings.push( mb ) %]
1164                     [% END %]
1165                 [% END %]
1166                 [% FOR mb_type IN mb_type_hash.keys.sort %]
1167                     <li>[% IF mb_type == 1 %][%# Consolidated view of overdue billings %]
1168                         $[% mb_type_hash.$mb_type.sum %] for [% mb_type_hash.$mb_type.billings.0.btype.name %] 
1169                             on [% mb_type_hash.$mb_type.first_ts %] through [% mb_type_hash.$mb_type.last_ts %]
1170                     [% ELSE %][%# all other billings show individually %]
1171                         [% FOR mb IN mb_type_hash.$mb_type.billings %]
1172                             $[% mb.amount %] for [% mb.btype.name %] on [% mb.billing_ts %] [% mb.note %]
1173                         [% END %]
1174                     [% END %]</li>
1175                 [% END %]
1176             </ol>
1177             Line item payments:<ol>
1178                 [% FOR mp IN xact_mp_hash.$xact_id.payments %]
1179                     <li>Payment ID: [% mp.id %]
1180                         Paid [% mp.amount %] via [% SWITCH mp.payment_type -%]
1181                             [% CASE "cash_payment" %]cash
1182                             [% CASE "check_payment" %]check
1183                             [% CASE "credit_card_payment" %]credit card
1184                             [%- IF mp.credit_card_payment.cc_number %] ([% mp.credit_card_payment.cc_number %])[% END %]
1185                             [% CASE "credit_payment" %]credit
1186                             [% CASE "forgive_payment" %]forgiveness
1187                             [% CASE "goods_payment" %]goods
1188                             [% CASE "work_payment" %]work
1189                         [%- END %] on [% mp.payment_ts %] [% mp.note %]
1190                     </li>
1191                 [% END %]
1192             </ol>
1193         </li>
1194     [% END %]
1195     </ol>
1196 </div>
1197 $$
1198
1199 WHERE id = 30 AND template =
1200
1201 $$
1202 [%- USE date -%][%- SET user = target.0.xact.usr -%]
1203 <div style="li { padding: 8px; margin 5px; }">
1204     <div>[% date.format %]</div><br/>
1205     <ol>
1206     [% SET xact_mp_hash = {} %]
1207     [% FOR mp IN target %][%# Template is hooked around payments, but let us make the receipt focused on transactions %]
1208         [% SET xact_id = mp.xact.id %]
1209         [% IF ! xact_mp_hash.defined( xact_id ) %][% xact_mp_hash.$xact_id = { 'xact' => mp.xact, 'payments' => [] } %][% END %]
1210         [% xact_mp_hash.$xact_id.payments.push(mp) %]
1211     [% END %]
1212     [% FOR xact_id IN xact_mp_hash.keys.sort %]
1213         [% SET xact = xact_mp_hash.$xact_id.xact %]
1214         <li>Transaction ID: [% xact_id %]
1215             [% IF xact.circulation %][% helpers.get_copy_bib_basics(xact.circulation.target_copy).title %]
1216             [% ELSE %]Miscellaneous
1217             [% END %]
1218             Line item billings:<ol>
1219                 [% SET mb_type_hash = {} %]
1220                 [% FOR mb IN xact.billings %][%# Group billings by their btype %]
1221                     [% IF mb.voided == 'f' %]
1222                         [% SET mb_type = mb.btype.id %]
1223                         [% IF ! mb_type_hash.defined( mb_type ) %][% mb_type_hash.$mb_type = { 'sum' => 0.00, 'billings' => [] } %][% END %]
1224                         [% IF ! mb_type_hash.$mb_type.defined( 'first_ts' ) %][% mb_type_hash.$mb_type.first_ts = mb.billing_ts %][% END %]
1225                         [% mb_type_hash.$mb_type.last_ts = mb.billing_ts %]
1226                         [% mb_type_hash.$mb_type.sum = mb_type_hash.$mb_type.sum + mb.amount %]
1227                         [% mb_type_hash.$mb_type.billings.push( mb ) %]
1228                     [% END %]
1229                 [% END %]
1230                 [% FOR mb_type IN mb_type_hash.keys.sort %]
1231                     <li>[% IF mb_type == 1 %][%# Consolidated view of overdue billings %]
1232                         $[% mb_type_hash.$mb_type.sum %] for [% mb_type_hash.$mb_type.billings.0.btype.name %] 
1233                             on [% mb_type_hash.$mb_type.first_ts %] through [% mb_type_hash.$mb_type.last_ts %]
1234                     [% ELSE %][%# all other billings show individually %]
1235                         [% FOR mb IN mb_type_hash.$mb_type.billings %]
1236                             $[% mb.amount %] for [% mb.btype.name %] on [% mb.billing_ts %] [% mb.note %]
1237                         [% END %]
1238                     [% END %]</li>
1239                 [% END %]
1240             </ol>
1241             Line item payments:<ol>
1242                 [% FOR mp IN xact_mp_hash.$xact_id.payments %]
1243                     <li>Payment ID: [% mp.id %]
1244                         Paid [% mp.amount %] via [% SWITCH mp.payment_type -%]
1245                             [% CASE "cash_payment" %]cash
1246                             [% CASE "check_payment" %]check
1247                             [% CASE "credit_card_payment" %]credit card (
1248                                 [%- SET cc_chunks = mp.credit_card_payment.cc_number.replace(' ','').chunk(4); -%]
1249                                 [%- cc_chunks.slice(0, -1+cc_chunks.max).join.replace('\S','X') -%] 
1250                                 [% cc_chunks.last -%]
1251                                 exp [% mp.credit_card_payment.expire_month %]/[% mp.credit_card_payment.expire_year -%]
1252                             )
1253                             [% CASE "credit_payment" %]credit
1254                             [% CASE "forgive_payment" %]forgiveness
1255                             [% CASE "goods_payment" %]goods
1256                             [% CASE "work_payment" %]work
1257                         [%- END %] on [% mp.payment_ts %] [% mp.note %]
1258                     </li>
1259                 [% END %]
1260             </ol>
1261         </li>
1262     [% END %]
1263     </ol>
1264 </div>
1265 $$;
1266
1267
1268 SELECT evergreen.upgrade_deps_block_check('0958', :eg_version);
1269
1270 CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes TEXT[], hits BIGINT[]) RETURNS TABLE (id INT, value TEXT, count BIGINT) AS $$
1271     SELECT id, value, count FROM (
1272         SELECT mfae.field AS id,
1273                mfae.value,
1274                COUNT(DISTINCT mmrsm.source),
1275                row_number() OVER (
1276                 PARTITION BY mfae.field ORDER BY COUNT(distinct mmrsm.source) DESC
1277                ) AS rownum
1278         FROM metabib.facet_entry mfae
1279         JOIN metabib.metarecord_source_map mmrsm ON (mfae.source = mmrsm.source)
1280         JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
1281         WHERE mmrsm.source IN (SELECT * FROM unnest($2))
1282         AND cmf.facet_field
1283         AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
1284         GROUP by 1, 2
1285     ) all_facets
1286     WHERE rownum <= (SELECT COALESCE((SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled), 1000));
1287 $$ LANGUAGE SQL;
1288
1289 CREATE OR REPLACE FUNCTION search.facets_for_metarecord_set(ignore_facet_classes TEXT[], hits BIGINT[]) RETURNS TABLE (id INT, value TEXT, count BIGINT) AS $$
1290     SELECT id, value, count FROM (
1291         SELECT mfae.field AS id,
1292                mfae.value,
1293                COUNT(DISTINCT mmrsm.metarecord),
1294                row_number() OVER (
1295                 PARTITION BY mfae.field ORDER BY COUNT(distinct mmrsm.metarecord) DESC
1296                ) AS rownum
1297         FROM metabib.facet_entry mfae
1298         JOIN metabib.metarecord_source_map mmrsm ON (mfae.source = mmrsm.source)
1299         JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
1300         WHERE mmrsm.metarecord IN (SELECT * FROM unnest($2))
1301         AND cmf.facet_field
1302         AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
1303         GROUP by 1, 2
1304     ) all_facets
1305     WHERE rownum <= (SELECT COALESCE((SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled), 1000));
1306 $$ LANGUAGE SQL;
1307
1308 INSERT INTO config.global_flag (name, value, label, enabled)
1309     VALUES (
1310         'search.max_facets_per_field',
1311         '1000',
1312         oils_i18n_gettext(
1313             'search.max_facets_per_field',
1314             'Search: maximum number of facet values to retrieve for each facet field',
1315             'cgf',
1316             'label'
1317         ),
1318         TRUE
1319     );
1320
1321 SELECT evergreen.upgrade_deps_block_check('0960', :eg_version); 
1322
1323 CREATE TABLE action.usr_circ_history (
1324     id           BIGSERIAL PRIMARY KEY,
1325     usr          INTEGER NOT NULL REFERENCES actor.usr(id)
1326                  DEFERRABLE INITIALLY DEFERRED,
1327     xact_start   TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1328     target_copy  BIGINT NOT NULL REFERENCES asset.copy(id)
1329                  DEFERRABLE INITIALLY DEFERRED,
1330     due_date     TIMESTAMP WITH TIME ZONE NOT NULL,
1331     checkin_time TIMESTAMP WITH TIME ZONE,
1332     source_circ  BIGINT REFERENCES action.circulation(id)
1333                  ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
1334 );
1335
1336 CREATE OR REPLACE FUNCTION action.maintain_usr_circ_history() 
1337     RETURNS TRIGGER AS $FUNK$
1338 DECLARE
1339     cur_circ  BIGINT;
1340     first_circ BIGINT;
1341 BEGIN                                                                          
1342
1343     -- Any retention value signifies history is enabled.
1344     -- This assumes that clearing these values via external 
1345     -- process deletes the action.usr_circ_history rows.
1346     -- TODO: replace these settings w/ a single bool setting?
1347     PERFORM 1 FROM actor.usr_setting 
1348         WHERE usr = NEW.usr AND value IS NOT NULL AND name IN (
1349             'history.circ.retention_age', 
1350             'history.circ.retention_start'
1351         );
1352
1353     IF NOT FOUND THEN
1354         RETURN NEW;
1355     END IF;
1356
1357     IF TG_OP = 'INSERT' AND NEW.parent_circ IS NULL THEN
1358         -- Starting a new circulation.  Insert the history row.
1359         INSERT INTO action.usr_circ_history 
1360             (usr, xact_start, target_copy, due_date, source_circ)
1361         VALUES (
1362             NEW.usr, 
1363             NEW.xact_start, 
1364             NEW.target_copy, 
1365             NEW.due_date, 
1366             NEW.id
1367         );
1368
1369         RETURN NEW;
1370     END IF;
1371
1372     -- find the first and last circs in the circ chain 
1373     -- for the currently modified circ.
1374     FOR cur_circ IN SELECT id FROM action.circ_chain(NEW.id) LOOP
1375         IF first_circ IS NULL THEN
1376             first_circ := cur_circ;
1377             CONTINUE;
1378         END IF;
1379         -- Allow the loop to continue so that at as the loop
1380         -- completes cur_circ points to the final circulation.
1381     END LOOP;
1382
1383     IF NEW.id <> cur_circ THEN
1384         -- Modifying an intermediate circ.  Ignore it.
1385         RETURN NEW;
1386     END IF;
1387
1388     -- Update the due_date/checkin_time on the history row if the current 
1389     -- circ is the last circ in the chain and an update is warranted.
1390
1391     UPDATE action.usr_circ_history 
1392         SET 
1393             due_date = NEW.due_date,
1394             checkin_time = NEW.checkin_time
1395         WHERE 
1396             source_circ = first_circ 
1397             AND (
1398                 due_date <> NEW.due_date OR (
1399                     (checkin_time IS NULL AND NEW.checkin_time IS NOT NULL) OR
1400                     (checkin_time IS NOT NULL AND NEW.checkin_time IS NULL) OR
1401                     (checkin_time <> NEW.checkin_time)
1402                 )
1403             );
1404     RETURN NEW;
1405 END;                                                                           
1406 $FUNK$ LANGUAGE PLPGSQL; 
1407
1408 CREATE TRIGGER maintain_usr_circ_history_tgr 
1409     AFTER INSERT OR UPDATE ON action.circulation 
1410     FOR EACH ROW EXECUTE PROCEDURE action.maintain_usr_circ_history();
1411
1412 UPDATE action_trigger.hook 
1413     SET core_type = 'auch' 
1414     WHERE key ~ '^circ.format.history.'; 
1415
1416 UPDATE action_trigger.event_definition SET template = 
1417 $$
1418 [%- USE date -%]
1419 [%- SET user = target.0.usr -%]
1420 To: [%- params.recipient_email || user.email %]
1421 From: [%- params.sender_email || default_sender %]
1422 Subject: Circulation History
1423
1424     [% FOR circ IN target %]
1425             [% helpers.get_copy_bib_basics(circ.target_copy.id).title %]
1426             Barcode: [% circ.target_copy.barcode %]
1427             Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]
1428             Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
1429             Returned: [%
1430                 date.format(
1431                     helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
1432                     IF circ.checkin_time; 
1433             %]
1434     [% END %]
1435 $$
1436 WHERE id = 25 AND template = 
1437 $$
1438 [%- USE date -%]
1439 [%- SET user = target.0.usr -%]
1440 To: [%- params.recipient_email || user.email %]
1441 From: [%- params.sender_email || default_sender %]
1442 Subject: Circulation History
1443
1444     [% FOR circ IN target %]
1445             [% helpers.get_copy_bib_basics(circ.target_copy.id).title %]
1446             Barcode: [% circ.target_copy.barcode %]
1447             Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]
1448             Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
1449             Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]
1450     [% END %]
1451 $$;
1452
1453 -- avoid TT undef date errors
1454 UPDATE action_trigger.event_definition SET template = 
1455 $$
1456 [%- USE date -%]
1457 <div>
1458     <style> li { padding: 8px; margin 5px; }</style>
1459     <div>[% date.format %]</div>
1460     <br/>
1461
1462     [% user.family_name %], [% user.first_given_name %]
1463     <ol>
1464     [% FOR circ IN target %]
1465         <li>
1466             <div>[% helpers.get_copy_bib_basics(circ.target_copy.id).title %]</div>
1467             <div>Barcode: [% circ.target_copy.barcode %]</div>
1468             <div>Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]</div>
1469             <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
1470             <div>Returned: [%
1471                 date.format(
1472                     helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
1473                     IF circ.checkin_time; -%]
1474             </div>
1475         </li>
1476     [% END %]
1477     </ol>
1478 </div>
1479 $$
1480 WHERE id = 26 AND template = -- only replace template if it matches stock
1481 $$
1482 [%- USE date -%]
1483 <div>
1484     <style> li { padding: 8px; margin 5px; }</style>
1485     <div>[% date.format %]</div>
1486     <br/>
1487
1488     [% user.family_name %], [% user.first_given_name %]
1489     <ol>
1490     [% FOR circ IN target %]
1491         <li>
1492             <div>[% helpers.get_copy_bib_basics(circ.target_copy.id).title %]</div>
1493             <div>Barcode: [% circ.target_copy.barcode %]</div>
1494             <div>Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]</div>
1495             <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
1496             <div>Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]</div>
1497         </li>
1498     [% END %]
1499     </ol>
1500 </div>
1501 $$;
1502
1503 -- NOTE: ^-- stock CSV template does not include checkin_time, so 
1504 -- no modifications are required.
1505
1506 -- Create circ history rows for existing circ history data.
1507 DO $FUNK$
1508 DECLARE
1509     cur_usr   INTEGER;
1510     cur_circ  action.circulation%ROWTYPE;
1511     last_circ action.circulation%ROWTYPE;
1512     counter   INTEGER DEFAULT 1;
1513 BEGIN
1514
1515     RAISE NOTICE 
1516         'Migrating circ history for % users.  This might take a while...',
1517         (SELECT COUNT(DISTINCT(au.id)) FROM actor.usr au
1518             JOIN actor.usr_setting aus ON (aus.usr = au.id)
1519             WHERE NOT au.deleted AND 
1520                 aus.name ~ '^history.circ.retention_');
1521
1522     FOR cur_usr IN 
1523         SELECT DISTINCT(au.id)
1524             FROM actor.usr au 
1525             JOIN actor.usr_setting aus ON (aus.usr = au.id)
1526             WHERE NOT au.deleted AND 
1527                 aus.name ~ '^history.circ.retention_' LOOP
1528
1529         FOR cur_circ IN SELECT * FROM action.usr_visible_circs(cur_usr) LOOP
1530
1531             -- Find the last circ in the circ chain.
1532             SELECT INTO last_circ * 
1533                 FROM action.circ_chain(cur_circ.id) 
1534                 ORDER BY xact_start DESC LIMIT 1;
1535
1536             -- Create the history row.
1537             -- It's OK if last_circ = cur_circ
1538             INSERT INTO action.usr_circ_history 
1539                 (usr, xact_start, target_copy, 
1540                     due_date, checkin_time, source_circ)
1541             VALUES (
1542                 cur_circ.usr, 
1543                 cur_circ.xact_start, 
1544                 cur_circ.target_copy, 
1545                 last_circ.due_date, 
1546                 last_circ.checkin_time,
1547                 cur_circ.id
1548             );
1549
1550             -- useful for alleviating administrator anxiety.
1551             IF counter % 10000 = 0 THEN
1552                 RAISE NOTICE 'Migrated history for % total users', counter;
1553             END IF;
1554
1555             counter := counter + 1;
1556
1557         END LOOP;
1558     END LOOP;
1559
1560 END $FUNK$;
1561
1562 DROP FUNCTION IF EXISTS action.usr_visible_circs (INTEGER);
1563 DROP FUNCTION IF EXISTS action.usr_visible_circ_copies (INTEGER);
1564
1565 -- remove user retention age checks
1566 CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
1567 DECLARE
1568     org_keep_age    INTERVAL;
1569     org_use_last    BOOL = false;
1570     org_age_is_min  BOOL = false;
1571     org_keep_count  INT;
1572
1573     keep_age        INTERVAL;
1574
1575     target_acp      RECORD;
1576     circ_chain_head action.circulation%ROWTYPE;
1577     circ_chain_tail action.circulation%ROWTYPE;
1578
1579     count_purged    INT;
1580     num_incomplete  INT;
1581
1582     last_finished   TIMESTAMP WITH TIME ZONE;
1583 BEGIN
1584
1585     count_purged := 0;
1586
1587     SELECT value::INTERVAL INTO org_keep_age FROM config.global_flag WHERE name = 'history.circ.retention_age' AND enabled;
1588
1589     SELECT value::INT INTO org_keep_count FROM config.global_flag WHERE name = 'history.circ.retention_count' AND enabled;
1590     IF org_keep_count IS NULL THEN
1591         RETURN count_purged; -- Gimme a count to keep, or I keep them all, forever
1592     END IF;
1593
1594     SELECT enabled INTO org_use_last FROM config.global_flag WHERE name = 'history.circ.retention_uses_last_finished';
1595     SELECT enabled INTO org_age_is_min FROM config.global_flag WHERE name = 'history.circ.retention_age_is_min';
1596
1597     -- First, find copies with more than keep_count non-renewal circs
1598     FOR target_acp IN
1599         SELECT  target_copy,
1600                 COUNT(*) AS total_real_circs
1601           FROM  action.circulation
1602           WHERE parent_circ IS NULL
1603                 AND xact_finish IS NOT NULL
1604           GROUP BY target_copy
1605           HAVING COUNT(*) > org_keep_count
1606     LOOP
1607         -- And, for those, select circs that are finished and older than keep_age
1608         FOR circ_chain_head IN
1609             -- For reference, the subquery uses a window function to order the circs newest to oldest and number them
1610             -- The outer query then uses that information to skip the most recent set the library wants to keep
1611             -- End result is we don't care what order they come out in, as they are all potentials for deletion.
1612             SELECT ac.* FROM action.circulation ac JOIN (
1613               SELECT  rank() OVER (ORDER BY xact_start DESC), ac.id
1614                 FROM  action.circulation ac
1615                 WHERE ac.target_copy = target_acp.target_copy
1616                   AND ac.parent_circ IS NULL
1617                 ORDER BY ac.xact_start ) ranked USING (id)
1618                 WHERE ranked.rank > org_keep_count
1619         LOOP
1620
1621             SELECT * INTO circ_chain_tail FROM action.circ_chain(circ_chain_head.id) ORDER BY xact_start DESC LIMIT 1;
1622             SELECT COUNT(CASE WHEN xact_finish IS NULL THEN 1 ELSE NULL END), MAX(xact_finish) INTO num_incomplete, last_finished FROM action.circ_chain(circ_chain_head.id);
1623             CONTINUE WHEN circ_chain_tail.xact_finish IS NULL OR num_incomplete > 0;
1624
1625             IF NOT org_use_last THEN
1626                 last_finished := circ_chain_tail.xact_finish;
1627             END IF;
1628
1629             keep_age := COALESCE( org_keep_age, '2000 years'::INTERVAL );
1630
1631             IF org_age_is_min THEN
1632                 keep_age := GREATEST( keep_age, org_keep_age );
1633             END IF;
1634
1635             CONTINUE WHEN AGE(NOW(), last_finished) < keep_age;
1636
1637             -- We've passed the purging tests, purge the circ chain starting at the end
1638             -- A trigger should auto-purge the rest of the chain.
1639             DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
1640
1641             count_purged := count_purged + 1;
1642
1643         END LOOP;
1644     END LOOP;
1645
1646     return count_purged;
1647 END;
1648 $func$ LANGUAGE PLPGSQL;
1649
1650 -- delete circ history rows when a user is purged.
1651 CREATE OR REPLACE FUNCTION actor.usr_purge_data(
1652         src_usr  IN INTEGER,
1653         specified_dest_usr IN INTEGER
1654 ) RETURNS VOID AS $$
1655 DECLARE
1656         suffix TEXT;
1657         renamable_row RECORD;
1658         dest_usr INTEGER;
1659 BEGIN
1660
1661         IF specified_dest_usr IS NULL THEN
1662                 dest_usr := 1; -- Admin user on stock installs
1663         ELSE
1664                 dest_usr := specified_dest_usr;
1665         END IF;
1666
1667         -- acq.*
1668         UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
1669         UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
1670         UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
1671         UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
1672         UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
1673         UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
1674         DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
1675
1676         -- Update with a rename to avoid collisions
1677         FOR renamable_row in
1678                 SELECT id, name
1679                 FROM   acq.picklist
1680                 WHERE  owner = src_usr
1681         LOOP
1682                 suffix := ' (' || src_usr || ')';
1683                 LOOP
1684                         BEGIN
1685                                 UPDATE  acq.picklist
1686                                 SET     owner = dest_usr, name = name || suffix
1687                                 WHERE   id = renamable_row.id;
1688                         EXCEPTION WHEN unique_violation THEN
1689                                 suffix := suffix || ' ';
1690                                 CONTINUE;
1691                         END;
1692                         EXIT;
1693                 END LOOP;
1694         END LOOP;
1695
1696         UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
1697         UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
1698         UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
1699         UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
1700         UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
1701         UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
1702         UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
1703         UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
1704
1705         -- action.*
1706         DELETE FROM action.circulation WHERE usr = src_usr;
1707         UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
1708         UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
1709         UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
1710         UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
1711         UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
1712         DELETE FROM action.hold_request WHERE usr = src_usr;
1713         UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
1714         UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
1715         DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
1716         UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
1717         DELETE FROM action.survey_response WHERE usr = src_usr;
1718         UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
1719     DELETE FROM action.usr_circ_history WHERE usr = src_usr;
1720
1721         -- actor.*
1722         DELETE FROM actor.card WHERE usr = src_usr;
1723         DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
1724
1725         -- The following update is intended to avoid transient violations of a foreign
1726         -- key constraint, whereby actor.usr_address references itself.  It may not be
1727         -- necessary, but it does no harm.
1728         UPDATE actor.usr_address SET replaces = NULL
1729                 WHERE usr = src_usr AND replaces IS NOT NULL;
1730         DELETE FROM actor.usr_address WHERE usr = src_usr;
1731         DELETE FROM actor.usr_note WHERE usr = src_usr;
1732         UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
1733         DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
1734         UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
1735         DELETE FROM actor.usr_setting WHERE usr = src_usr;
1736         DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
1737         UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
1738
1739         -- asset.*
1740         UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
1741         UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
1742         UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
1743         UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
1744         UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
1745         UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
1746
1747         -- auditor.*
1748         DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
1749         DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
1750         UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
1751         UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
1752         UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
1753         UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
1754         UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
1755         UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
1756
1757         -- biblio.*
1758         UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
1759         UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
1760         UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
1761         UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
1762
1763         -- container.*
1764         -- Update buckets with a rename to avoid collisions
1765         FOR renamable_row in
1766                 SELECT id, name
1767                 FROM   container.biblio_record_entry_bucket
1768                 WHERE  owner = src_usr
1769         LOOP
1770                 suffix := ' (' || src_usr || ')';
1771                 LOOP
1772                         BEGIN
1773                                 UPDATE  container.biblio_record_entry_bucket
1774                                 SET     owner = dest_usr, name = name || suffix
1775                                 WHERE   id = renamable_row.id;
1776                         EXCEPTION WHEN unique_violation THEN
1777                                 suffix := suffix || ' ';
1778                                 CONTINUE;
1779                         END;
1780                         EXIT;
1781                 END LOOP;
1782         END LOOP;
1783
1784         FOR renamable_row in
1785                 SELECT id, name
1786                 FROM   container.call_number_bucket
1787                 WHERE  owner = src_usr
1788         LOOP
1789                 suffix := ' (' || src_usr || ')';
1790                 LOOP
1791                         BEGIN
1792                                 UPDATE  container.call_number_bucket
1793                                 SET     owner = dest_usr, name = name || suffix
1794                                 WHERE   id = renamable_row.id;
1795                         EXCEPTION WHEN unique_violation THEN
1796                                 suffix := suffix || ' ';
1797                                 CONTINUE;
1798                         END;
1799                         EXIT;
1800                 END LOOP;
1801         END LOOP;
1802
1803         FOR renamable_row in
1804                 SELECT id, name
1805                 FROM   container.copy_bucket
1806                 WHERE  owner = src_usr
1807         LOOP
1808                 suffix := ' (' || src_usr || ')';
1809                 LOOP
1810                         BEGIN
1811                                 UPDATE  container.copy_bucket
1812                                 SET     owner = dest_usr, name = name || suffix
1813                                 WHERE   id = renamable_row.id;
1814                         EXCEPTION WHEN unique_violation THEN
1815                                 suffix := suffix || ' ';
1816                                 CONTINUE;
1817                         END;
1818                         EXIT;
1819                 END LOOP;
1820         END LOOP;
1821
1822         FOR renamable_row in
1823                 SELECT id, name
1824                 FROM   container.user_bucket
1825                 WHERE  owner = src_usr
1826         LOOP
1827                 suffix := ' (' || src_usr || ')';
1828                 LOOP
1829                         BEGIN
1830                                 UPDATE  container.user_bucket
1831                                 SET     owner = dest_usr, name = name || suffix
1832                                 WHERE   id = renamable_row.id;
1833                         EXCEPTION WHEN unique_violation THEN
1834                                 suffix := suffix || ' ';
1835                                 CONTINUE;
1836                         END;
1837                         EXIT;
1838                 END LOOP;
1839         END LOOP;
1840
1841         DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
1842
1843         -- money.*
1844         DELETE FROM money.billable_xact WHERE usr = src_usr;
1845         DELETE FROM money.collections_tracker WHERE usr = src_usr;
1846         UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
1847
1848         -- permission.*
1849         DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
1850         DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
1851         DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
1852         DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
1853
1854         -- reporter.*
1855         -- Update with a rename to avoid collisions
1856         BEGIN
1857                 FOR renamable_row in
1858                         SELECT id, name
1859                         FROM   reporter.output_folder
1860                         WHERE  owner = src_usr
1861                 LOOP
1862                         suffix := ' (' || src_usr || ')';
1863                         LOOP
1864                                 BEGIN
1865                                         UPDATE  reporter.output_folder
1866                                         SET     owner = dest_usr, name = name || suffix
1867                                         WHERE   id = renamable_row.id;
1868                                 EXCEPTION WHEN unique_violation THEN
1869                                         suffix := suffix || ' ';
1870                                         CONTINUE;
1871                                 END;
1872                                 EXIT;
1873                         END LOOP;
1874                 END LOOP;
1875         EXCEPTION WHEN undefined_table THEN
1876                 -- do nothing
1877         END;
1878
1879         BEGIN
1880                 UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
1881         EXCEPTION WHEN undefined_table THEN
1882                 -- do nothing
1883         END;
1884
1885         -- Update with a rename to avoid collisions
1886         BEGIN
1887                 FOR renamable_row in
1888                         SELECT id, name
1889                         FROM   reporter.report_folder
1890                         WHERE  owner = src_usr
1891                 LOOP
1892                         suffix := ' (' || src_usr || ')';
1893                         LOOP
1894                                 BEGIN
1895                                         UPDATE  reporter.report_folder
1896                                         SET     owner = dest_usr, name = name || suffix
1897                                         WHERE   id = renamable_row.id;
1898                                 EXCEPTION WHEN unique_violation THEN
1899                                         suffix := suffix || ' ';
1900                                         CONTINUE;
1901                                 END;
1902                                 EXIT;
1903                         END LOOP;
1904                 END LOOP;
1905         EXCEPTION WHEN undefined_table THEN
1906                 -- do nothing
1907         END;
1908
1909         BEGIN
1910                 UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
1911         EXCEPTION WHEN undefined_table THEN
1912                 -- do nothing
1913         END;
1914
1915         BEGIN
1916                 UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
1917         EXCEPTION WHEN undefined_table THEN
1918                 -- do nothing
1919         END;
1920
1921         -- Update with a rename to avoid collisions
1922         BEGIN
1923                 FOR renamable_row in
1924                         SELECT id, name
1925                         FROM   reporter.template_folder
1926                         WHERE  owner = src_usr
1927                 LOOP
1928                         suffix := ' (' || src_usr || ')';
1929                         LOOP
1930                                 BEGIN
1931                                         UPDATE  reporter.template_folder
1932                                         SET     owner = dest_usr, name = name || suffix
1933                                         WHERE   id = renamable_row.id;
1934                                 EXCEPTION WHEN unique_violation THEN
1935                                         suffix := suffix || ' ';
1936                                         CONTINUE;
1937                                 END;
1938                                 EXIT;
1939                         END LOOP;
1940                 END LOOP;
1941         EXCEPTION WHEN undefined_table THEN
1942         -- do nothing
1943         END;
1944
1945         -- vandelay.*
1946         -- Update with a rename to avoid collisions
1947         FOR renamable_row in
1948                 SELECT id, name
1949                 FROM   vandelay.queue
1950                 WHERE  owner = src_usr
1951         LOOP
1952                 suffix := ' (' || src_usr || ')';
1953                 LOOP
1954                         BEGIN
1955                                 UPDATE  vandelay.queue
1956                                 SET     owner = dest_usr, name = name || suffix
1957                                 WHERE   id = renamable_row.id;
1958                         EXCEPTION WHEN unique_violation THEN
1959                                 suffix := suffix || ' ';
1960                                 CONTINUE;
1961                         END;
1962                         EXIT;
1963                 END LOOP;
1964         END LOOP;
1965
1966     -- NULL-ify addresses last so other cleanup (e.g. circ anonymization)
1967     -- can access the information before deletion.
1968         UPDATE actor.usr SET
1969                 active = FALSE,
1970                 card = NULL,
1971                 mailing_address = NULL,
1972                 billing_address = NULL
1973         WHERE id = src_usr;
1974
1975 END;
1976 $$ LANGUAGE plpgsql;
1977
1978 SELECT evergreen.upgrade_deps_block_check('0961', :eg_version);
1979
1980 CREATE EXTENSION IF NOT EXISTS pgcrypto;
1981
1982 CREATE TABLE actor.passwd_type (
1983     code        TEXT PRIMARY KEY,
1984     name        TEXT UNIQUE NOT NULL,
1985     login       BOOLEAN NOT NULL DEFAULT FALSE,
1986     regex       TEXT,   -- pending
1987     crypt_algo  TEXT,   -- e.g. 'bf'
1988
1989     -- gen_salt() iter count used with each new salt.
1990     -- A non-NULL value for iter_count is our indication the 
1991     -- password is salted and encrypted via crypt()
1992     iter_count  INTEGER CHECK (iter_count IS NULL OR iter_count > 0)
1993 );
1994
1995 CREATE TABLE actor.passwd (
1996     id          SERIAL PRIMARY KEY,
1997     usr         INTEGER NOT NULL REFERENCES actor.usr(id)
1998                 ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1999     salt        TEXT, -- will be NULL for non-crypt'ed passwords
2000     passwd      TEXT NOT NULL,
2001     passwd_type TEXT NOT NULL REFERENCES actor.passwd_type(code)
2002                 DEFERRABLE INITIALLY DEFERRED,
2003     create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
2004     edit_date   TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
2005     CONSTRAINT  passwd_type_once_per_user UNIQUE (usr, passwd_type)
2006 );
2007
2008 CREATE OR REPLACE FUNCTION actor.create_salt(pw_type TEXT)
2009     RETURNS TEXT AS $$
2010 DECLARE
2011     type_row actor.passwd_type%ROWTYPE;
2012 BEGIN
2013     /* Returns a new salt based on the passwd_type encryption settings.
2014      * Returns NULL If the password type is not crypt()'ed.
2015      */
2016
2017     SELECT INTO type_row * FROM actor.passwd_type WHERE code = pw_type;
2018
2019     IF NOT FOUND THEN
2020         RETURN EXCEPTION 'No such password type: %', pw_type;
2021     END IF;
2022
2023     IF type_row.iter_count IS NULL THEN
2024         -- This password type is unsalted.  That's OK.
2025         RETURN NULL;
2026     END IF;
2027
2028     RETURN gen_salt(type_row.crypt_algo, type_row.iter_count);
2029 END;
2030 $$ LANGUAGE PLPGSQL;
2031
2032
2033 /* 
2034     TODO: when a user changes their password in the application, the
2035     app layer has access to the bare password.  At that point, we have
2036     the opportunity to store the new password without the MD5(MD5())
2037     intermediate hashing.  Do we care?  We would need a way to indicate
2038     which passwords have the legacy intermediate hashing and which don't
2039     so the app layer would know whether it should perform the intermediate
2040     hashing.  In either event, with the exception of migrate_passwd(), the
2041     DB functions know or care nothing about intermediate hashing.  Every
2042     password is just a value that may or may not be internally crypt'ed. 
2043 */
2044
2045 CREATE OR REPLACE FUNCTION actor.set_passwd(
2046     pw_usr INTEGER, pw_type TEXT, new_pass TEXT, new_salt TEXT DEFAULT NULL)
2047     RETURNS BOOLEAN AS $$
2048 DECLARE
2049     pw_salt TEXT;
2050     pw_text TEXT;
2051 BEGIN
2052     /* Sets the password value, creating a new actor.passwd row if needed.
2053      * If the password type supports it, the new_pass value is crypt()'ed.
2054      * For crypt'ed passwords, the salt comes from one of 3 places in order:
2055      * new_salt (if present), existing salt (if present), newly created 
2056      * salt.
2057      */
2058
2059     IF new_salt IS NOT NULL THEN
2060         pw_salt := new_salt;
2061     ELSE 
2062         pw_salt := actor.get_salt(pw_usr, pw_type);
2063
2064         IF pw_salt IS NULL THEN
2065             /* We have no salt for this user + type.  Assume they want a 
2066              * new salt.  If this type is unsalted, create_salt() will 
2067              * return NULL. */
2068             pw_salt := actor.create_salt(pw_type);
2069         END IF;
2070     END IF;
2071
2072     IF pw_salt IS NULL THEN 
2073         pw_text := new_pass; -- unsalted, use as-is.
2074     ELSE
2075         pw_text := CRYPT(new_pass, pw_salt);
2076     END IF;
2077
2078     UPDATE actor.passwd 
2079         SET passwd = pw_text, salt = pw_salt, edit_date = NOW()
2080         WHERE usr = pw_usr AND passwd_type = pw_type;
2081
2082     IF NOT FOUND THEN
2083         -- no password row exists for this user + type.  Create one.
2084         INSERT INTO actor.passwd (usr, passwd_type, salt, passwd) 
2085             VALUES (pw_usr, pw_type, pw_salt, pw_text);
2086     END IF;
2087
2088     RETURN TRUE;
2089 END;
2090 $$ LANGUAGE PLPGSQL;
2091
2092 CREATE OR REPLACE FUNCTION actor.get_salt(pw_usr INTEGER, pw_type TEXT)
2093     RETURNS TEXT AS $$
2094 DECLARE
2095     pw_salt TEXT;
2096     type_row actor.passwd_type%ROWTYPE;
2097 BEGIN
2098     /* Returns the salt for the requested user + type.  If the password 
2099      * type of "main" is requested and no password exists in actor.passwd, 
2100      * the user's existing password is migrated and the new salt is returned.
2101      * Returns NULL if the password type is not crypt'ed (iter_count is NULL).
2102      */
2103
2104     SELECT INTO pw_salt salt FROM actor.passwd 
2105         WHERE usr = pw_usr AND passwd_type = pw_type;
2106
2107     IF FOUND THEN
2108         RETURN pw_salt;
2109     END IF;
2110
2111     IF pw_type = 'main' THEN
2112         -- Main password has not yet been migrated. 
2113         -- Do it now and return the newly created salt.
2114         RETURN actor.migrate_passwd(pw_usr);
2115     END IF;
2116
2117     -- We have no salt to return.  actor.create_salt() needed.
2118     RETURN NULL;
2119 END;
2120 $$ LANGUAGE PLPGSQL;
2121
2122 CREATE OR REPLACE FUNCTION 
2123     actor.migrate_passwd(pw_usr INTEGER) RETURNS TEXT AS $$
2124 DECLARE
2125     pw_salt TEXT;
2126     usr_row actor.usr%ROWTYPE;
2127 BEGIN
2128     /* Migrates legacy actor.usr.passwd value to actor.passwd with 
2129      * a password type 'main' and returns the new salt.  For backwards
2130      * compatibility with existing CHAP-style API's, we perform a 
2131      * layer of intermediate MD5(MD5()) hashing.  This is intermediate
2132      * hashing is not required of other passwords.
2133      */
2134
2135     -- Avoid calling get_salt() here, because it may result in a 
2136     -- migrate_passwd() call, creating a loop.
2137     SELECT INTO pw_salt salt FROM actor.passwd 
2138         WHERE usr = pw_usr AND passwd_type = 'main';
2139
2140     -- Only migrate passwords that have not already been migrated.
2141     IF FOUND THEN
2142         RETURN pw_salt;
2143     END IF;
2144
2145     SELECT INTO usr_row * FROM actor.usr WHERE id = pw_usr;
2146
2147     pw_salt := actor.create_salt('main');
2148
2149     PERFORM actor.set_passwd(
2150         pw_usr, 'main', MD5(pw_salt || usr_row.passwd), pw_salt);
2151
2152     -- clear the existing password
2153     UPDATE actor.usr SET passwd = '' WHERE id = usr_row.id;
2154
2155     RETURN pw_salt;
2156 END;
2157 $$ LANGUAGE PLPGSQL;
2158
2159 CREATE OR REPLACE FUNCTION 
2160     actor.verify_passwd(pw_usr INTEGER, pw_type TEXT, test_passwd TEXT) 
2161     RETURNS BOOLEAN AS $$
2162 DECLARE
2163     pw_salt TEXT;
2164 BEGIN
2165     /* Returns TRUE if the password provided matches the in-db password.  
2166      * If the password type is salted, we compare the output of CRYPT().
2167      * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy 
2168      * 'main' passwords.
2169      */
2170
2171     SELECT INTO pw_salt salt FROM actor.passwd 
2172         WHERE usr = pw_usr AND passwd_type = pw_type;
2173
2174     IF NOT FOUND THEN
2175         -- no such password
2176         RETURN FALSE;
2177     END IF;
2178
2179     IF pw_salt IS NULL THEN
2180         -- Password is unsalted, compare the un-CRYPT'ed values.
2181         RETURN EXISTS (
2182             SELECT TRUE FROM actor.passwd WHERE 
2183                 usr = pw_usr AND
2184                 passwd_type = pw_type AND
2185                 passwd = test_passwd
2186         );
2187     END IF;
2188
2189     RETURN EXISTS (
2190         SELECT TRUE FROM actor.passwd WHERE 
2191             usr = pw_usr AND
2192             passwd_type = pw_type AND
2193             passwd = CRYPT(test_passwd, pw_salt)
2194     );
2195 END;
2196 $$ STRICT LANGUAGE PLPGSQL;
2197
2198 --- DATA ----------------------
2199
2200 INSERT INTO actor.passwd_type 
2201     (code, name, login, crypt_algo, iter_count) 
2202     VALUES ('main', 'Main Login Password', TRUE, 'bf', 10);
2203
2204 SELECT evergreen.upgrade_deps_block_check('0962', :eg_version);
2205
2206 ALTER TABLE vandelay.import_item_attr_definition
2207     ADD COLUMN parts_data TEXT;
2208
2209 ALTER TABLE vandelay.import_item
2210     ADD COLUMN parts_data TEXT;
2211
2212 CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
2213 DECLARE
2214
2215     owning_lib      TEXT;
2216     circ_lib        TEXT;
2217     call_number     TEXT;
2218     copy_number     TEXT;
2219     status          TEXT;
2220     location        TEXT;
2221     circulate       TEXT;
2222     deposit         TEXT;
2223     deposit_amount  TEXT;
2224     ref             TEXT;
2225     holdable        TEXT;
2226     price           TEXT;
2227     barcode         TEXT;
2228     circ_modifier   TEXT;
2229     circ_as_type    TEXT;
2230     alert_message   TEXT;
2231     opac_visible    TEXT;
2232     pub_note        TEXT;
2233     priv_note       TEXT;
2234     internal_id     TEXT;
2235     stat_cat_data   TEXT;
2236     parts_data      TEXT;
2237
2238     attr_def        RECORD;
2239     tmp_attr_set    RECORD;
2240     attr_set        vandelay.import_item%ROWTYPE;
2241
2242     xpaths          TEXT[];
2243     tmp_str         TEXT;
2244
2245 BEGIN
2246
2247     SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
2248
2249     IF FOUND THEN
2250
2251         attr_set.definition := attr_def.id;
2252
2253         -- Build the combined XPath
2254
2255         owning_lib :=
2256             CASE
2257                 WHEN attr_def.owning_lib IS NULL THEN 'null()'
2258                 WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '*[@code="' || attr_def.owning_lib || '"]'
2259                 ELSE '*' || attr_def.owning_lib
2260             END;
2261
2262         circ_lib :=
2263             CASE
2264                 WHEN attr_def.circ_lib IS NULL THEN 'null()'
2265                 WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '*[@code="' || attr_def.circ_lib || '"]'
2266                 ELSE '*' || attr_def.circ_lib
2267             END;
2268
2269         call_number :=
2270             CASE
2271                 WHEN attr_def.call_number IS NULL THEN 'null()'
2272                 WHEN LENGTH( attr_def.call_number ) = 1 THEN '*[@code="' || attr_def.call_number || '"]'
2273                 ELSE '*' || attr_def.call_number
2274             END;
2275
2276         copy_number :=
2277             CASE
2278                 WHEN attr_def.copy_number IS NULL THEN 'null()'
2279                 WHEN LENGTH( attr_def.copy_number ) = 1 THEN '*[@code="' || attr_def.copy_number || '"]'
2280                 ELSE '*' || attr_def.copy_number
2281             END;
2282
2283         status :=
2284             CASE
2285                 WHEN attr_def.status IS NULL THEN 'null()'
2286                 WHEN LENGTH( attr_def.status ) = 1 THEN '*[@code="' || attr_def.status || '"]'
2287                 ELSE '*' || attr_def.status
2288             END;
2289
2290         location :=
2291             CASE
2292                 WHEN attr_def.location IS NULL THEN 'null()'
2293                 WHEN LENGTH( attr_def.location ) = 1 THEN '*[@code="' || attr_def.location || '"]'
2294                 ELSE '*' || attr_def.location
2295             END;
2296
2297         circulate :=
2298             CASE
2299                 WHEN attr_def.circulate IS NULL THEN 'null()'
2300                 WHEN LENGTH( attr_def.circulate ) = 1 THEN '*[@code="' || attr_def.circulate || '"]'
2301                 ELSE '*' || attr_def.circulate
2302             END;
2303
2304         deposit :=
2305             CASE
2306                 WHEN attr_def.deposit IS NULL THEN 'null()'
2307                 WHEN LENGTH( attr_def.deposit ) = 1 THEN '*[@code="' || attr_def.deposit || '"]'
2308                 ELSE '*' || attr_def.deposit
2309             END;
2310
2311         deposit_amount :=
2312             CASE
2313                 WHEN attr_def.deposit_amount IS NULL THEN 'null()'
2314                 WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '*[@code="' || attr_def.deposit_amount || '"]'
2315                 ELSE '*' || attr_def.deposit_amount
2316             END;
2317
2318         ref :=
2319             CASE
2320                 WHEN attr_def.ref IS NULL THEN 'null()'
2321                 WHEN LENGTH( attr_def.ref ) = 1 THEN '*[@code="' || attr_def.ref || '"]'
2322                 ELSE '*' || attr_def.ref
2323             END;
2324
2325         holdable :=
2326             CASE
2327                 WHEN attr_def.holdable IS NULL THEN 'null()'
2328                 WHEN LENGTH( attr_def.holdable ) = 1 THEN '*[@code="' || attr_def.holdable || '"]'
2329                 ELSE '*' || attr_def.holdable
2330             END;
2331
2332         price :=
2333             CASE
2334                 WHEN attr_def.price IS NULL THEN 'null()'
2335                 WHEN LENGTH( attr_def.price ) = 1 THEN '*[@code="' || attr_def.price || '"]'
2336                 ELSE '*' || attr_def.price
2337             END;
2338
2339         barcode :=
2340             CASE
2341                 WHEN attr_def.barcode IS NULL THEN 'null()'
2342                 WHEN LENGTH( attr_def.barcode ) = 1 THEN '*[@code="' || attr_def.barcode || '"]'
2343                 ELSE '*' || attr_def.barcode
2344             END;
2345
2346         circ_modifier :=
2347             CASE
2348                 WHEN attr_def.circ_modifier IS NULL THEN 'null()'
2349                 WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '*[@code="' || attr_def.circ_modifier || '"]'
2350                 ELSE '*' || attr_def.circ_modifier
2351             END;
2352
2353         circ_as_type :=
2354             CASE
2355                 WHEN attr_def.circ_as_type IS NULL THEN 'null()'
2356                 WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '*[@code="' || attr_def.circ_as_type || '"]'
2357                 ELSE '*' || attr_def.circ_as_type
2358             END;
2359
2360         alert_message :=
2361             CASE
2362                 WHEN attr_def.alert_message IS NULL THEN 'null()'
2363                 WHEN LENGTH( attr_def.alert_message ) = 1 THEN '*[@code="' || attr_def.alert_message || '"]'
2364                 ELSE '*' || attr_def.alert_message
2365             END;
2366
2367         opac_visible :=
2368             CASE
2369                 WHEN attr_def.opac_visible IS NULL THEN 'null()'
2370                 WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '*[@code="' || attr_def.opac_visible || '"]'
2371                 ELSE '*' || attr_def.opac_visible
2372             END;
2373
2374         pub_note :=
2375             CASE
2376                 WHEN attr_def.pub_note IS NULL THEN 'null()'
2377                 WHEN LENGTH( attr_def.pub_note ) = 1 THEN '*[@code="' || attr_def.pub_note || '"]'
2378                 ELSE '*' || attr_def.pub_note
2379             END;
2380         priv_note :=
2381             CASE
2382                 WHEN attr_def.priv_note IS NULL THEN 'null()'
2383                 WHEN LENGTH( attr_def.priv_note ) = 1 THEN '*[@code="' || attr_def.priv_note || '"]'
2384                 ELSE '*' || attr_def.priv_note
2385             END;
2386
2387         internal_id :=
2388             CASE
2389                 WHEN attr_def.internal_id IS NULL THEN 'null()'
2390                 WHEN LENGTH( attr_def.internal_id ) = 1 THEN '*[@code="' || attr_def.internal_id || '"]'
2391                 ELSE '*' || attr_def.internal_id
2392             END;
2393
2394         stat_cat_data :=
2395             CASE
2396                 WHEN attr_def.stat_cat_data IS NULL THEN 'null()'
2397                 WHEN LENGTH( attr_def.stat_cat_data ) = 1 THEN '*[@code="' || attr_def.stat_cat_data || '"]'
2398                 ELSE '*' || attr_def.stat_cat_data
2399             END;
2400
2401         parts_data :=
2402             CASE
2403                 WHEN attr_def.parts_data IS NULL THEN 'null()'
2404                 WHEN LENGTH( attr_def.parts_data ) = 1 THEN '*[@code="' || attr
2405                 ELSE '*' || attr_def.parts_data
2406             END;
2407
2408
2409
2410         xpaths := ARRAY[owning_lib, circ_lib, call_number, copy_number, status, location, circulate,
2411                         deposit, deposit_amount, ref, holdable, price, barcode, circ_modifier, circ_as_type,
2412                         alert_message, pub_note, priv_note, internal_id, stat_cat_data, parts_data, opac_visible];
2413
2414         FOR tmp_attr_set IN
2415                 SELECT  *
2416                   FROM  oils_xpath_tag_to_table( (SELECT marc FROM vandelay.queued_bib_record WHERE id = import_id), attr_def.tag, xpaths)
2417                             AS t( ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
2418                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
2419                                   circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, internal_id TEXT,
2420                                   stat_cat_data TEXT, parts_data TEXT, opac_vis TEXT )
2421         LOOP
2422
2423             attr_set.import_error := NULL;
2424             attr_set.error_detail := NULL;
2425             attr_set.deposit_amount := NULL;
2426             attr_set.copy_number := NULL;
2427             attr_set.price := NULL;
2428             attr_set.circ_modifier := NULL;
2429             attr_set.location := NULL;
2430             attr_set.barcode := NULL;
2431             attr_set.call_number := NULL;
2432
2433             IF tmp_attr_set.pr != '' THEN
2434                 tmp_str = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
2435                 IF tmp_str = '' THEN
2436                     attr_set.import_error := 'import.item.invalid.price';
2437                     attr_set.error_detail := tmp_attr_set.pr; -- original value
2438                     RETURN NEXT attr_set; CONTINUE;
2439                 END IF;
2440                 attr_set.price := tmp_str::NUMERIC(8,2);
2441             END IF;
2442
2443             IF tmp_attr_set.dep_amount != '' THEN
2444                 tmp_str = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
2445                 IF tmp_str = '' THEN
2446                     attr_set.import_error := 'import.item.invalid.deposit_amount';
2447                     attr_set.error_detail := tmp_attr_set.dep_amount;
2448                     RETURN NEXT attr_set; CONTINUE;
2449                 END IF;
2450                 attr_set.deposit_amount := tmp_str::NUMERIC(8,2);
2451             END IF;
2452
2453             IF tmp_attr_set.cnum != '' THEN
2454                 tmp_str = REGEXP_REPLACE(tmp_attr_set.cnum, E'[^0-9]', '', 'g');
2455                 IF tmp_str = '' THEN
2456                     attr_set.import_error := 'import.item.invalid.copy_number';
2457                     attr_set.error_detail := tmp_attr_set.cnum;
2458                     RETURN NEXT attr_set; CONTINUE;
2459                 END IF;
2460                 attr_set.copy_number := tmp_str::INT;
2461             END IF;
2462
2463             IF tmp_attr_set.ol != '' THEN
2464                 SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
2465                 IF NOT FOUND THEN
2466                     attr_set.import_error := 'import.item.invalid.owning_lib';
2467                     attr_set.error_detail := tmp_attr_set.ol;
2468                     RETURN NEXT attr_set; CONTINUE;
2469                 END IF;
2470             END IF;
2471
2472             IF tmp_attr_set.clib != '' THEN
2473                 SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
2474                 IF NOT FOUND THEN
2475                     attr_set.import_error := 'import.item.invalid.circ_lib';
2476                     attr_set.error_detail := tmp_attr_set.clib;
2477                     RETURN NEXT attr_set; CONTINUE;
2478                 END IF;
2479             END IF;
2480
2481             IF tmp_attr_set.cs != '' THEN
2482                 SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
2483                 IF NOT FOUND THEN
2484                     attr_set.import_error := 'import.item.invalid.status';
2485                     attr_set.error_detail := tmp_attr_set.cs;
2486                     RETURN NEXT attr_set; CONTINUE;
2487                 END IF;
2488             END IF;
2489
2490             IF COALESCE(tmp_attr_set.circ_mod, '') = '' THEN
2491
2492                 -- no circ mod defined, see if we should apply a default
2493                 SELECT INTO attr_set.circ_modifier TRIM(BOTH '"' FROM value)
2494                     FROM actor.org_unit_ancestor_setting(
2495                         'vandelay.item.circ_modifier.default',
2496                         attr_set.owning_lib
2497                     );
2498
2499                 -- make sure the value from the org setting is still valid
2500                 PERFORM 1 FROM config.circ_modifier WHERE code = attr_set.circ_modifier;
2501                 IF NOT FOUND THEN
2502                     attr_set.import_error := 'import.item.invalid.circ_modifier';
2503                     attr_set.error_detail := tmp_attr_set.circ_mod;
2504                     RETURN NEXT attr_set; CONTINUE;
2505                 END IF;
2506
2507             ELSE
2508
2509                 SELECT code INTO attr_set.circ_modifier FROM config.circ_modifier WHERE code = tmp_attr_set.circ_mod;
2510                 IF NOT FOUND THEN
2511                     attr_set.import_error := 'import.item.invalid.circ_modifier';
2512                     attr_set.error_detail := tmp_attr_set.circ_mod;
2513                     RETURN NEXT attr_set; CONTINUE;
2514                 END IF;
2515             END IF;
2516
2517             IF tmp_attr_set.circ_as != '' THEN
2518                 SELECT code INTO attr_set.circ_as_type FROM config.coded_value_map WHERE ctype = 'item_type' AND code = tmp_attr_set.circ_as;
2519                 IF NOT FOUND THEN
2520                     attr_set.import_error := 'import.item.invalid.circ_as_type';
2521                     attr_set.error_detail := tmp_attr_set.circ_as;
2522                     RETURN NEXT attr_set; CONTINUE;
2523                 END IF;
2524             END IF;
2525
2526             IF COALESCE(tmp_attr_set.cl, '') = '' THEN
2527                 -- no location specified, see if we should apply a default
2528
2529                 SELECT INTO attr_set.location TRIM(BOTH '"' FROM value)
2530                     FROM actor.org_unit_ancestor_setting(
2531                         'vandelay.item.copy_location.default',
2532                         attr_set.owning_lib
2533                     );
2534
2535                 -- make sure the value from the org setting is still valid
2536                 PERFORM 1 FROM asset.copy_location WHERE id = attr_set.location;
2537                 IF NOT FOUND THEN
2538                     attr_set.import_error := 'import.item.invalid.location';
2539                     attr_set.error_detail := tmp_attr_set.cs;
2540                     RETURN NEXT attr_set; CONTINUE;
2541                 END IF;
2542             ELSE
2543
2544                 -- search up the org unit tree for a matching copy location
2545                 WITH RECURSIVE anscestor_depth AS (
2546                     SELECT  ou.id,
2547                         out.depth AS depth,
2548                         ou.parent_ou
2549                     FROM  actor.org_unit ou
2550                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2551                     WHERE ou.id = COALESCE(attr_set.owning_lib, attr_set.circ_lib)
2552                         UNION ALL
2553                     SELECT  ou.id,
2554                         out.depth,
2555                         ou.parent_ou
2556                     FROM  actor.org_unit ou
2557                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2558                         JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
2559                 ) SELECT  cpl.id INTO attr_set.location
2560                     FROM  anscestor_depth a
2561                         JOIN asset.copy_location cpl ON (cpl.owning_lib = a.id)
2562                     WHERE LOWER(cpl.name) = LOWER(tmp_attr_set.cl)
2563                     ORDER BY a.depth DESC
2564                     LIMIT 1;
2565
2566                 IF NOT FOUND THEN
2567                     attr_set.import_error := 'import.item.invalid.location';
2568                     attr_set.error_detail := tmp_attr_set.cs;
2569                     RETURN NEXT attr_set; CONTINUE;
2570                 END IF;
2571             END IF;
2572
2573             attr_set.circulate      :=
2574                 LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
2575                 OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
2576
2577             attr_set.deposit        :=
2578                 LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
2579                 OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
2580
2581             attr_set.holdable       :=
2582                 LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
2583                 OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
2584
2585             attr_set.opac_visible   :=
2586                 LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
2587                 OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
2588
2589             attr_set.ref            :=
2590                 LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
2591                 OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
2592
2593             attr_set.call_number    := tmp_attr_set.cn; -- TEXT
2594             attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
2595             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
2596             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
2597             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
2598             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
2599             attr_set.internal_id    := tmp_attr_set.internal_id::BIGINT;
2600             attr_set.stat_cat_data  := tmp_attr_set.stat_cat_data; -- TEXT,
2601             attr_set.parts_data     := tmp_attr_set.parts_data; -- TEXT,
2602
2603             RETURN NEXT attr_set;
2604
2605         END LOOP;
2606
2607     END IF;
2608
2609     RETURN;
2610
2611 END;
2612 $$ LANGUAGE PLPGSQL;
2613
2614 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
2615 DECLARE
2616     attr_def    BIGINT;
2617     item_data   vandelay.import_item%ROWTYPE;
2618 BEGIN
2619
2620     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2621         RETURN NEW;
2622     END IF;
2623
2624     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
2625
2626     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
2627         INSERT INTO vandelay.import_item (
2628             record,
2629             definition,
2630             owning_lib,
2631             circ_lib,
2632             call_number,
2633             copy_number,
2634             status,
2635             location,
2636             circulate,
2637             deposit,
2638             deposit_amount,
2639             ref,
2640             holdable,
2641             price,
2642             barcode,
2643             circ_modifier,
2644             circ_as_type,
2645             alert_message,
2646             pub_note,
2647             priv_note,
2648             internal_id,
2649             opac_visible,
2650             stat_cat_data,
2651             parts_data,
2652             import_error,
2653             error_detail
2654         ) VALUES (
2655             NEW.id,
2656             item_data.definition,
2657             item_data.owning_lib,
2658             item_data.circ_lib,
2659             item_data.call_number,
2660             item_data.copy_number,
2661             item_data.status,
2662             item_data.location,
2663             item_data.circulate,
2664             item_data.deposit,
2665             item_data.deposit_amount,
2666             item_data.ref,
2667             item_data.holdable,
2668             item_data.price,
2669             item_data.barcode,
2670             item_data.circ_modifier,
2671             item_data.circ_as_type,
2672             item_data.alert_message,
2673             item_data.pub_note,
2674             item_data.priv_note,
2675             item_data.internal_id,
2676             item_data.opac_visible,
2677             item_data.stat_cat_data,
2678             item_data.parts_data,
2679             item_data.import_error,
2680             item_data.error_detail
2681         );
2682     END LOOP;
2683
2684     RETURN NULL;
2685 END;
2686 $func$ LANGUAGE PLPGSQL;
2687
2688 SELECT evergreen.upgrade_deps_block_check('0963', :eg_version);
2689
2690 ALTER TABLE config.z3950_index_field_map DROP CONSTRAINT "valid_z3950_attr_type";
2691
2692 DROP FUNCTION evergreen.z3950_attr_name_is_valid(text);
2693
2694 CREATE OR REPLACE FUNCTION evergreen.z3950_attr_name_is_valid() RETURNS TRIGGER AS $func$
2695 BEGIN
2696
2697   PERFORM * FROM config.z3950_attr WHERE name = NEW.z3950_attr_type;
2698
2699   IF FOUND THEN
2700     RETURN NULL;
2701   END IF;
2702
2703   RAISE EXCEPTION '% is not a valid Z39.50 attribute type', NEW.z3950_attr_type;
2704
2705 END;
2706 $func$ LANGUAGE PLPGSQL STABLE;
2707
2708 COMMENT ON FUNCTION evergreen.z3950_attr_name_is_valid() IS $$
2709 Used by a config.z3950_index_field_map constraint trigger
2710 to verify z3950_attr_type maps.
2711 $$;
2712
2713 CREATE CONSTRAINT TRIGGER valid_z3950_attr_type AFTER INSERT OR UPDATE ON config.z3950_index_field_map
2714   DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN (NEW.z3950_attr_type IS NOT NULL)
2715   EXECUTE PROCEDURE evergreen.z3950_attr_name_is_valid();
2716
2717 SELECT evergreen.upgrade_deps_block_check('0964', :eg_version);
2718
2719 INSERT INTO config.coded_value_map
2720     (id, ctype, code, opac_visible, value, search_label) VALUES
2721 (712, 'search_format', 'electronic', FALSE,
2722     oils_i18n_gettext(712, 'Electronic', 'ccvm', 'value'),
2723     oils_i18n_gettext(712, 'Electronic', 'ccvm', 'search_label'));
2724
2725 INSERT INTO config.composite_attr_entry_definition
2726     (coded_value, definition) VALUES
2727 (712, '[{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"o"}]');
2728
2729
2730 SELECT evergreen.upgrade_deps_block_check('0965', :eg_version);
2731
2732 UPDATE action_trigger.event_definition SET template =
2733 $$
2734 [%- USE date -%]
2735 [%- SET user = target.0.usr -%]
2736 [%- SET lib = target.0.circ_lib -%]
2737 [%- SET lib_addr = target.0.circ_lib.billing_address -%]
2738 [%- SET hours = lib.hours_of_operation -%]
2739 <div>
2740     <style> li { padding: 8px; margin 5px; }</style>
2741     <div>[% date.format %]</div>
2742     <div>[% lib.name %]</div>
2743     <div>[% lib_addr.street1 %] [% lib_addr.street2 %]</div>
2744     <div>[% lib_addr.city %], [% lib_addr.state %] [% lib_addr.post_code %]</div>
2745     <div>[% lib.phone %]</div>
2746     <br/>
2747
2748     [% user.family_name %], [% user.first_given_name %]
2749     <ol>
2750     [% FOR circ IN target %]
2751         [%-
2752             SET idx = loop.count - 1;
2753             SET udata =  user_data.$idx
2754         -%]
2755         <li>
2756             <div>[% helpers.get_copy_bib_basics(circ.target_copy.id).title %]</div>
2757             <div>Barcode: [% circ.target_copy.barcode %]</div>
2758             [% IF user_data.renewal_failure %]
2759                 <div style='color:red;'>Renewal Failed</div>
2760             [% ELSE %]
2761                 <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
2762             [% END %]
2763         </li>
2764     [% END %]
2765     </ol>
2766     
2767     <div>
2768         Library Hours
2769         [%- BLOCK format_time; date.format(time _ ' 1/1/1000', format='%I:%M %p'); END -%]
2770         <div>
2771             Monday 
2772             [% PROCESS format_time time = hours.dow_0_open %] 
2773             [% PROCESS format_time time = hours.dow_0_close %] 
2774         </div>
2775         <div>
2776             Tuesday 
2777             [% PROCESS format_time time = hours.dow_1_open %] 
2778             [% PROCESS format_time time = hours.dow_1_close %] 
2779         </div>
2780         <div>
2781             Wednesday 
2782             [% PROCESS format_time time = hours.dow_2_open %] 
2783             [% PROCESS format_time time = hours.dow_2_close %] 
2784         </div>
2785         <div>
2786             Thursday
2787             [% PROCESS format_time time = hours.dow_3_open %] 
2788             [% PROCESS format_time time = hours.dow_3_close %] 
2789         </div>
2790         <div>
2791             Friday
2792             [% PROCESS format_time time = hours.dow_4_open %] 
2793             [% PROCESS format_time time = hours.dow_4_close %] 
2794         </div>
2795         <div>
2796             Saturday
2797             [% PROCESS format_time time = hours.dow_5_open %] 
2798             [% PROCESS format_time time = hours.dow_5_close %] 
2799         </div>
2800         <div>
2801             Sunday 
2802             [% PROCESS format_time time = hours.dow_6_open %] 
2803             [% PROCESS format_time time = hours.dow_6_close %] 
2804         </div>
2805     </div>
2806 </div>
2807 $$
2808 WHERE id = 10 AND template =
2809 $$
2810 [%- USE date -%]
2811 [%- SET user = target.0.usr -%]
2812 [%- SET lib = target.0.circ_lib -%]
2813 [%- SET lib_addr = target.0.circ_lib.billing_address -%]
2814 [%- SET hours = lib.hours_of_operation -%]
2815 <div>
2816     <style> li { padding: 8px; margin 5px; }</style>
2817     <div>[% date.format %]</div>
2818     <div>[% lib.name %]</div>
2819     <div>[% lib_addr.street1 %] [% lib_addr.street2 %]</div>
2820     <div>[% lib_addr.city %], [% lib_addr.state %] [% lb_addr.post_code %]</div>
2821     <div>[% lib.phone %]</div>
2822     <br/>
2823
2824     [% user.family_name %], [% user.first_given_name %]
2825     <ol>
2826     [% FOR circ IN target %]
2827         [%-
2828             SET idx = loop.count - 1;
2829             SET udata =  user_data.$idx
2830         -%]
2831         <li>
2832             <div>[% helpers.get_copy_bib_basics(circ.target_copy.id).title %]</div>
2833             <div>Barcode: [% circ.target_copy.barcode %]</div>
2834             [% IF user_data.renewal_failure %]
2835                 <div style='color:red;'>Renewal Failed</div>
2836             [% ELSE %]
2837                 <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
2838             [% END %]
2839         </li>
2840     [% END %]
2841     </ol>
2842     
2843     <div>
2844         Library Hours
2845         [%- BLOCK format_time; date.format(time _ ' 1/1/1000', format='%I:%M %p'); END -%]
2846         <div>
2847             Monday 
2848             [% PROCESS format_time time = hours.dow_0_open %] 
2849             [% PROCESS format_time time = hours.dow_0_close %] 
2850         </div>
2851         <div>
2852             Tuesday 
2853             [% PROCESS format_time time = hours.dow_1_open %] 
2854             [% PROCESS format_time time = hours.dow_1_close %] 
2855         </div>
2856         <div>
2857             Wednesday 
2858             [% PROCESS format_time time = hours.dow_2_open %] 
2859             [% PROCESS format_time time = hours.dow_2_close %]