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