]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/200.schema.acq.sql
LP#1479953: Add indexes to vqbr foreign key references
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 200.schema.acq.sql
1 /*
2  * Copyright (C) 2009  Georgia Public Library Service
3  * Scott McKellar <scott@esilibrary.com>
4  *
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.
9  *
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.
14  */
15
16 DROP SCHEMA IF EXISTS acq CASCADE;
17
18 BEGIN;
19
20 CREATE SCHEMA acq;
21
22
23 -- Tables
24
25
26 CREATE TABLE acq.currency_type (
27         code    TEXT PRIMARY KEY,
28         label   TEXT
29 );
30
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)
37 );
38
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,
43         name            TEXT         NOT NULL,
44         description     TEXT         NOT NULL,
45         CONSTRAINT name_once_per_org UNIQUE (org_unit, name)
46 );
47
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,
52         code           TEXT             NOT NULL,
53         description    TEXT             NOT NULL,
54         library_initiated BOOL          NOT NULL DEFAULT FALSE,
55         CONSTRAINT event_type_once_per_org UNIQUE ( org_unit, code )
56 );
57
58 CREATE TABLE acq.claim_policy_action (
59         id              SERIAL       PRIMARY KEY,
60         claim_policy    INT          NOT NULL REFERENCES acq.claim_policy
61                                  ON DELETE CASCADE
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)
67 );
68
69 CREATE TABLE acq.provider (
70     id                  SERIAL  PRIMARY KEY,
71     name                TEXT    NOT NULL,
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,
74     code                TEXT    NOT NULL,
75     holding_tag         TEXT,
76     san                 TEXT,
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,
80     url                 TEXT,
81     email               TEXT,
82     phone               TEXT,
83     fax_phone           TEXT,
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)
89 );
90
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)
97 );
98
99 CREATE TABLE acq.provider_address (
100         id              SERIAL  PRIMARY KEY,
101         valid           BOOL    NOT NULL DEFAULT TRUE,
102         address_type    TEXT,
103     provider    INT     NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
104         street1         TEXT    NOT NULL,
105         street2         TEXT,
106         city            TEXT    NOT NULL,
107         county          TEXT,
108         state           TEXT    NOT NULL,
109         country         TEXT    NOT NULL,
110         post_code       TEXT    NOT NULL,
111         fax_phone       TEXT
112 );
113
114 CREATE TABLE acq.provider_contact (
115         id              SERIAL  PRIMARY KEY,
116     provider    INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
117     name    TEXT NOT NULL,
118     role    TEXT, -- free-form.. e.g. "our sales guy"
119     email   TEXT,
120     phone   TEXT
121 );
122
123 CREATE TABLE acq.provider_contact_address (
124         id                      SERIAL  PRIMARY KEY,
125         valid                   BOOL    NOT NULL DEFAULT TRUE,
126         address_type    TEXT,
127         contact                 INT         NOT NULL REFERENCES acq.provider_contact (id) DEFERRABLE INITIALLY DEFERRED,
128         street1                 TEXT    NOT NULL,
129         street2                 TEXT,
130         city                    TEXT    NOT NULL,
131         county                  TEXT,
132         state                   TEXT    NOT NULL,
133         country                 TEXT    NOT NULL,
134         post_code               TEXT    NOT NULL,
135         fax_phone               TEXT
136 );
137
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(),
145         value           TEXT                    NOT NULL
146 );
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 );
150
151
152 CREATE TABLE acq.funding_source (
153         id              SERIAL  PRIMARY KEY,
154         name            TEXT    NOT NULL,
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,
157         code            TEXT    UNIQUE,
158         CONSTRAINT funding_source_name_once_per_owner UNIQUE (name,owner)
159 );
160
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,
165         note           TEXT,
166         deadline_date  TIMESTAMPTZ,
167         effective_date TIMESTAMPTZ NOT NULL default now()
168 );
169
170 CREATE VIEW acq.ordered_funding_source_credit AS
171     SELECT
172         CASE WHEN deadline_date IS NULL THEN
173             2
174         ELSE
175             1
176         END AS sort_priority,
177         CASE WHEN deadline_date IS NULL THEN
178             effective_date
179         ELSE
180             deadline_date
181         END AS sort_date,
182         id,
183         funding_source,
184         amount,
185         note
186     FROM
187         acq.funding_source_credit;
188
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.
195
196 The first column reflects the principle that we should spend
197 money with deadlines before spending money without deadlines.
198
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.
204
205 The third column is a tie breaker to ensure a consistent
206 ordering.
207 $$;
208
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,
212     name            TEXT    NOT NULL,
213     year            INT     NOT NULL DEFAULT EXTRACT( YEAR FROM NOW() ),
214     currency_type   TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
215     code            TEXT,
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 )
224 );
225
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()
236 );
237
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,
244     note        TEXT,
245         create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
246 );
247 CREATE INDEX fund_alloc_allocator_idx ON acq.fund_allocation ( allocator );
248
249 CREATE TABLE acq.fund_allocation_percent
250 (
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,
256     fund_code            TEXT,
257     percent              NUMERIC           NOT NULL,
258     allocator            INTEGER           NOT NULL REFERENCES actor.usr
259                                                DEFERRABLE INITIALLY DEFERRED,
260     note                 TEXT,
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 )
264 );
265
266 -- Trigger function to validate combination of org_unit and fund_code
267
268 CREATE OR REPLACE FUNCTION acq.fund_alloc_percent_val()
269 RETURNS TRIGGER AS $$
270 --
271 DECLARE
272 --
273 dummy int := 0;
274 --
275 BEGIN
276     SELECT
277         1
278     INTO
279         dummy
280     FROM
281         acq.fund
282     WHERE
283         org = NEW.org
284         AND code = NEW.fund_code
285         LIMIT 1;
286     --
287     IF dummy = 1 then
288         RETURN NEW;
289     ELSE
290         RAISE EXCEPTION 'No fund exists for org % and code %', NEW.org, NEW.fund_code;
291     END IF;
292 END;
293 $$ LANGUAGE plpgsql;
294
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();
298
299 -- To do: trigger to verify that percentages don't add up to more than 100
300
301 CREATE OR REPLACE FUNCTION acq.fap_limit_100()
302 RETURNS TRIGGER AS $$
303 DECLARE
304 --
305 total_percent numeric;
306 --
307 BEGIN
308     SELECT
309         sum( percent )
310     INTO
311         total_percent
312     FROM
313         acq.fund_allocation_percent AS fap
314     WHERE
315         fap.funding_source = NEW.funding_source;
316     --
317     IF total_percent > 100 THEN
318         RAISE EXCEPTION 'Total percentages exceed 100 for funding_source %',
319             NEW.funding_source;
320     ELSE
321         RETURN NEW;
322     END IF;
323 END;
324 $$ LANGUAGE plpgsql;
325
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();
329
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,
336         name            TEXT                            NOT NULL,
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)
340 );
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 );
344
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,
349         label         TEXT              NOT NULL,
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 )
353 );
354
355 -- Reserve ids 1-999 for stock reasons
356 -- Reserve ids 1000-1999 for EDI reasons
357 -- 2000+ are available for staff to create
358
359 SELECT SETVAL('acq.cancel_reason_id_seq'::TEXT, 2000);
360
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,
372         name                    TEXT                                    NOT NULL,
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'))
377 );
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 );
384
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:
387
388 CREATE OR REPLACE FUNCTION acq.purchase_order_name_default () RETURNS TRIGGER 
389 AS $$
390 BEGIN
391         IF NEW.name IS NULL THEN
392                 NEW.name := NEW.id::TEXT;
393         END IF;
394
395         RETURN NEW;
396 END;
397 $$ LANGUAGE PLPGSQL;
398
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 ();
402
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.
407
408 CREATE OR REPLACE FUNCTION acq.po_org_name_date_unique () RETURNS TRIGGER 
409 AS $$
410 DECLARE
411         collision INT;
412 BEGIN
413         --
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
416         --
417         IF NEW.order_date IS NULL THEN
418                 RETURN NEW;
419         END IF;
420         --
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.
424         --
425         SELECT 1 INTO collision
426         FROM acq.purchase_order
427         WHERE
428                 ordering_agency = NEW.ordering_agency
429                 AND name = NEW.name
430                 AND order_date >= date_trunc( 'day', NEW.order_date )
431                 AND order_date <  date_trunc( 'day', NEW.order_date ) + '1 day'::INTERVAL
432                 AND id <> NEW.id;
433         --
434         IF collision IS NULL THEN
435                 -- okay, no collision
436                 RETURN NEW;
437         ELSE
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;
441         END IF;
442 END;
443 $$ LANGUAGE PLPGSQL;
444
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 ();
448
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(),
456         value           TEXT                    NOT NULL,
457         vendor_public BOOLEAN       NOT NULL DEFAULT FALSE
458 );
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 );
462
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(),
474         marc                TEXT                        NOT NULL,
475         eg_bib_id           BIGINT                      REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
476         source_label        TEXT,
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)
486 );
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 );
492 CREATE INDEX li_queued_record ON acq.lineitem ( queued_record );
493
494 CREATE TABLE acq.lineitem_alert_text (
495     id               SERIAL         PRIMARY KEY,
496     code             TEXT           NOT NULL,
497     description      TEXT,
498         owning_lib       INT            NOT NULL
499                                         REFERENCES actor.org_unit(id)
500                                         DEFERRABLE INITIALLY DEFERRED,
501         CONSTRAINT alert_one_code_per_org UNIQUE (code, owning_lib)
502 );
503
504 CREATE TABLE acq.lineitem_note (
505         id              SERIAL                          PRIMARY KEY,
506         lineitem        INT                             NOT NULL REFERENCES acq.lineitem (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
507         creator         INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
508         editor          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
509         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
510         edit_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
511         value           TEXT                    NOT NULL,
512         alert_text      INT                                              REFERENCES acq.lineitem_alert_text(id)
513                                                                                  DEFERRABLE INITIALLY DEFERRED,
514         vendor_public BOOLEAN       NOT NULL DEFAULT FALSE
515 );
516 CREATE INDEX li_note_li_idx ON acq.lineitem_note (lineitem);
517 CREATE INDEX li_note_creator_idx  ON acq.lineitem_note ( creator );
518 CREATE INDEX li_note_editor_idx   ON acq.lineitem_note ( editor );
519
520 CREATE TABLE acq.lineitem_detail (
521     id          BIGSERIAL       PRIMARY KEY,
522     lineitem    INT         NOT NULL REFERENCES acq.lineitem (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
523     fund        INT         REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
524     fund_debit  INT         REFERENCES acq.fund_debit (id) DEFERRABLE INITIALLY DEFERRED,
525     eg_copy_id  BIGINT,     -- REFERENCES asset.copy (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
526     barcode     TEXT,
527     cn_label    TEXT,
528     note        TEXT,
529     collection_code TEXT,
530     circ_modifier   TEXT    REFERENCES config.circ_modifier (code) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
531     owning_lib  INT         REFERENCES actor.org_unit (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
532     location    INT         REFERENCES asset.copy_location (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
533     recv_time   TIMESTAMP WITH TIME ZONE,
534         receiver                INT         REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
535         cancel_reason   INT     REFERENCES acq.cancel_reason( id ) DEFERRABLE INITIALLY DEFERRED
536 );
537
538 CREATE INDEX li_detail_li_idx ON acq.lineitem_detail (lineitem);
539
540 CREATE TABLE acq.lineitem_attr_definition (
541         id              BIGSERIAL       PRIMARY KEY,
542         code            TEXT            NOT NULL,
543         description     TEXT            NOT NULL,
544         remove          TEXT            NOT NULL DEFAULT '',
545         ident           BOOL            NOT NULL DEFAULT FALSE
546 );
547
548 CREATE TABLE acq.lineitem_marc_attr_definition (
549         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
550         xpath           TEXT            NOT NULL
551 ) INHERITS (acq.lineitem_attr_definition);
552
553 CREATE TABLE acq.lineitem_provider_attr_definition (
554         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
555         xpath           TEXT            NOT NULL,
556         provider        INT     NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED
557 ) INHERITS (acq.lineitem_attr_definition);
558
559 CREATE TABLE acq.lineitem_generated_attr_definition (
560         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
561         xpath           TEXT            NOT NULL
562 ) INHERITS (acq.lineitem_attr_definition);
563
564 CREATE TABLE acq.lineitem_usr_attr_definition (
565         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
566         usr             INT     NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED
567 ) INHERITS (acq.lineitem_attr_definition);
568 CREATE INDEX li_usr_attr_def_usr_idx  ON acq.lineitem_usr_attr_definition ( usr );
569
570 CREATE TABLE acq.lineitem_local_attr_definition (
571         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq')
572 ) INHERITS (acq.lineitem_attr_definition);
573
574 CREATE TABLE acq.lineitem_attr (
575         id              BIGSERIAL       PRIMARY KEY,
576         definition      BIGINT          NOT NULL,
577         lineitem        BIGINT          NOT NULL REFERENCES acq.lineitem (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
578         attr_type       TEXT            NOT NULL,
579         attr_name       TEXT            NOT NULL,
580         attr_value      TEXT            NOT NULL,
581         order_ident     BOOLEAN         NOT NULL DEFAULT FALSE
582 );
583
584 CREATE INDEX li_attr_li_idx ON acq.lineitem_attr (lineitem);
585 CREATE INDEX li_attr_value_idx ON acq.lineitem_attr (attr_value);
586 CREATE INDEX li_attr_definition_idx ON acq.lineitem_attr (definition);
587
588
589 -- Seed data
590
591
592 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('title','Title of work','//*[@tag="245"]/*[contains("abcmnopr",@code)]');
593 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)]');
594 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('language','Language of work','//*[@tag="240"]/*[@code="l"][1]');
595 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('pagination','Pagination','//*[@tag="300"]/*[@code="a"][1]');
596 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('isbn','ISBN','//*[@tag="020"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
597 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('issn','ISSN','//*[@tag="022"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
598 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('upc', 'UPC', '//*[@tag="024" and @ind1="1"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
599 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('price','Price','//*[@tag="020" or @tag="022"]/*[@code="c"][1]');
600 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('identifier','Identifier','//*[@tag="001"]');
601 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('publisher','Publisher','//*[@tag="260"]/*[@code="b"][1]');
602 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('pubdate','Publication Date','//*[@tag="260"]/*[@code="c"][1]');
603 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('edition','Edition','//*[@tag="250"]/*[@code="a"][1]');
604
605 INSERT INTO acq.lineitem_local_attr_definition ( code, description ) VALUES ('estimated_price', 'Estimated Price');
606
607
608 CREATE TABLE acq.distribution_formula (
609         id              SERIAL PRIMARY KEY,
610         owner   INT NOT NULL
611                         REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
612         name    TEXT NOT NULL,
613         skip_count      INT NOT NULL DEFAULT 0,
614         CONSTRAINT acqdf_name_once_per_owner UNIQUE (name, owner)
615 );
616
617 CREATE TABLE acq.distribution_formula_entry (
618         id                      SERIAL PRIMARY KEY,
619         formula         INTEGER NOT NULL REFERENCES acq.distribution_formula(id)
620                                 ON DELETE CASCADE
621                                 DEFERRABLE INITIALLY DEFERRED,
622         position        INTEGER NOT NULL,
623         item_count      INTEGER NOT NULL,
624         owning_lib      INTEGER REFERENCES actor.org_unit(id)
625                                 DEFERRABLE INITIALLY DEFERRED,
626         location        INTEGER REFERENCES asset.copy_location(id),
627         fund            INTEGER REFERENCES acq.fund (id),
628         circ_modifier   TEXT REFERENCES config.circ_modifier (code),
629         collection_code TEXT,
630         CONSTRAINT acqdfe_lib_once_per_formula UNIQUE( formula, position ),
631         CONSTRAINT acqdfe_must_be_somewhere
632                                 CHECK( owning_lib IS NOT NULL OR location IS NOT NULL ) 
633 );
634
635 CREATE TABLE acq.distribution_formula_application (
636     id BIGSERIAL PRIMARY KEY,
637     creator INT NOT NULL REFERENCES actor.usr(id) DEFERRABLE INITIALLY DEFERRED,
638     create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
639     formula INT NOT NULL
640         REFERENCES acq.distribution_formula(id) DEFERRABLE INITIALLY DEFERRED,
641     lineitem INT NOT NULL
642         REFERENCES acq.lineitem(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
643 );
644
645 CREATE INDEX acqdfa_df_idx
646     ON acq.distribution_formula_application(formula);
647 CREATE INDEX acqdfa_li_idx
648     ON acq.distribution_formula_application(lineitem);
649 CREATE INDEX acqdfa_creator_idx
650     ON acq.distribution_formula_application(creator);
651
652 CREATE TABLE acq.fund_tag (
653         id              SERIAL PRIMARY KEY,
654         owner   INT NOT NULL
655                         REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
656         name    TEXT NOT NULL,
657         CONSTRAINT acqft_tag_once_per_owner UNIQUE (name, owner)
658 );
659
660 CREATE TABLE acq.fund_tag_map (
661         id                      SERIAL PRIMARY KEY,
662         fund            INTEGER NOT NULL REFERENCES acq.fund(id)
663                                 DEFERRABLE INITIALLY DEFERRED,
664         tag         INTEGER REFERENCES acq.fund_tag(id)
665                                 ON DELETE CASCADE
666                                 DEFERRABLE INITIALLY DEFERRED,
667         CONSTRAINT acqftm_fund_once_per_tag UNIQUE( fund, tag )
668 );
669
670 CREATE TABLE acq.fund_transfer (
671     id               SERIAL         PRIMARY KEY,
672     src_fund         INT            NOT NULL REFERENCES acq.fund( id )
673                                     DEFERRABLE INITIALLY DEFERRED,
674     src_amount       NUMERIC        NOT NULL,
675     dest_fund        INT            REFERENCES acq.fund( id )
676                                     DEFERRABLE INITIALLY DEFERRED,
677     dest_amount      NUMERIC,
678     transfer_time    TIMESTAMPTZ    NOT NULL DEFAULT now(),
679     transfer_user    INT            NOT NULL REFERENCES actor.usr( id )
680                                     DEFERRABLE INITIALLY DEFERRED,
681     note             TEXT,
682         funding_source_credit INT       NOT NULL REFERENCES acq.funding_source_credit( id )
683                                     DEFERRABLE INITIALLY DEFERRED
684 );
685
686 CREATE INDEX acqftr_usr_idx
687 ON acq.fund_transfer( transfer_user );
688
689 COMMENT ON TABLE acq.fund_transfer IS $$
690 Fund Transfer
691 Each row represents the transfer of money from a source fund
692 to a destination fund.  There should be corresponding entries
693 in acq.fund_allocation.  The purpose of acq.fund_transfer is
694 to record how much money moved from which fund to which other
695 fund.
696
697 The presence of two amount fields, rather than one, reflects
698 the possibility that the two funds are denominated in different
699 currencies.  If they use the same currency type, the two
700 amounts should be the same.
701 $$;
702
703 CREATE TABLE acq.fiscal_calendar (
704         id              SERIAL         PRIMARY KEY,
705         name            TEXT           NOT NULL
706 );
707
708 -- Create a default calendar (though we don't specify its contents). 
709 -- Create a foreign key in actor.org_unit, initially pointing to
710 -- the default calendar.
711
712 INSERT INTO acq.fiscal_calendar (
713     name
714 ) VALUES (
715
716     'Default'
717 );
718
719 ALTER TABLE actor.org_unit ADD FOREIGN KEY
720         (fiscal_calendar) REFERENCES acq.fiscal_calendar( id )
721         DEFERRABLE INITIALLY DEFERRED;
722
723 CREATE TABLE acq.fiscal_year (
724         id              SERIAL         PRIMARY KEY,
725         calendar        INT            NOT NULL
726                                        REFERENCES acq.fiscal_calendar
727                                        ON DELETE CASCADE
728                                        DEFERRABLE INITIALLY DEFERRED,
729         year            INT            NOT NULL,
730         year_begin      TIMESTAMPTZ    NOT NULL,
731         year_end        TIMESTAMPTZ    NOT NULL,
732         CONSTRAINT acq_fy_logical_key  UNIQUE ( calendar, year ),
733     CONSTRAINT acq_fy_physical_key UNIQUE ( calendar, year_begin )
734 );
735
736 CREATE TABLE acq.edi_account (      -- similar tables can extend remote_account for other parts of EG
737     provider    INT     NOT NULL REFERENCES acq.provider          (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
738     in_dir      TEXT,   -- incoming messages dir (probably different than config.remote_account.path, the outgoing dir)
739     vendcode    TEXT,
740     vendacct    TEXT
741 ) INHERITS (config.remote_account);
742
743 -- We need a UNIQUE constraint here also, to support the FK from acq.provider.edi_default
744 ALTER TABLE acq.edi_account ADD PRIMARY KEY (id);
745
746 CREATE TABLE acq.edi_message (
747     id               SERIAL          PRIMARY KEY,
748     account          INTEGER         REFERENCES acq.edi_account(id)
749                                      DEFERRABLE INITIALLY DEFERRED,
750     remote_file      TEXT,
751     create_time      TIMESTAMPTZ     NOT NULL DEFAULT now(),
752     translate_time   TIMESTAMPTZ,
753     process_time     TIMESTAMPTZ,
754     error_time       TIMESTAMPTZ,
755     status           TEXT            NOT NULL DEFAULT 'new'
756                                      CONSTRAINT status_value CHECK
757                                      ( status IN (
758                                         'new',          -- needs to be translated
759                                         'translated',   -- needs to be processed
760                                         'trans_error',  -- error in translation step
761                                         'processed',    -- needs to have remote_file deleted
762                                         'proc_error',   -- error in processing step
763                                         'delete_error', -- error in deletion
764                                                                                 'retry',        -- need to retry
765                                         'complete'      -- done
766                                      )),
767     edi              TEXT,
768     jedi             TEXT,
769     error            TEXT,
770     purchase_order   INT             REFERENCES acq.purchase_order
771                                      DEFERRABLE INITIALLY DEFERRED,
772         message_type     TEXT            NOT NULL CONSTRAINT valid_message_type CHECK
773                                          ( message_type IN (
774                                                                              'ORDERS',
775                                                                              'ORDRSP',
776                                                                              'INVOIC',
777                                                                              'OSTENQ',
778                                                                              'OSTRPT'
779                                                                          ))
780 );
781 CREATE INDEX edi_message_account_status_idx ON acq.edi_message (account,status);
782 CREATE INDEX edi_message_po_idx ON acq.edi_message (purchase_order);
783
784 -- Note below that the primary key is NOT a SERIAL type.  We will periodically truncate and rebuild
785 -- the table, assigning ids programmatically instead of using a sequence.
786 CREATE TABLE acq.debit_attribution (
787     id                     INT         NOT NULL PRIMARY KEY,
788     fund_debit             INT         NOT NULL
789                                        REFERENCES acq.fund_debit
790                                        DEFERRABLE INITIALLY DEFERRED,
791     debit_amount           NUMERIC     NOT NULL,
792     funding_source_credit  INT         REFERENCES acq.funding_source_credit
793                                        DEFERRABLE INITIALLY DEFERRED,
794     credit_amount          NUMERIC
795 );
796
797 CREATE INDEX acq_attribution_debit_idx
798     ON acq.debit_attribution( fund_debit );
799
800 CREATE INDEX acq_attribution_credit_idx
801     ON acq.debit_attribution( funding_source_credit );
802
803 -- Invoicing
804
805 CREATE TABLE acq.invoice_method (
806     code    TEXT    PRIMARY KEY,
807     name    TEXT    NOT NULL -- i18n-ize
808 );
809
810 CREATE TABLE acq.invoice_payment_method (
811     code    TEXT    PRIMARY KEY,
812     name    TEXT    NOT NULL -- i18n-ize
813 );
814
815 CREATE TABLE acq.invoice (
816     id          SERIAL      PRIMARY KEY,
817     receiver    INT         NOT NULL REFERENCES actor.org_unit (id),
818     provider    INT         NOT NULL REFERENCES acq.provider (id),
819     shipper     INT         NOT NULL REFERENCES acq.provider (id),
820     recv_date   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
821     recv_method TEXT        NOT NULL REFERENCES acq.invoice_method (code) DEFAULT 'EDI',
822     inv_type    TEXT,       -- A "type" field is desired, but no idea what goes here
823     inv_ident   TEXT        NOT NULL, -- vendor-supplied invoice id/number
824         payment_auth TEXT,
825         payment_method TEXT     REFERENCES acq.invoice_payment_method (code)
826                                 DEFERRABLE INITIALLY DEFERRED,
827         note        TEXT,
828     complete    BOOL        NOT NULL DEFAULT FALSE,
829     CONSTRAINT  inv_ident_once_per_provider UNIQUE(provider, inv_ident)
830 );
831
832 CREATE TABLE acq.invoice_entry (
833     id              SERIAL      PRIMARY KEY,
834     invoice         INT         NOT NULL REFERENCES acq.invoice (id) ON DELETE CASCADE,
835     purchase_order  INT         REFERENCES acq.purchase_order (id) ON UPDATE CASCADE ON DELETE SET NULL,
836     lineitem        INT         REFERENCES acq.lineitem (id) ON UPDATE CASCADE ON DELETE SET NULL,
837     inv_item_count  INT         NOT NULL, -- How many acqlids did they say they sent
838     phys_item_count INT, -- and how many did staff count
839     note            TEXT,
840     billed_per_item BOOL,
841     cost_billed     NUMERIC(8,2),
842     actual_cost     NUMERIC(8,2),
843         amount_paid     NUMERIC (8,2)
844 );
845
846 CREATE INDEX ie_inv_idx on acq.invoice_entry (invoice);
847 CREATE INDEX ie_po_idx on acq.invoice_entry (purchase_order);
848 CREATE INDEX ie_li_idx on acq.invoice_entry (lineitem);
849
850 CREATE TABLE acq.invoice_item_type (
851     code    TEXT    PRIMARY KEY,
852     name    TEXT    NOT NULL,  -- i18n-ize
853         prorate BOOL    NOT NULL DEFAULT FALSE
854 );
855
856 CREATE TABLE acq.po_item (
857         id              SERIAL      PRIMARY KEY,
858         purchase_order  INT         REFERENCES acq.purchase_order (id)
859                                     ON UPDATE CASCADE ON DELETE SET NULL
860                                     DEFERRABLE INITIALLY DEFERRED,
861         fund_debit      INT         REFERENCES acq.fund_debit (id)
862                                     DEFERRABLE INITIALLY DEFERRED,
863         inv_item_type   TEXT        NOT NULL
864                                     REFERENCES acq.invoice_item_type (code)
865                                     DEFERRABLE INITIALLY DEFERRED,
866         title           TEXT,
867         author          TEXT,
868         note            TEXT,
869         estimated_cost  NUMERIC(8,2),
870         fund            INT         REFERENCES acq.fund (id)
871                                     DEFERRABLE INITIALLY DEFERRED,
872     target          BIGINT
873 );
874
875 CREATE INDEX poi_po_idx ON acq.po_item (purchase_order);
876
877 CREATE TABLE acq.invoice_item ( -- for invoice-only debits: taxes/fees/non-bib items/etc
878     id              SERIAL      PRIMARY KEY,
879     invoice         INT         NOT NULL REFERENCES acq.invoice (id) ON UPDATE CASCADE ON DELETE CASCADE,
880     purchase_order  INT         REFERENCES acq.purchase_order (id) ON UPDATE CASCADE ON DELETE SET NULL,
881     fund_debit      INT         REFERENCES acq.fund_debit (id),
882     inv_item_type   TEXT        NOT NULL REFERENCES acq.invoice_item_type (code),
883     title           TEXT,
884     author          TEXT,
885     note            TEXT,
886     cost_billed     NUMERIC(8,2),
887     actual_cost     NUMERIC(8,2),
888         fund            INT         REFERENCES acq.fund (id)
889                                     DEFERRABLE INITIALLY DEFERRED,
890         amount_paid     NUMERIC (8,2),
891         po_item         INT         REFERENCES acq.po_item (id)
892                                     DEFERRABLE INITIALLY DEFERRED,
893     target          BIGINT
894 );
895
896 CREATE INDEX ii_inv_idx on acq.invoice_item (invoice);
897 CREATE INDEX ii_po_idx on acq.invoice_item (purchase_order);
898 CREATE INDEX ii_poi_idx on acq.invoice_item (po_item);
899
900 -- Patron requests
901 CREATE TABLE acq.user_request_type (
902     id      SERIAL  PRIMARY KEY,
903     label   TEXT    NOT NULL UNIQUE -- i18n-ize
904 );
905
906 CREATE TABLE acq.user_request (
907     id                  SERIAL  PRIMARY KEY,
908     usr                 INT     NOT NULL REFERENCES actor.usr (id), -- requesting user
909     hold                BOOL    NOT NULL DEFAULT TRUE,
910
911     pickup_lib          INT     NOT NULL REFERENCES actor.org_unit (id), -- pickup lib
912     holdable_formats    TEXT,           -- nullable, for use in hold creation
913     phone_notify        TEXT,
914     email_notify        BOOL    NOT NULL DEFAULT TRUE,
915     lineitem            INT     REFERENCES acq.lineitem (id) ON DELETE CASCADE,
916     eg_bib              BIGINT  REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
917     request_date        TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- when they requested it
918     need_before         TIMESTAMPTZ,    -- don't create holds after this
919     max_fee             TEXT,
920   
921     request_type        INT     NOT NULL REFERENCES acq.user_request_type (id),
922     isxn                TEXT,
923     title               TEXT,
924     volume              TEXT,
925     author              TEXT,
926     article_title       TEXT,
927     article_pages       TEXT,
928     publisher           TEXT,
929     location            TEXT,
930     pubdate             TEXT,
931     mentioned           TEXT,
932     other_info          TEXT,
933         cancel_reason       INT    REFERENCES acq.cancel_reason( id )
934                                    DEFERRABLE INITIALLY DEFERRED
935 );
936
937
938 -- Functions
939
940 CREATE TYPE acq.flat_lineitem_holding_subfield AS (lineitem int, holding int, subfield text, data text);
941 CREATE OR REPLACE FUNCTION acq.extract_holding_attr_table (lineitem int, tag text) RETURNS SETOF acq.flat_lineitem_holding_subfield AS $$
942 DECLARE
943     counter INT;
944     lida    acq.flat_lineitem_holding_subfield%ROWTYPE;
945 BEGIN
946
947     SELECT  COUNT(*) INTO counter
948       FROM  oils_xpath_table(
949                 'id',
950                 'marc',
951                 'acq.lineitem',
952                 '//*[@tag="' || tag || '"]',
953                 'id=' || lineitem
954             ) as t(i int,c text);
955
956     FOR i IN 1 .. counter LOOP
957         FOR lida IN
958             SELECT  * 
959               FROM  (   SELECT  id,i,t,v
960                           FROM  oils_xpath_table(
961                                     'id',
962                                     'marc',
963                                     'acq.lineitem',
964                                     '//*[@tag="' || tag || '"][position()=' || i || ']/*/@code|' ||
965                                         '//*[@tag="' || tag || '"][position()=' || i || ']/*[@code]',
966                                     'id=' || lineitem
967                                 ) as t(id int,t text,v text)
968                     )x
969         LOOP
970             RETURN NEXT lida;
971         END LOOP;
972     END LOOP;
973
974     RETURN;
975 END;
976 $$ LANGUAGE PLPGSQL;
977
978 CREATE TYPE acq.flat_lineitem_detail AS (lineitem int, holding int, attr text, data text);
979 CREATE OR REPLACE FUNCTION acq.extract_provider_holding_data ( lineitem_i int ) RETURNS SETOF acq.flat_lineitem_detail AS $$
980 DECLARE
981     prov_i  INT;
982     tag_t   TEXT;
983     lida    acq.flat_lineitem_detail%ROWTYPE;
984 BEGIN
985     SELECT provider INTO prov_i FROM acq.lineitem WHERE id = lineitem_i;
986     IF NOT FOUND THEN RETURN; END IF;
987
988     SELECT holding_tag INTO tag_t FROM acq.provider WHERE id = prov_i;
989     IF NOT FOUND OR tag_t IS NULL THEN RETURN; END IF;
990
991     FOR lida IN
992         SELECT  lineitem_i,
993                 h.holding,
994                 a.name,
995                 h.data
996           FROM  acq.extract_holding_attr_table( lineitem_i, tag_t ) h
997                 JOIN acq.provider_holding_subfield_map a USING (subfield)
998           WHERE a.provider = prov_i
999     LOOP
1000         RETURN NEXT lida;
1001     END LOOP;
1002
1003     RETURN;
1004 END;
1005 $$ LANGUAGE PLPGSQL;
1006
1007 -- select * from acq.extract_provider_holding_data(699);
1008
1009 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$
1010         SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
1011 $$ LANGUAGE SQL;
1012
1013 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field_set ( BIGINT, TEXT, TEXT) RETURNS SETOF TEXT AS $$
1014         SELECT extract_marc_field_set('acq.lineitem', $1, $2, $3);
1015 $$ LANGUAGE SQL;
1016
1017
1018 /*
1019 CREATE OR REPLACE FUNCTION public.extract_bib_marc_field ( BIGINT, TEXT ) RETURNS TEXT AS $$
1020         SELECT public.extract_marc_field('biblio.record_entry', $1, $2);
1021 $$ LANGUAGE SQL;
1022
1023 CREATE OR REPLACE FUNCTION public.extract_authority_marc_field ( BIGINT, TEXT ) RETURNS TEXT AS $$
1024         SELECT public.extract_marc_field('authority.record_entry', $1, $2);
1025 $$ LANGUAGE SQL;
1026 */
1027 -- For example:
1028 -- INSERT INTO acq.lineitem_provider_attr_definition ( provider, code, description, xpath ) VALUES (1,'price','Price','//*[@tag="020" or @tag="022"]/*[@code="a"][1]');
1029
1030 /*
1031 Suggested vendor fields:
1032         vendor_price
1033         vendor_currency
1034         vendor_avail
1035         vendor_po
1036         vendor_identifier
1037 */
1038
1039 CREATE OR REPLACE FUNCTION public.ingest_acq_marc ( ) RETURNS TRIGGER AS $function$
1040 DECLARE
1041         value           TEXT;
1042         atype           TEXT;
1043         prov            INT;
1044         pos             INT;
1045         adef            RECORD;
1046         xpath_string    TEXT;
1047 BEGIN
1048         FOR adef IN SELECT *,tableoid FROM acq.lineitem_attr_definition LOOP
1049
1050                 SELECT relname::TEXT INTO atype FROM pg_class WHERE oid = adef.tableoid;
1051
1052                 IF (atype NOT IN ('lineitem_usr_attr_definition','lineitem_local_attr_definition')) THEN
1053                         IF (atype = 'lineitem_provider_attr_definition') THEN
1054                                 SELECT provider INTO prov FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
1055                                 CONTINUE WHEN NEW.provider IS NULL OR prov <> NEW.provider;
1056                         END IF;
1057                         
1058                         IF (atype = 'lineitem_provider_attr_definition') THEN
1059                                 SELECT xpath INTO xpath_string FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
1060                         ELSIF (atype = 'lineitem_marc_attr_definition') THEN
1061                                 SELECT xpath INTO xpath_string FROM acq.lineitem_marc_attr_definition WHERE id = adef.id;
1062                         ELSIF (atype = 'lineitem_generated_attr_definition') THEN
1063                                 SELECT xpath INTO xpath_string FROM acq.lineitem_generated_attr_definition WHERE id = adef.id;
1064                         END IF;
1065
1066             xpath_string := REGEXP_REPLACE(xpath_string,$re$//?text\(\)$$re$,'');
1067
1068             IF (adef.code = 'title' OR adef.code = 'author') THEN
1069                 -- title and author should not be split
1070                 -- FIXME: once oils_xpath can grok XPATH 2.0 functions, we can use
1071                 -- string-join in the xpath and remove this special case
1072                         SELECT extract_acq_marc_field(id, xpath_string, adef.remove) INTO value FROM acq.lineitem WHERE id = NEW.id;
1073                         IF (value IS NOT NULL AND value <> '') THEN
1074                                     INSERT INTO acq.lineitem_attr (lineitem, definition, attr_type, attr_name, attr_value)
1075                                     VALUES (NEW.id, adef.id, atype, adef.code, value);
1076                 END IF;
1077             ELSE
1078                 pos := 1;
1079                 LOOP
1080                     -- each application of the regex may produce multiple values
1081                     FOR value IN
1082                         SELECT * FROM extract_acq_marc_field_set(
1083                             NEW.id, xpath_string || '[' || pos || ']', adef.remove)
1084                         LOOP
1085
1086                         IF (value IS NOT NULL AND value <> '') THEN
1087                             INSERT INTO acq.lineitem_attr
1088                                 (lineitem, definition, attr_type, attr_name, attr_value)
1089                                 VALUES (NEW.id, adef.id, atype, adef.code, value);
1090                         ELSE
1091                             EXIT;
1092                         END IF;
1093                     END LOOP;
1094                     IF NOT FOUND THEN
1095                         EXIT;
1096                     END IF;
1097                     pos := pos + 1;
1098                END LOOP;
1099             END IF;
1100
1101                 END IF;
1102
1103         END LOOP;
1104
1105         RETURN NULL;
1106 END;
1107 $function$ LANGUAGE PLPGSQL;
1108
1109 CREATE OR REPLACE FUNCTION public.cleanup_acq_marc ( ) RETURNS TRIGGER AS $$
1110 BEGIN
1111         IF TG_OP = 'UPDATE' THEN
1112                 DELETE FROM acq.lineitem_attr
1113                         WHERE lineitem = OLD.id AND attr_type IN ('lineitem_provider_attr_definition', 'lineitem_marc_attr_definition','lineitem_generated_attr_definition');
1114                 RETURN NEW;
1115         ELSE
1116                 DELETE FROM acq.lineitem_attr WHERE lineitem = OLD.id;
1117                 RETURN OLD;
1118         END IF;
1119 END;
1120 $$ LANGUAGE PLPGSQL;
1121
1122 CREATE TRIGGER cleanup_lineitem_trigger
1123         BEFORE UPDATE OR DELETE ON acq.lineitem
1124         FOR EACH ROW EXECUTE PROCEDURE public.cleanup_acq_marc();
1125
1126 CREATE TRIGGER ingest_lineitem_trigger
1127         AFTER INSERT OR UPDATE ON acq.lineitem
1128         FOR EACH ROW EXECUTE PROCEDURE public.ingest_acq_marc();
1129
1130 CREATE OR REPLACE FUNCTION acq.exchange_ratio ( from_ex TEXT, to_ex TEXT ) RETURNS NUMERIC AS $$
1131 DECLARE
1132     rat NUMERIC;
1133 BEGIN
1134     IF from_ex = to_ex THEN
1135         RETURN 1.0;
1136     END IF;
1137
1138     SELECT ratio INTO rat FROM acq.exchange_rate WHERE from_currency = from_ex AND to_currency = to_ex;
1139
1140     IF FOUND THEN
1141         RETURN rat;
1142     ELSE
1143         SELECT ratio INTO rat FROM acq.exchange_rate WHERE from_currency = to_ex AND to_currency = from_ex;
1144         IF FOUND THEN
1145             RETURN 1.0/rat;
1146         END IF;
1147     END IF;
1148
1149     RETURN NULL;
1150
1151 END;
1152 $$ LANGUAGE PLPGSQL;
1153
1154 CREATE OR REPLACE FUNCTION acq.exchange_ratio ( TEXT, TEXT, NUMERIC ) RETURNS NUMERIC AS $$
1155     SELECT $3 * acq.exchange_ratio($1, $2);
1156 $$ LANGUAGE SQL;
1157
1158 CREATE OR REPLACE FUNCTION acq.find_bad_fy()
1159 /*
1160         Examine the acq.fiscal_year table, comparing successive years.
1161         Report any inconsistencies, i.e. years that overlap, have gaps
1162     between them, or are out of sequence.
1163 */
1164 RETURNS SETOF RECORD AS $$
1165 DECLARE
1166         first_row  BOOLEAN;
1167         curr_year  RECORD;
1168         prev_year  RECORD;
1169         return_rec RECORD;
1170 BEGIN
1171         first_row := true;
1172         FOR curr_year in
1173                 SELECT
1174                         id,
1175                         calendar,
1176                         year,
1177                         year_begin,
1178                         year_end
1179                 FROM
1180                         acq.fiscal_year
1181                 ORDER BY
1182                         calendar,
1183                         year_begin
1184         LOOP
1185                 --
1186                 IF first_row THEN
1187                         first_row := FALSE;
1188                 ELSIF curr_year.calendar    = prev_year.calendar THEN
1189                         IF curr_year.year_begin > prev_year.year_end THEN
1190                                 -- This ugly kludge works around the fact that older
1191                                 -- versions of PostgreSQL don't support RETURN QUERY SELECT
1192                                 FOR return_rec IN SELECT
1193                                         prev_year.id,
1194                                         prev_year.year,
1195                                         'Gap between fiscal years'::TEXT
1196                                 LOOP
1197                                         RETURN NEXT return_rec;
1198                                 END LOOP;
1199                         ELSIF curr_year.year_begin < prev_year.year_end THEN
1200                                 FOR return_rec IN SELECT
1201                                         prev_year.id,
1202                                         prev_year.year,
1203                                         'Overlapping fiscal years'::TEXT
1204                                 LOOP
1205                                         RETURN NEXT return_rec;
1206                                 END LOOP;
1207                         ELSIF curr_year.year < prev_year.year THEN
1208                                 FOR return_rec IN SELECT
1209                                         prev_year.id,
1210                                         prev_year.year,
1211                                         'Fiscal years out of order'::TEXT
1212                                 LOOP
1213                                         RETURN NEXT return_rec;
1214                                 END LOOP;
1215                         END IF;
1216                 END IF;
1217                 --
1218                 prev_year := curr_year;
1219         END LOOP;
1220         --
1221         RETURN;
1222 END;
1223 $$ LANGUAGE plpgsql;
1224
1225 CREATE OR REPLACE FUNCTION acq.transfer_fund(
1226         old_fund   IN INT,
1227         old_amount IN NUMERIC,     -- in currency of old fund
1228         new_fund   IN INT,
1229         new_amount IN NUMERIC,     -- in currency of new fund
1230         user_id    IN INT,
1231         xfer_note  IN TEXT         -- to be recorded in acq.fund_transfer
1232         -- ,funding_source_in IN INT  -- if user wants to specify a funding source (see notes)
1233 ) RETURNS VOID AS $$
1234 /* -------------------------------------------------------------------------------
1235
1236 Function to transfer money from one fund to another.
1237
1238 A transfer is represented as a pair of entries in acq.fund_allocation, with a
1239 negative amount for the old (losing) fund and a positive amount for the new
1240 (gaining) fund.  In some cases there may be more than one such pair of entries
1241 in order to pull the money from different funding sources, or more specifically
1242 from different funding source credits.  For each such pair there is also an
1243 entry in acq.fund_transfer.
1244
1245 Since funding_source is a non-nullable column in acq.fund_allocation, we must
1246 choose a funding source for the transferred money to come from.  This choice
1247 must meet two constraints, so far as possible:
1248
1249 1. The amount transferred from a given funding source must not exceed the
1250 amount allocated to the old fund by the funding source.  To that end we
1251 compare the amount being transferred to the amount allocated.
1252
1253 2. We shouldn't transfer money that has already been spent or encumbered, as
1254 defined by the funding attribution process.  We attribute expenses to the
1255 oldest funding source credits first.  In order to avoid transferring that
1256 attributed money, we reverse the priority, transferring from the newest funding
1257 source credits first.  There can be no guarantee that this approach will
1258 avoid overcommitting a fund, but no other approach can do any better.
1259
1260 In this context the age of a funding source credit is defined by the
1261 deadline_date for credits with deadline_dates, and by the effective_date for
1262 credits without deadline_dates, with the proviso that credits with deadline_dates
1263 are all considered "older" than those without.
1264
1265 ----------
1266
1267 In the signature for this function, there is one last parameter commented out,
1268 named "funding_source_in".  Correspondingly, the WHERE clause for the query
1269 driving the main loop has an OR clause commented out, which references the
1270 funding_source_in parameter.
1271
1272 If these lines are uncommented, this function will allow the user optionally to
1273 restrict a fund transfer to a specified funding source.  If the source
1274 parameter is left NULL, then there will be no such restriction.
1275
1276 ------------------------------------------------------------------------------- */ 
1277 DECLARE
1278         same_currency      BOOLEAN;
1279         currency_ratio     NUMERIC;
1280         old_fund_currency  TEXT;
1281         old_remaining      NUMERIC;  -- in currency of old fund
1282         new_fund_currency  TEXT;
1283         new_fund_active    BOOLEAN;
1284         new_remaining      NUMERIC;  -- in currency of new fund
1285         curr_old_amt       NUMERIC;  -- in currency of old fund
1286         curr_new_amt       NUMERIC;  -- in currency of new fund
1287         source_addition    NUMERIC;  -- in currency of funding source
1288         source_deduction   NUMERIC;  -- in currency of funding source
1289         orig_allocated_amt NUMERIC;  -- in currency of funding source
1290         allocated_amt      NUMERIC;  -- in currency of fund
1291         source             RECORD;
1292 BEGIN
1293         --
1294         -- Sanity checks
1295         --
1296         IF old_fund IS NULL THEN
1297                 RAISE EXCEPTION 'acq.transfer_fund: old fund id is NULL';
1298         END IF;
1299         --
1300         IF old_amount IS NULL THEN
1301                 RAISE EXCEPTION 'acq.transfer_fund: amount to transfer is NULL';
1302         END IF;
1303         --
1304         -- The new fund and its amount must be both NULL or both not NULL.
1305         --
1306         IF new_fund IS NOT NULL AND new_amount IS NULL THEN
1307                 RAISE EXCEPTION 'acq.transfer_fund: amount to transfer to receiving fund is NULL';
1308         END IF;
1309         --
1310         IF new_fund IS NULL AND new_amount IS NOT NULL THEN
1311                 RAISE EXCEPTION 'acq.transfer_fund: receiving fund is NULL, its amount is not NULL';
1312         END IF;
1313         --
1314         IF user_id IS NULL THEN
1315                 RAISE EXCEPTION 'acq.transfer_fund: user id is NULL';
1316         END IF;
1317         --
1318         -- Initialize the amounts to be transferred, each denominated
1319         -- in the currency of its respective fund.  They will be
1320         -- reduced on each iteration of the loop.
1321         --
1322         old_remaining := old_amount;
1323         new_remaining := new_amount;
1324         --
1325         -- RAISE NOTICE 'Transferring % in fund % to % in fund %',
1326         --      old_amount, old_fund, new_amount, new_fund;
1327         --
1328         -- Get the currency types of the old and new funds.
1329         --
1330         SELECT
1331                 currency_type
1332         INTO
1333                 old_fund_currency
1334         FROM
1335                 acq.fund
1336         WHERE
1337                 id = old_fund;
1338         --
1339         IF old_fund_currency IS NULL THEN
1340                 RAISE EXCEPTION 'acq.transfer_fund: old fund id % is not defined', old_fund;
1341         END IF;
1342         --
1343         IF new_fund IS NOT NULL THEN
1344                 SELECT
1345                         currency_type,
1346                         active
1347                 INTO
1348                         new_fund_currency,
1349                         new_fund_active
1350                 FROM
1351                         acq.fund
1352                 WHERE
1353                         id = new_fund;
1354                 --
1355                 IF new_fund_currency IS NULL THEN
1356                         RAISE EXCEPTION 'acq.transfer_fund: new fund id % is not defined', new_fund;
1357                 ELSIF NOT new_fund_active THEN
1358                         --
1359                         -- No point in putting money into a fund from whence you can't spend it
1360                         --
1361                         RAISE EXCEPTION 'acq.transfer_fund: new fund id % is inactive', new_fund;
1362                 END IF;
1363                 --
1364                 IF new_amount = old_amount THEN
1365                         same_currency := true;
1366                         currency_ratio := 1;
1367                 ELSE
1368                         --
1369                         -- We'll have to translate currency between funds.  We presume that
1370                         -- the calling code has already applied an appropriate exchange rate,
1371                         -- so we'll apply the same conversion to each sub-transfer.
1372                         --
1373                         same_currency := false;
1374                         currency_ratio := new_amount / old_amount;
1375                 END IF;
1376         END IF;
1377         --
1378         -- Identify the funding source(s) from which we want to transfer the money.
1379         -- The principle is that we want to transfer the newest money first, because
1380         -- we spend the oldest money first.  The priority for spending is defined
1381         -- by a sort of the view acq.ordered_funding_source_credit.
1382         --
1383         FOR source in
1384                 SELECT
1385                         ofsc.id,
1386                         ofsc.funding_source,
1387                         ofsc.amount,
1388                         ofsc.amount * acq.exchange_ratio( fs.currency_type, old_fund_currency )
1389                                 AS converted_amt,
1390                         fs.currency_type
1391                 FROM
1392                         acq.ordered_funding_source_credit AS ofsc,
1393                         acq.funding_source fs
1394                 WHERE
1395                         ofsc.funding_source = fs.id
1396                         and ofsc.funding_source IN
1397                         (
1398                                 SELECT funding_source
1399                                 FROM acq.fund_allocation
1400                                 WHERE fund = old_fund
1401                         )
1402                         -- and
1403                         -- (
1404                         --      ofsc.funding_source = funding_source_in
1405                         --      OR funding_source_in IS NULL
1406                         -- )
1407                 ORDER BY
1408                         ofsc.sort_priority desc,
1409                         ofsc.sort_date desc,
1410                         ofsc.id desc
1411         LOOP
1412                 --
1413                 -- Determine how much money the old fund got from this funding source,
1414                 -- denominated in the currency types of the source and of the fund.
1415                 -- This result may reflect transfers from previous iterations.
1416                 --
1417                 SELECT
1418                         COALESCE( sum( amount ), 0 ),
1419                         COALESCE( sum( amount )
1420                                 * acq.exchange_ratio( source.currency_type, old_fund_currency ), 0 )
1421                 INTO
1422                         orig_allocated_amt,     -- in currency of the source
1423                         allocated_amt           -- in currency of the old fund
1424                 FROM
1425                         acq.fund_allocation
1426                 WHERE
1427                         fund = old_fund
1428                         and funding_source = source.funding_source;
1429                 --      
1430                 -- Determine how much to transfer from this credit, in the currency
1431                 -- of the fund.   Begin with the amount remaining to be attributed:
1432                 --
1433                 curr_old_amt := old_remaining;
1434                 --
1435                 -- Can't attribute more than was allocated from the fund:
1436                 --
1437                 IF curr_old_amt > allocated_amt THEN
1438                         curr_old_amt := allocated_amt;
1439                 END IF;
1440                 --
1441                 -- Can't attribute more than the amount of the current credit:
1442                 --
1443                 IF curr_old_amt > source.converted_amt THEN
1444                         curr_old_amt := source.converted_amt;
1445                 END IF;
1446                 --
1447                 curr_old_amt := trunc( curr_old_amt, 2 );
1448                 --
1449                 old_remaining := old_remaining - curr_old_amt;
1450                 --
1451                 -- Determine the amount to be deducted, if any,
1452                 -- from the old allocation.
1453                 --
1454                 IF old_remaining > 0 THEN
1455                         --
1456                         -- In this case we're using the whole allocation, so use that
1457                         -- amount directly instead of applying a currency translation
1458                         -- and thereby inviting round-off errors.
1459                         --
1460                         source_deduction := - curr_old_amt;
1461                 ELSE 
1462                         source_deduction := trunc(
1463                                 ( - curr_old_amt ) *
1464                                         acq.exchange_ratio( old_fund_currency, source.currency_type ),
1465                                 2 );
1466                 END IF;
1467                 --
1468                 IF source_deduction <> 0 THEN
1469                         --
1470                         -- Insert negative allocation for old fund in fund_allocation,
1471                         -- converted into the currency of the funding source
1472                         --
1473                         INSERT INTO acq.fund_allocation (
1474                                 funding_source,
1475                                 fund,
1476                                 amount,
1477                                 allocator,
1478                                 note
1479                         ) VALUES (
1480                                 source.funding_source,
1481                                 old_fund,
1482                                 source_deduction,
1483                                 user_id,
1484                                 'Transfer to fund ' || new_fund
1485                         );
1486                 END IF;
1487                 --
1488                 IF new_fund IS NOT NULL THEN
1489                         --
1490                         -- Determine how much to add to the new fund, in
1491                         -- its currency, and how much remains to be added:
1492                         --
1493                         IF same_currency THEN
1494                                 curr_new_amt := curr_old_amt;
1495                         ELSE
1496                                 IF old_remaining = 0 THEN
1497                                         --
1498                                         -- This is the last iteration, so nothing should be left
1499                                         --
1500                                         curr_new_amt := new_remaining;
1501                                         new_remaining := 0;
1502                                 ELSE
1503                                         curr_new_amt := trunc( curr_old_amt * currency_ratio, 2 );
1504                                         new_remaining := new_remaining - curr_new_amt;
1505                                 END IF;
1506                         END IF;
1507                         --
1508                         -- Determine how much to add, if any,
1509                         -- to the new fund's allocation.
1510                         --
1511                         IF old_remaining > 0 THEN
1512                                 --
1513                                 -- In this case we're using the whole allocation, so use that amount
1514                                 -- amount directly instead of applying a currency translation and
1515                                 -- thereby inviting round-off errors.
1516                                 --
1517                                 source_addition := curr_new_amt;
1518                         ELSIF source.currency_type = old_fund_currency THEN
1519                                 --
1520                                 -- In this case we don't need a round trip currency translation,
1521                                 -- thereby inviting round-off errors:
1522                                 --
1523                                 source_addition := curr_old_amt;
1524                         ELSE 
1525                                 source_addition := trunc(
1526                                         curr_new_amt *
1527                                                 acq.exchange_ratio( new_fund_currency, source.currency_type ),
1528                                         2 );
1529                         END IF;
1530                         --
1531                         IF source_addition <> 0 THEN
1532                                 --
1533                                 -- Insert positive allocation for new fund in fund_allocation,
1534                                 -- converted to the currency of the founding source
1535                                 --
1536                                 INSERT INTO acq.fund_allocation (
1537                                         funding_source,
1538                                         fund,
1539                                         amount,
1540                                         allocator,
1541                                         note
1542                                 ) VALUES (
1543                                         source.funding_source,
1544                                         new_fund,
1545                                         source_addition,
1546                                         user_id,
1547                                         'Transfer from fund ' || old_fund
1548                                 );
1549                         END IF;
1550                 END IF;
1551                 --
1552                 IF trunc( curr_old_amt, 2 ) <> 0
1553                 OR trunc( curr_new_amt, 2 ) <> 0 THEN
1554                         --
1555                         -- Insert row in fund_transfer, using amounts in the currency of the funds
1556                         --
1557                         INSERT INTO acq.fund_transfer (
1558                                 src_fund,
1559                                 src_amount,
1560                                 dest_fund,
1561                                 dest_amount,
1562                                 transfer_user,
1563                                 note,
1564                                 funding_source_credit
1565                         ) VALUES (
1566                                 old_fund,
1567                                 trunc( curr_old_amt, 2 ),
1568                                 new_fund,
1569                                 trunc( curr_new_amt, 2 ),
1570                                 user_id,
1571                                 xfer_note,
1572                                 source.id
1573                         );
1574                 END IF;
1575                 --
1576                 if old_remaining <= 0 THEN
1577                         EXIT;                   -- Nothing more to be transferred
1578                 END IF;
1579         END LOOP;
1580 END;
1581 $$ LANGUAGE plpgsql;
1582
1583 CREATE OR REPLACE FUNCTION acq.attribute_debits() RETURNS VOID AS $$
1584 /*
1585 Function to attribute expenditures and encumbrances to funding source credits,
1586 and thereby to funding sources.
1587
1588 Read the debits in chonological order, attributing each one to one or
1589 more funding source credits.  Constraints:
1590
1591 1. Don't attribute more to a credit than the amount of the credit.
1592
1593 2. For a given fund, don't attribute more to a funding source than the
1594 source has allocated to that fund.
1595
1596 3. Attribute debits to credits with deadlines before attributing them to
1597 credits without deadlines.  Otherwise attribute to the earliest credits
1598 first, based on the deadline date when present, or on the effective date
1599 when there is no deadline.  Use funding_source_credit.id as a tie-breaker.
1600 This ordering is defined by an ORDER BY clause on the view
1601 acq.ordered_funding_source_credit.
1602
1603 Start by truncating the table acq.debit_attribution.  Then insert a row
1604 into that table for each attribution.  If a debit cannot be fully
1605 attributed, insert a row for the unattributable balance, with the 
1606 funding_source_credit and credit_amount columns NULL.
1607 */
1608 DECLARE
1609         curr_fund_source_bal RECORD;
1610         seqno                INT;     -- sequence num for credits applicable to a fund
1611         fund_credit          RECORD;  -- current row in temp t_fund_credit table
1612         fc                   RECORD;  -- used for loading t_fund_credit table
1613         sc                   RECORD;  -- used for loading t_fund_credit table
1614         --
1615         -- Used exclusively in the main loop:
1616         --
1617         deb                 RECORD;   -- current row from acq.fund_debit table
1618         curr_credit_bal     RECORD;   -- current row from temp t_credit table
1619         debit_balance       NUMERIC;  -- amount left to attribute for current debit
1620         conv_debit_balance  NUMERIC;  -- debit balance in currency of the fund
1621         attr_amount         NUMERIC;  -- amount being attributed, in currency of debit
1622         conv_attr_amount    NUMERIC;  -- amount being attributed, in currency of source
1623         conv_cred_balance   NUMERIC;  -- credit_balance in the currency of the fund
1624         conv_alloc_balance  NUMERIC;  -- allocated balance in the currency of the fund
1625         attrib_count        INT;      -- populates id of acq.debit_attribution
1626 BEGIN
1627         --
1628         -- Load a temporary table.  For each combination of fund and funding source,
1629         -- load an entry with the total amount allocated to that fund by that source.
1630         -- This sum may reflect transfers as well as original allocations.  We will
1631         -- reduce this balance whenever we attribute debits to it.
1632         --
1633         CREATE TEMP TABLE t_fund_source_bal
1634         ON COMMIT DROP AS
1635                 SELECT
1636                         fund AS fund,
1637                         funding_source AS source,
1638                         sum( amount ) AS balance
1639                 FROM
1640                         acq.fund_allocation
1641                 GROUP BY
1642                         fund,
1643                         funding_source
1644                 HAVING
1645                         sum( amount ) > 0;
1646         --
1647         CREATE INDEX t_fund_source_bal_idx
1648                 ON t_fund_source_bal( fund, source );
1649         -------------------------------------------------------------------------------
1650         --
1651         -- Load another temporary table.  For each fund, load zero or more
1652         -- funding source credits from which that fund can get money.
1653         --
1654         CREATE TEMP TABLE t_fund_credit (
1655                 fund        INT,
1656                 seq         INT,
1657                 credit      INT
1658         ) ON COMMIT DROP;
1659         --
1660         FOR fc IN
1661                 SELECT DISTINCT fund
1662                 FROM acq.fund_allocation
1663                 ORDER BY fund
1664         LOOP                  -- Loop over the funds
1665                 seqno := 1;
1666                 FOR sc IN
1667                         SELECT
1668                                 ofsc.id
1669                         FROM
1670                                 acq.ordered_funding_source_credit AS ofsc
1671                         WHERE
1672                                 ofsc.funding_source IN
1673                                 (
1674                                         SELECT funding_source
1675                                         FROM acq.fund_allocation
1676                                         WHERE fund = fc.fund
1677                                 )
1678                 ORDER BY
1679                     ofsc.sort_priority,
1680                     ofsc.sort_date,
1681                     ofsc.id
1682                 LOOP                        -- Add each credit to the list
1683                         INSERT INTO t_fund_credit (
1684                                 fund,
1685                                 seq,
1686                                 credit
1687                         ) VALUES (
1688                                 fc.fund,
1689                                 seqno,
1690                                 sc.id
1691                         );
1692                         --RAISE NOTICE 'Fund % credit %', fc.fund, sc.id;
1693                         seqno := seqno + 1;
1694                 END LOOP;     -- Loop over credits for a given fund
1695         END LOOP;         -- Loop over funds
1696         --
1697         CREATE INDEX t_fund_credit_idx
1698                 ON t_fund_credit( fund, seq );
1699         -------------------------------------------------------------------------------
1700         --
1701         -- Load yet another temporary table.  This one is a list of funding source
1702         -- credits, with their balances.  We shall reduce those balances as we
1703         -- attribute debits to them.
1704         --
1705         CREATE TEMP TABLE t_credit
1706         ON COMMIT DROP AS
1707         SELECT
1708             fsc.id AS credit,
1709             fsc.funding_source AS source,
1710             fsc.amount AS balance,
1711             fs.currency_type AS currency_type
1712         FROM
1713             acq.funding_source_credit AS fsc,
1714             acq.funding_source fs
1715         WHERE
1716             fsc.funding_source = fs.id
1717                         AND fsc.amount > 0;
1718         --
1719         CREATE INDEX t_credit_idx
1720                 ON t_credit( credit );
1721         --
1722         -------------------------------------------------------------------------------
1723         --
1724         -- Now that we have loaded the lookup tables: loop through the debits,
1725         -- attributing each one to one or more funding source credits.
1726         -- 
1727         truncate table acq.debit_attribution;
1728         --
1729         attrib_count := 0;
1730         FOR deb in
1731                 SELECT
1732                         fd.id,
1733                         fd.fund,
1734                         fd.amount,
1735                         f.currency_type,
1736                         fd.encumbrance
1737                 FROM
1738                         acq.fund_debit fd,
1739                         acq.fund f
1740                 WHERE
1741                         fd.fund = f.id
1742                 ORDER BY
1743                         fd.id
1744         LOOP
1745                 --RAISE NOTICE 'Debit %, fund %', deb.id, deb.fund;
1746                 --
1747                 debit_balance := deb.amount;
1748                 --
1749                 -- Loop over the funding source credits that are eligible
1750                 -- to pay for this debit
1751                 --
1752                 FOR fund_credit IN
1753                         SELECT
1754                                 credit
1755                         FROM
1756                                 t_fund_credit
1757                         WHERE
1758                                 fund = deb.fund
1759                         ORDER BY
1760                                 seq
1761                 LOOP
1762                         --RAISE NOTICE '   Examining credit %', fund_credit.credit;
1763                         --
1764                         -- Look up the balance for this credit.  If it's zero, then
1765                         -- it's not useful, so treat it as if you didn't find it.
1766                         -- (Actually there shouldn't be any zero balances in the table,
1767                         -- but we check just to make sure.)
1768                         --
1769                         SELECT *
1770                         INTO curr_credit_bal
1771                         FROM t_credit
1772                         WHERE
1773                                 credit = fund_credit.credit
1774                                 AND balance > 0;
1775                         --
1776                         IF curr_credit_bal IS NULL THEN
1777                                 --
1778                                 -- This credit is exhausted; try the next one.
1779                                 --
1780                                 CONTINUE;
1781                         END IF;
1782                         --
1783                         --
1784                         -- At this point we have an applicable credit with some money left.
1785                         -- Now see if the relevant funding_source has any money left.
1786                         --
1787                         -- Look up the balance of the allocation for this combination of
1788                         -- fund and source.  If you find such an entry, but it has a zero
1789                         -- balance, then it's not useful, so treat it as unfound.
1790                         -- (Actually there shouldn't be any zero balances in the table,
1791                         -- but we check just to make sure.)
1792                         --
1793                         SELECT *
1794                         INTO curr_fund_source_bal
1795                         FROM t_fund_source_bal
1796                         WHERE
1797                                 fund = deb.fund
1798                                 AND source = curr_credit_bal.source
1799                                 AND balance > 0;
1800                         --
1801                         IF curr_fund_source_bal IS NULL THEN
1802                                 --
1803                                 -- This fund/source doesn't exist or is already exhausted,
1804                                 -- so we can't use this credit.  Go on to the next one.
1805                                 --
1806                                 CONTINUE;
1807                         END IF;
1808                         --
1809                         -- Convert the available balances to the currency of the fund
1810                         --
1811                         conv_alloc_balance := curr_fund_source_bal.balance * acq.exchange_ratio(
1812                                 curr_credit_bal.currency_type, deb.currency_type );
1813                         conv_cred_balance := curr_credit_bal.balance * acq.exchange_ratio(
1814                                 curr_credit_bal.currency_type, deb.currency_type );
1815                         --
1816                         -- Determine how much we can attribute to this credit: the minimum
1817                         -- of the debit amount, the fund/source balance, and the
1818                         -- credit balance
1819                         --
1820                         --RAISE NOTICE '   deb bal %', debit_balance;
1821                         --RAISE NOTICE '      source % balance %', curr_credit_bal.source, conv_alloc_balance;
1822                         --RAISE NOTICE '      credit % balance %', curr_credit_bal.credit, conv_cred_balance;
1823                         --
1824                         conv_attr_amount := NULL;
1825                         attr_amount := debit_balance;
1826                         --
1827                         IF attr_amount > conv_alloc_balance THEN
1828                                 attr_amount := conv_alloc_balance;
1829                                 conv_attr_amount := curr_fund_source_bal.balance;
1830                         END IF;
1831                         IF attr_amount > conv_cred_balance THEN
1832                                 attr_amount := conv_cred_balance;
1833                                 conv_attr_amount := curr_credit_bal.balance;
1834                         END IF;
1835                         --
1836                         -- If we're attributing all of one of the balances, then that's how
1837                         -- much we will deduct from the balances, and we already captured
1838                         -- that amount above.  Otherwise we must convert the amount of the
1839                         -- attribution from the currency of the fund back to the currency of
1840                         -- the funding source.
1841                         --
1842                         IF conv_attr_amount IS NULL THEN
1843                                 conv_attr_amount := attr_amount * acq.exchange_ratio(
1844                                         deb.currency_type, curr_credit_bal.currency_type );
1845                         END IF;
1846                         --
1847                         -- Insert a row to record the attribution
1848                         --
1849                         attrib_count := attrib_count + 1;
1850                         INSERT INTO acq.debit_attribution (
1851                                 id,
1852                                 fund_debit,
1853                                 debit_amount,
1854                                 funding_source_credit,
1855                                 credit_amount
1856                         ) VALUES (
1857                                 attrib_count,
1858                                 deb.id,
1859                                 attr_amount,
1860                                 curr_credit_bal.credit,
1861                                 conv_attr_amount
1862                         );
1863                         --
1864                         -- Subtract the attributed amount from the various balances
1865                         --
1866                         debit_balance := debit_balance - attr_amount;
1867                         curr_fund_source_bal.balance := curr_fund_source_bal.balance - conv_attr_amount;
1868                         --
1869                         IF curr_fund_source_bal.balance <= 0 THEN
1870                                 --
1871                                 -- This allocation is exhausted.  Delete it so
1872                                 -- that we don't waste time looking at it again.
1873                                 --
1874                                 DELETE FROM t_fund_source_bal
1875                                 WHERE
1876                                         fund = curr_fund_source_bal.fund
1877                                         AND source = curr_fund_source_bal.source;
1878                         ELSE
1879                                 UPDATE t_fund_source_bal
1880                                 SET balance = balance - conv_attr_amount
1881                                 WHERE
1882                                         fund = curr_fund_source_bal.fund
1883                                         AND source = curr_fund_source_bal.source;
1884                         END IF;
1885                         --
1886                         IF curr_credit_bal.balance <= 0 THEN
1887                                 --
1888                                 -- This funding source credit is exhausted.  Delete it
1889                                 -- so that we don't waste time looking at it again.
1890                                 --
1891                                 --DELETE FROM t_credit
1892                                 --WHERE
1893                                 --      credit = curr_credit_bal.credit;
1894                                 --
1895                                 DELETE FROM t_fund_credit
1896                                 WHERE
1897                                         credit = curr_credit_bal.credit;
1898                         ELSE
1899                                 UPDATE t_credit
1900                                 SET balance = curr_credit_bal.balance
1901                                 WHERE
1902                                         credit = curr_credit_bal.credit;
1903                         END IF;
1904                         --
1905                         -- Are we done with this debit yet?
1906                         --
1907                         IF debit_balance <= 0 THEN
1908                                 EXIT;       -- We've fully attributed this debit; stop looking at credits.
1909                         END IF;
1910                 END LOOP;       -- End loop over credits
1911                 --
1912                 IF debit_balance <> 0 THEN
1913                         --
1914                         -- We weren't able to attribute this debit, or at least not
1915                         -- all of it.  Insert a row for the unattributed balance.
1916                         --
1917                         attrib_count := attrib_count + 1;
1918                         INSERT INTO acq.debit_attribution (
1919                                 id,
1920                                 fund_debit,
1921                                 debit_amount,
1922                                 funding_source_credit,
1923                                 credit_amount
1924                         ) VALUES (
1925                                 attrib_count,
1926                                 deb.id,
1927                                 debit_balance,
1928                                 NULL,
1929                                 NULL
1930                         );
1931                 END IF;
1932         END LOOP;   -- End of loop over debits
1933 END;
1934 $$ LANGUAGE 'plpgsql';
1935
1936 CREATE OR REPLACE FUNCTION acq.propagate_funds_by_org_tree(
1937         old_year INTEGER,
1938         user_id INTEGER,
1939         org_unit_id INTEGER,
1940     include_desc BOOL DEFAULT TRUE
1941 ) RETURNS VOID AS $$
1942 DECLARE
1943 --
1944 new_id      INT;
1945 old_fund    RECORD;
1946 org_found   BOOLEAN;
1947 --
1948 BEGIN
1949         --
1950         -- Sanity checks
1951         --
1952         IF old_year IS NULL THEN
1953                 RAISE EXCEPTION 'Input year argument is NULL';
1954         ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
1955                 RAISE EXCEPTION 'Input year is out of range';
1956         END IF;
1957         --
1958         IF user_id IS NULL THEN
1959                 RAISE EXCEPTION 'Input user id argument is NULL';
1960         END IF;
1961         --
1962         IF org_unit_id IS NULL THEN
1963                 RAISE EXCEPTION 'Org unit id argument is NULL';
1964         ELSE
1965                 SELECT TRUE INTO org_found
1966                 FROM actor.org_unit
1967                 WHERE id = org_unit_id;
1968                 --
1969                 IF org_found IS NULL THEN
1970                         RAISE EXCEPTION 'Org unit id is invalid';
1971                 END IF;
1972         END IF;
1973         --
1974         -- Loop over the applicable funds
1975         --
1976         FOR old_fund in SELECT * FROM acq.fund
1977         WHERE
1978                 year = old_year
1979                 AND propagate
1980                 AND ( ( include_desc AND org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
1981                 OR (NOT include_desc AND org = org_unit_id ) )
1982     
1983         LOOP
1984                 BEGIN
1985                         INSERT INTO acq.fund (
1986                                 org,
1987                                 name,
1988                                 year,
1989                                 currency_type,
1990                                 code,
1991                                 rollover,
1992                                 propagate,
1993                                 balance_warning_percent,
1994                                 balance_stop_percent
1995                         ) VALUES (
1996                                 old_fund.org,
1997                                 old_fund.name,
1998                                 old_year + 1,
1999                                 old_fund.currency_type,
2000                                 old_fund.code,
2001                                 old_fund.rollover,
2002                                 true,
2003                                 old_fund.balance_warning_percent,
2004                                 old_fund.balance_stop_percent
2005                         )
2006                         RETURNING id INTO new_id;
2007                 EXCEPTION
2008                         WHEN unique_violation THEN
2009                                 --RAISE NOTICE 'Fund % already propagated', old_fund.id;
2010                                 CONTINUE;
2011                 END;
2012                 --RAISE NOTICE 'Propagating fund % to fund %',
2013                 --      old_fund.code, new_id;
2014         END LOOP;
2015 END;
2016 $$ LANGUAGE plpgsql;
2017
2018 CREATE OR REPLACE FUNCTION acq.propagate_funds_by_org_unit( old_year INTEGER, user_id INTEGER, org_unit_id INTEGER ) RETURNS VOID AS $$
2019     SELECT acq.propagate_funds_by_org_tree( $1, $2, $3, FALSE );
2020 $$ LANGUAGE SQL;
2021
2022 CREATE OR REPLACE FUNCTION acq.rollover_funds_by_org_tree(
2023         old_year INTEGER,
2024         user_id INTEGER,
2025         org_unit_id INTEGER,
2026     encumb_only BOOL DEFAULT FALSE,
2027     include_desc BOOL DEFAULT TRUE
2028 ) RETURNS VOID AS $$
2029 DECLARE
2030 --
2031 new_fund    INT;
2032 new_year    INT := old_year + 1;
2033 org_found   BOOL;
2034 perm_ous    BOOL;
2035 xfer_amount NUMERIC := 0;
2036 roll_fund   RECORD;
2037 deb         RECORD;
2038 detail      RECORD;
2039 roll_distrib_forms BOOL;
2040 --
2041 BEGIN
2042         --
2043         -- Sanity checks
2044         --
2045         IF old_year IS NULL THEN
2046                 RAISE EXCEPTION 'Input year argument is NULL';
2047     ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
2048         RAISE EXCEPTION 'Input year is out of range';
2049         END IF;
2050         --
2051         IF user_id IS NULL THEN
2052                 RAISE EXCEPTION 'Input user id argument is NULL';
2053         END IF;
2054         --
2055         IF org_unit_id IS NULL THEN
2056                 RAISE EXCEPTION 'Org unit id argument is NULL';
2057         ELSE
2058                 --
2059                 -- Validate the org unit
2060                 --
2061                 SELECT TRUE
2062                 INTO org_found
2063                 FROM actor.org_unit
2064                 WHERE id = org_unit_id;
2065                 --
2066                 IF org_found IS NULL THEN
2067                         RAISE EXCEPTION 'Org unit id % is invalid', org_unit_id;
2068                 ELSIF encumb_only THEN
2069                         SELECT INTO perm_ous value::BOOL FROM
2070                         actor.org_unit_ancestor_setting(
2071                                 'acq.fund.allow_rollover_without_money', org_unit_id
2072                         );
2073                         IF NOT FOUND OR NOT perm_ous THEN
2074                                 RAISE EXCEPTION 'Encumbrance-only rollover not permitted at org %', org_unit_id;
2075                         END IF;
2076                 END IF;
2077         END IF;
2078         --
2079         -- Loop over the propagable funds to identify the details
2080         -- from the old fund plus the id of the new one, if it exists.
2081         --
2082         FOR roll_fund in
2083         SELECT
2084             oldf.id AS old_fund,
2085             oldf.org,
2086             oldf.name,
2087             oldf.currency_type,
2088             oldf.code,
2089                 oldf.rollover,
2090             newf.id AS new_fund_id
2091         FROM
2092         acq.fund AS oldf
2093         LEFT JOIN acq.fund AS newf
2094                 ON ( oldf.code = newf.code )
2095         WHERE
2096                     oldf.year = old_year
2097                 AND oldf.propagate
2098         AND newf.year = new_year
2099                 AND ( ( include_desc AND oldf.org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
2100                 OR (NOT include_desc AND oldf.org = org_unit_id ) )
2101         LOOP
2102                 --RAISE NOTICE 'Processing fund %', roll_fund.old_fund;
2103                 --
2104                 IF roll_fund.new_fund_id IS NULL THEN
2105                         --
2106                         -- The old fund hasn't been propagated yet.  Propagate it now.
2107                         --
2108                         INSERT INTO acq.fund (
2109                                 org,
2110                                 name,
2111                                 year,
2112                                 currency_type,
2113                                 code,
2114                                 rollover,
2115                                 propagate,
2116                                 balance_warning_percent,
2117                                 balance_stop_percent
2118                         ) VALUES (
2119                                 roll_fund.org,
2120                                 roll_fund.name,
2121                                 new_year,
2122                                 roll_fund.currency_type,
2123                                 roll_fund.code,
2124                                 true,
2125                                 true,
2126                                 roll_fund.balance_warning_percent,
2127                                 roll_fund.balance_stop_percent
2128                         )
2129                         RETURNING id INTO new_fund;
2130                 ELSE
2131                         new_fund = roll_fund.new_fund_id;
2132                 END IF;
2133                 --
2134                 -- Determine the amount to transfer
2135                 --
2136                 SELECT amount
2137                 INTO xfer_amount
2138                 FROM acq.fund_spent_balance
2139                 WHERE fund = roll_fund.old_fund;
2140                 --
2141                 IF xfer_amount <> 0 THEN
2142                         IF NOT encumb_only AND roll_fund.rollover THEN
2143                                 --
2144                                 -- Transfer balance from old fund to new
2145                                 --
2146                                 --RAISE NOTICE 'Transferring % from fund % to %', xfer_amount, roll_fund.old_fund, new_fund;
2147                                 --
2148                                 PERFORM acq.transfer_fund(
2149                                         roll_fund.old_fund,
2150                                         xfer_amount,
2151                                         new_fund,
2152                                         xfer_amount,
2153                                         user_id,
2154                                         'Rollover'
2155                                 );
2156                         ELSE
2157                                 --
2158                                 -- Transfer balance from old fund to the void
2159                                 --
2160                                 -- RAISE NOTICE 'Transferring % from fund % to the void', xfer_amount, roll_fund.old_fund;
2161                                 --
2162                                 PERFORM acq.transfer_fund(
2163                                         roll_fund.old_fund,
2164                                         xfer_amount,
2165                                         NULL,
2166                                         NULL,
2167                                         user_id,
2168                                         'Rollover into the void'
2169                                 );
2170                         END IF;
2171                 END IF;
2172                 --
2173                 IF roll_fund.rollover THEN
2174                         --
2175                         -- Move any lineitems from the old fund to the new one
2176                         -- where the associated debit is an encumbrance.
2177                         --
2178                         -- Any other tables tying expenditure details to funds should
2179                         -- receive similar treatment.  At this writing there are none.
2180                         --
2181                         UPDATE acq.lineitem_detail
2182                         SET fund = new_fund
2183                         WHERE
2184                         fund = roll_fund.old_fund -- this condition may be redundant
2185                         AND fund_debit in
2186                         (
2187                                 SELECT id
2188                                 FROM acq.fund_debit
2189                                 WHERE
2190                                 fund = roll_fund.old_fund
2191                                 AND encumbrance
2192                         );
2193                         --
2194                         -- Move encumbrance debits from the old fund to the new fund
2195                         --
2196                         UPDATE acq.fund_debit
2197                         SET fund = new_fund
2198                         wHERE
2199                                 fund = roll_fund.old_fund
2200                                 AND encumbrance;
2201                 END IF;
2202
2203                 -- Rollover distribution formulae funds
2204                 SELECT INTO roll_distrib_forms value::BOOL FROM
2205                         actor.org_unit_ancestor_setting(
2206                                 'acq.fund.rollover_distrib_forms', org_unit_id
2207                         );
2208
2209                 IF roll_distrib_forms THEN
2210                         UPDATE acq.distribution_formula_entry 
2211                                 SET fund = roll_fund.new_fund_id
2212                                 WHERE fund = roll_fund.old_fund;
2213                 END IF;
2214
2215                 --
2216                 -- Mark old fund as inactive, now that we've closed it
2217                 --
2218                 UPDATE acq.fund
2219                 SET active = FALSE
2220                 WHERE id = roll_fund.old_fund;
2221         END LOOP;
2222 END;
2223 $$ LANGUAGE plpgsql;
2224
2225
2226
2227 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 $$
2228     SELECT acq.rollover_funds_by_org_tree( $1, $2, $3, $4, FALSE );
2229 $$ LANGUAGE SQL;
2230
2231 CREATE OR REPLACE VIEW acq.funding_source_credit_total AS
2232     SELECT  funding_source,
2233             SUM(amount) AS amount
2234       FROM  acq.funding_source_credit
2235       GROUP BY 1;
2236
2237 CREATE OR REPLACE VIEW acq.funding_source_allocation_total AS
2238     SELECT  funding_source,
2239             SUM(a.amount)::NUMERIC(100,2) AS amount
2240     FROM  acq.fund_allocation a
2241     GROUP BY 1;
2242
2243 CREATE OR REPLACE VIEW acq.funding_source_balance AS
2244     SELECT  COALESCE(c.funding_source, a.funding_source) AS funding_source,
2245             SUM(COALESCE(c.amount,0.0) - COALESCE(a.amount,0.0))::NUMERIC(100,2) AS amount
2246       FROM  acq.funding_source_credit_total c
2247             FULL JOIN acq.funding_source_allocation_total a USING (funding_source)
2248       GROUP BY 1;
2249
2250 CREATE OR REPLACE VIEW acq.fund_allocation_total AS
2251     SELECT  fund,
2252             SUM(a.amount * acq.exchange_ratio(s.currency_type, f.currency_type))::NUMERIC(100,2) AS amount
2253     FROM acq.fund_allocation a
2254          JOIN acq.fund f ON (a.fund = f.id)
2255          JOIN acq.funding_source s ON (a.funding_source = s.id)
2256     GROUP BY 1;
2257
2258 CREATE OR REPLACE VIEW acq.fund_debit_total AS
2259     SELECT  fund.id AS fund, 
2260             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
2261     FROM acq.fund fund
2262         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
2263     GROUP BY fund.id;
2264
2265 CREATE OR REPLACE VIEW acq.fund_encumbrance_total AS
2266     SELECT 
2267         fund.id AS fund, 
2268         sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
2269     FROM acq.fund fund
2270         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
2271     WHERE fund_debit.encumbrance GROUP BY fund.id;
2272
2273 CREATE OR REPLACE VIEW acq.fund_spent_total AS
2274     SELECT  fund.id AS fund, 
2275             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
2276     FROM acq.fund fund
2277         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
2278     WHERE NOT fund_debit.encumbrance 
2279     GROUP BY fund.id;
2280
2281 CREATE OR REPLACE VIEW acq.fund_combined_balance AS
2282     SELECT  c.fund, 
2283             c.amount - COALESCE(d.amount, 0.0) AS amount
2284     FROM acq.fund_allocation_total c
2285     LEFT JOIN acq.fund_debit_total d USING (fund);
2286
2287 CREATE OR REPLACE VIEW acq.fund_spent_balance AS
2288     SELECT  c.fund,
2289             c.amount - COALESCE(d.amount,0.0) AS amount
2290       FROM  acq.fund_allocation_total c
2291             LEFT JOIN acq.fund_spent_total d USING (fund);
2292
2293 -- For each fund: the total allocation from all sources, in the
2294 -- currency of the fund (or 0 if there are no allocations)
2295
2296 CREATE VIEW acq.all_fund_allocation_total AS
2297 SELECT
2298     f.id AS fund,
2299     COALESCE( SUM( a.amount * acq.exchange_ratio(
2300         s.currency_type, f.currency_type))::numeric(100,2), 0 )
2301     AS amount
2302 FROM
2303     acq.fund f
2304         LEFT JOIN acq.fund_allocation a
2305             ON a.fund = f.id
2306         LEFT JOIN acq.funding_source s
2307             ON a.funding_source = s.id
2308 GROUP BY
2309     f.id;
2310
2311 -- For every fund: the total encumbrances (or 0 if none),
2312 -- in the currency of the fund.
2313
2314 CREATE VIEW acq.all_fund_encumbrance_total AS
2315 SELECT
2316         f.id AS fund,
2317         COALESCE( encumb.amount, 0 ) AS amount
2318 FROM
2319         acq.fund AS f
2320                 LEFT JOIN (
2321                         SELECT
2322                                 fund,
2323                                 sum( amount ) AS amount
2324                         FROM
2325                                 acq.fund_debit
2326                         WHERE
2327                                 encumbrance
2328                         GROUP BY fund
2329                 ) AS encumb
2330                         ON f.id = encumb.fund;
2331
2332 -- For every fund: the total spent (or 0 if none),
2333 -- in the currency of the fund.
2334
2335 CREATE VIEW acq.all_fund_spent_total AS
2336 SELECT
2337     f.id AS fund,
2338     COALESCE( spent.amount, 0 ) AS amount
2339 FROM
2340     acq.fund AS f
2341         LEFT JOIN (
2342             SELECT
2343                 fund,
2344                 sum( amount ) AS amount
2345             FROM
2346                 acq.fund_debit
2347             WHERE
2348                 NOT encumbrance
2349             GROUP BY fund
2350         ) AS spent
2351             ON f.id = spent.fund;
2352
2353 -- For each fund: the amount not yet spent, in the currency
2354 -- of the fund.  May include encumbrances.
2355
2356 CREATE VIEW acq.all_fund_spent_balance AS
2357 SELECT
2358         c.fund,
2359         c.amount - d.amount AS amount
2360 FROM acq.all_fund_allocation_total c
2361     LEFT JOIN acq.all_fund_spent_total d USING (fund);
2362
2363 -- For each fund: the amount neither spent nor encumbered,
2364 -- in the currency of the fund
2365
2366 CREATE VIEW acq.all_fund_combined_balance AS
2367 SELECT
2368      a.fund,
2369      a.amount - COALESCE( c.amount, 0 ) AS amount
2370 FROM
2371      acq.all_fund_allocation_total a
2372         LEFT OUTER JOIN (
2373             SELECT
2374                 fund,
2375                 SUM( amount ) AS amount
2376             FROM
2377                 acq.fund_debit
2378             GROUP BY
2379                 fund
2380         ) AS c USING ( fund );
2381
2382 CREATE TABLE acq.claim_type (
2383         id             SERIAL           PRIMARY KEY,
2384         org_unit       INT              NOT NULL REFERENCES actor.org_unit(id)
2385                                                  DEFERRABLE INITIALLY DEFERRED,
2386         code           TEXT             NOT NULL,
2387         description    TEXT             NOT NULL,
2388         CONSTRAINT claim_type_once_per_org UNIQUE ( org_unit, code )
2389 );
2390
2391 CREATE TABLE acq.claim (
2392         id             SERIAL           PRIMARY KEY,
2393         type           INT              NOT NULL REFERENCES acq.claim_type
2394                                                  DEFERRABLE INITIALLY DEFERRED,
2395         lineitem_detail BIGINT          NOT NULL REFERENCES acq.lineitem_detail
2396                                                  DEFERRABLE INITIALLY DEFERRED
2397 );
2398
2399 CREATE INDEX claim_lid_idx ON acq.claim( lineitem_detail );
2400
2401 CREATE TABLE acq.claim_event (
2402         id             BIGSERIAL        PRIMARY KEY,
2403         type           INT              NOT NULL REFERENCES acq.claim_event_type
2404                                                  DEFERRABLE INITIALLY DEFERRED,
2405         claim          SERIAL           NOT NULL REFERENCES acq.claim
2406                                                  DEFERRABLE INITIALLY DEFERRED,
2407         event_date     TIMESTAMPTZ      NOT NULL DEFAULT now(),
2408         creator        INT              NOT NULL REFERENCES actor.usr
2409                                                  DEFERRABLE INITIALLY DEFERRED,
2410         note           TEXT
2411 );
2412
2413 CREATE INDEX claim_event_claim_date_idx ON acq.claim_event( claim, event_date );
2414
2415 -- And the serials version of claiming
2416 CREATE TABLE acq.serial_claim (
2417     id     SERIAL           PRIMARY KEY,
2418     type   INT              NOT NULL REFERENCES acq.claim_type
2419                                      DEFERRABLE INITIALLY DEFERRED,
2420     item    BIGINT          NOT NULL REFERENCES serial.item
2421                                      DEFERRABLE INITIALLY DEFERRED
2422 );
2423
2424 CREATE INDEX serial_claim_lid_idx ON acq.serial_claim( item );
2425
2426 CREATE TABLE acq.serial_claim_event (
2427     id             BIGSERIAL        PRIMARY KEY,
2428     type           INT              NOT NULL REFERENCES acq.claim_event_type
2429                                              DEFERRABLE INITIALLY DEFERRED,
2430     claim          SERIAL           NOT NULL REFERENCES acq.serial_claim
2431                                              DEFERRABLE INITIALLY DEFERRED,
2432     event_date     TIMESTAMPTZ      NOT NULL DEFAULT now(),
2433     creator        INT              NOT NULL REFERENCES actor.usr
2434                                              DEFERRABLE INITIALLY DEFERRED,
2435     note           TEXT
2436 );
2437
2438 CREATE INDEX serial_claim_event_claim_date_idx ON acq.serial_claim_event( claim, event_date );
2439
2440 CREATE OR REPLACE VIEW acq.lineitem_summary AS
2441     SELECT 
2442         li.id AS lineitem, 
2443         (
2444             SELECT COUNT(lid.id) 
2445             FROM acq.lineitem_detail lid
2446             WHERE lineitem = li.id
2447         ) AS item_count,
2448         (
2449             SELECT COUNT(lid.id) 
2450             FROM acq.lineitem_detail lid
2451             WHERE recv_time IS NOT NULL AND lineitem = li.id
2452         ) AS recv_count,
2453         (
2454             SELECT COUNT(lid.id) 
2455             FROM acq.lineitem_detail lid
2456                 JOIN acq.cancel_reason acqcr ON (acqcr.id = lid.cancel_reason)
2457             WHERE acqcr.keep_debits IS FALSE AND lineitem = li.id
2458         ) AS cancel_count,
2459         (
2460             SELECT COUNT(lid.id) 
2461             FROM acq.lineitem_detail lid
2462                 JOIN acq.cancel_reason acqcr ON (acqcr.id = lid.cancel_reason)
2463             WHERE acqcr.keep_debits IS TRUE AND lineitem = li.id
2464         ) AS delay_count,
2465         (
2466             SELECT COUNT(lid.id) 
2467             FROM acq.lineitem_detail lid
2468                 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
2469             WHERE NOT debit.encumbrance AND lineitem = li.id
2470         ) AS invoice_count,
2471         (
2472             SELECT COUNT(DISTINCT(lid.id)) 
2473             FROM acq.lineitem_detail lid
2474                 JOIN acq.claim claim ON (claim.lineitem_detail = lid.id)
2475             WHERE lineitem = li.id
2476         ) AS claim_count,
2477         (
2478             SELECT (COUNT(lid.id) * li.estimated_unit_price)::NUMERIC(8,2)
2479             FROM acq.lineitem_detail lid
2480             WHERE lid.cancel_reason IS NULL AND lineitem = li.id
2481         ) AS estimated_amount,
2482         (
2483             SELECT SUM(debit.amount)::NUMERIC(8,2)
2484             FROM acq.lineitem_detail lid
2485                 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
2486             WHERE debit.encumbrance AND lineitem = li.id
2487         ) AS encumbrance_amount,
2488         (
2489             SELECT SUM(debit.amount)::NUMERIC(8,2)
2490             FROM acq.lineitem_detail lid
2491                 JOIN acq.fund_debit debit ON (lid.fund_debit = debit.id)
2492             WHERE NOT debit.encumbrance AND lineitem = li.id
2493         ) AS paid_amount
2494
2495         FROM acq.lineitem AS li;
2496
2497 COMMIT;