2 * Copyright (C) 2004-2008 Georgia Public Library Service
3 * Copyright (C) 2007-2008 Equinox Software, Inc.
4 * Mike Rylander <miker@esilibrary.com>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
18 DROP SCHEMA IF EXISTS action CASCADE;
24 CREATE TABLE action.in_house_use (
25 id SERIAL PRIMARY KEY,
26 item BIGINT NOT NULL, -- REFERENCES asset.copy (id) DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
27 staff INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
28 org_unit INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
29 use_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
31 CREATE INDEX action_in_house_use_staff_idx ON action.in_house_use ( staff );
33 CREATE TABLE action.non_cataloged_circulation (
34 id SERIAL PRIMARY KEY,
35 patron INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
36 staff INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
37 circ_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
38 item_type INT NOT NULL REFERENCES config.non_cataloged_type (id) DEFERRABLE INITIALLY DEFERRED,
39 circ_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
41 CREATE INDEX action_non_cat_circ_patron_idx ON action.non_cataloged_circulation ( patron );
42 CREATE INDEX action_non_cat_circ_staff_idx ON action.non_cataloged_circulation ( staff );
44 CREATE TABLE action.non_cat_in_house_use (
45 id SERIAL PRIMARY KEY,
46 item_type BIGINT NOT NULL REFERENCES config.non_cataloged_type(id) DEFERRABLE INITIALLY DEFERRED,
47 staff INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
48 org_unit INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
49 use_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
51 CREATE INDEX non_cat_in_house_use_staff_idx ON action.non_cat_in_house_use ( staff );
53 CREATE TABLE action.survey (
54 id SERIAL PRIMARY KEY,
55 owner INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
56 start_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
57 end_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + '10 years'::INTERVAL,
58 usr_summary BOOL NOT NULL DEFAULT FALSE,
59 opac BOOL NOT NULL DEFAULT FALSE,
60 poll BOOL NOT NULL DEFAULT FALSE,
61 required BOOL NOT NULL DEFAULT FALSE,
63 description TEXT NOT NULL
65 CREATE UNIQUE INDEX asv_once_per_owner_idx ON action.survey (owner,name);
67 CREATE TABLE action.survey_question (
68 id SERIAL PRIMARY KEY,
69 survey INT NOT NULL REFERENCES action.survey DEFERRABLE INITIALLY DEFERRED,
70 question TEXT NOT NULL
73 CREATE TABLE action.survey_answer (
74 id SERIAL PRIMARY KEY,
75 question INT NOT NULL REFERENCES action.survey_question DEFERRABLE INITIALLY DEFERRED,
79 CREATE SEQUENCE action.survey_response_group_id_seq;
81 CREATE TABLE action.survey_response (
82 id BIGSERIAL PRIMARY KEY,
83 response_group_id INT,
84 usr INT, -- REFERENCES actor.usr
85 survey INT NOT NULL REFERENCES action.survey DEFERRABLE INITIALLY DEFERRED,
86 question INT NOT NULL REFERENCES action.survey_question DEFERRABLE INITIALLY DEFERRED,
87 answer INT NOT NULL REFERENCES action.survey_answer DEFERRABLE INITIALLY DEFERRED,
88 answer_date TIMESTAMP WITH TIME ZONE,
89 effective_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
91 CREATE INDEX action_survey_response_usr_idx ON action.survey_response ( usr );
93 CREATE OR REPLACE FUNCTION action.survey_response_answer_date_fixup () RETURNS TRIGGER AS '
95 NEW.answer_date := NOW();
99 CREATE TRIGGER action_survey_response_answer_date_fixup_tgr
100 BEFORE INSERT ON action.survey_response
102 EXECUTE PROCEDURE action.survey_response_answer_date_fixup ();
104 CREATE TABLE action.archive_actor_stat_cat (
105 id BIGSERIAL PRIMARY KEY,
106 xact BIGINT NOT NULL, -- action.circulation (+aged/all)
107 stat_cat INT NOT NULL,
111 CREATE TABLE action.archive_asset_stat_cat (
112 id BIGSERIAL PRIMARY KEY,
113 xact BIGINT NOT NULL, -- action.circulation (+aged/all)
114 stat_cat INT NOT NULL,
119 CREATE TABLE action.circulation (
120 target_copy BIGINT NOT NULL, -- asset.copy.id
121 circ_lib INT NOT NULL, -- actor.org_unit.id
122 circ_staff INT NOT NULL, -- actor.usr.id
123 checkin_staff INT, -- actor.usr.id
124 checkin_lib INT, -- actor.org_unit.id
125 renewal_remaining INT NOT NULL, -- derived from "circ duration" rule
126 grace_period INTERVAL NOT NULL, -- derived from "circ fine" rule
127 due_date TIMESTAMP WITH TIME ZONE,
128 stop_fines_time TIMESTAMP WITH TIME ZONE,
129 checkin_time TIMESTAMP WITH TIME ZONE,
130 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
131 duration INTERVAL, -- derived from "circ duration" rule
132 fine_interval INTERVAL NOT NULL DEFAULT '1 day'::INTERVAL, -- derived from "circ fine" rule
133 recurring_fine NUMERIC(6,2), -- derived from "circ fine" rule
134 max_fine NUMERIC(6,2), -- derived from "max fine" rule
135 phone_renewal BOOL NOT NULL DEFAULT FALSE,
136 desk_renewal BOOL NOT NULL DEFAULT FALSE,
137 opac_renewal BOOL NOT NULL DEFAULT FALSE,
138 duration_rule TEXT NOT NULL, -- name of "circ duration" rule
139 recurring_fine_rule TEXT NOT NULL, -- name of "circ fine" rule
140 max_fine_rule TEXT NOT NULL, -- name of "max fine" rule
141 stop_fines TEXT CHECK (stop_fines IN (
142 'CHECKIN','CLAIMSRETURNED','LOST','MAXFINES','RENEW','LONGOVERDUE','CLAIMSNEVERCHECKEDOUT')),
143 workstation INT REFERENCES actor.workstation(id)
145 DEFERRABLE INITIALLY DEFERRED,
146 checkin_workstation INT REFERENCES actor.workstation(id)
148 DEFERRABLE INITIALLY DEFERRED,
149 copy_location INT NOT NULL DEFAULT 1 REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED,
150 checkin_scan_time TIMESTAMP WITH TIME ZONE,
151 auto_renewal BOOLEAN,
152 auto_renewal_remaining INTEGER
153 ) INHERITS (money.billable_xact);
154 ALTER TABLE action.circulation ADD PRIMARY KEY (id);
155 ALTER TABLE action.circulation
156 ADD COLUMN parent_circ BIGINT
157 REFERENCES action.circulation( id )
158 DEFERRABLE INITIALLY DEFERRED;
159 CREATE INDEX circ_open_xacts_idx ON action.circulation (usr) WHERE xact_finish IS NULL;
160 CREATE INDEX circ_outstanding_idx ON action.circulation (usr) WHERE checkin_time IS NULL;
161 CREATE INDEX circ_checkin_time ON "action".circulation (checkin_time) WHERE checkin_time IS NOT NULL;
162 CREATE INDEX circ_circ_lib_idx ON "action".circulation (circ_lib);
163 CREATE INDEX circ_open_date_idx ON "action".circulation (xact_start) WHERE xact_finish IS NULL;
164 CREATE INDEX circ_all_usr_idx ON action.circulation ( usr );
165 CREATE INDEX circ_circ_staff_idx ON action.circulation ( circ_staff );
166 CREATE INDEX circ_checkin_staff_idx ON action.circulation ( checkin_staff );
167 CREATE INDEX action_circulation_target_copy_idx ON action.circulation (target_copy);
168 CREATE UNIQUE INDEX circ_parent_idx ON action.circulation ( parent_circ ) WHERE parent_circ IS NOT NULL;
169 CREATE UNIQUE INDEX only_one_concurrent_checkout_per_copy ON action.circulation(target_copy) WHERE checkin_time IS NULL;
171 CREATE TRIGGER action_circulation_target_copy_trig AFTER INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
173 CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('circulation');
174 CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update ();
175 CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
177 CREATE OR REPLACE FUNCTION action.push_circ_due_time () RETURNS TRIGGER AS $$
179 proper_tz TEXT := COALESCE(
182 FROM actor.org_unit_ancestor_setting('lib.timezone',NEW.circ_lib)
185 CURRENT_SETTING('timezone')
189 IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 -- day-granular duration
190 AND SUBSTRING((NEW.due_date AT TIME ZONE proper_tz)::TIME::TEXT FROM 1 FOR 8) <> '23:59:59' THEN -- has not yet been pushed
191 NEW.due_date = ((NEW.due_date AT TIME ZONE proper_tz)::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL) || ' ' || proper_tz;
198 CREATE TRIGGER push_due_date_tgr BEFORE INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.push_circ_due_time();
200 CREATE OR REPLACE FUNCTION action.fill_circ_copy_location () RETURNS TRIGGER AS $$
202 SELECT INTO NEW.copy_location location FROM asset.copy WHERE id = NEW.target_copy;
207 CREATE TRIGGER fill_circ_copy_location_tgr BEFORE INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.fill_circ_copy_location();
209 CREATE OR REPLACE FUNCTION action.archive_stat_cats () RETURNS TRIGGER AS $$
211 INSERT INTO action.archive_actor_stat_cat(xact, stat_cat, value)
212 SELECT NEW.id, asceum.stat_cat, asceum.stat_cat_entry
213 FROM actor.stat_cat_entry_usr_map asceum
214 JOIN actor.stat_cat sc ON asceum.stat_cat = sc.id
215 WHERE NEW.usr = asceum.target_usr AND sc.checkout_archive;
216 INSERT INTO action.archive_asset_stat_cat(xact, stat_cat, value)
217 SELECT NEW.id, ascecm.stat_cat, asce.value
218 FROM asset.stat_cat_entry_copy_map ascecm
219 JOIN asset.stat_cat sc ON ascecm.stat_cat = sc.id
220 JOIN asset.stat_cat_entry asce ON ascecm.stat_cat_entry = asce.id
221 WHERE NEW.target_copy = ascecm.owning_copy AND sc.checkout_archive;
226 CREATE TRIGGER archive_stat_cats_tgr AFTER INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.archive_stat_cats();
228 CREATE TABLE action.aged_circulation (
230 usr_home_ou INT NOT NULL,
231 usr_profile INT NOT NULL,
233 copy_call_number INT NOT NULL,
234 copy_owning_lib INT NOT NULL,
235 copy_circ_lib INT NOT NULL,
236 copy_bib_record BIGINT NOT NULL,
237 LIKE action.circulation
240 ALTER TABLE action.aged_circulation ADD PRIMARY KEY (id);
241 ALTER TABLE action.aged_circulation DROP COLUMN usr;
242 CREATE INDEX aged_circ_circ_lib_idx ON "action".aged_circulation (circ_lib);
243 CREATE INDEX aged_circ_start_idx ON "action".aged_circulation (xact_start);
244 CREATE INDEX aged_circ_copy_circ_lib_idx ON "action".aged_circulation (copy_circ_lib);
245 CREATE INDEX aged_circ_copy_owning_lib_idx ON "action".aged_circulation (copy_owning_lib);
246 CREATE INDEX aged_circ_copy_location_idx ON "action".aged_circulation (copy_location);
247 CREATE INDEX action_aged_circulation_target_copy_idx ON action.aged_circulation (target_copy);
248 CREATE INDEX action_aged_circulation_parent_circ_idx ON action.aged_circulation (parent_circ);
250 CREATE OR REPLACE VIEW action.all_circulation AS
251 SELECT id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
252 copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
253 circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
254 stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
255 max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
256 max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ,
258 FROM action.aged_circulation
260 SELECT DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
261 cp.call_number AS copy_call_number, circ.copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
262 cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
263 circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
264 circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
265 circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
266 circ.parent_circ, circ.usr
267 FROM action.circulation circ
268 JOIN asset.copy cp ON (circ.target_copy = cp.id)
269 JOIN asset.call_number cn ON (cp.call_number = cn.id)
270 JOIN actor.usr p ON (circ.usr = p.id)
271 LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
272 LEFT JOIN actor.usr_address b ON (p.billing_address = b.id);
274 CREATE OR REPLACE VIEW action.all_circulation_slim AS
275 SELECT * FROM action.circulation
310 FROM action.aged_circulation
315 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
320 -- If there are any renewals for this circulation, don't archive or delete
321 -- it yet. We'll do so later, when we archive and delete the renewals.
323 SELECT 'Y' INTO found
324 FROM action.circulation
325 WHERE parent_circ = OLD.id
329 RETURN NULL; -- don't delete
332 -- Archive a copy of the old row to action.aged_circulation
334 INSERT INTO action.aged_circulation
335 (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
336 copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
337 circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
338 stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
339 max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
340 max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
342 id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
343 copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
344 circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
345 stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
346 max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
347 max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
348 FROM action.all_circulation WHERE id = OLD.id;
352 $$ LANGUAGE 'plpgsql';
354 CREATE TRIGGER action_circulation_aging_tgr
355 BEFORE DELETE ON action.circulation
357 EXECUTE PROCEDURE action.age_circ_on_delete ();
360 CREATE OR REPLACE FUNCTION action.age_parent_circ_on_delete () RETURNS TRIGGER AS $$
363 -- Having deleted a renewal, we can delete the original circulation (or a previous
364 -- renewal, if that's what parent_circ is pointing to). That deletion will trigger
365 -- deletion of any prior parents, etc. recursively.
367 IF OLD.parent_circ IS NOT NULL THEN
368 DELETE FROM action.circulation
369 WHERE id = OLD.parent_circ;
374 $$ LANGUAGE 'plpgsql';
376 CREATE TRIGGER age_parent_circ
377 AFTER DELETE ON action.circulation
379 EXECUTE PROCEDURE action.age_parent_circ_on_delete ();
382 CREATE OR REPLACE VIEW action.open_circulation AS
384 FROM action.circulation
385 WHERE checkin_time IS NULL
389 CREATE OR REPLACE VIEW action.billable_circulations AS
391 FROM action.circulation
392 WHERE xact_finish IS NULL;
394 CREATE OR REPLACE FUNCTION action.circulation_claims_returned () RETURNS TRIGGER AS $$
396 IF OLD.stop_fines IS NULL OR OLD.stop_fines <> NEW.stop_fines THEN
397 IF NEW.stop_fines = 'CLAIMSRETURNED' THEN
398 UPDATE actor.usr SET claims_returned_count = claims_returned_count + 1 WHERE id = NEW.usr;
400 IF NEW.stop_fines = 'CLAIMSNEVERCHECKEDOUT' THEN
401 UPDATE actor.usr SET claims_never_checked_out_count = claims_never_checked_out_count + 1 WHERE id = NEW.usr;
403 IF NEW.stop_fines = 'LOST' THEN
404 UPDATE asset.copy SET status = 3 WHERE id = NEW.target_copy;
409 $$ LANGUAGE 'plpgsql';
410 CREATE TRIGGER action_circulation_stop_fines_tgr
411 BEFORE UPDATE ON action.circulation
413 EXECUTE PROCEDURE action.circulation_claims_returned ();
415 CREATE TABLE action.hold_request_cancel_cause (
416 id SERIAL PRIMARY KEY,
420 CREATE TABLE action.hold_request (
421 id SERIAL PRIMARY KEY,
422 request_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
423 capture_time TIMESTAMP WITH TIME ZONE,
424 fulfillment_time TIMESTAMP WITH TIME ZONE,
425 checkin_time TIMESTAMP WITH TIME ZONE,
426 return_time TIMESTAMP WITH TIME ZONE,
427 prev_check_time TIMESTAMP WITH TIME ZONE,
428 expire_time TIMESTAMP WITH TIME ZONE,
429 cancel_time TIMESTAMP WITH TIME ZONE,
430 cancel_cause INT REFERENCES action.hold_request_cancel_cause (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
432 target BIGINT NOT NULL, -- see hold_type
433 current_copy BIGINT, -- REFERENCES asset.copy (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.unit now...
434 fulfillment_staff INT REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
435 fulfillment_lib INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
436 request_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
437 requestor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
438 usr INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
439 selection_ou INT NOT NULL,
440 selection_depth INT NOT NULL DEFAULT 0,
441 pickup_lib INT NOT NULL REFERENCES actor.org_unit DEFERRABLE INITIALLY DEFERRED,
442 hold_type TEXT NOT NULL, -- CHECK (hold_type IN ('M','T','V','C')), -- XXX constraint too constraining...
443 holdable_formats TEXT,
445 email_notify BOOL NOT NULL DEFAULT FALSE,
447 sms_carrier INT REFERENCES config.sms_carrier (id),
448 frozen BOOL NOT NULL DEFAULT FALSE,
449 thaw_date TIMESTAMP WITH TIME ZONE,
450 shelf_time TIMESTAMP WITH TIME ZONE,
452 mint_condition BOOL NOT NULL DEFAULT TRUE,
453 shelf_expire_time TIMESTAMPTZ,
454 current_shelf_lib INT REFERENCES actor.org_unit DEFERRABLE INITIALLY DEFERRED,
455 behind_desk BOOLEAN NOT NULL DEFAULT FALSE
457 ALTER TABLE action.hold_request ADD CONSTRAINT sms_check CHECK (
459 OR sms_carrier IS NOT NULL -- and implied sms_notify IS NOT NULL
463 CREATE OR REPLACE FUNCTION action.hold_request_clear_map () RETURNS TRIGGER AS $$
465 DELETE FROM action.hold_copy_map WHERE hold = NEW.id;
470 CREATE TRIGGER hold_request_clear_map_tgr
471 AFTER UPDATE ON action.hold_request
474 (NEW.cancel_time IS NOT NULL AND OLD.cancel_time IS NULL)
475 OR (NEW.fulfillment_time IS NOT NULL AND OLD.fulfillment_time IS NULL)
477 EXECUTE PROCEDURE action.hold_request_clear_map();
479 CREATE INDEX hold_request_target_idx ON action.hold_request (target);
480 CREATE INDEX hold_request_usr_idx ON action.hold_request (usr);
481 CREATE INDEX hold_request_pickup_lib_idx ON action.hold_request (pickup_lib);
482 CREATE INDEX hold_request_current_copy_idx ON action.hold_request (current_copy);
483 CREATE INDEX hold_request_prev_check_time_idx ON action.hold_request (prev_check_time);
484 CREATE INDEX hold_request_fulfillment_staff_idx ON action.hold_request ( fulfillment_staff );
485 CREATE INDEX hold_request_requestor_idx ON action.hold_request ( requestor );
486 CREATE INDEX hold_request_open_idx ON action.hold_request (id) WHERE cancel_time IS NULL AND fulfillment_time IS NULL;
487 CREATE INDEX hold_request_current_copy_before_cap_idx ON action.hold_request (current_copy) WHERE capture_time IS NULL AND cancel_time IS NULL;
488 CREATE UNIQUE INDEX hold_request_capture_protect_idx ON action.hold_request (current_copy) WHERE current_copy IS NOT NULL AND capture_time IS NOT NULL AND cancel_time IS NULL AND fulfillment_time IS NULL;
489 CREATE INDEX hold_request_copy_capture_time_idx ON action.hold_request (current_copy,capture_time);
490 CREATE INDEX hold_request_open_captured_shelf_lib_idx ON action.hold_request (current_shelf_lib) WHERE capture_time IS NOT NULL AND fulfillment_time IS NULL AND (pickup_lib <> current_shelf_lib);
491 CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
492 CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
495 CREATE TABLE action.hold_request_note (
497 id BIGSERIAL PRIMARY KEY,
498 hold BIGINT NOT NULL REFERENCES action.hold_request (id)
500 DEFERRABLE INITIALLY DEFERRED,
503 slip BOOL NOT NULL DEFAULT FALSE,
504 pub BOOL NOT NULL DEFAULT FALSE,
505 staff BOOL NOT NULL DEFAULT FALSE -- created by staff
508 CREATE INDEX ahrn_hold_idx ON action.hold_request_note (hold);
511 CREATE TABLE action.hold_notification (
512 id SERIAL PRIMARY KEY,
513 hold INT NOT NULL REFERENCES action.hold_request (id)
515 DEFERRABLE INITIALLY DEFERRED,
516 notify_staff INT REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
517 notify_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
518 method TEXT NOT NULL, -- email address or phone number
521 CREATE INDEX ahn_hold_idx ON action.hold_notification (hold);
522 CREATE INDEX ahn_notify_staff_idx ON action.hold_notification ( notify_staff );
524 CREATE TABLE action.hold_copy_map (
525 id BIGSERIAL PRIMARY KEY,
526 hold INT NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
527 target_copy BIGINT NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
529 CONSTRAINT copy_once_per_hold UNIQUE (hold,target_copy)
531 -- CREATE INDEX acm_hold_idx ON action.hold_copy_map (hold);
532 CREATE INDEX acm_copy_idx ON action.hold_copy_map (target_copy);
534 CREATE OR REPLACE FUNCTION
535 action.hold_request_regen_copy_maps(
536 hold_id INTEGER, copy_ids INTEGER[]) RETURNS VOID AS $$
537 DELETE FROM action.hold_copy_map WHERE hold = $1;
538 INSERT INTO action.hold_copy_map (hold, target_copy) SELECT $1, UNNEST($2);
541 CREATE TABLE action.transit_copy (
542 id SERIAL PRIMARY KEY,
543 source_send_time TIMESTAMP WITH TIME ZONE,
544 dest_recv_time TIMESTAMP WITH TIME ZONE,
545 target_copy BIGINT NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
546 source INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
547 dest INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
548 prev_hop INT REFERENCES action.transit_copy (id) DEFERRABLE INITIALLY DEFERRED,
549 copy_status INT NOT NULL REFERENCES config.copy_status (id) DEFERRABLE INITIALLY DEFERRED,
550 persistant_transfer BOOL NOT NULL DEFAULT FALSE,
551 prev_dest INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
552 cancel_time TIMESTAMP WITH TIME ZONE
554 CREATE INDEX active_transit_dest_idx ON "action".transit_copy (dest);
555 CREATE INDEX active_transit_source_idx ON "action".transit_copy (source);
556 CREATE INDEX active_transit_cp_idx ON "action".transit_copy (target_copy);
559 CREATE TABLE action.hold_transit_copy (
560 hold INT REFERENCES action.hold_request (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
561 ) INHERITS (action.transit_copy);
562 ALTER TABLE action.hold_transit_copy ADD PRIMARY KEY (id);
563 -- ALTER TABLE action.hold_transit_copy ADD CONSTRAINT ahtc_tc_fkey FOREIGN KEY (target_copy) REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; -- XXX could be an serial.issuance
564 CREATE INDEX active_hold_transit_dest_idx ON "action".hold_transit_copy (dest);
565 CREATE INDEX active_hold_transit_source_idx ON "action".hold_transit_copy (source);
566 CREATE INDEX active_hold_transit_cp_idx ON "action".hold_transit_copy (target_copy);
567 CREATE INDEX hold_transit_copy_hold_idx on action.hold_transit_copy (hold);
570 CREATE TABLE action.unfulfilled_hold_list (
571 id BIGSERIAL PRIMARY KEY,
572 current_copy BIGINT NOT NULL,
574 circ_lib INT NOT NULL,
575 fail_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
577 CREATE INDEX uhr_hold_idx ON action.unfulfilled_hold_list (hold);
579 CREATE OR REPLACE VIEW action.unfulfilled_hold_loops AS
583 FROM action.unfulfilled_hold_list u
584 JOIN asset.copy c ON (c.id = u.current_copy)
587 CREATE OR REPLACE VIEW action.unfulfilled_hold_min_loop AS
590 FROM action.unfulfilled_hold_loops
593 CREATE OR REPLACE VIEW action.unfulfilled_hold_innermost_loop AS
595 FROM action.unfulfilled_hold_loops l
596 JOIN action.unfulfilled_hold_min_loop m USING (hold)
597 WHERE l.count = m.min;
599 CREATE VIEW action.unfulfilled_hold_max_loop AS
602 FROM action.unfulfilled_hold_loops
606 CREATE TABLE action.aged_hold_request (
608 usr_home_ou INT NOT NULL,
609 usr_profile INT NOT NULL,
611 staff_placed BOOLEAN NOT NULL,
612 LIKE action.hold_request
614 ALTER TABLE action.aged_hold_request
615 ADD PRIMARY KEY (id),
617 DROP COLUMN requestor,
618 DROP COLUMN sms_carrier,
619 ALTER COLUMN phone_notify TYPE BOOLEAN
620 USING CASE WHEN phone_notify IS NULL OR phone_notify = '' THEN FALSE ELSE TRUE END,
621 ALTER COLUMN sms_notify TYPE BOOLEAN
622 USING CASE WHEN sms_notify IS NULL OR sms_notify = '' THEN FALSE ELSE TRUE END,
623 ALTER COLUMN phone_notify SET NOT NULL,
624 ALTER COLUMN sms_notify SET NOT NULL;
625 CREATE INDEX aged_hold_request_target_idx ON action.aged_hold_request (target);
626 CREATE INDEX aged_hold_request_pickup_lib_idx ON action.aged_hold_request (pickup_lib);
627 CREATE INDEX aged_hold_request_current_copy_idx ON action.aged_hold_request (current_copy);
628 CREATE INDEX aged_hold_request_fulfillment_staff_idx ON action.aged_hold_request ( fulfillment_staff );
630 CREATE OR REPLACE VIEW action.all_hold_request AS
632 COALESCE(a.post_code, b.post_code) AS usr_post_code,
633 p.home_ou AS usr_home_ou,
634 p.profile AS usr_profile,
635 EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
636 CAST(ahr.requestor <> ahr.usr AS BOOLEAN) AS staff_placed,
640 ahr.fulfillment_time,
650 ahr.fulfillment_staff,
657 ahr.holdable_formats,
659 WHEN ahr.phone_notify IS NULL THEN FALSE
660 WHEN ahr.phone_notify = '' THEN FALSE
665 WHEN ahr.sms_notify IS NULL THEN FALSE
666 WHEN ahr.sms_notify = '' THEN FALSE
674 ahr.shelf_expire_time,
675 ahr.current_shelf_lib,
677 FROM action.hold_request ahr
678 JOIN actor.usr p ON (ahr.usr = p.id)
679 LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
680 LEFT JOIN actor.usr_address b ON (p.billing_address = b.id)
720 FROM action.aged_hold_request;
722 CREATE OR REPLACE FUNCTION action.age_hold_on_delete () RETURNS TRIGGER AS $$
725 -- Archive a copy of the old row to action.aged_hold_request
727 INSERT INTO action.aged_hold_request
803 FROM action.all_hold_request WHERE id = OLD.id;
807 $$ LANGUAGE 'plpgsql';
809 CREATE TRIGGER action_hold_request_aging_tgr
810 BEFORE DELETE ON action.hold_request
812 EXECUTE PROCEDURE action.age_hold_on_delete ();
814 CREATE TABLE action.fieldset_group (
815 id SERIAL PRIMARY KEY,
817 create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
818 complete_time TIMESTAMPTZ,
819 container INT, -- Points to a container of some type ...
820 container_type TEXT, -- One of 'biblio_record_entry', 'user', 'call_number', 'copy'
821 can_rollback BOOL DEFAULT TRUE,
822 rollback_group INT REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
823 rollback_time TIMESTAMPTZ,
824 creator INT NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
825 owning_lib INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
828 CREATE TABLE action.fieldset (
829 id SERIAL PRIMARY KEY,
830 fieldset_group INT REFERENCES action.fieldset_group (id)
831 ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
832 owner INT NOT NULL REFERENCES actor.usr (id)
833 DEFERRABLE INITIALLY DEFERRED,
834 owning_lib INT NOT NULL REFERENCES actor.org_unit (id)
835 DEFERRABLE INITIALLY DEFERRED,
837 CONSTRAINT valid_status CHECK ( status in
838 ( 'PENDING', 'APPLIED', 'ERROR' )),
839 creation_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
840 scheduled_time TIMESTAMPTZ,
841 applied_time TIMESTAMPTZ,
842 classname TEXT NOT NULL, -- an IDL class name
845 stored_query INT REFERENCES query.stored_query (id)
846 DEFERRABLE INITIALLY DEFERRED,
848 CONSTRAINT lib_name_unique UNIQUE (owning_lib, name),
849 CONSTRAINT fieldset_one_or_the_other CHECK (
850 (stored_query IS NOT NULL AND pkey_value IS NULL) OR
851 (pkey_value IS NOT NULL AND stored_query IS NULL)
853 -- the CHECK constraint means we can update the fields for a single
854 -- row without all the extra overhead involved in a query
857 CREATE INDEX action_fieldset_sched_time_idx ON action.fieldset( scheduled_time );
858 CREATE INDEX action_owner_idx ON action.fieldset( owner );
861 CREATE TABLE action.fieldset_col_val (
862 id SERIAL PRIMARY KEY,
863 fieldset INT NOT NULL REFERENCES action.fieldset
865 DEFERRABLE INITIALLY DEFERRED,
866 col TEXT NOT NULL, -- "field" from the idl ... the column on the table
867 val TEXT, -- value for the column ... NULL means, well, NULL
868 CONSTRAINT fieldset_col_once_per_set UNIQUE (fieldset, col)
872 -- represents a circ chain summary
873 CREATE TYPE action.circ_chain_summary AS (
875 start_time TIMESTAMP WITH TIME ZONE,
876 checkout_workstation TEXT,
877 last_renewal_time TIMESTAMP WITH TIME ZONE, -- NULL if no renewals
878 last_stop_fines TEXT,
879 last_stop_fines_time TIMESTAMP WITH TIME ZONE,
880 last_renewal_workstation TEXT, -- NULL if no renewals
881 last_checkin_workstation TEXT,
882 last_checkin_time TIMESTAMP WITH TIME ZONE,
883 last_checkin_scan_time TIMESTAMP WITH TIME ZONE
887 CREATE OR REPLACE FUNCTION action.circ_chain ( ctx_circ_id BIGINT ) RETURNS SETOF action.circulation AS $$
889 tmp_circ action.circulation%ROWTYPE;
890 circ_0 action.circulation%ROWTYPE;
893 SELECT INTO tmp_circ * FROM action.circulation WHERE id = ctx_circ_id;
895 IF tmp_circ IS NULL THEN
896 RETURN NEXT tmp_circ;
900 -- find the front of the chain
902 SELECT INTO tmp_circ * FROM action.circulation WHERE id = tmp_circ.parent_circ;
903 IF tmp_circ IS NULL THEN
909 -- now send the circs to the caller, oldest to newest
912 IF tmp_circ IS NULL THEN
915 RETURN NEXT tmp_circ;
916 SELECT INTO tmp_circ * FROM action.circulation WHERE parent_circ = tmp_circ.id;
920 $$ LANGUAGE 'plpgsql';
922 CREATE OR REPLACE FUNCTION action.summarize_circ_chain ( ctx_circ_id BIGINT ) RETURNS action.circ_chain_summary AS $$
926 -- first circ in the chain
927 circ_0 action.circulation%ROWTYPE;
929 -- last circ in the chain
930 circ_n action.circulation%ROWTYPE;
932 -- circ chain under construction
933 chain action.circ_chain_summary;
934 tmp_circ action.circulation%ROWTYPE;
938 chain.num_circs := 0;
939 FOR tmp_circ IN SELECT * FROM action.circ_chain(ctx_circ_id) LOOP
941 IF chain.num_circs = 0 THEN
945 chain.num_circs := chain.num_circs + 1;
949 chain.start_time := circ_0.xact_start;
950 chain.last_stop_fines := circ_n.stop_fines;
951 chain.last_stop_fines_time := circ_n.stop_fines_time;
952 chain.last_checkin_time := circ_n.checkin_time;
953 chain.last_checkin_scan_time := circ_n.checkin_scan_time;
954 SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
955 SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
957 IF chain.num_circs > 1 THEN
958 chain.last_renewal_time := circ_n.xact_start;
959 SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
965 $$ LANGUAGE 'plpgsql';
967 -- same as action.circ_chain, but returns action.all_circulation
968 -- rows which may include aged circulations.
969 CREATE OR REPLACE FUNCTION action.all_circ_chain (ctx_circ_id INTEGER)
970 RETURNS SETOF action.all_circulation_slim AS $$
972 tmp_circ action.all_circulation_slim%ROWTYPE;
973 circ_0 action.all_circulation_slim%ROWTYPE;
976 SELECT INTO tmp_circ * FROM action.all_circulation_slim WHERE id = ctx_circ_id;
978 IF tmp_circ IS NULL THEN
979 RETURN NEXT tmp_circ;
983 -- find the front of the chain
985 SELECT INTO tmp_circ * FROM action.all_circulation_slim
986 WHERE id = tmp_circ.parent_circ;
987 IF tmp_circ IS NULL THEN
993 -- now send the circs to the caller, oldest to newest
996 IF tmp_circ IS NULL THEN
999 RETURN NEXT tmp_circ;
1000 SELECT INTO tmp_circ * FROM action.all_circulation_slim
1001 WHERE parent_circ = tmp_circ.id;
1005 $$ LANGUAGE 'plpgsql';
1007 -- same as action.summarize_circ_chain, but returns data collected
1008 -- from action.all_circulation, which may include aged circulations.
1009 CREATE OR REPLACE FUNCTION action.summarize_all_circ_chain
1010 (ctx_circ_id INTEGER) RETURNS action.circ_chain_summary AS $$
1014 -- first circ in the chain
1015 circ_0 action.all_circulation_slim%ROWTYPE;
1017 -- last circ in the chain
1018 circ_n action.all_circulation_slim%ROWTYPE;
1020 -- circ chain under construction
1021 chain action.circ_chain_summary;
1022 tmp_circ action.all_circulation_slim%ROWTYPE;
1026 chain.num_circs := 0;
1027 FOR tmp_circ IN SELECT * FROM action.all_circ_chain(ctx_circ_id) LOOP
1029 IF chain.num_circs = 0 THEN
1033 chain.num_circs := chain.num_circs + 1;
1037 chain.start_time := circ_0.xact_start;
1038 chain.last_stop_fines := circ_n.stop_fines;
1039 chain.last_stop_fines_time := circ_n.stop_fines_time;
1040 chain.last_checkin_time := circ_n.checkin_time;
1041 chain.last_checkin_scan_time := circ_n.checkin_scan_time;
1042 SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
1043 SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
1045 IF chain.num_circs > 1 THEN
1046 chain.last_renewal_time := circ_n.xact_start;
1047 SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
1053 $$ LANGUAGE 'plpgsql';
1055 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
1057 h action.hold_request%ROWTYPE;
1060 usr_view_count actor.usr_setting%ROWTYPE;
1061 usr_view_age actor.usr_setting%ROWTYPE;
1062 usr_view_start actor.usr_setting%ROWTYPE;
1064 SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
1065 SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
1066 SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
1070 FROM action.hold_request
1072 AND fulfillment_time IS NULL
1073 AND cancel_time IS NULL
1074 ORDER BY request_time DESC
1079 IF usr_view_start.value IS NULL THEN
1083 IF usr_view_age.value IS NOT NULL THEN
1084 -- User opted in and supplied a retention age
1085 IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
1086 view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
1088 view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
1092 view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
1095 IF usr_view_count.value IS NOT NULL THEN
1096 view_count := oils_json_to_text(usr_view_count.value)::INT;
1101 -- show some fulfilled/canceled holds
1104 FROM action.hold_request
1106 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
1107 AND COALESCE(fulfillment_time, cancel_time) > NOW() - view_age
1108 ORDER BY COALESCE(fulfillment_time, cancel_time) DESC
1116 $func$ LANGUAGE PLPGSQL;
1118 CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
1120 org_keep_age INTERVAL;
1121 org_use_last BOOL = false;
1122 org_age_is_min BOOL = false;
1128 circ_chain_head action.circulation%ROWTYPE;
1129 circ_chain_tail action.circulation%ROWTYPE;
1134 last_finished TIMESTAMP WITH TIME ZONE;
1139 SELECT value::INTERVAL INTO org_keep_age FROM config.global_flag WHERE name = 'history.circ.retention_age' AND enabled;
1141 SELECT value::INT INTO org_keep_count FROM config.global_flag WHERE name = 'history.circ.retention_count' AND enabled;
1142 IF org_keep_count IS NULL THEN
1143 RETURN count_purged; -- Gimme a count to keep, or I keep them all, forever
1146 SELECT enabled INTO org_use_last FROM config.global_flag WHERE name = 'history.circ.retention_uses_last_finished';
1147 SELECT enabled INTO org_age_is_min FROM config.global_flag WHERE name = 'history.circ.retention_age_is_min';
1149 -- First, find copies with more than keep_count non-renewal circs
1152 COUNT(*) AS total_real_circs
1153 FROM action.circulation
1154 WHERE parent_circ IS NULL
1155 AND xact_finish IS NOT NULL
1156 GROUP BY target_copy
1157 HAVING COUNT(*) > org_keep_count
1159 -- And, for those, select circs that are finished and older than keep_age
1160 FOR circ_chain_head IN
1161 -- For reference, the subquery uses a window function to order the circs newest to oldest and number them
1162 -- The outer query then uses that information to skip the most recent set the library wants to keep
1163 -- End result is we don't care what order they come out in, as they are all potentials for deletion.
1164 SELECT ac.* FROM action.circulation ac JOIN (
1165 SELECT rank() OVER (ORDER BY xact_start DESC), ac.id
1166 FROM action.circulation ac
1167 WHERE ac.target_copy = target_acp.target_copy
1168 AND ac.parent_circ IS NULL
1169 ORDER BY ac.xact_start ) ranked USING (id)
1170 WHERE ranked.rank > org_keep_count
1173 SELECT * INTO circ_chain_tail FROM action.circ_chain(circ_chain_head.id) ORDER BY xact_start DESC LIMIT 1;
1174 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);
1175 CONTINUE WHEN circ_chain_tail.xact_finish IS NULL OR num_incomplete > 0;
1177 IF NOT org_use_last THEN
1178 last_finished := circ_chain_tail.xact_finish;
1181 keep_age := COALESCE( org_keep_age, '2000 years'::INTERVAL );
1183 IF org_age_is_min THEN
1184 keep_age := GREATEST( keep_age, org_keep_age );
1187 CONTINUE WHEN AGE(NOW(), last_finished) < keep_age;
1189 -- We've passed the purging tests, purge the circ chain starting at the end
1190 -- A trigger should auto-purge the rest of the chain.
1191 DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
1193 count_purged := count_purged + 1;
1198 return count_purged;
1200 $func$ LANGUAGE PLPGSQL;
1202 CREATE OR REPLACE FUNCTION action.purge_holds() RETURNS INT AS $func$
1204 current_hold RECORD;
1210 user_start TIMESTAMPTZ;
1215 SELECT INTO cgf_d value::INTERVAL FROM config.global_flag WHERE name = 'history.hold.retention_age' AND enabled;
1216 SELECT INTO cgf_f value::INTERVAL FROM config.global_flag WHERE name = 'history.hold.retention_age_fulfilled' AND enabled;
1217 SELECT INTO cgf_c value::INTERVAL FROM config.global_flag WHERE name = 'history.hold.retention_age_canceled' AND enabled;
1220 rank() OVER (PARTITION BY usr ORDER BY COALESCE(fulfillment_time, cancel_time) DESC),
1221 cgf_cs.value::INTERVAL as cgf_cs,
1224 action.hold_request ahr
1225 LEFT JOIN config.global_flag cgf_cs ON (ahr.cancel_cause IS NOT NULL AND cgf_cs.name = 'history.hold.retention_age_canceled_' || ahr.cancel_cause AND cgf_cs.enabled)
1227 (fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL)
1229 IF prev_usr IS NULL OR prev_usr != current_hold.usr THEN
1230 prev_usr := current_hold.usr;
1231 SELECT INTO user_start oils_json_to_text(value)::TIMESTAMPTZ FROM actor.usr_setting WHERE usr = prev_usr AND name = 'history.hold.retention_start';
1232 SELECT INTO user_age oils_json_to_text(value)::INTERVAL FROM actor.usr_setting WHERE usr = prev_usr AND name = 'history.hold.retention_age';
1233 SELECT INTO user_count oils_json_to_text(value)::INT FROM actor.usr_setting WHERE usr = prev_usr AND name = 'history.hold.retention_count';
1234 IF user_start IS NOT NULL THEN
1235 user_age := LEAST(user_age, AGE(NOW(), user_start));
1237 IF user_count IS NULL THEN
1238 user_count := 1000; -- Assumption based on the user visible holds routine
1241 -- Library keep age trumps user keep anything, for purposes of being able to hold on to things when staff canceled and such.
1242 IF current_hold.fulfillment_time IS NOT NULL AND current_hold.fulfillment_time > NOW() - COALESCE(cgf_f, cgf_d) THEN
1245 IF current_hold.cancel_time IS NOT NULL AND current_hold.cancel_time > NOW() - COALESCE(current_hold.cgf_cs, cgf_c, cgf_d) THEN
1249 -- User keep age needs combining with count. If too old AND within the count, keep!
1250 IF user_start IS NOT NULL AND COALESCE(current_hold.fulfillment_time, current_hold.cancel_time) > NOW() - user_age AND current_hold.rank <= user_count THEN
1254 -- All checks should have passed, delete!
1255 DELETE FROM action.hold_request WHERE id = current_hold.id;
1256 purged_holds := purged_holds + 1;
1258 RETURN purged_holds;
1260 $func$ LANGUAGE plpgsql;
1262 CREATE OR REPLACE FUNCTION action.apply_fieldset(
1263 fieldset_id IN INT, -- id from action.fieldset
1264 table_name IN TEXT, -- table to be updated
1265 pkey_name IN TEXT, -- name of primary key column in that table
1266 query IN TEXT -- query constructed by qstore (for query-based
1267 -- fieldsets only; otherwise null
1283 fs_obj action.fieldset%ROWTYPE;
1284 fs_group action.fieldset_group%ROWTYPE;
1288 IF fieldset_id IS NULL THEN
1289 RETURN 'Fieldset ID parameter is NULL';
1291 IF table_name IS NULL THEN
1292 RETURN 'Table name parameter is NULL';
1294 IF pkey_name IS NULL THEN
1295 RETURN 'Primary key name parameter is NULL';
1300 quote_literal( pkey_value )
1310 -- Build the WHERE clause. This differs according to whether it's a
1311 -- single-row fieldset or a query-based fieldset.
1313 IF query IS NULL AND fs_pkey_value IS NULL THEN
1314 RETURN 'Incomplete fieldset: neither a primary key nor a query available';
1315 ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
1316 fs_query := rtrim( query, ';' );
1317 where_clause := 'WHERE ' || pkey_name || ' IN ( '
1318 || fs_query || ' )';
1319 ELSIF query IS NULL AND fs_pkey_value IS NOT NULL THEN
1320 where_clause := 'WHERE ' || pkey_name || ' = ';
1321 IF pkey_name = 'id' THEN
1322 where_clause := where_clause || fs_pkey_value;
1323 ELSIF pkey_name = 'code' THEN
1324 where_clause := where_clause || quote_literal(fs_pkey_value);
1326 RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
1328 ELSE -- both are not null
1329 RETURN 'Ambiguous fieldset: both a primary key and a query provided';
1332 IF fs_status IS NULL THEN
1333 RETURN 'No fieldset found for id = ' || fieldset_id;
1334 ELSIF fs_status = 'APPLIED' THEN
1335 RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
1338 SELECT * INTO fs_obj FROM action.fieldset WHERE id = fieldset_id;
1339 SELECT * INTO fs_group FROM action.fieldset_group WHERE id = fs_obj.fieldset_group;
1341 IF fs_group.can_rollback THEN
1342 -- This is part of a non-rollback group. We need to record the current values for future rollback.
1344 INSERT INTO action.fieldset_group (can_rollback, name, creator, owning_lib, container, container_type)
1345 VALUES (FALSE, 'ROLLBACK: '|| fs_group.name, fs_group.creator, fs_group.owning_lib, fs_group.container, fs_group.container_type);
1347 fsg_id := CURRVAL('action.fieldset_group_id_seq');
1349 FOR rb_row IN EXECUTE 'SELECT * FROM ' || table_name || ' ' || where_clause LOOP
1350 IF pkey_name = 'id' THEN
1351 fs_pkey_value := rb_row.id;
1352 ELSIF pkey_name = 'code' THEN
1353 fs_pkey_value := rb_row.code;
1355 RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
1357 INSERT INTO action.fieldset (fieldset_group,owner,owning_lib,status,classname,name,pkey_value)
1358 VALUES (fsg_id, fs_obj.owner, fs_obj.owning_lib, 'PENDING', fs_obj.classname, fs_obj.name || ' ROLLBACK FOR ' || fs_pkey_value, fs_pkey_value);
1360 fs_id := CURRVAL('action.fieldset_id_seq');
1364 FROM action.fieldset_col_val
1365 WHERE fieldset = fieldset_id
1367 EXECUTE 'INSERT INTO action.fieldset_col_val (fieldset, col, val) ' ||
1368 'SELECT '|| fs_id || ', '||quote_literal(cv.col)||', '||cv.col||' FROM '||table_name||' WHERE '||pkey_name||' = '||fs_pkey_value;
1373 statement := 'UPDATE ' || table_name || ' SET';
1379 FROM action.fieldset_col_val
1380 WHERE fieldset = fieldset_id
1382 statement := statement || sep || ' ' || cv.col
1383 || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
1388 RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
1390 statement := statement || ' ' || where_clause;
1393 -- Execute the update
1397 GET DIAGNOSTICS update_count = ROW_COUNT;
1399 IF update_count = 0 THEN
1400 RAISE data_exception;
1403 IF fsg_id IS NOT NULL THEN
1404 UPDATE action.fieldset_group SET rollback_group = fsg_id WHERE id = fs_group.id;
1407 IF fs_group.id IS NOT NULL THEN
1408 UPDATE action.fieldset_group SET complete_time = now() WHERE id = fs_group.id;
1411 UPDATE action.fieldset SET status = 'APPLIED', applied_time = now() WHERE id = fieldset_id;
1413 EXCEPTION WHEN data_exception THEN
1414 msg := 'No eligible rows found for fieldset ' || fieldset_id;
1415 UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
1422 EXCEPTION WHEN OTHERS THEN
1423 msg := 'Unable to apply fieldset ' || fieldset_id || ': ' || sqlerrm;
1424 UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
1428 $$ LANGUAGE plpgsql;
1430 COMMENT ON FUNCTION action.apply_fieldset( INT, TEXT, TEXT, TEXT ) IS $$
1431 Applies a specified fieldset, using a supplied table name and primary
1432 key name. The query parameter should be non-null only for
1433 query-based fieldsets.
1435 Returns NULL if successful, or an error message if not.
1438 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(
1441 copy_context_ou INT DEFAULT NULL
1442 -- TODO maybe? hold_context_ou INT DEFAULT NULL. This would optionally
1443 -- support an "ahprox" measurement: adjust prox between copy circ lib and
1444 -- hold request lib, but I'm unsure whether to use this theoretical
1445 -- argument only in the baseline calculation or later in the other
1446 -- queries in this function.
1447 ) RETURNS NUMERIC AS $f$
1449 aoupa actor.org_unit_proximity_adjustment%ROWTYPE;
1450 ahr action.hold_request%ROWTYPE;
1451 acp asset.copy%ROWTYPE;
1452 acn asset.call_number%ROWTYPE;
1453 acl asset.copy_location%ROWTYPE;
1454 baseline_prox NUMERIC;
1464 SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id;
1465 SELECT * INTO acp FROM asset.copy WHERE id = acp_id;
1466 SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
1467 SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
1469 IF copy_context_ou IS NULL THEN
1470 copy_context_ou := acp.circ_lib;
1473 -- First, gather the baseline proximity of "here" to pickup lib
1474 SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = copy_context_ou AND to_org = ahr.pickup_lib;
1476 -- Find any absolute adjustments, and set the baseline prox to that
1477 SELECT adj.* INTO aoupa
1478 FROM actor.org_unit_proximity_adjustment adj
1479 LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
1480 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
1481 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acl_ol.id = adj.copy_location)
1482 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
1483 LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
1484 WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
1485 (adj.item_circ_lib IS NULL OR adj.item_circ_lib = acp_cl.id) AND
1486 (adj.item_owning_lib IS NULL OR adj.item_owning_lib = acn_ol.id) AND
1487 (adj.copy_location IS NULL OR adj.copy_location = acl_ol.id) AND
1488 (adj.hold_pickup_lib IS NULL OR adj.hold_pickup_lib = ahr_pl.id) AND
1489 (adj.hold_request_lib IS NULL OR adj.hold_request_lib = ahr_rl.id) AND
1490 absolute_adjustment AND
1491 COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
1493 COALESCE(acp_cl.distance,999)
1494 + COALESCE(acn_ol.distance,999)
1495 + COALESCE(acl_ol.distance,999)
1496 + COALESCE(ahr_pl.distance,999)
1497 + COALESCE(ahr_rl.distance,999),
1502 baseline_prox := aoupa.prox_adjustment;
1505 -- Now find any relative adjustments, and change the baseline prox based on them
1508 FROM actor.org_unit_proximity_adjustment adj
1509 LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
1510 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
1511 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
1512 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
1513 LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
1514 WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
1515 (adj.item_circ_lib IS NULL OR adj.item_circ_lib = acp_cl.id) AND
1516 (adj.item_owning_lib IS NULL OR adj.item_owning_lib = acn_ol.id) AND
1517 (adj.copy_location IS NULL OR adj.copy_location = acl_ol.id) AND
1518 (adj.hold_pickup_lib IS NULL OR adj.hold_pickup_lib = ahr_pl.id) AND
1519 (adj.hold_request_lib IS NULL OR adj.hold_request_lib = ahr_rl.id) AND
1520 NOT absolute_adjustment AND
1521 COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
1523 baseline_prox := baseline_prox + aoupa.prox_adjustment;
1526 RETURN baseline_prox;
1528 $f$ LANGUAGE PLPGSQL;
1530 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity_update () RETURNS TRIGGER AS $f$
1532 NEW.proximity := action.hold_copy_calculated_proximity(NEW.hold,NEW.target_copy);
1535 $f$ LANGUAGE PLPGSQL;
1537 CREATE TRIGGER hold_copy_proximity_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_proximity_update ();
1539 CREATE TABLE action.usr_circ_history (
1540 id BIGSERIAL PRIMARY KEY,
1541 usr INTEGER NOT NULL REFERENCES actor.usr(id)
1542 DEFERRABLE INITIALLY DEFERRED,
1543 xact_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1544 target_copy BIGINT NOT NULL, -- asset.copy.id / serial.unit.id
1545 due_date TIMESTAMP WITH TIME ZONE NOT NULL,
1546 checkin_time TIMESTAMP WITH TIME ZONE,
1547 source_circ BIGINT REFERENCES action.circulation(id)
1548 ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
1551 CREATE INDEX action_usr_circ_history_usr_idx ON action.usr_circ_history ( usr );
1552 CREATE INDEX action_usr_circ_history_source_circ_idx ON action.usr_circ_history ( source_circ );
1554 CREATE TRIGGER action_usr_circ_history_target_copy_trig
1555 AFTER INSERT OR UPDATE ON action.usr_circ_history
1556 FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
1558 CREATE OR REPLACE FUNCTION action.maintain_usr_circ_history()
1559 RETURNS TRIGGER AS $FUNK$
1565 -- Any retention value signifies history is enabled.
1566 -- This assumes that clearing these values via external
1567 -- process deletes the action.usr_circ_history rows.
1568 -- TODO: replace these settings w/ a single bool setting?
1569 PERFORM 1 FROM actor.usr_setting
1570 WHERE usr = NEW.usr AND value IS NOT NULL AND name IN (
1571 'history.circ.retention_age',
1572 'history.circ.retention_start'
1579 IF TG_OP = 'INSERT' AND NEW.parent_circ IS NULL THEN
1580 -- Starting a new circulation. Insert the history row.
1581 INSERT INTO action.usr_circ_history
1582 (usr, xact_start, target_copy, due_date, source_circ)
1594 -- find the first and last circs in the circ chain
1595 -- for the currently modified circ.
1596 FOR cur_circ IN SELECT id FROM action.circ_chain(NEW.id) LOOP
1597 IF first_circ IS NULL THEN
1598 first_circ := cur_circ;
1601 -- Allow the loop to continue so that at as the loop
1602 -- completes cur_circ points to the final circulation.
1605 IF NEW.id <> cur_circ THEN
1606 -- Modifying an intermediate circ. Ignore it.
1610 -- Update the due_date/checkin_time on the history row if the current
1611 -- circ is the last circ in the chain and an update is warranted.
1613 UPDATE action.usr_circ_history
1615 due_date = NEW.due_date,
1616 checkin_time = NEW.checkin_time
1618 source_circ = first_circ
1620 due_date <> NEW.due_date OR (
1621 (checkin_time IS NULL AND NEW.checkin_time IS NOT NULL) OR
1622 (checkin_time IS NOT NULL AND NEW.checkin_time IS NULL) OR
1623 (checkin_time <> NEW.checkin_time)
1628 $FUNK$ LANGUAGE PLPGSQL;
1630 CREATE TRIGGER maintain_usr_circ_history_tgr
1631 AFTER INSERT OR UPDATE ON action.circulation
1632 FOR EACH ROW EXECUTE PROCEDURE action.maintain_usr_circ_history();
1634 CREATE OR REPLACE VIEW action.all_circulation_combined_types AS
1635 SELECT acirc.id AS id,
1640 ac_acirc.circ_modifier AS item_type,
1641 'regular_circ'::text AS circ_type
1642 FROM action.circulation acirc,
1644 WHERE acirc.target_copy = ac_acirc.id
1646 SELECT ancc.id::BIGINT AS id,
1647 ancc.circ_time AS xact_start,
1649 ancc.staff AS circ_staff,
1650 ancc.circ_time AS create_time,
1651 cnct_ancc.name AS item_type,
1652 'non-cat_circ'::text AS circ_type
1653 FROM action.non_cataloged_circulation ancc,
1654 config.non_cataloged_type cnct_ancc
1655 WHERE ancc.item_type = cnct_ancc.id
1657 SELECT aihu.id::BIGINT AS id,
1658 aihu.use_time AS xact_start,
1659 aihu.org_unit AS circ_lib,
1660 aihu.staff AS circ_staff,
1661 aihu.use_time AS create_time,
1662 ac_aihu.circ_modifier AS item_type,
1663 'in-house_use'::text AS circ_type
1664 FROM action.in_house_use aihu,
1666 WHERE aihu.item = ac_aihu.id
1668 SELECT ancihu.id::BIGINT AS id,
1669 ancihu.use_time AS xact_start,
1670 ancihu.org_unit AS circ_lib,
1671 ancihu.staff AS circ_staff,
1672 ancihu.use_time AS create_time,
1673 cnct_ancihu.name AS item_type,
1674 'non-cat_circ'::text AS circ_type
1675 FROM action.non_cat_in_house_use ancihu,
1676 config.non_cataloged_type cnct_ancihu
1677 WHERE ancihu.item_type = cnct_ancihu.id
1679 SELECT aacirc.id AS id,
1684 ac_aacirc.circ_modifier AS item_type,
1685 'aged_circ'::text AS circ_type
1686 FROM action.aged_circulation aacirc,
1687 asset.copy ac_aacirc
1688 WHERE aacirc.target_copy = ac_aacirc.id;