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 ) INHERITS (money.billable_xact);
152 ALTER TABLE action.circulation ADD PRIMARY KEY (id);
153 ALTER TABLE action.circulation
154 ADD COLUMN parent_circ BIGINT
155 REFERENCES action.circulation( id )
156 DEFERRABLE INITIALLY DEFERRED;
157 CREATE INDEX circ_open_xacts_idx ON action.circulation (usr) WHERE xact_finish IS NULL;
158 CREATE INDEX circ_outstanding_idx ON action.circulation (usr) WHERE checkin_time IS NULL;
159 CREATE INDEX circ_checkin_time ON "action".circulation (checkin_time) WHERE checkin_time IS NOT NULL;
160 CREATE INDEX circ_circ_lib_idx ON "action".circulation (circ_lib);
161 CREATE INDEX circ_open_date_idx ON "action".circulation (xact_start) WHERE xact_finish IS NULL;
162 CREATE INDEX circ_all_usr_idx ON action.circulation ( usr );
163 CREATE INDEX circ_circ_staff_idx ON action.circulation ( circ_staff );
164 CREATE INDEX circ_checkin_staff_idx ON action.circulation ( checkin_staff );
165 CREATE INDEX action_circulation_target_copy_idx ON action.circulation (target_copy);
166 CREATE UNIQUE INDEX circ_parent_idx ON action.circulation ( parent_circ ) WHERE parent_circ IS NOT NULL;
167 CREATE UNIQUE INDEX only_one_concurrent_checkout_per_copy ON action.circulation(target_copy) WHERE checkin_time IS NULL;
169 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');
171 CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('circulation');
172 CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update ();
173 CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
175 CREATE OR REPLACE FUNCTION action.push_circ_due_time () RETURNS TRIGGER AS $$
177 IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 THEN
178 NEW.due_date = (NEW.due_date::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL)::TIMESTAMPTZ;
185 CREATE TRIGGER push_due_date_tgr BEFORE INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.push_circ_due_time();
187 CREATE OR REPLACE FUNCTION action.fill_circ_copy_location () RETURNS TRIGGER AS $$
189 SELECT INTO NEW.copy_location location FROM asset.copy WHERE id = NEW.target_copy;
194 CREATE TRIGGER fill_circ_copy_location_tgr BEFORE INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.fill_circ_copy_location();
196 CREATE OR REPLACE FUNCTION action.archive_stat_cats () RETURNS TRIGGER AS $$
198 INSERT INTO action.archive_actor_stat_cat(xact, stat_cat, value)
199 SELECT NEW.id, asceum.stat_cat, asceum.stat_cat_entry
200 FROM actor.stat_cat_entry_usr_map asceum
201 JOIN actor.stat_cat sc ON asceum.stat_cat = sc.id
202 WHERE NEW.usr = asceum.target_usr AND sc.checkout_archive;
203 INSERT INTO action.archive_asset_stat_cat(xact, stat_cat, value)
204 SELECT NEW.id, ascecm.stat_cat, asce.value
205 FROM asset.stat_cat_entry_copy_map ascecm
206 JOIN asset.stat_cat sc ON ascecm.stat_cat = sc.id
207 JOIN asset.stat_cat_entry asce ON ascecm.stat_cat_entry = asce.id
208 WHERE NEW.target_copy = ascecm.owning_copy AND sc.checkout_archive;
213 CREATE TRIGGER archive_stat_cats_tgr AFTER INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.archive_stat_cats();
215 CREATE TABLE action.aged_circulation (
217 usr_home_ou INT NOT NULL,
218 usr_profile INT NOT NULL,
220 copy_call_number INT NOT NULL,
221 copy_owning_lib INT NOT NULL,
222 copy_circ_lib INT NOT NULL,
223 copy_bib_record BIGINT NOT NULL,
224 LIKE action.circulation
227 ALTER TABLE action.aged_circulation ADD PRIMARY KEY (id);
228 ALTER TABLE action.aged_circulation DROP COLUMN usr;
229 CREATE INDEX aged_circ_circ_lib_idx ON "action".aged_circulation (circ_lib);
230 CREATE INDEX aged_circ_start_idx ON "action".aged_circulation (xact_start);
231 CREATE INDEX aged_circ_copy_circ_lib_idx ON "action".aged_circulation (copy_circ_lib);
232 CREATE INDEX aged_circ_copy_owning_lib_idx ON "action".aged_circulation (copy_owning_lib);
233 CREATE INDEX aged_circ_copy_location_idx ON "action".aged_circulation (copy_location);
234 CREATE INDEX action_aged_circulation_target_copy_idx ON action.aged_circulation (target_copy);
236 CREATE OR REPLACE VIEW action.all_circulation AS
237 SELECT id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
238 copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
239 circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
240 stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
241 max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
242 max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ,
244 FROM action.aged_circulation
246 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,
247 cp.call_number AS copy_call_number, circ.copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
248 cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
249 circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
250 circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
251 circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
252 circ.parent_circ, circ.usr
253 FROM action.circulation circ
254 JOIN asset.copy cp ON (circ.target_copy = cp.id)
255 JOIN asset.call_number cn ON (cp.call_number = cn.id)
256 JOIN actor.usr p ON (circ.usr = p.id)
257 LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
258 LEFT JOIN actor.usr_address b ON (p.billing_address = b.id);
260 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
265 -- If there are any renewals for this circulation, don't archive or delete
266 -- it yet. We'll do so later, when we archive and delete the renewals.
268 SELECT 'Y' INTO found
269 FROM action.circulation
270 WHERE parent_circ = OLD.id
274 RETURN NULL; -- don't delete
277 -- Archive a copy of the old row to action.aged_circulation
279 INSERT INTO action.aged_circulation
280 (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
281 copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
282 circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
283 stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
284 max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
285 max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
287 id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
288 copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
289 circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
290 stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
291 max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
292 max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
293 FROM action.all_circulation WHERE id = OLD.id;
297 $$ LANGUAGE 'plpgsql';
299 CREATE TRIGGER action_circulation_aging_tgr
300 BEFORE DELETE ON action.circulation
302 EXECUTE PROCEDURE action.age_circ_on_delete ();
305 CREATE OR REPLACE FUNCTION action.age_parent_circ_on_delete () RETURNS TRIGGER AS $$
308 -- Having deleted a renewal, we can delete the original circulation (or a previous
309 -- renewal, if that's what parent_circ is pointing to). That deletion will trigger
310 -- deletion of any prior parents, etc. recursively.
312 IF OLD.parent_circ IS NOT NULL THEN
313 DELETE FROM action.circulation
314 WHERE id = OLD.parent_circ;
319 $$ LANGUAGE 'plpgsql';
321 CREATE TRIGGER age_parent_circ
322 AFTER DELETE ON action.circulation
324 EXECUTE PROCEDURE action.age_parent_circ_on_delete ();
327 CREATE OR REPLACE VIEW action.open_circulation AS
329 FROM action.circulation
330 WHERE checkin_time IS NULL
334 CREATE OR REPLACE VIEW action.billable_circulations AS
336 FROM action.circulation
337 WHERE xact_finish IS NULL;
339 CREATE OR REPLACE FUNCTION action.circulation_claims_returned () RETURNS TRIGGER AS $$
341 IF OLD.stop_fines IS NULL OR OLD.stop_fines <> NEW.stop_fines THEN
342 IF NEW.stop_fines = 'CLAIMSRETURNED' THEN
343 UPDATE actor.usr SET claims_returned_count = claims_returned_count + 1 WHERE id = NEW.usr;
345 IF NEW.stop_fines = 'CLAIMSNEVERCHECKEDOUT' THEN
346 UPDATE actor.usr SET claims_never_checked_out_count = claims_never_checked_out_count + 1 WHERE id = NEW.usr;
348 IF NEW.stop_fines = 'LOST' THEN
349 UPDATE asset.copy SET status = 3 WHERE id = NEW.target_copy;
354 $$ LANGUAGE 'plpgsql';
355 CREATE TRIGGER action_circulation_stop_fines_tgr
356 BEFORE UPDATE ON action.circulation
358 EXECUTE PROCEDURE action.circulation_claims_returned ();
360 CREATE TABLE action.hold_request_cancel_cause (
361 id SERIAL PRIMARY KEY,
365 CREATE TABLE action.hold_request (
366 id SERIAL PRIMARY KEY,
367 request_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
368 capture_time TIMESTAMP WITH TIME ZONE,
369 fulfillment_time TIMESTAMP WITH TIME ZONE,
370 checkin_time TIMESTAMP WITH TIME ZONE,
371 return_time TIMESTAMP WITH TIME ZONE,
372 prev_check_time TIMESTAMP WITH TIME ZONE,
373 expire_time TIMESTAMP WITH TIME ZONE,
374 cancel_time TIMESTAMP WITH TIME ZONE,
375 cancel_cause INT REFERENCES action.hold_request_cancel_cause (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
377 target BIGINT NOT NULL, -- see hold_type
378 current_copy BIGINT, -- REFERENCES asset.copy (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.unit now...
379 fulfillment_staff INT REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
380 fulfillment_lib INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
381 request_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
382 requestor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
383 usr INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
384 selection_ou INT NOT NULL,
385 selection_depth INT NOT NULL DEFAULT 0,
386 pickup_lib INT NOT NULL REFERENCES actor.org_unit DEFERRABLE INITIALLY DEFERRED,
387 hold_type TEXT NOT NULL, -- CHECK (hold_type IN ('M','T','V','C')), -- XXX constraint too constraining...
388 holdable_formats TEXT,
390 email_notify BOOL NOT NULL DEFAULT FALSE,
392 sms_carrier INT REFERENCES config.sms_carrier (id),
393 frozen BOOL NOT NULL DEFAULT FALSE,
394 thaw_date TIMESTAMP WITH TIME ZONE,
395 shelf_time TIMESTAMP WITH TIME ZONE,
397 mint_condition BOOL NOT NULL DEFAULT TRUE,
398 shelf_expire_time TIMESTAMPTZ,
399 current_shelf_lib INT REFERENCES actor.org_unit DEFERRABLE INITIALLY DEFERRED,
400 behind_desk BOOLEAN NOT NULL DEFAULT FALSE
402 ALTER TABLE action.hold_request ADD CONSTRAINT sms_check CHECK (
404 OR sms_carrier IS NOT NULL -- and implied sms_notify IS NOT NULL
408 CREATE OR REPLACE FUNCTION action.hold_request_clear_map () RETURNS TRIGGER AS $$
410 DELETE FROM action.hold_copy_map WHERE hold = NEW.id;
415 CREATE TRIGGER hold_request_clear_map_tgr
416 AFTER UPDATE ON action.hold_request
419 (NEW.cancel_time IS NOT NULL AND OLD.cancel_time IS NULL)
420 OR (NEW.fulfillment_time IS NOT NULL AND OLD.fulfillment_time IS NULL)
422 EXECUTE PROCEDURE action.hold_request_clear_map();
424 CREATE INDEX hold_request_target_idx ON action.hold_request (target);
425 CREATE INDEX hold_request_usr_idx ON action.hold_request (usr);
426 CREATE INDEX hold_request_pickup_lib_idx ON action.hold_request (pickup_lib);
427 CREATE INDEX hold_request_current_copy_idx ON action.hold_request (current_copy);
428 CREATE INDEX hold_request_prev_check_time_idx ON action.hold_request (prev_check_time);
429 CREATE INDEX hold_request_fulfillment_staff_idx ON action.hold_request ( fulfillment_staff );
430 CREATE INDEX hold_request_requestor_idx ON action.hold_request ( requestor );
431 CREATE INDEX hold_request_open_idx ON action.hold_request (id) WHERE cancel_time IS NULL AND fulfillment_time IS NULL;
432 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;
433 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;
434 CREATE INDEX hold_request_copy_capture_time_idx ON action.hold_request (current_copy,capture_time);
435 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);
436 CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
437 CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
440 CREATE TABLE action.hold_request_note (
442 id BIGSERIAL PRIMARY KEY,
443 hold BIGINT NOT NULL REFERENCES action.hold_request (id)
445 DEFERRABLE INITIALLY DEFERRED,
448 slip BOOL NOT NULL DEFAULT FALSE,
449 pub BOOL NOT NULL DEFAULT FALSE,
450 staff BOOL NOT NULL DEFAULT FALSE -- created by staff
453 CREATE INDEX ahrn_hold_idx ON action.hold_request_note (hold);
456 CREATE TABLE action.hold_notification (
457 id SERIAL PRIMARY KEY,
458 hold INT NOT NULL REFERENCES action.hold_request (id)
460 DEFERRABLE INITIALLY DEFERRED,
461 notify_staff INT REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
462 notify_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
463 method TEXT NOT NULL, -- email address or phone number
466 CREATE INDEX ahn_hold_idx ON action.hold_notification (hold);
467 CREATE INDEX ahn_notify_staff_idx ON action.hold_notification ( notify_staff );
469 CREATE TABLE action.hold_copy_map (
470 id BIGSERIAL PRIMARY KEY,
471 hold INT NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
472 target_copy BIGINT NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
474 CONSTRAINT copy_once_per_hold UNIQUE (hold,target_copy)
476 -- CREATE INDEX acm_hold_idx ON action.hold_copy_map (hold);
477 CREATE INDEX acm_copy_idx ON action.hold_copy_map (target_copy);
479 CREATE TABLE action.transit_copy (
480 id SERIAL PRIMARY KEY,
481 source_send_time TIMESTAMP WITH TIME ZONE,
482 dest_recv_time TIMESTAMP WITH TIME ZONE,
483 target_copy BIGINT NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
484 source INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
485 dest INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
486 prev_hop INT REFERENCES action.transit_copy (id) DEFERRABLE INITIALLY DEFERRED,
487 copy_status INT NOT NULL REFERENCES config.copy_status (id) DEFERRABLE INITIALLY DEFERRED,
488 persistant_transfer BOOL NOT NULL DEFAULT FALSE,
489 prev_dest INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
491 CREATE INDEX active_transit_dest_idx ON "action".transit_copy (dest);
492 CREATE INDEX active_transit_source_idx ON "action".transit_copy (source);
493 CREATE INDEX active_transit_cp_idx ON "action".transit_copy (target_copy);
496 CREATE TABLE action.hold_transit_copy (
497 hold INT REFERENCES action.hold_request (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
498 ) INHERITS (action.transit_copy);
499 ALTER TABLE action.hold_transit_copy ADD PRIMARY KEY (id);
500 -- 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
501 CREATE INDEX active_hold_transit_dest_idx ON "action".hold_transit_copy (dest);
502 CREATE INDEX active_hold_transit_source_idx ON "action".hold_transit_copy (source);
503 CREATE INDEX active_hold_transit_cp_idx ON "action".hold_transit_copy (target_copy);
504 CREATE INDEX hold_transit_copy_hold_idx on action.hold_transit_copy (hold);
507 CREATE TABLE action.unfulfilled_hold_list (
508 id BIGSERIAL PRIMARY KEY,
509 current_copy BIGINT NOT NULL,
511 circ_lib INT NOT NULL,
512 fail_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
514 CREATE INDEX uhr_hold_idx ON action.unfulfilled_hold_list (hold);
516 CREATE OR REPLACE VIEW action.unfulfilled_hold_loops AS
520 FROM action.unfulfilled_hold_list u
521 JOIN asset.copy c ON (c.id = u.current_copy)
524 CREATE OR REPLACE VIEW action.unfulfilled_hold_min_loop AS
527 FROM action.unfulfilled_hold_loops
530 CREATE OR REPLACE VIEW action.unfulfilled_hold_innermost_loop AS
532 FROM action.unfulfilled_hold_loops l
533 JOIN action.unfulfilled_hold_min_loop m USING (hold)
534 WHERE l.count = m.min;
536 CREATE VIEW action.unfulfilled_hold_max_loop AS
539 FROM action.unfulfilled_hold_loops
543 CREATE TABLE action.aged_hold_request (
545 usr_home_ou INT NOT NULL,
546 usr_profile INT NOT NULL,
548 staff_placed BOOLEAN NOT NULL,
549 LIKE action.hold_request
551 ALTER TABLE action.aged_hold_request
552 ADD PRIMARY KEY (id),
554 DROP COLUMN requestor,
555 DROP COLUMN sms_carrier,
556 ALTER COLUMN phone_notify TYPE BOOLEAN
557 USING CASE WHEN phone_notify IS NULL OR phone_notify = '' THEN FALSE ELSE TRUE END,
558 ALTER COLUMN sms_notify TYPE BOOLEAN
559 USING CASE WHEN sms_notify IS NULL OR sms_notify = '' THEN FALSE ELSE TRUE END,
560 ALTER COLUMN phone_notify SET NOT NULL,
561 ALTER COLUMN sms_notify SET NOT NULL;
562 CREATE INDEX aged_hold_request_target_idx ON action.aged_hold_request (target);
563 CREATE INDEX aged_hold_request_pickup_lib_idx ON action.aged_hold_request (pickup_lib);
564 CREATE INDEX aged_hold_request_current_copy_idx ON action.aged_hold_request (current_copy);
565 CREATE INDEX aged_hold_request_fulfillment_staff_idx ON action.aged_hold_request ( fulfillment_staff );
567 CREATE OR REPLACE VIEW action.all_hold_request AS
569 COALESCE(a.post_code, b.post_code) AS usr_post_code,
570 p.home_ou AS usr_home_ou,
571 p.profile AS usr_profile,
572 EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
573 CAST(ahr.requestor <> ahr.usr AS BOOLEAN) AS staff_placed,
577 ahr.fulfillment_time,
587 ahr.fulfillment_staff,
594 ahr.holdable_formats,
596 WHEN ahr.phone_notify IS NULL THEN FALSE
597 WHEN ahr.phone_notify = '' THEN FALSE
602 WHEN ahr.sms_notify IS NULL THEN FALSE
603 WHEN ahr.sms_notify = '' THEN FALSE
611 ahr.shelf_expire_time,
612 ahr.current_shelf_lib,
614 FROM action.hold_request ahr
615 JOIN actor.usr p ON (ahr.usr = p.id)
616 LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
617 LEFT JOIN actor.usr_address b ON (p.billing_address = b.id)
657 FROM action.aged_hold_request;
659 CREATE OR REPLACE FUNCTION action.age_hold_on_delete () RETURNS TRIGGER AS $$
662 -- Archive a copy of the old row to action.aged_hold_request
664 INSERT INTO action.aged_hold_request
740 FROM action.all_hold_request WHERE id = OLD.id;
744 $$ LANGUAGE 'plpgsql';
746 CREATE TRIGGER action_hold_request_aging_tgr
747 BEFORE DELETE ON action.hold_request
749 EXECUTE PROCEDURE action.age_hold_on_delete ();
751 CREATE TABLE action.fieldset (
752 id SERIAL PRIMARY KEY,
753 owner INT NOT NULL REFERENCES actor.usr (id)
754 DEFERRABLE INITIALLY DEFERRED,
755 owning_lib INT NOT NULL REFERENCES actor.org_unit (id)
756 DEFERRABLE INITIALLY DEFERRED,
758 CONSTRAINT valid_status CHECK ( status in
759 ( 'PENDING', 'APPLIED', 'ERROR' )),
760 creation_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
761 scheduled_time TIMESTAMPTZ,
762 applied_time TIMESTAMPTZ,
763 classname TEXT NOT NULL, -- an IDL class name
765 stored_query INT REFERENCES query.stored_query (id)
766 DEFERRABLE INITIALLY DEFERRED,
768 CONSTRAINT lib_name_unique UNIQUE (owning_lib, name),
769 CONSTRAINT fieldset_one_or_the_other CHECK (
770 (stored_query IS NOT NULL AND pkey_value IS NULL) OR
771 (pkey_value IS NOT NULL AND stored_query IS NULL)
773 -- the CHECK constraint means we can update the fields for a single
774 -- row without all the extra overhead involved in a query
777 CREATE INDEX action_fieldset_sched_time_idx ON action.fieldset( scheduled_time );
778 CREATE INDEX action_owner_idx ON action.fieldset( owner );
781 CREATE TABLE action.fieldset_col_val (
782 id SERIAL PRIMARY KEY,
783 fieldset INT NOT NULL REFERENCES action.fieldset
785 DEFERRABLE INITIALLY DEFERRED,
786 col TEXT NOT NULL, -- "field" from the idl ... the column on the table
787 val TEXT, -- value for the column ... NULL means, well, NULL
788 CONSTRAINT fieldset_col_once_per_set UNIQUE (fieldset, col)
792 -- represents a circ chain summary
793 CREATE TYPE action.circ_chain_summary AS (
795 start_time TIMESTAMP WITH TIME ZONE,
796 checkout_workstation TEXT,
797 last_renewal_time TIMESTAMP WITH TIME ZONE, -- NULL if no renewals
798 last_stop_fines TEXT,
799 last_stop_fines_time TIMESTAMP WITH TIME ZONE,
800 last_renewal_workstation TEXT, -- NULL if no renewals
801 last_checkin_workstation TEXT,
802 last_checkin_time TIMESTAMP WITH TIME ZONE,
803 last_checkin_scan_time TIMESTAMP WITH TIME ZONE
807 CREATE OR REPLACE FUNCTION action.circ_chain ( ctx_circ_id BIGINT ) RETURNS SETOF action.circulation AS $$
809 tmp_circ action.circulation%ROWTYPE;
810 circ_0 action.circulation%ROWTYPE;
813 SELECT INTO tmp_circ * FROM action.circulation WHERE id = ctx_circ_id;
815 IF tmp_circ IS NULL THEN
816 RETURN NEXT tmp_circ;
820 -- find the front of the chain
822 SELECT INTO tmp_circ * FROM action.circulation WHERE id = tmp_circ.parent_circ;
823 IF tmp_circ IS NULL THEN
829 -- now send the circs to the caller, oldest to newest
832 IF tmp_circ IS NULL THEN
835 RETURN NEXT tmp_circ;
836 SELECT INTO tmp_circ * FROM action.circulation WHERE parent_circ = tmp_circ.id;
840 $$ LANGUAGE 'plpgsql';
842 CREATE OR REPLACE FUNCTION action.summarize_circ_chain ( ctx_circ_id BIGINT ) RETURNS action.circ_chain_summary AS $$
846 -- first circ in the chain
847 circ_0 action.circulation%ROWTYPE;
849 -- last circ in the chain
850 circ_n action.circulation%ROWTYPE;
852 -- circ chain under construction
853 chain action.circ_chain_summary;
854 tmp_circ action.circulation%ROWTYPE;
858 chain.num_circs := 0;
859 FOR tmp_circ IN SELECT * FROM action.circ_chain(ctx_circ_id) LOOP
861 IF chain.num_circs = 0 THEN
865 chain.num_circs := chain.num_circs + 1;
869 chain.start_time := circ_0.xact_start;
870 chain.last_stop_fines := circ_n.stop_fines;
871 chain.last_stop_fines_time := circ_n.stop_fines_time;
872 chain.last_checkin_time := circ_n.checkin_time;
873 chain.last_checkin_scan_time := circ_n.checkin_scan_time;
874 SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
875 SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
877 IF chain.num_circs > 1 THEN
878 chain.last_renewal_time := circ_n.xact_start;
879 SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
885 $$ LANGUAGE 'plpgsql';
887 -- same as action.circ_chain, but returns action.all_circulation
888 -- rows which may include aged circulations.
889 CREATE OR REPLACE FUNCTION action.all_circ_chain (ctx_circ_id INTEGER)
890 RETURNS SETOF action.all_circulation AS $$
892 tmp_circ action.all_circulation%ROWTYPE;
893 circ_0 action.all_circulation%ROWTYPE;
896 SELECT INTO tmp_circ * FROM action.all_circulation WHERE id = ctx_circ_id;
898 IF tmp_circ IS NULL THEN
899 RETURN NEXT tmp_circ;
903 -- find the front of the chain
905 SELECT INTO tmp_circ * FROM action.all_circulation
906 WHERE id = tmp_circ.parent_circ;
907 IF tmp_circ IS NULL THEN
913 -- now send the circs to the caller, oldest to newest
916 IF tmp_circ IS NULL THEN
919 RETURN NEXT tmp_circ;
920 SELECT INTO tmp_circ * FROM action.all_circulation
921 WHERE parent_circ = tmp_circ.id;
925 $$ LANGUAGE 'plpgsql';
927 -- same as action.summarize_circ_chain, but returns data collected
928 -- from action.all_circulation, which may include aged circulations.
929 CREATE OR REPLACE FUNCTION action.summarize_all_circ_chain
930 (ctx_circ_id INTEGER) RETURNS action.circ_chain_summary AS $$
934 -- first circ in the chain
935 circ_0 action.all_circulation%ROWTYPE;
937 -- last circ in the chain
938 circ_n action.all_circulation%ROWTYPE;
940 -- circ chain under construction
941 chain action.circ_chain_summary;
942 tmp_circ action.all_circulation%ROWTYPE;
946 chain.num_circs := 0;
947 FOR tmp_circ IN SELECT * FROM action.all_circ_chain(ctx_circ_id) LOOP
949 IF chain.num_circs = 0 THEN
953 chain.num_circs := chain.num_circs + 1;
957 chain.start_time := circ_0.xact_start;
958 chain.last_stop_fines := circ_n.stop_fines;
959 chain.last_stop_fines_time := circ_n.stop_fines_time;
960 chain.last_checkin_time := circ_n.checkin_time;
961 chain.last_checkin_scan_time := circ_n.checkin_scan_time;
962 SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
963 SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
965 IF chain.num_circs > 1 THEN
966 chain.last_renewal_time := circ_n.xact_start;
967 SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
973 $$ LANGUAGE 'plpgsql';
976 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
978 h action.hold_request%ROWTYPE;
981 usr_view_count actor.usr_setting%ROWTYPE;
982 usr_view_age actor.usr_setting%ROWTYPE;
983 usr_view_start actor.usr_setting%ROWTYPE;
985 SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
986 SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
987 SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
991 FROM action.hold_request
993 AND fulfillment_time IS NULL
994 AND cancel_time IS NULL
995 ORDER BY request_time DESC
1000 IF usr_view_start.value IS NULL THEN
1004 IF usr_view_age.value IS NOT NULL THEN
1005 -- User opted in and supplied a retention age
1006 IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
1007 view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
1009 view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
1013 view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
1016 IF usr_view_count.value IS NOT NULL THEN
1017 view_count := oils_json_to_text(usr_view_count.value)::INT;
1022 -- show some fulfilled/canceled holds
1025 FROM action.hold_request
1027 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
1028 AND COALESCE(fulfillment_time, cancel_time) > NOW() - view_age
1029 ORDER BY COALESCE(fulfillment_time, cancel_time) DESC
1037 $func$ LANGUAGE PLPGSQL;
1039 CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
1041 org_keep_age INTERVAL;
1042 org_use_last BOOL = false;
1043 org_age_is_min BOOL = false;
1049 circ_chain_head action.circulation%ROWTYPE;
1050 circ_chain_tail action.circulation%ROWTYPE;
1055 last_finished TIMESTAMP WITH TIME ZONE;
1060 SELECT value::INTERVAL INTO org_keep_age FROM config.global_flag WHERE name = 'history.circ.retention_age' AND enabled;
1062 SELECT value::INT INTO org_keep_count FROM config.global_flag WHERE name = 'history.circ.retention_count' AND enabled;
1063 IF org_keep_count IS NULL THEN
1064 RETURN count_purged; -- Gimme a count to keep, or I keep them all, forever
1067 SELECT enabled INTO org_use_last FROM config.global_flag WHERE name = 'history.circ.retention_uses_last_finished';
1068 SELECT enabled INTO org_age_is_min FROM config.global_flag WHERE name = 'history.circ.retention_age_is_min';
1070 -- First, find copies with more than keep_count non-renewal circs
1073 COUNT(*) AS total_real_circs
1074 FROM action.circulation
1075 WHERE parent_circ IS NULL
1076 AND xact_finish IS NOT NULL
1077 GROUP BY target_copy
1078 HAVING COUNT(*) > org_keep_count
1080 -- And, for those, select circs that are finished and older than keep_age
1081 FOR circ_chain_head IN
1082 -- For reference, the subquery uses a window function to order the circs newest to oldest and number them
1083 -- The outer query then uses that information to skip the most recent set the library wants to keep
1084 -- End result is we don't care what order they come out in, as they are all potentials for deletion.
1085 SELECT ac.* FROM action.circulation ac JOIN (
1086 SELECT rank() OVER (ORDER BY xact_start DESC), ac.id
1087 FROM action.circulation ac
1088 WHERE ac.target_copy = target_acp.target_copy
1089 AND ac.parent_circ IS NULL
1090 ORDER BY ac.xact_start ) ranked USING (id)
1091 WHERE ranked.rank > org_keep_count
1094 SELECT * INTO circ_chain_tail FROM action.circ_chain(circ_chain_head.id) ORDER BY xact_start DESC LIMIT 1;
1095 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);
1096 CONTINUE WHEN circ_chain_tail.xact_finish IS NULL OR num_incomplete > 0;
1098 IF NOT org_use_last THEN
1099 last_finished := circ_chain_tail.xact_finish;
1102 keep_age := COALESCE( org_keep_age, '2000 years'::INTERVAL );
1104 IF org_age_is_min THEN
1105 keep_age := GREATEST( keep_age, org_keep_age );
1108 CONTINUE WHEN AGE(NOW(), last_finished) < keep_age;
1110 -- We've passed the purging tests, purge the circ chain starting at the end
1111 -- A trigger should auto-purge the rest of the chain.
1112 DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
1114 count_purged := count_purged + 1;
1119 return count_purged;
1121 $func$ LANGUAGE PLPGSQL;
1123 CREATE OR REPLACE FUNCTION action.purge_holds() RETURNS INT AS $func$
1125 current_hold RECORD;
1131 user_start TIMESTAMPTZ;
1136 SELECT INTO cgf_d value::INTERVAL FROM config.global_flag WHERE name = 'history.hold.retention_age' AND enabled;
1137 SELECT INTO cgf_f value::INTERVAL FROM config.global_flag WHERE name = 'history.hold.retention_age_fulfilled' AND enabled;
1138 SELECT INTO cgf_c value::INTERVAL FROM config.global_flag WHERE name = 'history.hold.retention_age_canceled' AND enabled;
1141 rank() OVER (PARTITION BY usr ORDER BY COALESCE(fulfillment_time, cancel_time) DESC),
1142 cgf_cs.value::INTERVAL as cgf_cs,
1145 action.hold_request ahr
1146 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)
1148 (fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL)
1150 IF prev_usr IS NULL OR prev_usr != current_hold.usr THEN
1151 prev_usr := current_hold.usr;
1152 SELECT INTO user_start oils_json_to_text(value)::TIMESTAMPTZ FROM actor.usr_setting WHERE usr = prev_usr AND name = 'history.hold.retention_start';
1153 SELECT INTO user_age oils_json_to_text(value)::INTERVAL FROM actor.usr_setting WHERE usr = prev_usr AND name = 'history.hold.retention_age';
1154 SELECT INTO user_count oils_json_to_text(value)::INT FROM actor.usr_setting WHERE usr = prev_usr AND name = 'history.hold.retention_count';
1155 IF user_start IS NOT NULL THEN
1156 user_age := LEAST(user_age, AGE(NOW(), user_start));
1158 IF user_count IS NULL THEN
1159 user_count := 1000; -- Assumption based on the user visible holds routine
1162 -- Library keep age trumps user keep anything, for purposes of being able to hold on to things when staff canceled and such.
1163 IF current_hold.fulfillment_time IS NOT NULL AND current_hold.fulfillment_time > NOW() - COALESCE(cgf_f, cgf_d) THEN
1166 IF current_hold.cancel_time IS NOT NULL AND current_hold.cancel_time > NOW() - COALESCE(current_hold.cgf_cs, cgf_c, cgf_d) THEN
1170 -- User keep age needs combining with count. If too old AND within the count, keep!
1171 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
1175 -- All checks should have passed, delete!
1176 DELETE FROM action.hold_request WHERE id = current_hold.id;
1177 purged_holds := purged_holds + 1;
1179 RETURN purged_holds;
1181 $func$ LANGUAGE plpgsql;
1183 CREATE OR REPLACE FUNCTION action.apply_fieldset(
1184 fieldset_id IN INT, -- id from action.fieldset
1185 table_name IN TEXT, -- table to be updated
1186 pkey_name IN TEXT, -- name of primary key column in that table
1187 query IN TEXT -- query constructed by qstore (for query-based
1188 -- fieldsets only; otherwise null
1203 IF fieldset_id IS NULL THEN
1204 RETURN 'Fieldset ID parameter is NULL';
1206 IF table_name IS NULL THEN
1207 RETURN 'Table name parameter is NULL';
1209 IF pkey_name IS NULL THEN
1210 RETURN 'Primary key name parameter is NULL';
1213 statement := 'UPDATE ' || table_name || ' SET';
1217 quote_literal( pkey_value )
1226 IF fs_status IS NULL THEN
1227 RETURN 'No fieldset found for id = ' || fieldset_id;
1228 ELSIF fs_status = 'APPLIED' THEN
1229 RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
1236 FROM action.fieldset_col_val
1237 WHERE fieldset = fieldset_id
1239 statement := statement || sep || ' ' || cv.col
1240 || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
1245 RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
1248 -- Add the WHERE clause. This differs according to whether it's a
1249 -- single-row fieldset or a query-based fieldset.
1251 IF query IS NULL AND fs_pkey_value IS NULL THEN
1252 RETURN 'Incomplete fieldset: neither a primary key nor a query available';
1253 ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
1254 fs_query := rtrim( query, ';' );
1255 statement := statement || ' WHERE ' || pkey_name || ' IN ( '
1256 || fs_query || ' );';
1257 ELSIF query IS NULL AND fs_pkey_value IS NOT NULL THEN
1258 statement := statement || ' WHERE ' || pkey_name || ' = '
1259 || fs_pkey_value || ';';
1260 ELSE -- both are not null
1261 RETURN 'Ambiguous fieldset: both a primary key and a query provided';
1264 -- Execute the update
1268 GET DIAGNOSTICS update_count = ROW_COUNT;
1270 IF UPDATE_COUNT > 0 THEN
1271 status_code := 'APPLIED';
1274 status_code := 'ERROR';
1275 msg := 'No eligible rows found for fieldset ' || fieldset_id;
1277 EXCEPTION WHEN OTHERS THEN
1278 status_code := 'ERROR';
1279 msg := 'Unable to apply fieldset ' || fieldset_id
1283 -- Update fieldset status
1285 UPDATE action.fieldset
1286 SET status = status_code,
1287 applied_time = now()
1288 WHERE id = fieldset_id;
1292 $$ LANGUAGE plpgsql;
1294 COMMENT ON FUNCTION action.apply_fieldset( INT, TEXT, TEXT, TEXT ) IS $$
1295 Applies a specified fieldset, using a supplied table name and primary
1296 key name. The query parameter should be non-null only for
1297 query-based fieldsets.
1299 Returns NULL if successful, or an error message if not.
1302 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(
1305 copy_context_ou INT DEFAULT NULL
1306 -- TODO maybe? hold_context_ou INT DEFAULT NULL. This would optionally
1307 -- support an "ahprox" measurement: adjust prox between copy circ lib and
1308 -- hold request lib, but I'm unsure whether to use this theoretical
1309 -- argument only in the baseline calculation or later in the other
1310 -- queries in this function.
1311 ) RETURNS NUMERIC AS $f$
1313 aoupa actor.org_unit_proximity_adjustment%ROWTYPE;
1314 ahr action.hold_request%ROWTYPE;
1315 acp asset.copy%ROWTYPE;
1316 acn asset.call_number%ROWTYPE;
1317 acl asset.copy_location%ROWTYPE;
1318 baseline_prox NUMERIC;
1328 SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id;
1329 SELECT * INTO acp FROM asset.copy WHERE id = acp_id;
1330 SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
1331 SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
1333 IF copy_context_ou IS NULL THEN
1334 copy_context_ou := acp.circ_lib;
1337 -- First, gather the baseline proximity of "here" to pickup lib
1338 SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = copy_context_ou AND to_org = ahr.pickup_lib;
1340 -- Find any absolute adjustments, and set the baseline prox to that
1341 SELECT adj.* INTO aoupa
1342 FROM actor.org_unit_proximity_adjustment adj
1343 LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
1344 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
1345 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acl_ol.id = adj.copy_location)
1346 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
1347 LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
1348 WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
1349 (adj.item_circ_lib IS NULL OR adj.item_circ_lib = acp_cl.id) AND
1350 (adj.item_owning_lib IS NULL OR adj.item_owning_lib = acn_ol.id) AND
1351 (adj.copy_location IS NULL OR adj.copy_location = acl_ol.id) AND
1352 (adj.hold_pickup_lib IS NULL OR adj.hold_pickup_lib = ahr_pl.id) AND
1353 (adj.hold_request_lib IS NULL OR adj.hold_request_lib = ahr_rl.id) AND
1354 absolute_adjustment AND
1355 COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
1357 COALESCE(acp_cl.distance,999)
1358 + COALESCE(acn_ol.distance,999)
1359 + COALESCE(acl_ol.distance,999)
1360 + COALESCE(ahr_pl.distance,999)
1361 + COALESCE(ahr_rl.distance,999),
1366 baseline_prox := aoupa.prox_adjustment;
1369 -- Now find any relative adjustments, and change the baseline prox based on them
1372 FROM actor.org_unit_proximity_adjustment adj
1373 LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
1374 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
1375 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
1376 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
1377 LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
1378 WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
1379 (adj.item_circ_lib IS NULL OR adj.item_circ_lib = acp_cl.id) AND
1380 (adj.item_owning_lib IS NULL OR adj.item_owning_lib = acn_ol.id) AND
1381 (adj.copy_location IS NULL OR adj.copy_location = acl_ol.id) AND
1382 (adj.hold_pickup_lib IS NULL OR adj.hold_pickup_lib = ahr_pl.id) AND
1383 (adj.hold_request_lib IS NULL OR adj.hold_request_lib = ahr_rl.id) AND
1384 NOT absolute_adjustment AND
1385 COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
1387 baseline_prox := baseline_prox + aoupa.prox_adjustment;
1390 RETURN baseline_prox;
1392 $f$ LANGUAGE PLPGSQL;
1394 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity_update () RETURNS TRIGGER AS $f$
1396 NEW.proximity := action.hold_copy_calculated_proximity(NEW.hold,NEW.target_copy);
1399 $f$ LANGUAGE PLPGSQL;
1401 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 ();
1403 CREATE TABLE action.usr_circ_history (
1404 id BIGSERIAL PRIMARY KEY,
1405 usr INTEGER NOT NULL REFERENCES actor.usr(id)
1406 DEFERRABLE INITIALLY DEFERRED,
1407 xact_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1408 target_copy BIGINT NOT NULL, -- asset.copy.id / serial.unit.id
1409 due_date TIMESTAMP WITH TIME ZONE NOT NULL,
1410 checkin_time TIMESTAMP WITH TIME ZONE,
1411 source_circ BIGINT REFERENCES action.circulation(id)
1412 ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
1415 CREATE TRIGGER action_usr_circ_history_target_copy_trig
1416 AFTER INSERT OR UPDATE ON action.usr_circ_history
1417 FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
1419 CREATE OR REPLACE FUNCTION action.maintain_usr_circ_history()
1420 RETURNS TRIGGER AS $FUNK$
1426 -- Any retention value signifies history is enabled.
1427 -- This assumes that clearing these values via external
1428 -- process deletes the action.usr_circ_history rows.
1429 -- TODO: replace these settings w/ a single bool setting?
1430 PERFORM 1 FROM actor.usr_setting
1431 WHERE usr = NEW.usr AND value IS NOT NULL AND name IN (
1432 'history.circ.retention_age',
1433 'history.circ.retention_start'
1440 IF TG_OP = 'INSERT' AND NEW.parent_circ IS NULL THEN
1441 -- Starting a new circulation. Insert the history row.
1442 INSERT INTO action.usr_circ_history
1443 (usr, xact_start, target_copy, due_date, source_circ)
1455 -- find the first and last circs in the circ chain
1456 -- for the currently modified circ.
1457 FOR cur_circ IN SELECT id FROM action.circ_chain(NEW.id) LOOP
1458 IF first_circ IS NULL THEN
1459 first_circ := cur_circ;
1462 -- Allow the loop to continue so that at as the loop
1463 -- completes cur_circ points to the final circulation.
1466 IF NEW.id <> cur_circ THEN
1467 -- Modifying an intermediate circ. Ignore it.
1471 -- Update the due_date/checkin_time on the history row if the current
1472 -- circ is the last circ in the chain and an update is warranted.
1474 UPDATE action.usr_circ_history
1476 due_date = NEW.due_date,
1477 checkin_time = NEW.checkin_time
1479 source_circ = first_circ
1481 due_date <> NEW.due_date OR (
1482 (checkin_time IS NULL AND NEW.checkin_time IS NOT NULL) OR
1483 (checkin_time IS NOT NULL AND NEW.checkin_time IS NULL) OR
1484 (checkin_time <> NEW.checkin_time)
1489 $FUNK$ LANGUAGE PLPGSQL;
1491 CREATE TRIGGER maintain_usr_circ_history_tgr
1492 AFTER INSERT OR UPDATE ON action.circulation
1493 FOR EACH ROW EXECUTE PROCEDURE action.maintain_usr_circ_history();