2 * Copyright (C) 2009 Georgia Public Library Service
3 * Scott McKellar <scott@esilibrary.com>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
16 DROP SCHEMA IF EXISTS acq CASCADE;
26 CREATE TABLE acq.currency_type (
27 code TEXT PRIMARY KEY,
31 CREATE TABLE acq.exchange_rate (
32 id SERIAL PRIMARY KEY,
33 from_currency TEXT NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
34 to_currency TEXT NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
35 ratio NUMERIC NOT NULL,
36 CONSTRAINT exchange_rate_from_to_once UNIQUE (from_currency,to_currency)
39 CREATE TABLE acq.claim_policy (
40 id SERIAL PRIMARY KEY,
41 org_unit INT NOT NULL REFERENCES actor.org_unit
42 DEFERRABLE INITIALLY DEFERRED,
44 description TEXT NOT NULL,
45 CONSTRAINT name_once_per_org UNIQUE (org_unit, name)
48 CREATE TABLE acq.claim_event_type (
49 id SERIAL PRIMARY KEY,
50 org_unit INT NOT NULL REFERENCES actor.org_unit(id)
51 DEFERRABLE INITIALLY DEFERRED,
53 description TEXT NOT NULL,
54 library_initiated BOOL NOT NULL DEFAULT FALSE,
55 CONSTRAINT event_type_once_per_org UNIQUE ( org_unit, code )
58 CREATE TABLE acq.claim_policy_action (
59 id SERIAL PRIMARY KEY,
60 claim_policy INT NOT NULL REFERENCES acq.claim_policy
62 DEFERRABLE INITIALLY DEFERRED,
63 action_interval INTERVAL NOT NULL,
64 action INT NOT NULL REFERENCES acq.claim_event_type
65 DEFERRABLE INITIALLY DEFERRED,
66 CONSTRAINT action_sequence UNIQUE (claim_policy, action_interval)
69 CREATE TABLE acq.provider (
70 id SERIAL PRIMARY KEY,
72 owner INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
73 currency_type TEXT NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
77 edi_default INT, -- REFERENCES acq.edi_account (id) DEFERRABLE INITIALLY DEFERRED
78 active BOOL NOT NULL DEFAULT TRUE,
79 prepayment_required BOOL NOT NULL DEFAULT FALSE,
84 default_copy_count INT NOT NULL DEFAULT 0,
85 default_claim_policy INT REFERENCES acq.claim_policy
86 DEFERRABLE INITIALLY DEFERRED,
87 CONSTRAINT provider_name_once_per_owner UNIQUE (name,owner),
88 CONSTRAINT code_once_per_owner UNIQUE (code, owner)
91 CREATE TABLE acq.provider_holding_subfield_map (
92 id SERIAL PRIMARY KEY,
93 provider INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
94 name TEXT NOT NULL, -- barcode, price, etc
95 subfield TEXT NOT NULL,
96 CONSTRAINT name_once_per_provider UNIQUE (provider,name)
99 CREATE TABLE acq.provider_address (
100 id SERIAL PRIMARY KEY,
101 valid BOOL NOT NULL DEFAULT TRUE,
103 provider INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
104 street1 TEXT NOT NULL,
109 country TEXT NOT NULL,
110 post_code TEXT NOT NULL,
114 CREATE TABLE acq.provider_contact (
115 id SERIAL PRIMARY KEY,
116 provider INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
118 role TEXT, -- free-form.. e.g. "our sales guy"
123 CREATE TABLE acq.provider_contact_address (
124 id SERIAL PRIMARY KEY,
125 valid BOOL NOT NULL DEFAULT TRUE,
127 contact INT NOT NULL REFERENCES acq.provider_contact (id) DEFERRABLE INITIALLY DEFERRED,
128 street1 TEXT NOT NULL,
133 country TEXT NOT NULL,
134 post_code TEXT NOT NULL,
138 CREATE TABLE acq.provider_note (
139 id SERIAL PRIMARY KEY,
140 provider INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
141 creator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
142 editor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
143 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
144 edit_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
147 CREATE INDEX acq_pro_note_pro_idx ON acq.provider_note ( provider );
148 CREATE INDEX acq_pro_note_creator_idx ON acq.provider_note ( creator );
149 CREATE INDEX acq_pro_note_editor_idx ON acq.provider_note ( editor );
152 CREATE TABLE acq.funding_source (
153 id SERIAL PRIMARY KEY,
155 owner INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
156 currency_type TEXT NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
158 CONSTRAINT funding_source_name_once_per_owner UNIQUE (name,owner)
161 CREATE TABLE acq.funding_source_credit (
162 id SERIAL PRIMARY KEY,
163 funding_source INT NOT NULL REFERENCES acq.funding_source (id) DEFERRABLE INITIALLY DEFERRED,
164 amount NUMERIC NOT NULL,
166 deadline_date TIMESTAMPTZ,
167 effective_date TIMESTAMPTZ NOT NULL default now()
170 CREATE VIEW acq.ordered_funding_source_credit AS
172 CASE WHEN deadline_date IS NULL THEN
176 END AS sort_priority,
177 CASE WHEN deadline_date IS NULL THEN
187 acq.funding_source_credit;
189 COMMENT ON VIEW acq.ordered_funding_source_credit IS $$
190 The acq.ordered_funding_source_credit view is a prioritized
191 ordering of funding source credits. When ordered by the first
192 three columns, this view defines the order in which the various
193 credits are to be tapped for spending, subject to the allocations
194 in the acq.fund_allocation table.
196 The first column reflects the principle that we should spend
197 money with deadlines before spending money without deadlines.
199 The second column reflects the principle that we should spend the
200 oldest money first. For money with deadlines, that means that we
201 spend first from the credit with the earliest deadline. For
202 money without deadlines, we spend first from the credit with the
203 earliest effective date.
205 The third column is a tie breaker to ensure a consistent
209 CREATE TABLE acq.fund (
210 id SERIAL PRIMARY KEY,
211 org INT NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
213 year INT NOT NULL DEFAULT EXTRACT( YEAR FROM NOW() ),
214 currency_type TEXT NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
216 rollover BOOL NOT NULL DEFAULT FALSE,
217 propagate BOOL NOT NULL DEFAULT TRUE,
218 active BOOL NOT NULL DEFAULT TRUE,
219 balance_warning_percent INT,
220 balance_stop_percent INT,
221 CONSTRAINT name_once_per_org_year UNIQUE (org,name,year),
222 CONSTRAINT code_once_per_org_year UNIQUE (org, code, year),
223 CONSTRAINT acq_fund_rollover_implies_propagate CHECK ( propagate OR NOT rollover )
226 CREATE TABLE acq.fund_debit (
227 id SERIAL PRIMARY KEY,
228 fund INT NOT NULL REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
229 origin_amount NUMERIC NOT NULL, -- pre-exchange-rate amount
230 origin_currency_type TEXT NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
231 amount NUMERIC NOT NULL,
232 encumbrance BOOL NOT NULL DEFAULT TRUE,
233 debit_type TEXT NOT NULL,
234 xfer_destination INT REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
235 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
238 CREATE TABLE acq.fund_allocation (
239 id SERIAL PRIMARY KEY,
240 funding_source INT NOT NULL REFERENCES acq.funding_source (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
241 fund INT NOT NULL REFERENCES acq.fund (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
242 amount NUMERIC NOT NULL,
243 allocator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
245 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
247 CREATE INDEX fund_alloc_allocator_idx ON acq.fund_allocation ( allocator );
249 CREATE TABLE acq.fund_allocation_percent
251 id SERIAL PRIMARY KEY,
252 funding_source INT NOT NULL REFERENCES acq.funding_source
253 DEFERRABLE INITIALLY DEFERRED,
254 org INT NOT NULL REFERENCES actor.org_unit
255 DEFERRABLE INITIALLY DEFERRED,
257 percent NUMERIC NOT NULL,
258 allocator INTEGER NOT NULL REFERENCES actor.usr
259 DEFERRABLE INITIALLY DEFERRED,
261 create_time TIMESTAMPTZ NOT NULL DEFAULT now(),
262 CONSTRAINT logical_key UNIQUE( funding_source, org, fund_code ),
263 CONSTRAINT percentage_range CHECK( percent >= 0 AND percent <= 100 )
266 -- Trigger function to validate combination of org_unit and fund_code
268 CREATE OR REPLACE FUNCTION acq.fund_alloc_percent_val()
269 RETURNS TRIGGER AS $$
284 AND code = NEW.fund_code
290 RAISE EXCEPTION 'No fund exists for org % and code %', NEW.org, NEW.fund_code;
295 CREATE TRIGGER acq_fund_alloc_percent_val_trig
296 BEFORE INSERT OR UPDATE ON acq.fund_allocation_percent
297 FOR EACH ROW EXECUTE PROCEDURE acq.fund_alloc_percent_val();
299 -- To do: trigger to verify that percentages don't add up to more than 100
301 CREATE OR REPLACE FUNCTION acq.fap_limit_100()
302 RETURNS TRIGGER AS $$
305 total_percent numeric;
313 acq.fund_allocation_percent AS fap
315 fap.funding_source = NEW.funding_source;
317 IF total_percent > 100 THEN
318 RAISE EXCEPTION 'Total percentages exceed 100 for funding_source %',
326 CREATE TRIGGER acqfap_limit_100_trig
327 AFTER INSERT OR UPDATE ON acq.fund_allocation_percent
328 FOR EACH ROW EXECUTE PROCEDURE acq.fap_limit_100();
330 CREATE TABLE acq.picklist (
331 id SERIAL PRIMARY KEY,
332 owner INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
333 creator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
334 editor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
335 org_unit INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
337 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
338 edit_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
339 CONSTRAINT name_once_per_owner UNIQUE (name,owner)
341 CREATE INDEX acq_picklist_owner_idx ON acq.picklist ( owner );
342 CREATE INDEX acq_picklist_creator_idx ON acq.picklist ( creator );
343 CREATE INDEX acq_picklist_editor_idx ON acq.picklist ( editor );
345 CREATE TABLE acq.cancel_reason (
346 id SERIAL PRIMARY KEY,
347 org_unit INTEGER NOT NULL REFERENCES actor.org_unit( id )
348 DEFERRABLE INITIALLY DEFERRED,
350 description TEXT NOT NULL,
351 keep_debits BOOL NOT NULL DEFAULT FALSE,
352 CONSTRAINT acq_cancel_reason_one_per_org_unit UNIQUE( org_unit, label )
355 -- Reserve ids 1-999 for stock reasons
356 -- Reserve ids 1000-1999 for EDI reasons
357 -- 2000+ are available for staff to create
359 SELECT SETVAL('acq.cancel_reason_id_seq'::TEXT, 2000);
361 CREATE TABLE acq.purchase_order (
362 id SERIAL PRIMARY KEY,
363 owner INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
364 creator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
365 editor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
366 ordering_agency INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
367 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
368 edit_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
369 provider INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
370 state TEXT NOT NULL DEFAULT 'new',
371 order_date TIMESTAMP WITH TIME ZONE,
373 cancel_reason INT REFERENCES acq.cancel_reason( id )
374 DEFERRABLE INITIALLY DEFERRED,
375 prepayment_required BOOLEAN NOT NULL DEFAULT FALSE,
376 CONSTRAINT valid_po_state CHECK (state IN ('new','pending','on-order','received','cancelled'))
378 CREATE INDEX po_owner_idx ON acq.purchase_order (owner);
379 CREATE INDEX po_provider_idx ON acq.purchase_order (provider);
380 CREATE INDEX po_state_idx ON acq.purchase_order (state);
381 CREATE INDEX po_creator_idx ON acq.purchase_order ( creator );
382 CREATE INDEX po_editor_idx ON acq.purchase_order ( editor );
383 CREATE INDEX acq_po_org_name_order_date_idx ON acq.purchase_order( ordering_agency, name, order_date );
385 -- The name should default to the id, as text. We can't reference a column
386 -- in a DEFAULT clause, so we use a trigger:
388 CREATE OR REPLACE FUNCTION acq.purchase_order_name_default () RETURNS TRIGGER
391 IF NEW.name IS NULL THEN
392 NEW.name := NEW.id::TEXT;
399 CREATE TRIGGER po_name_default_trg
400 BEFORE INSERT OR UPDATE ON acq.purchase_order
401 FOR EACH ROW EXECUTE PROCEDURE acq.purchase_order_name_default ();
403 -- The order name should be unique for a given ordering agency on a given order date
404 -- (truncated to midnight), but only where the order_date is not NULL. Conceptually
405 -- this rule requires a check constraint with a subquery. However you can't have a
406 -- subquery in a CHECK constraint, so we fake it with a trigger.
408 CREATE OR REPLACE FUNCTION acq.po_org_name_date_unique () RETURNS TRIGGER
414 -- If order_date is not null, then make sure we don't have a collision
415 -- on order_date (truncated to day), org, and name
417 IF NEW.order_date IS NULL THEN
421 -- In the WHERE clause, we compare the order_dates without regard to time of day.
422 -- We use a pair of inequalities instead of comparing truncated dates so that the
423 -- query can do an indexed range scan.
425 SELECT 1 INTO collision
426 FROM acq.purchase_order
428 ordering_agency = NEW.ordering_agency
430 AND order_date >= date_trunc( 'day', NEW.order_date )
431 AND order_date < date_trunc( 'day', NEW.order_date ) + '1 day'::INTERVAL
434 IF collision IS NULL THEN
435 -- okay, no collision
438 -- collision; nip it in the bud
439 RAISE EXCEPTION 'Colliding purchase orders: ordering_agency %, date %, name ''%''',
440 NEW.ordering_agency, NEW.order_date, NEW.name;
445 CREATE TRIGGER po_org_name_date_unique_trg
446 BEFORE INSERT OR UPDATE ON acq.purchase_order
447 FOR EACH ROW EXECUTE PROCEDURE acq.po_org_name_date_unique ();
449 CREATE TABLE acq.po_note (
450 id SERIAL PRIMARY KEY,
451 purchase_order INT NOT NULL REFERENCES acq.purchase_order (id) DEFERRABLE INITIALLY DEFERRED,
452 creator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
453 editor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
454 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
455 edit_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
457 vendor_public BOOLEAN NOT NULL DEFAULT FALSE
459 CREATE INDEX po_note_po_idx ON acq.po_note (purchase_order);
460 CREATE INDEX acq_po_note_creator_idx ON acq.po_note ( creator );
461 CREATE INDEX acq_po_note_editor_idx ON acq.po_note ( editor );
463 CREATE TABLE acq.lineitem (
464 id BIGSERIAL PRIMARY KEY,
465 creator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
466 editor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
467 selector INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
468 provider INT REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
469 purchase_order INT REFERENCES acq.purchase_order (id) DEFERRABLE INITIALLY DEFERRED,
470 picklist INT REFERENCES acq.picklist (id) DEFERRABLE INITIALLY DEFERRED,
471 expected_recv_time TIMESTAMP WITH TIME ZONE,
472 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
473 edit_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
475 eg_bib_id BIGINT REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
477 state TEXT NOT NULL DEFAULT 'new',
478 cancel_reason INT REFERENCES acq.cancel_reason( id )
479 DEFERRABLE INITIALLY DEFERRED,
480 estimated_unit_price NUMERIC,
481 claim_policy INT REFERENCES acq.claim_policy
482 DEFERRABLE INITIALLY DEFERRED,
483 queued_record BIGINT REFERENCES vandelay.queued_bib_record (id)
484 ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
485 CONSTRAINT picklist_or_po CHECK (picklist IS NOT NULL OR purchase_order IS NOT NULL)
487 CREATE INDEX li_po_idx ON acq.lineitem (purchase_order);
488 CREATE INDEX li_pl_idx ON acq.lineitem (picklist);
489 CREATE INDEX li_creator_idx ON acq.lineitem ( creator );
490 CREATE INDEX li_editor_idx ON acq.lineitem ( editor );
491 CREATE INDEX li_selector_idx ON acq.lineitem ( selector );
493 CREATE TABLE acq.lineitem_alert_text (
494 id SERIAL PRIMARY KEY,
497 owning_lib INT NOT NULL
498 REFERENCES actor.org_unit(id)
499 DEFERRABLE INITIALLY DEFERRED,
500 CONSTRAINT alert_one_code_per_org UNIQUE (code, owning_lib)
503 CREATE TABLE acq.lineitem_note (
504 id SERIAL PRIMARY KEY,
505 lineitem INT NOT NULL REFERENCES acq.lineitem (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
506 creator INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
507 editor INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
508 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
509 edit_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
511 alert_text INT REFERENCES acq.lineitem_alert_text(id)
512 DEFERRABLE INITIALLY DEFERRED,
513 vendor_public BOOLEAN NOT NULL DEFAULT FALSE
515 CREATE INDEX li_note_li_idx ON acq.lineitem_note (lineitem);
516 CREATE INDEX li_note_creator_idx ON acq.lineitem_note ( creator );
517 CREATE INDEX li_note_editor_idx ON acq.lineitem_note ( editor );
519 CREATE TABLE acq.lineitem_detail (
520 id BIGSERIAL PRIMARY KEY,
521 lineitem INT NOT NULL REFERENCES acq.lineitem (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
522 fund INT REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
523 fund_debit INT REFERENCES acq.fund_debit (id) DEFERRABLE INITIALLY DEFERRED,
524 eg_copy_id BIGINT, -- REFERENCES asset.copy (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
528 collection_code TEXT,
529 circ_modifier TEXT REFERENCES config.circ_modifier (code) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
530 owning_lib INT REFERENCES actor.org_unit (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
531 location INT REFERENCES asset.copy_location (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
532 recv_time TIMESTAMP WITH TIME ZONE,
533 receiver INT REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
534 cancel_reason INT REFERENCES acq.cancel_reason( id ) DEFERRABLE INITIALLY DEFERRED
537 CREATE INDEX li_detail_li_idx ON acq.lineitem_detail (lineitem);
539 CREATE TABLE acq.lineitem_attr_definition (
540 id BIGSERIAL PRIMARY KEY,
542 description TEXT NOT NULL,
543 remove TEXT NOT NULL DEFAULT '',
544 ident BOOL NOT NULL DEFAULT FALSE
547 CREATE TABLE acq.lineitem_marc_attr_definition (
548 id BIGINT PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
550 ) INHERITS (acq.lineitem_attr_definition);
552 CREATE TABLE acq.lineitem_provider_attr_definition (
553 id BIGINT PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
555 provider INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED
556 ) INHERITS (acq.lineitem_attr_definition);
558 CREATE TABLE acq.lineitem_generated_attr_definition (
559 id BIGINT PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
561 ) INHERITS (acq.lineitem_attr_definition);
563 CREATE TABLE acq.lineitem_usr_attr_definition (
564 id BIGINT PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
565 usr INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED
566 ) INHERITS (acq.lineitem_attr_definition);
567 CREATE INDEX li_usr_attr_def_usr_idx ON acq.lineitem_usr_attr_definition ( usr );
569 CREATE TABLE acq.lineitem_local_attr_definition (
570 id BIGINT PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq')
571 ) INHERITS (acq.lineitem_attr_definition);
573 CREATE TABLE acq.lineitem_attr (
574 id BIGSERIAL PRIMARY KEY,
575 definition BIGINT NOT NULL,
576 lineitem BIGINT NOT NULL REFERENCES acq.lineitem (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
577 attr_type TEXT NOT NULL,
578 attr_name TEXT NOT NULL,
579 attr_value TEXT NOT NULL,
580 order_ident BOOLEAN NOT NULL DEFAULT FALSE
583 CREATE INDEX li_attr_li_idx ON acq.lineitem_attr (lineitem);
584 CREATE INDEX li_attr_value_idx ON acq.lineitem_attr (attr_value);
585 CREATE INDEX li_attr_definition_idx ON acq.lineitem_attr (definition);
591 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('title','Title of work','//*[@tag="245"]/*[contains("abcmnopr",@code)]');
592 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('author','Author of work','//*[@tag="100" or @tag="110" or @tag="113"]/*[contains("ad",@code)]');
593 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('language','Language of work','//*[@tag="240"]/*[@code="l"][1]');
594 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('pagination','Pagination','//*[@tag="300"]/*[@code="a"][1]');
595 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('isbn','ISBN','//*[@tag="020"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
596 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('issn','ISSN','//*[@tag="022"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
597 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('upc', 'UPC', '//*[@tag="024" and @ind1="1"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
598 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('price','Price','//*[@tag="020" or @tag="022"]/*[@code="c"][1]');
599 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('identifier','Identifier','//*[@tag="001"]');
600 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('publisher','Publisher','//*[@tag="260"]/*[@code="b"][1]');
601 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('pubdate','Publication Date','//*[@tag="260"]/*[@code="c"][1]');
602 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('edition','Edition','//*[@tag="250"]/*[@code="a"][1]');
604 INSERT INTO acq.lineitem_local_attr_definition ( code, description ) VALUES ('estimated_price', 'Estimated Price');
607 CREATE TABLE acq.distribution_formula (
608 id SERIAL PRIMARY KEY,
610 REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
612 skip_count INT NOT NULL DEFAULT 0,
613 CONSTRAINT acqdf_name_once_per_owner UNIQUE (name, owner)
616 CREATE TABLE acq.distribution_formula_entry (
617 id SERIAL PRIMARY KEY,
618 formula INTEGER NOT NULL REFERENCES acq.distribution_formula(id)
620 DEFERRABLE INITIALLY DEFERRED,
621 position INTEGER NOT NULL,
622 item_count INTEGER NOT NULL,
623 owning_lib INTEGER REFERENCES actor.org_unit(id)
624 DEFERRABLE INITIALLY DEFERRED,
625 location INTEGER REFERENCES asset.copy_location(id),
626 fund INTEGER REFERENCES acq.fund (id),
627 circ_modifier TEXT REFERENCES config.circ_modifier (code),
628 collection_code TEXT,
629 CONSTRAINT acqdfe_lib_once_per_formula UNIQUE( formula, position ),
630 CONSTRAINT acqdfe_must_be_somewhere
631 CHECK( owning_lib IS NOT NULL OR location IS NOT NULL )
634 CREATE TABLE acq.distribution_formula_application (
635 id BIGSERIAL PRIMARY KEY,
636 creator INT NOT NULL REFERENCES actor.usr(id) DEFERRABLE INITIALLY DEFERRED,
637 create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
639 REFERENCES acq.distribution_formula(id) DEFERRABLE INITIALLY DEFERRED,
640 lineitem INT NOT NULL
641 REFERENCES acq.lineitem(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
644 CREATE INDEX acqdfa_df_idx
645 ON acq.distribution_formula_application(formula);
646 CREATE INDEX acqdfa_li_idx
647 ON acq.distribution_formula_application(lineitem);
648 CREATE INDEX acqdfa_creator_idx
649 ON acq.distribution_formula_application(creator);
651 CREATE TABLE acq.fund_tag (
652 id SERIAL PRIMARY KEY,
654 REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
656 CONSTRAINT acqft_tag_once_per_owner UNIQUE (name, owner)
659 CREATE TABLE acq.fund_tag_map (
660 id SERIAL PRIMARY KEY,
661 fund INTEGER NOT NULL REFERENCES acq.fund(id)
662 DEFERRABLE INITIALLY DEFERRED,
663 tag INTEGER REFERENCES acq.fund_tag(id)
665 DEFERRABLE INITIALLY DEFERRED,
666 CONSTRAINT acqftm_fund_once_per_tag UNIQUE( fund, tag )
669 CREATE TABLE acq.fund_transfer (
670 id SERIAL PRIMARY KEY,
671 src_fund INT NOT NULL REFERENCES acq.fund( id )
672 DEFERRABLE INITIALLY DEFERRED,
673 src_amount NUMERIC NOT NULL,
674 dest_fund INT REFERENCES acq.fund( id )
675 DEFERRABLE INITIALLY DEFERRED,
677 transfer_time TIMESTAMPTZ NOT NULL DEFAULT now(),
678 transfer_user INT NOT NULL REFERENCES actor.usr( id )
679 DEFERRABLE INITIALLY DEFERRED,
681 funding_source_credit INT NOT NULL REFERENCES acq.funding_source_credit( id )
682 DEFERRABLE INITIALLY DEFERRED
685 CREATE INDEX acqftr_usr_idx
686 ON acq.fund_transfer( transfer_user );
688 COMMENT ON TABLE acq.fund_transfer IS $$
690 Each row represents the transfer of money from a source fund
691 to a destination fund. There should be corresponding entries
692 in acq.fund_allocation. The purpose of acq.fund_transfer is
693 to record how much money moved from which fund to which other
696 The presence of two amount fields, rather than one, reflects
697 the possibility that the two funds are denominated in different
698 currencies. If they use the same currency type, the two
699 amounts should be the same.
702 CREATE TABLE acq.fiscal_calendar (
703 id SERIAL PRIMARY KEY,
707 -- Create a default calendar (though we don't specify its contents).
708 -- Create a foreign key in actor.org_unit, initially pointing to
709 -- the default calendar.
711 INSERT INTO acq.fiscal_calendar (
718 ALTER TABLE actor.org_unit ADD FOREIGN KEY
719 (fiscal_calendar) REFERENCES acq.fiscal_calendar( id )
720 DEFERRABLE INITIALLY DEFERRED;
722 CREATE TABLE acq.fiscal_year (
723 id SERIAL PRIMARY KEY,
724 calendar INT NOT NULL
725 REFERENCES acq.fiscal_calendar
727 DEFERRABLE INITIALLY DEFERRED,
729 year_begin TIMESTAMPTZ NOT NULL,
730 year_end TIMESTAMPTZ NOT NULL,
731 CONSTRAINT acq_fy_logical_key UNIQUE ( calendar, year ),
732 CONSTRAINT acq_fy_physical_key UNIQUE ( calendar, year_begin )
735 CREATE TABLE acq.edi_account ( -- similar tables can extend remote_account for other parts of EG
736 provider INT NOT NULL REFERENCES acq.provider (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
737 in_dir TEXT, -- incoming messages dir (probably different than config.remote_account.path, the outgoing dir)
740 ) INHERITS (config.remote_account);
742 -- We need a UNIQUE constraint here also, to support the FK from acq.provider.edi_default
743 ALTER TABLE acq.edi_account ADD PRIMARY KEY (id);
745 CREATE TABLE acq.edi_message (
746 id SERIAL PRIMARY KEY,
747 account INTEGER REFERENCES acq.edi_account(id)
748 DEFERRABLE INITIALLY DEFERRED,
750 create_time TIMESTAMPTZ NOT NULL DEFAULT now(),
751 translate_time TIMESTAMPTZ,
752 process_time TIMESTAMPTZ,
753 error_time TIMESTAMPTZ,
754 status TEXT NOT NULL DEFAULT 'new'
755 CONSTRAINT status_value CHECK
757 'new', -- needs to be translated
758 'translated', -- needs to be processed
759 'trans_error', -- error in translation step
760 'processed', -- needs to have remote_file deleted
761 'proc_error', -- error in processing step
762 'delete_error', -- error in deletion
763 'retry', -- need to retry
769 purchase_order INT REFERENCES acq.purchase_order
770 DEFERRABLE INITIALLY DEFERRED,
771 message_type TEXT NOT NULL CONSTRAINT valid_message_type CHECK
780 CREATE INDEX edi_message_account_status_idx ON acq.edi_message (account,status);
781 CREATE INDEX edi_message_po_idx ON acq.edi_message (purchase_order);
783 -- Note below that the primary key is NOT a SERIAL type. We will periodically truncate and rebuild
784 -- the table, assigning ids programmatically instead of using a sequence.
785 CREATE TABLE acq.debit_attribution (
786 id INT NOT NULL PRIMARY KEY,
787 fund_debit INT NOT NULL
788 REFERENCES acq.fund_debit
789 DEFERRABLE INITIALLY DEFERRED,
790 debit_amount NUMERIC NOT NULL,
791 funding_source_credit INT REFERENCES acq.funding_source_credit
792 DEFERRABLE INITIALLY DEFERRED,
793 credit_amount NUMERIC
796 CREATE INDEX acq_attribution_debit_idx
797 ON acq.debit_attribution( fund_debit );
799 CREATE INDEX acq_attribution_credit_idx
800 ON acq.debit_attribution( funding_source_credit );
804 CREATE TABLE acq.invoice_method (
805 code TEXT PRIMARY KEY,
806 name TEXT NOT NULL -- i18n-ize
809 CREATE TABLE acq.invoice_payment_method (
810 code TEXT PRIMARY KEY,
811 name TEXT NOT NULL -- i18n-ize
814 CREATE TABLE acq.invoice (
815 id SERIAL PRIMARY KEY,
816 receiver INT NOT NULL REFERENCES actor.org_unit (id),
817 provider INT NOT NULL REFERENCES acq.provider (id),
818 shipper INT NOT NULL REFERENCES acq.provider (id),
819 recv_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
820 recv_method TEXT NOT NULL REFERENCES acq.invoice_method (code) DEFAULT 'EDI',
821 inv_type TEXT, -- A "type" field is desired, but no idea what goes here
822 inv_ident TEXT NOT NULL, -- vendor-supplied invoice id/number
824 payment_method TEXT REFERENCES acq.invoice_payment_method (code)
825 DEFERRABLE INITIALLY DEFERRED,
827 complete BOOL NOT NULL DEFAULT FALSE,
828 CONSTRAINT inv_ident_once_per_provider UNIQUE(provider, inv_ident)
831 CREATE TABLE acq.invoice_entry (
832 id SERIAL PRIMARY KEY,
833 invoice INT NOT NULL REFERENCES acq.invoice (id) ON DELETE CASCADE,
834 purchase_order INT REFERENCES acq.purchase_order (id) ON UPDATE CASCADE ON DELETE SET NULL,
835 lineitem INT REFERENCES acq.lineitem (id) ON UPDATE CASCADE ON DELETE SET NULL,
836 inv_item_count INT NOT NULL, -- How many acqlids did they say they sent
837 phys_item_count INT, -- and how many did staff count
839 billed_per_item BOOL,
840 cost_billed NUMERIC(8,2),
841 actual_cost NUMERIC(8,2),
842 amount_paid NUMERIC (8,2)
845 CREATE INDEX ie_inv_idx on acq.invoice_entry (invoice);
846 CREATE INDEX ie_po_idx on acq.invoice_entry (purchase_order);
847 CREATE INDEX ie_li_idx on acq.invoice_entry (lineitem);
849 CREATE TABLE acq.invoice_item_type (
850 code TEXT PRIMARY KEY,
851 name TEXT NOT NULL, -- i18n-ize
852 prorate BOOL NOT NULL DEFAULT FALSE
855 CREATE TABLE acq.po_item (
856 id SERIAL PRIMARY KEY,
857 purchase_order INT REFERENCES acq.purchase_order (id)
858 ON UPDATE CASCADE ON DELETE SET NULL
859 DEFERRABLE INITIALLY DEFERRED,
860 fund_debit INT REFERENCES acq.fund_debit (id)
861 DEFERRABLE INITIALLY DEFERRED,
862 inv_item_type TEXT NOT NULL
863 REFERENCES acq.invoice_item_type (code)
864 DEFERRABLE INITIALLY DEFERRED,
868 estimated_cost NUMERIC(8,2),
869 fund INT REFERENCES acq.fund (id)
870 DEFERRABLE INITIALLY DEFERRED,
874 CREATE INDEX poi_po_idx ON acq.po_item (purchase_order);
876 CREATE TABLE acq.invoice_item ( -- for invoice-only debits: taxes/fees/non-bib items/etc
877 id SERIAL PRIMARY KEY,
878 invoice INT NOT NULL REFERENCES acq.invoice (id) ON UPDATE CASCADE ON DELETE CASCADE,
879 purchase_order INT REFERENCES acq.purchase_order (id) ON UPDATE CASCADE ON DELETE SET NULL,
880 fund_debit INT REFERENCES acq.fund_debit (id),
881 inv_item_type TEXT NOT NULL REFERENCES acq.invoice_item_type (code),
885 cost_billed NUMERIC(8,2),
886 actual_cost NUMERIC(8,2),
887 fund INT REFERENCES acq.fund (id)
888 DEFERRABLE INITIALLY DEFERRED,
889 amount_paid NUMERIC (8,2),
890 po_item INT REFERENCES acq.po_item (id)
891 DEFERRABLE INITIALLY DEFERRED,
895 CREATE INDEX ii_inv_idx on acq.invoice_item (invoice);
896 CREATE INDEX ii_po_idx on acq.invoice_item (purchase_order);
897 CREATE INDEX ii_poi_idx on acq.invoice_item (po_item);
900 CREATE TABLE acq.user_request_type (
901 id SERIAL PRIMARY KEY,
902 label TEXT NOT NULL UNIQUE -- i18n-ize
905 CREATE TABLE acq.user_request (
906 id SERIAL PRIMARY KEY,
907 usr INT NOT NULL REFERENCES actor.usr (id), -- requesting user
908 hold BOOL NOT NULL DEFAULT TRUE,
910 pickup_lib INT NOT NULL REFERENCES actor.org_unit (id), -- pickup lib
911 holdable_formats TEXT, -- nullable, for use in hold creation
913 email_notify BOOL NOT NULL DEFAULT TRUE,
914 lineitem INT REFERENCES acq.lineitem (id) ON DELETE CASCADE,
915 eg_bib BIGINT REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
916 request_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- when they requested it
917 need_before TIMESTAMPTZ, -- don't create holds after this
920 request_type INT NOT NULL REFERENCES acq.user_request_type (id),
932 cancel_reason INT REFERENCES acq.cancel_reason( id )
933 DEFERRABLE INITIALLY DEFERRED
939 CREATE TYPE acq.flat_lineitem_holding_subfield AS (lineitem int, holding int, subfield text, data text);
940 CREATE OR REPLACE FUNCTION acq.extract_holding_attr_table (lineitem int, tag text) RETURNS SETOF acq.flat_lineitem_holding_subfield AS $$
943 lida acq.flat_lineitem_holding_subfield%ROWTYPE;
946 SELECT COUNT(*) INTO counter
947 FROM oils_xpath_table(
951 '//*[@tag="' || tag || '"]',
953 ) as t(i int,c text);
955 FOR i IN 1 .. counter LOOP
958 FROM ( SELECT id,i,t,v
959 FROM oils_xpath_table(
963 '//*[@tag="' || tag || '"][position()=' || i || ']/*/@code|' ||
964 '//*[@tag="' || tag || '"][position()=' || i || ']/*[@code]',
966 ) as t(id int,t text,v text)
977 CREATE TYPE acq.flat_lineitem_detail AS (lineitem int, holding int, attr text, data text);
978 CREATE OR REPLACE FUNCTION acq.extract_provider_holding_data ( lineitem_i int ) RETURNS SETOF acq.flat_lineitem_detail AS $$
982 lida acq.flat_lineitem_detail%ROWTYPE;
984 SELECT provider INTO prov_i FROM acq.lineitem WHERE id = lineitem_i;
985 IF NOT FOUND THEN RETURN; END IF;
987 SELECT holding_tag INTO tag_t FROM acq.provider WHERE id = prov_i;
988 IF NOT FOUND OR tag_t IS NULL THEN RETURN; END IF;
995 FROM acq.extract_holding_attr_table( lineitem_i, tag_t ) h
996 JOIN acq.provider_holding_subfield_map a USING (subfield)
997 WHERE a.provider = prov_i
1004 $$ LANGUAGE PLPGSQL;
1006 -- select * from acq.extract_provider_holding_data(699);
1008 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$
1009 SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
1012 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field_set ( BIGINT, TEXT, TEXT) RETURNS SETOF TEXT AS $$
1013 SELECT extract_marc_field_set('acq.lineitem', $1, $2, $3);
1018 CREATE OR REPLACE FUNCTION public.extract_bib_marc_field ( BIGINT, TEXT ) RETURNS TEXT AS $$
1019 SELECT public.extract_marc_field('biblio.record_entry', $1, $2);
1022 CREATE OR REPLACE FUNCTION public.extract_authority_marc_field ( BIGINT, TEXT ) RETURNS TEXT AS $$
1023 SELECT public.extract_marc_field('authority.record_entry', $1, $2);
1027 -- INSERT INTO acq.lineitem_provider_attr_definition ( provider, code, description, xpath ) VALUES (1,'price','Price','//*[@tag="020" or @tag="022"]/*[@code="a"][1]');
1030 Suggested vendor fields:
1038 CREATE OR REPLACE FUNCTION public.ingest_acq_marc ( ) RETURNS TRIGGER AS $function$
1047 FOR adef IN SELECT *,tableoid FROM acq.lineitem_attr_definition LOOP
1049 SELECT relname::TEXT INTO atype FROM pg_class WHERE oid = adef.tableoid;
1051 IF (atype NOT IN ('lineitem_usr_attr_definition','lineitem_local_attr_definition')) THEN
1052 IF (atype = 'lineitem_provider_attr_definition') THEN
1053 SELECT provider INTO prov FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
1054 CONTINUE WHEN NEW.provider IS NULL OR prov <> NEW.provider;
1057 IF (atype = 'lineitem_provider_attr_definition') THEN
1058 SELECT xpath INTO xpath_string FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
1059 ELSIF (atype = 'lineitem_marc_attr_definition') THEN
1060 SELECT xpath INTO xpath_string FROM acq.lineitem_marc_attr_definition WHERE id = adef.id;
1061 ELSIF (atype = 'lineitem_generated_attr_definition') THEN
1062 SELECT xpath INTO xpath_string FROM acq.lineitem_generated_attr_definition WHERE id = adef.id;
1065 xpath_string := REGEXP_REPLACE(xpath_string,$re$//?text\(\)$$re$,'');
1067 IF (adef.code = 'title' OR adef.code = 'author') THEN
1068 -- title and author should not be split
1069 -- FIXME: once oils_xpath can grok XPATH 2.0 functions, we can use
1070 -- string-join in the xpath and remove this special case
1071 SELECT extract_acq_marc_field(id, xpath_string, adef.remove) INTO value FROM acq.lineitem WHERE id = NEW.id;
1072 IF (value IS NOT NULL AND value <> '') THEN
1073 INSERT INTO acq.lineitem_attr (lineitem, definition, attr_type, attr_name, attr_value)
1074 VALUES (NEW.id, adef.id, atype, adef.code, value);
1079 -- each application of the regex may produce multiple values
1081 SELECT * FROM extract_acq_marc_field_set(
1082 NEW.id, xpath_string || '[' || pos || ']', adef.remove)
1085 IF (value IS NOT NULL AND value <> '') THEN
1086 INSERT INTO acq.lineitem_attr
1087 (lineitem, definition, attr_type, attr_name, attr_value)
1088 VALUES (NEW.id, adef.id, atype, adef.code, value);
1106 $function$ LANGUAGE PLPGSQL;
1108 CREATE OR REPLACE FUNCTION public.cleanup_acq_marc ( ) RETURNS TRIGGER AS $$
1110 IF TG_OP = 'UPDATE' THEN
1111 DELETE FROM acq.lineitem_attr
1112 WHERE lineitem = OLD.id AND attr_type IN ('lineitem_provider_attr_definition', 'lineitem_marc_attr_definition','lineitem_generated_attr_definition');
1115 DELETE FROM acq.lineitem_attr WHERE lineitem = OLD.id;
1119 $$ LANGUAGE PLPGSQL;
1121 CREATE TRIGGER cleanup_lineitem_trigger
1122 BEFORE UPDATE OR DELETE ON acq.lineitem
1123 FOR EACH ROW EXECUTE PROCEDURE public.cleanup_acq_marc();
1125 CREATE TRIGGER ingest_lineitem_trigger
1126 AFTER INSERT OR UPDATE ON acq.lineitem
1127 FOR EACH ROW EXECUTE PROCEDURE public.ingest_acq_marc();
1129 CREATE OR REPLACE FUNCTION acq.exchange_ratio ( from_ex TEXT, to_ex TEXT ) RETURNS NUMERIC AS $$
1133 IF from_ex = to_ex THEN
1137 SELECT ratio INTO rat FROM acq.exchange_rate WHERE from_currency = from_ex AND to_currency = to_ex;
1142 SELECT ratio INTO rat FROM acq.exchange_rate WHERE from_currency = to_ex AND to_currency = from_ex;
1151 $$ LANGUAGE PLPGSQL;
1153 CREATE OR REPLACE FUNCTION acq.exchange_ratio ( TEXT, TEXT, NUMERIC ) RETURNS NUMERIC AS $$
1154 SELECT $3 * acq.exchange_ratio($1, $2);
1157 CREATE OR REPLACE FUNCTION acq.find_bad_fy()
1159 Examine the acq.fiscal_year table, comparing successive years.
1160 Report any inconsistencies, i.e. years that overlap, have gaps
1161 between them, or are out of sequence.
1163 RETURNS SETOF RECORD AS $$
1187 ELSIF curr_year.calendar = prev_year.calendar THEN
1188 IF curr_year.year_begin > prev_year.year_end THEN
1189 -- This ugly kludge works around the fact that older
1190 -- versions of PostgreSQL don't support RETURN QUERY SELECT
1191 FOR return_rec IN SELECT
1194 'Gap between fiscal years'::TEXT
1196 RETURN NEXT return_rec;
1198 ELSIF curr_year.year_begin < prev_year.year_end THEN
1199 FOR return_rec IN SELECT
1202 'Overlapping fiscal years'::TEXT
1204 RETURN NEXT return_rec;
1206 ELSIF curr_year.year < prev_year.year THEN
1207 FOR return_rec IN SELECT
1210 'Fiscal years out of order'::TEXT
1212 RETURN NEXT return_rec;
1217 prev_year := curr_year;
1222 $$ LANGUAGE plpgsql;
1224 CREATE OR REPLACE FUNCTION acq.transfer_fund(
1226 old_amount IN NUMERIC, -- in currency of old fund
1228 new_amount IN NUMERIC, -- in currency of new fund
1230 xfer_note IN TEXT -- to be recorded in acq.fund_transfer
1231 -- ,funding_source_in IN INT -- if user wants to specify a funding source (see notes)
1232 ) RETURNS VOID AS $$
1233 /* -------------------------------------------------------------------------------
1235 Function to transfer money from one fund to another.
1237 A transfer is represented as a pair of entries in acq.fund_allocation, with a
1238 negative amount for the old (losing) fund and a positive amount for the new
1239 (gaining) fund. In some cases there may be more than one such pair of entries
1240 in order to pull the money from different funding sources, or more specifically
1241 from different funding source credits. For each such pair there is also an
1242 entry in acq.fund_transfer.
1244 Since funding_source is a non-nullable column in acq.fund_allocation, we must
1245 choose a funding source for the transferred money to come from. This choice
1246 must meet two constraints, so far as possible:
1248 1. The amount transferred from a given funding source must not exceed the
1249 amount allocated to the old fund by the funding source. To that end we
1250 compare the amount being transferred to the amount allocated.
1252 2. We shouldn't transfer money that has already been spent or encumbered, as
1253 defined by the funding attribution process. We attribute expenses to the
1254 oldest funding source credits first. In order to avoid transferring that
1255 attributed money, we reverse the priority, transferring from the newest funding
1256 source credits first. There can be no guarantee that this approach will
1257 avoid overcommitting a fund, but no other approach can do any better.
1259 In this context the age of a funding source credit is defined by the
1260 deadline_date for credits with deadline_dates, and by the effective_date for
1261 credits without deadline_dates, with the proviso that credits with deadline_dates
1262 are all considered "older" than those without.
1266 In the signature for this function, there is one last parameter commented out,
1267 named "funding_source_in". Correspondingly, the WHERE clause for the query
1268 driving the main loop has an OR clause commented out, which references the
1269 funding_source_in parameter.
1271 If these lines are uncommented, this function will allow the user optionally to
1272 restrict a fund transfer to a specified funding source. If the source
1273 parameter is left NULL, then there will be no such restriction.
1275 ------------------------------------------------------------------------------- */
1277 same_currency BOOLEAN;
1278 currency_ratio NUMERIC;
1279 old_fund_currency TEXT;
1280 old_remaining NUMERIC; -- in currency of old fund
1281 new_fund_currency TEXT;
1282 new_fund_active BOOLEAN;
1283 new_remaining NUMERIC; -- in currency of new fund
1284 curr_old_amt NUMERIC; -- in currency of old fund
1285 curr_new_amt NUMERIC; -- in currency of new fund
1286 source_addition NUMERIC; -- in currency of funding source
1287 source_deduction NUMERIC; -- in currency of funding source
1288 orig_allocated_amt NUMERIC; -- in currency of funding source
1289 allocated_amt NUMERIC; -- in currency of fund
1295 IF old_fund IS NULL THEN
1296 RAISE EXCEPTION 'acq.transfer_fund: old fund id is NULL';
1299 IF old_amount IS NULL THEN
1300 RAISE EXCEPTION 'acq.transfer_fund: amount to transfer is NULL';
1303 -- The new fund and its amount must be both NULL or both not NULL.
1305 IF new_fund IS NOT NULL AND new_amount IS NULL THEN
1306 RAISE EXCEPTION 'acq.transfer_fund: amount to transfer to receiving fund is NULL';
1309 IF new_fund IS NULL AND new_amount IS NOT NULL THEN
1310 RAISE EXCEPTION 'acq.transfer_fund: receiving fund is NULL, its amount is not NULL';
1313 IF user_id IS NULL THEN
1314 RAISE EXCEPTION 'acq.transfer_fund: user id is NULL';
1317 -- Initialize the amounts to be transferred, each denominated
1318 -- in the currency of its respective fund. They will be
1319 -- reduced on each iteration of the loop.
1321 old_remaining := old_amount;
1322 new_remaining := new_amount;
1324 -- RAISE NOTICE 'Transferring % in fund % to % in fund %',
1325 -- old_amount, old_fund, new_amount, new_fund;
1327 -- Get the currency types of the old and new funds.
1338 IF old_fund_currency IS NULL THEN
1339 RAISE EXCEPTION 'acq.transfer_fund: old fund id % is not defined', old_fund;
1342 IF new_fund IS NOT NULL THEN
1354 IF new_fund_currency IS NULL THEN
1355 RAISE EXCEPTION 'acq.transfer_fund: new fund id % is not defined', new_fund;
1356 ELSIF NOT new_fund_active THEN
1358 -- No point in putting money into a fund from whence you can't spend it
1360 RAISE EXCEPTION 'acq.transfer_fund: new fund id % is inactive', new_fund;
1363 IF new_amount = old_amount THEN
1364 same_currency := true;
1365 currency_ratio := 1;
1368 -- We'll have to translate currency between funds. We presume that
1369 -- the calling code has already applied an appropriate exchange rate,
1370 -- so we'll apply the same conversion to each sub-transfer.
1372 same_currency := false;
1373 currency_ratio := new_amount / old_amount;
1377 -- Identify the funding source(s) from which we want to transfer the money.
1378 -- The principle is that we want to transfer the newest money first, because
1379 -- we spend the oldest money first. The priority for spending is defined
1380 -- by a sort of the view acq.ordered_funding_source_credit.
1385 ofsc.funding_source,
1387 ofsc.amount * acq.exchange_ratio( fs.currency_type, old_fund_currency )
1391 acq.ordered_funding_source_credit AS ofsc,
1392 acq.funding_source fs
1394 ofsc.funding_source = fs.id
1395 and ofsc.funding_source IN
1397 SELECT funding_source
1398 FROM acq.fund_allocation
1399 WHERE fund = old_fund
1403 -- ofsc.funding_source = funding_source_in
1404 -- OR funding_source_in IS NULL
1407 ofsc.sort_priority desc,
1408 ofsc.sort_date desc,
1412 -- Determine how much money the old fund got from this funding source,
1413 -- denominated in the currency types of the source and of the fund.
1414 -- This result may reflect transfers from previous iterations.
1417 COALESCE( sum( amount ), 0 ),
1418 COALESCE( sum( amount )
1419 * acq.exchange_ratio( source.currency_type, old_fund_currency ), 0 )
1421 orig_allocated_amt, -- in currency of the source
1422 allocated_amt -- in currency of the old fund
1427 and funding_source = source.funding_source;
1429 -- Determine how much to transfer from this credit, in the currency
1430 -- of the fund. Begin with the amount remaining to be attributed:
1432 curr_old_amt := old_remaining;
1434 -- Can't attribute more than was allocated from the fund:
1436 IF curr_old_amt > allocated_amt THEN
1437 curr_old_amt := allocated_amt;
1440 -- Can't attribute more than the amount of the current credit:
1442 IF curr_old_amt > source.converted_amt THEN
1443 curr_old_amt := source.converted_amt;
1446 curr_old_amt := trunc( curr_old_amt, 2 );
1448 old_remaining := old_remaining - curr_old_amt;
1450 -- Determine the amount to be deducted, if any,
1451 -- from the old allocation.
1453 IF old_remaining > 0 THEN
1455 -- In this case we're using the whole allocation, so use that
1456 -- amount directly instead of applying a currency translation
1457 -- and thereby inviting round-off errors.
1459 source_deduction := - orig_allocated_amt;
1461 source_deduction := trunc(
1462 ( - curr_old_amt ) *
1463 acq.exchange_ratio( old_fund_currency, source.currency_type ),
1467 IF source_deduction <> 0 THEN
1469 -- Insert negative allocation for old fund in fund_allocation,
1470 -- converted into the currency of the funding source
1472 INSERT INTO acq.fund_allocation (
1479 source.funding_source,
1483 'Transfer to fund ' || new_fund
1487 IF new_fund IS NOT NULL THEN
1489 -- Determine how much to add to the new fund, in
1490 -- its currency, and how much remains to be added:
1492 IF same_currency THEN
1493 curr_new_amt := curr_old_amt;
1495 IF old_remaining = 0 THEN
1497 -- This is the last iteration, so nothing should be left
1499 curr_new_amt := new_remaining;
1502 curr_new_amt := trunc( curr_old_amt * currency_ratio, 2 );
1503 new_remaining := new_remaining - curr_new_amt;
1507 -- Determine how much to add, if any,
1508 -- to the new fund's allocation.
1510 IF old_remaining > 0 THEN
1512 -- In this case we're using the whole allocation, so use that amount
1513 -- amount directly instead of applying a currency translation and
1514 -- thereby inviting round-off errors.
1516 source_addition := orig_allocated_amt;
1517 ELSIF source.currency_type = old_fund_currency THEN
1519 -- In this case we don't need a round trip currency translation,
1520 -- thereby inviting round-off errors:
1522 source_addition := curr_old_amt;
1524 source_addition := trunc(
1526 acq.exchange_ratio( new_fund_currency, source.currency_type ),
1530 IF source_addition <> 0 THEN
1532 -- Insert positive allocation for new fund in fund_allocation,
1533 -- converted to the currency of the founding source
1535 INSERT INTO acq.fund_allocation (
1542 source.funding_source,
1546 'Transfer from fund ' || old_fund
1551 IF trunc( curr_old_amt, 2 ) <> 0
1552 OR trunc( curr_new_amt, 2 ) <> 0 THEN
1554 -- Insert row in fund_transfer, using amounts in the currency of the funds
1556 INSERT INTO acq.fund_transfer (
1563 funding_source_credit
1566 trunc( curr_old_amt, 2 ),
1568 trunc( curr_new_amt, 2 ),
1575 if old_remaining <= 0 THEN
1576 EXIT; -- Nothing more to be transferred
1580 $$ LANGUAGE plpgsql;
1582 CREATE OR REPLACE FUNCTION acq.attribute_debits() RETURNS VOID AS $$
1584 Function to attribute expenditures and encumbrances to funding source credits,
1585 and thereby to funding sources.
1587 Read the debits in chonological order, attributing each one to one or
1588 more funding source credits. Constraints:
1590 1. Don't attribute more to a credit than the amount of the credit.
1592 2. For a given fund, don't attribute more to a funding source than the
1593 source has allocated to that fund.
1595 3. Attribute debits to credits with deadlines before attributing them to
1596 credits without deadlines. Otherwise attribute to the earliest credits
1597 first, based on the deadline date when present, or on the effective date
1598 when there is no deadline. Use funding_source_credit.id as a tie-breaker.
1599 This ordering is defined by an ORDER BY clause on the view
1600 acq.ordered_funding_source_credit.
1602 Start by truncating the table acq.debit_attribution. Then insert a row
1603 into that table for each attribution. If a debit cannot be fully
1604 attributed, insert a row for the unattributable balance, with the
1605 funding_source_credit and credit_amount columns NULL.
1608 curr_fund_source_bal RECORD;
1609 seqno INT; -- sequence num for credits applicable to a fund
1610 fund_credit RECORD; -- current row in temp t_fund_credit table
1611 fc RECORD; -- used for loading t_fund_credit table
1612 sc RECORD; -- used for loading t_fund_credit table
1614 -- Used exclusively in the main loop:
1616 deb RECORD; -- current row from acq.fund_debit table
1617 curr_credit_bal RECORD; -- current row from temp t_credit table
1618 debit_balance NUMERIC; -- amount left to attribute for current debit
1619 conv_debit_balance NUMERIC; -- debit balance in currency of the fund
1620 attr_amount NUMERIC; -- amount being attributed, in currency of debit
1621 conv_attr_amount NUMERIC; -- amount being attributed, in currency of source
1622 conv_cred_balance NUMERIC; -- credit_balance in the currency of the fund
1623 conv_alloc_balance NUMERIC; -- allocated balance in the currency of the fund
1624 attrib_count INT; -- populates id of acq.debit_attribution
1627 -- Load a temporary table. For each combination of fund and funding source,
1628 -- load an entry with the total amount allocated to that fund by that source.
1629 -- This sum may reflect transfers as well as original allocations. We will
1630 -- reduce this balance whenever we attribute debits to it.
1632 CREATE TEMP TABLE t_fund_source_bal
1636 funding_source AS source,
1637 sum( amount ) AS balance
1646 CREATE INDEX t_fund_source_bal_idx
1647 ON t_fund_source_bal( fund, source );
1648 -------------------------------------------------------------------------------
1650 -- Load another temporary table. For each fund, load zero or more
1651 -- funding source credits from which that fund can get money.
1653 CREATE TEMP TABLE t_fund_credit (
1660 SELECT DISTINCT fund
1661 FROM acq.fund_allocation
1663 LOOP -- Loop over the funds
1669 acq.ordered_funding_source_credit AS ofsc
1671 ofsc.funding_source IN
1673 SELECT funding_source
1674 FROM acq.fund_allocation
1675 WHERE fund = fc.fund
1681 LOOP -- Add each credit to the list
1682 INSERT INTO t_fund_credit (
1691 --RAISE NOTICE 'Fund % credit %', fc.fund, sc.id;
1693 END LOOP; -- Loop over credits for a given fund
1694 END LOOP; -- Loop over funds
1696 CREATE INDEX t_fund_credit_idx
1697 ON t_fund_credit( fund, seq );
1698 -------------------------------------------------------------------------------
1700 -- Load yet another temporary table. This one is a list of funding source
1701 -- credits, with their balances. We shall reduce those balances as we
1702 -- attribute debits to them.
1704 CREATE TEMP TABLE t_credit
1708 fsc.funding_source AS source,
1709 fsc.amount AS balance,
1710 fs.currency_type AS currency_type
1712 acq.funding_source_credit AS fsc,
1713 acq.funding_source fs
1715 fsc.funding_source = fs.id
1718 CREATE INDEX t_credit_idx
1719 ON t_credit( credit );
1721 -------------------------------------------------------------------------------
1723 -- Now that we have loaded the lookup tables: loop through the debits,
1724 -- attributing each one to one or more funding source credits.
1726 truncate table acq.debit_attribution;
1744 --RAISE NOTICE 'Debit %, fund %', deb.id, deb.fund;
1746 debit_balance := deb.amount;
1748 -- Loop over the funding source credits that are eligible
1749 -- to pay for this debit
1761 --RAISE NOTICE ' Examining credit %', fund_credit.credit;
1763 -- Look up the balance for this credit. If it's zero, then
1764 -- it's not useful, so treat it as if you didn't find it.
1765 -- (Actually there shouldn't be any zero balances in the table,
1766 -- but we check just to make sure.)
1769 INTO curr_credit_bal
1772 credit = fund_credit.credit
1775 IF curr_credit_bal IS NULL THEN
1777 -- This credit is exhausted; try the next one.
1783 -- At this point we have an applicable credit with some money left.
1784 -- Now see if the relevant funding_source has any money left.
1786 -- Look up the balance of the allocation for this combination of
1787 -- fund and source. If you find such an entry, but it has a zero
1788 -- balance, then it's not useful, so treat it as unfound.
1789 -- (Actually there shouldn't be any zero balances in the table,
1790 -- but we check just to make sure.)
1793 INTO curr_fund_source_bal
1794 FROM t_fund_source_bal
1797 AND source = curr_credit_bal.source
1800 IF curr_fund_source_bal IS NULL THEN
1802 -- This fund/source doesn't exist or is already exhausted,
1803 -- so we can't use this credit. Go on to the next one.
1808 -- Convert the available balances to the currency of the fund
1810 conv_alloc_balance := curr_fund_source_bal.balance * acq.exchange_ratio(
1811 curr_credit_bal.currency_type, deb.currency_type );
1812 conv_cred_balance := curr_credit_bal.balance * acq.exchange_ratio(
1813 curr_credit_bal.currency_type, deb.currency_type );
1815 -- Determine how much we can attribute to this credit: the minimum
1816 -- of the debit amount, the fund/source balance, and the
1819 --RAISE NOTICE ' deb bal %', debit_balance;
1820 --RAISE NOTICE ' source % balance %', curr_credit_bal.source, conv_alloc_balance;
1821 --RAISE NOTICE ' credit % balance %', curr_credit_bal.credit, conv_cred_balance;
1823 conv_attr_amount := NULL;
1824 attr_amount := debit_balance;
1826 IF attr_amount > conv_alloc_balance THEN
1827 attr_amount := conv_alloc_balance;
1828 conv_attr_amount := curr_fund_source_bal.balance;
1830 IF attr_amount > conv_cred_balance THEN
1831 attr_amount := conv_cred_balance;
1832 conv_attr_amount := curr_credit_bal.balance;
1835 -- If we're attributing all of one of the balances, then that's how
1836 -- much we will deduct from the balances, and we already captured
1837 -- that amount above. Otherwise we must convert the amount of the
1838 -- attribution from the currency of the fund back to the currency of
1839 -- the funding source.
1841 IF conv_attr_amount IS NULL THEN
1842 conv_attr_amount := attr_amount * acq.exchange_ratio(
1843 deb.currency_type, curr_credit_bal.currency_type );
1846 -- Insert a row to record the attribution
1848 attrib_count := attrib_count + 1;
1849 INSERT INTO acq.debit_attribution (
1853 funding_source_credit,
1859 curr_credit_bal.credit,
1863 -- Subtract the attributed amount from the various balances
1865 debit_balance := debit_balance - attr_amount;
1866 curr_fund_source_bal.balance := curr_fund_source_bal.balance - conv_attr_amount;
1868 IF curr_fund_source_bal.balance <= 0 THEN
1870 -- This allocation is exhausted. Delete it so
1871 -- that we don't waste time looking at it again.
1873 DELETE FROM t_fund_source_bal
1875 fund = curr_fund_source_bal.fund
1876 AND source = curr_fund_source_bal.source;
1878 UPDATE t_fund_source_bal
1879 SET balance = balance - conv_attr_amount
1881 fund = curr_fund_source_bal.fund
1882 AND source = curr_fund_source_bal.source;
1885 IF curr_credit_bal.balance <= 0 THEN
1887 -- This funding source credit is exhausted. Delete it
1888 -- so that we don't waste time looking at it again.
1890 --DELETE FROM t_credit
1892 -- credit = curr_credit_bal.credit;
1894 DELETE FROM t_fund_credit
1896 credit = curr_credit_bal.credit;
1899 SET balance = curr_credit_bal.balance
1901 credit = curr_credit_bal.credit;
1904 -- Are we done with this debit yet?
1906 IF debit_balance <= 0 THEN
1907 EXIT; -- We've fully attributed this debit; stop looking at credits.
1909 END LOOP; -- End loop over credits
1911 IF debit_balance <> 0 THEN
1913 -- We weren't able to attribute this debit, or at least not
1914 -- all of it. Insert a row for the unattributed balance.
1916 attrib_count := attrib_count + 1;
1917 INSERT INTO acq.debit_attribution (
1921 funding_source_credit,
1931 END LOOP; -- End of loop over debits
1933 $$ LANGUAGE 'plpgsql';
1935 CREATE OR REPLACE FUNCTION acq.propagate_funds_by_org_tree(
1938 org_unit_id INTEGER,
1939 include_desc BOOL DEFAULT TRUE
1940 ) RETURNS VOID AS $$
1951 IF old_year IS NULL THEN
1952 RAISE EXCEPTION 'Input year argument is NULL';
1953 ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
1954 RAISE EXCEPTION 'Input year is out of range';
1957 IF user_id IS NULL THEN
1958 RAISE EXCEPTION 'Input user id argument is NULL';
1961 IF org_unit_id IS NULL THEN
1962 RAISE EXCEPTION 'Org unit id argument is NULL';
1964 SELECT TRUE INTO org_found
1966 WHERE id = org_unit_id;
1968 IF org_found IS NULL THEN
1969 RAISE EXCEPTION 'Org unit id is invalid';
1973 -- Loop over the applicable funds
1975 FOR old_fund in SELECT * FROM acq.fund
1979 AND ( ( include_desc AND org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
1980 OR (NOT include_desc AND org = org_unit_id ) )
1984 INSERT INTO acq.fund (
1992 balance_warning_percent,
1993 balance_stop_percent
1998 old_fund.currency_type,
2002 old_fund.balance_warning_percent,
2003 old_fund.balance_stop_percent
2005 RETURNING id INTO new_id;
2007 WHEN unique_violation THEN
2008 --RAISE NOTICE 'Fund % already propagated', old_fund.id;
2011 --RAISE NOTICE 'Propagating fund % to fund %',
2012 -- old_fund.code, new_id;
2015 $$ LANGUAGE plpgsql;
2017 CREATE OR REPLACE FUNCTION acq.propagate_funds_by_org_unit( old_year INTEGER, user_id INTEGER, org_unit_id INTEGER ) RETURNS VOID AS $$
2018 SELECT acq.propagate_funds_by_org_tree( $1, $2, $3, FALSE );
2021 CREATE OR REPLACE FUNCTION acq.rollover_funds_by_org_tree(
2024 org_unit_id INTEGER,
2025 encumb_only BOOL DEFAULT FALSE,
2026 include_desc BOOL DEFAULT TRUE
2027 ) RETURNS VOID AS $$
2031 new_year INT := old_year + 1;
2034 xfer_amount NUMERIC := 0;
2038 roll_distrib_forms BOOL;
2044 IF old_year IS NULL THEN
2045 RAISE EXCEPTION 'Input year argument is NULL';
2046 ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
2047 RAISE EXCEPTION 'Input year is out of range';
2050 IF user_id IS NULL THEN
2051 RAISE EXCEPTION 'Input user id argument is NULL';
2054 IF org_unit_id IS NULL THEN
2055 RAISE EXCEPTION 'Org unit id argument is NULL';
2058 -- Validate the org unit
2063 WHERE id = org_unit_id;
2065 IF org_found IS NULL THEN
2066 RAISE EXCEPTION 'Org unit id % is invalid', org_unit_id;
2067 ELSIF encumb_only THEN
2068 SELECT INTO perm_ous value::BOOL FROM
2069 actor.org_unit_ancestor_setting(
2070 'acq.fund.allow_rollover_without_money', org_unit_id
2072 IF NOT FOUND OR NOT perm_ous THEN
2073 RAISE EXCEPTION 'Encumbrance-only rollover not permitted at org %', org_unit_id;
2078 -- Loop over the propagable funds to identify the details
2079 -- from the old fund plus the id of the new one, if it exists.
2083 oldf.id AS old_fund,
2089 newf.id AS new_fund_id
2092 LEFT JOIN acq.fund AS newf
2093 ON ( oldf.code = newf.code )
2095 oldf.year = old_year
2097 AND newf.year = new_year
2098 AND ( ( include_desc AND oldf.org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
2099 OR (NOT include_desc AND oldf.org = org_unit_id ) )
2101 --RAISE NOTICE 'Processing fund %', roll_fund.old_fund;
2103 IF roll_fund.new_fund_id IS NULL THEN
2105 -- The old fund hasn't been propagated yet. Propagate it now.
2107 INSERT INTO acq.fund (
2115 balance_warning_percent,
2116 balance_stop_percent
2121 roll_fund.currency_type,
2125 roll_fund.balance_warning_percent,
2126 roll_fund.balance_stop_percent
2128 RETURNING id INTO new_fund;
2130 new_fund = roll_fund.new_fund_id;
2133 -- Determine the amount to transfer
2137 FROM acq.fund_spent_balance
2138 WHERE fund = roll_fund.old_fund;
2140 IF xfer_amount <> 0 THEN
2141 IF NOT encumb_only AND roll_fund.rollover THEN
2143 -- Transfer balance from old fund to new
2145 --RAISE NOTICE 'Transferring % from fund % to %', xfer_amount, roll_fund.old_fund, new_fund;
2147 PERFORM acq.transfer_fund(
2157 -- Transfer balance from old fund to the void
2159 -- RAISE NOTICE 'Transferring % from fund % to the void', xfer_amount, roll_fund.old_fund;
2161 PERFORM acq.transfer_fund(
2167 'Rollover into the void'
2172 IF roll_fund.rollover THEN
2174 -- Move any lineitems from the old fund to the new one
2175 -- where the associated debit is an encumbrance.
2177 -- Any other tables tying expenditure details to funds should
2178 -- receive similar treatment. At this writing there are none.
2180 UPDATE acq.lineitem_detail
2183 fund = roll_fund.old_fund -- this condition may be redundant
2189 fund = roll_fund.old_fund
2193 -- Move encumbrance debits from the old fund to the new fund
2195 UPDATE acq.fund_debit
2198 fund = roll_fund.old_fund
2202 -- Rollover distribution formulae funds
2203 SELECT INTO roll_distrib_forms value::BOOL FROM
2204 actor.org_unit_ancestor_setting(
2205 'acq.fund.rollover_distrib_forms', org_unit_id
2208 IF roll_distrib_forms THEN
2209 UPDATE acq.distribution_formula_entry
2210 SET fund = roll_fund.new_fund_id
2211 WHERE fund = roll_fund.old_fund;
2215 -- Mark old fund as inactive, now that we've closed it
2219 WHERE id = roll_fund.old_fund;
2222 $$ LANGUAGE plpgsql;
2226 CREATE OR REPLACE FUNCTION acq.rollover_funds_by_org_unit( old_year INTEGER, user_id INTEGER, org_unit_id INTEGER, encumb_only BOOL DEFAULT FALSE ) RETURNS VOID AS $$
2227 SELECT acq.rollover_funds_by_org_tree( $1, $2, $3, $4, FALSE );
2230 CREATE OR REPLACE VIEW acq.funding_source_credit_total AS
2231 SELECT funding_source,
2232 SUM(amount) AS amount
2233 FROM acq.funding_source_credit
2236 CREATE OR REPLACE VIEW acq.funding_source_allocation_total AS
2237 SELECT funding_source,
2238 SUM(a.amount)::NUMERIC(100,2) AS amount
2239 FROM acq.fund_allocation a
2242 CREATE OR REPLACE VIEW acq.funding_source_balance AS
2243 SELECT COALESCE(c.funding_source, a.funding_source) AS funding_source,
2244 SUM(COALESCE(c.amount,0.0) - COALESCE(a.amount,0.0))::NUMERIC(100,2) AS amount
2245 FROM acq.funding_source_credit_total c
2246 FULL JOIN acq.funding_source_allocation_total a USING (funding_source)
2249 CREATE OR REPLACE VIEW acq.fund_allocation_total AS
2251 SUM(a.amount * acq.exchange_ratio(s.currency_type, f.currency_type))::NUMERIC(100,2) AS amount
2252 FROM acq.fund_allocation a
2253 JOIN acq.fund f ON (a.fund = f.id)
2254 JOIN acq.funding_source s ON (a.funding_source = s.id)
2257 CREATE OR REPLACE VIEW acq.fund_debit_total AS
2258 SELECT fund.id AS fund,
2259 sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
2261 LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
2264 CREATE OR REPLACE VIEW acq.fund_encumbrance_total AS
2267 sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
2269 LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
2270 WHERE fund_debit.encumbrance GROUP BY fund.id;
2272 CREATE OR REPLACE VIEW acq.fund_spent_total AS
2273 SELECT fund.id AS fund,
2274 sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
2276 LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
2277 WHERE NOT fund_debit.encumbrance
2280 CREATE OR REPLACE VIEW acq.fund_combined_balance AS
2282 c.amount - COALESCE(d.amount, 0.0) AS amount
2283 FROM acq.fund_allocation_total c
2284 LEFT JOIN acq.fund_debit_total d USING (fund);
2286 CREATE OR REPLACE VIEW acq.fund_spent_balance AS
2288 c.amount - COALESCE(d.amount,0.0) AS amount
2289 FROM acq.fund_allocation_total c
2290 LEFT JOIN acq.fund_spent_total d USING (fund);
2292 -- For each fund: the total allocation from all sources, in the
2293 -- currency of the fund (or 0 if there are no allocations)
2295 CREATE VIEW acq.all_fund_allocation_total AS
2298 COALESCE( SUM( a.amount * acq.exchange_ratio(
2299 s.currency_type, f.currency_type))::numeric(100,2), 0 )
2303 LEFT JOIN acq.fund_allocation a
2305 LEFT JOIN acq.funding_source s
2306 ON a.funding_source = s.id
2310 -- For every fund: the total encumbrances (or 0 if none),
2311 -- in the currency of the fund.
2313 CREATE VIEW acq.all_fund_encumbrance_total AS
2316 COALESCE( encumb.amount, 0 ) AS amount
2322 sum( amount ) AS amount
2329 ON f.id = encumb.fund;
2331 -- For every fund: the total spent (or 0 if none),
2332 -- in the currency of the fund.
2334 CREATE VIEW acq.all_fund_spent_total AS
2337 COALESCE( spent.amount, 0 ) AS amount
2343 sum( amount ) AS amount
2350 ON f.id = spent.fund;
2352 -- For each fund: the amount not yet spent, in the currency
2353 -- of the fund. May include encumbrances.
2355 CREATE VIEW acq.all_fund_spent_balance AS
2358 c.amount - d.amount AS amount
2359 FROM acq.all_fund_allocation_total c
2360 LEFT JOIN acq.all_fund_spent_total d USING (fund);
2362 -- For each fund: the amount neither spent nor encumbered,
2363 -- in the currency of the fund
2365 CREATE VIEW acq.all_fund_combined_balance AS
2368 a.amount - COALESCE( c.amount, 0 ) AS amount
2370 acq.all_fund_allocation_total a
2374 SUM( amount ) AS amount
2379 ) AS c USING ( fund );
2381 CREATE TABLE acq.claim_type (
2382 id SERIAL PRIMARY KEY,
2383 org_unit INT NOT NULL REFERENCES actor.org_unit(id)
2384 DEFERRABLE INITIALLY DEFERRED,
2386 description TEXT NOT NULL,
2387 CONSTRAINT claim_type_once_per_org UNIQUE ( org_unit, code )
2390 CREATE TABLE acq.claim (
2391 id SERIAL PRIMARY KEY,
2392 type INT NOT NULL REFERENCES acq.claim_type
2393 DEFERRABLE INITIALLY DEFERRED,
2394 lineitem_detail BIGINT NOT NULL REFERENCES acq.lineitem_detail
2395 DEFERRABLE INITIALLY DEFERRED
2398 CREATE INDEX claim_lid_idx ON acq.claim( lineitem_detail );
2400 CREATE TABLE acq.claim_event (
2401 id BIGSERIAL PRIMARY KEY,
2402 type INT NOT NULL REFERENCES acq.claim_event_type
2403 DEFERRABLE INITIALLY DEFERRED,
2404 claim SERIAL NOT NULL REFERENCES acq.claim
2405 DEFERRABLE INITIALLY DEFERRED,
2406 event_date TIMESTAMPTZ NOT NULL DEFAULT now(),
2407 creator INT NOT NULL REFERENCES actor.usr
2408 DEFERRABLE INITIALLY DEFERRED,
2412 CREATE INDEX claim_event_claim_date_idx ON acq.claim_event( claim, event_date );
2414 -- And the serials version of claiming
2415 CREATE TABLE acq.serial_claim (
2416 id SERIAL PRIMARY KEY,
2417 type INT NOT NULL REFERENCES acq.claim_type
2418 DEFERRABLE INITIALLY DEFERRED,
2419 item BIGINT NOT NULL REFERENCES serial.item
2420 DEFERRABLE INITIALLY DEFERRED
2423 CREATE INDEX serial_claim_lid_idx ON acq.serial_claim( item );
2425 CREATE TABLE acq.serial_claim_event (
2426 id BIGSERIAL PRIMARY KEY,
2427 type INT NOT NULL REFERENCES acq.claim_event_type
2428 DEFERRABLE INITIALLY DEFERRED,
2429 claim SERIAL NOT NULL REFERENCES acq.serial_claim
2430 DEFERRABLE INITIALLY DEFERRED,
2431 event_date TIMESTAMPTZ NOT NULL DEFAULT now(),
2432 creator INT NOT NULL REFERENCES actor.usr
2433 DEFERRABLE INITIALLY DEFERRED,
2437 CREATE INDEX serial_claim_event_claim_date_idx ON acq.serial_claim_event( claim, event_date );
2439 CREATE OR REPLACE VIEW acq.lineitem_summary AS
2443 SELECT COUNT(lid.id)
2444 FROM acq.lineitem_detail lid
2445 WHERE lineitem = li.id
2448 SELECT COUNT(lid.id)
2449 FROM acq.lineitem_detail lid
2450 WHERE recv_time IS NOT NULL AND lineitem = li.id
2453 SELECT COUNT(lid.id)
2454 FROM acq.lineitem_detail lid
2455 JOIN acq.cancel_reason acqcr ON (acqcr.id = lid.cancel_reason)
2456 WHERE acqcr.keep_debits IS FALSE AND lineitem = li.id
2459 SELECT COUNT(lid.id)
2460 FROM acq.lineitem_detail lid
2461 JOIN acq.cancel_reason acqcr ON (acqcr.id = lid.cancel_reason)
2462 WHERE acqcr.keep_debits IS TRUE AND lineitem = li.id
2465 SELECT COUNT(lid.id)
2466 FROM acq.lineitem_detail lid
2467 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
2468 WHERE NOT debit.encumbrance AND lineitem = li.id
2471 SELECT COUNT(DISTINCT(lid.id))
2472 FROM acq.lineitem_detail lid
2473 JOIN acq.claim claim ON (claim.lineitem_detail = lid.id)
2474 WHERE lineitem = li.id
2477 SELECT (COUNT(lid.id) * li.estimated_unit_price)::NUMERIC(8,2)
2478 FROM acq.lineitem_detail lid
2479 WHERE lid.cancel_reason IS NULL AND lineitem = li.id
2480 ) AS estimated_amount,
2482 SELECT SUM(debit.amount)::NUMERIC(8,2)
2483 FROM acq.lineitem_detail lid
2484 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
2485 WHERE debit.encumbrance AND lineitem = li.id
2486 ) AS encumbrance_amount,
2488 SELECT SUM(debit.amount)::NUMERIC(8,2)
2489 FROM acq.lineitem_detail lid
2490 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
2491 WHERE NOT debit.encumbrance AND lineitem = li.id
2494 FROM acq.lineitem AS li;