]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/200.schema.acq.sql
Add new view acq.ordered_funding_source_credit, to define priorities
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 200.schema.acq.sql
1 DROP SCHEMA acq CASCADE;
2
3 BEGIN;
4
5 CREATE SCHEMA acq;
6
7
8 -- Tables
9
10
11 CREATE TABLE acq.currency_type (
12         code    TEXT PRIMARY KEY,
13         label   TEXT
14 );
15
16 -- Use the ISO 4217 abbreviations for currency codes
17 INSERT INTO acq.currency_type (code, label) VALUES ('USD','US Dollars');
18 INSERT INTO acq.currency_type (code, label) VALUES ('CAN','Canadian Dollars');
19 INSERT INTO acq.currency_type (code, label) VALUES ('EUR','Euros');
20
21 CREATE TABLE acq.exchange_rate (
22     id              SERIAL  PRIMARY KEY,
23     from_currency   TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
24     to_currency     TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
25     ratio           NUMERIC NOT NULL,
26     CONSTRAINT exchange_rate_from_to_once UNIQUE (from_currency,to_currency)
27 );
28
29 INSERT INTO acq.exchange_rate (from_currency,to_currency,ratio) VALUES ('USD','CAN',1.2);
30 INSERT INTO acq.exchange_rate (from_currency,to_currency,ratio) VALUES ('USD','EUR',0.5);
31
32 CREATE TABLE acq.provider (
33     id                  SERIAL  PRIMARY KEY,
34     name                TEXT    NOT NULL,
35     owner               INT     NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
36     currency_type       TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
37     code                TEXT    NOT NULL,
38     holding_tag         TEXT,
39     san                 TEXT,
40     CONSTRAINT provider_name_once_per_owner UNIQUE (name,owner),
41         CONSTRAINT code_once_per_owner UNIQUE (code, owner)
42 );
43
44 CREATE TABLE acq.provider_holding_subfield_map (
45     id          SERIAL  PRIMARY KEY,
46     provider    INT     NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
47     name        TEXT    NOT NULL, -- barcode, price, etc
48     subfield    TEXT    NOT NULL,
49     CONSTRAINT name_once_per_provider UNIQUE (provider,name)
50 );
51
52 CREATE TABLE acq.provider_address (
53         id              SERIAL  PRIMARY KEY,
54         valid           BOOL    NOT NULL DEFAULT TRUE,
55         address_type    TEXT,
56     provider    INT     NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
57         street1         TEXT    NOT NULL,
58         street2         TEXT,
59         city            TEXT    NOT NULL,
60         county          TEXT,
61         state           TEXT    NOT NULL,
62         country         TEXT    NOT NULL,
63         post_code       TEXT    NOT NULL
64 );
65
66 CREATE TABLE acq.provider_contact (
67         id              SERIAL  PRIMARY KEY,
68     provider    INT NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
69     name    TEXT NULL NULL,
70     role    TEXT, -- free-form.. e.g. "our sales guy"
71     email   TEXT,
72     phone   TEXT
73 );
74
75 CREATE TABLE acq.provider_contact_address (
76         id                      SERIAL  PRIMARY KEY,
77         valid                   BOOL    NOT NULL DEFAULT TRUE,
78         address_type    TEXT,
79         contact                 INT         NOT NULL REFERENCES acq.provider_contact (id) DEFERRABLE INITIALLY DEFERRED,
80         street1                 TEXT    NOT NULL,
81         street2                 TEXT,
82         city                    TEXT    NOT NULL,
83         county                  TEXT,
84         state                   TEXT    NOT NULL,
85         country                 TEXT    NOT NULL,
86         post_code               TEXT    NOT NULL
87 );
88
89
90 CREATE TABLE acq.funding_source (
91         id              SERIAL  PRIMARY KEY,
92         name            TEXT    NOT NULL,
93         owner           INT     NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
94         currency_type   TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
95         code            TEXT    UNIQUE,
96         CONSTRAINT funding_source_name_once_per_owner UNIQUE (name,owner)
97 );
98
99 CREATE TABLE acq.funding_source_credit (
100         id      SERIAL     PRIMARY KEY,
101         funding_source INT      NOT NULL REFERENCES acq.funding_source (id) DEFERRABLE INITIALLY DEFERRED,
102         amount         NUMERIC  NOT NULL,
103         note           TEXT,
104         deadline_date  TIMESTAMPTZ,
105         effective_date TIMESTAMPTZ NOT NULL default now()
106 );
107
108 CREATE VIEW acq.ordered_funding_source_credit AS
109     SELECT
110         CASE WHEN deadline_date IS NULL THEN
111             2
112         ELSE
113             1
114         END AS sort_priority,
115         CASE WHEN deadline_date IS NULL THEN
116             effective_date
117         ELSE
118             deadline_date
119         END AS sort_date,
120         id,
121         funding_source,
122         amount,
123         note
124     FROM
125         acq.funding_source_credit;
126
127 COMMENT ON VIEW acq.ordered_funding_source_credit IS $$
128 /*
129  * Copyright (C) 2009  Georgia Public Library Service
130  * Scott McKellar <scott@gmail.com>
131  *
132  * The acq.ordered_funding_source_credit view is a prioritized
133  * ordering of funding source credits.  When ordered by the first
134  * three columns, this view defines the order in which the various
135  * credits are to be tapped for spending, subject to the allocations
136  * in the acq.fund_allocation table.
137  *
138  * The first column reflects the principle that we should spend
139  * money with deadlines before spending money without deadlines.
140  *
141  * The second column reflects the principle that we should spend the
142  * oldest money first.  For money with deadlines, that means that we
143  * spend first from the credit with the earliest deadline.  For
144  * money without deadlines, we spend first from the credit with the
145  * earliest effective date.
146  *
147  * The third column is a tie breaker to ensure a consistent
148  * ordering.
149  *
150  * ****
151  *
152  * This program is free software; you can redistribute it and/or
153  * modify it under the terms of the GNU General Public License
154  * as published by the Free Software Foundation; either version 2
155  * of the License, or (at your option) any later version.
156  *
157  * This program is distributed in the hope that it will be useful,
158  * but WITHOUT ANY WARRANTY; without even the implied warranty of
159  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
160  * GNU General Public License for more details.
161  */
162 $$;
163
164 CREATE TABLE acq.fund (
165     id              SERIAL  PRIMARY KEY,
166     org             INT     NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
167     name            TEXT    NOT NULL,
168     year            INT     NOT NULL DEFAULT EXTRACT( YEAR FROM NOW() ),
169     currency_type   TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
170     code            TEXT,
171         rollover        BOOL    NOT NULL DEFAULT FALSE,
172     CONSTRAINT name_once_per_org_year UNIQUE (org,name,year),
173     CONSTRAINT code_once_per_org_year UNIQUE (org, code, year)
174 );
175
176 CREATE TABLE acq.fund_debit (
177         id                      SERIAL  PRIMARY KEY,
178         fund                    INT     NOT NULL REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
179         origin_amount           NUMERIC NOT NULL,  -- pre-exchange-rate amount
180         origin_currency_type    TEXT    NOT NULL REFERENCES acq.currency_type (code) DEFERRABLE INITIALLY DEFERRED,
181         amount                  NUMERIC NOT NULL,
182         encumbrance             BOOL    NOT NULL DEFAULT TRUE,
183         debit_type              TEXT    NOT NULL,
184         xfer_destination        INT     REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
185         create_time     TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
186 );
187
188 CREATE TABLE acq.fund_allocation (
189     id          SERIAL  PRIMARY KEY,
190     funding_source        INT     NOT NULL REFERENCES acq.funding_source (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
191     fund        INT     NOT NULL REFERENCES acq.fund (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
192     amount      NUMERIC NOT NULL,
193     allocator   INT NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
194     note        TEXT,
195         create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
196 );
197 CREATE INDEX fund_alloc_allocator_idx ON acq.fund_allocation ( allocator );
198
199 CREATE TABLE acq.picklist (
200         id              SERIAL                          PRIMARY KEY,
201         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
202         creator         INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
203         editor          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
204         org_unit        INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
205         name            TEXT                            NOT NULL,
206         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
207         edit_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
208         CONSTRAINT name_once_per_owner UNIQUE (name,owner)
209 );
210 CREATE INDEX acq_picklist_owner_idx   ON acq.picklist ( owner );
211 CREATE INDEX acq_picklist_creator_idx ON acq.picklist ( creator );
212 CREATE INDEX acq_picklist_editor_idx  ON acq.picklist ( editor );
213
214 CREATE TABLE acq.purchase_order (
215         id              SERIAL                          PRIMARY KEY,
216         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
217         creator         INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
218         editor          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
219         ordering_agency INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
220         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
221         edit_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
222         provider        INT                             NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
223         state                   TEXT                                    NOT NULL DEFAULT 'new',
224         order_date              TIMESTAMP WITH TIME ZONE,
225         name                    TEXT                                    NOT NULL
226 );
227 CREATE INDEX po_owner_idx ON acq.purchase_order (owner);
228 CREATE INDEX po_provider_idx ON acq.purchase_order (provider);
229 CREATE INDEX po_state_idx ON acq.purchase_order (state);
230 CREATE INDEX po_creator_idx  ON acq.purchase_order ( creator );
231 CREATE INDEX po_editor_idx   ON acq.purchase_order ( editor );
232 CREATE INDEX acq_po_org_name_order_date_idx ON acq.purchase_order( ordering_agency, name, order_date );
233
234 -- The name should default to the id, as text.  We can't reference a column
235 -- in a DEFAULT clause, so we use a trigger:
236
237 CREATE OR REPLACE FUNCTION acq.purchase_order_name_default () RETURNS TRIGGER 
238 AS $$
239 BEGIN
240         IF NEW.name IS NULL THEN
241                 NEW.name := NEW.id::TEXT;
242         END IF;
243
244         RETURN NEW;
245 END;
246 $$ LANGUAGE PLPGSQL;
247
248 CREATE TRIGGER po_name_default_trg
249   BEFORE INSERT OR UPDATE ON acq.purchase_order
250   FOR EACH ROW EXECUTE PROCEDURE acq.purchase_order_name_default ();
251
252 -- The order name should be unique for a given ordering agency on a given order date
253 -- (truncated to midnight), but only where the order_date is not NULL.  Conceptually
254 -- this rule requires a check constraint with a subquery.  However you can't have a
255 -- subquery in a CHECK constraint, so we fake it with a trigger.
256
257 CREATE OR REPLACE FUNCTION acq.po_org_name_date_unique () RETURNS TRIGGER 
258 AS $$
259 DECLARE
260         collision INT;
261 BEGIN
262         --
263         -- If order_date is not null, then make sure we don't have a collision
264         -- on order_date (truncated to day), org, and name
265         --
266         IF NEW.order_date IS NULL THEN
267                 RETURN NEW;
268         END IF;
269         --
270         -- In the WHERE clause, we compare the order_dates without regard to time of day.
271         -- We use a pair of inequalities instead of comparing truncated dates so that the
272         -- query can do an indexed range scan.
273         --
274         SELECT 1 INTO collision
275         FROM acq.purchase_order
276         WHERE
277                 ordering_agency = NEW.ordering_agency
278                 AND name = NEW.name
279                 AND order_date >= date_trunc( 'day', NEW.order_date )
280                 AND order_date <  date_trunc( 'day', NEW.order_date ) + '1 day'::INTERVAL
281                 AND id <> NEW.id;
282         --
283         IF collision IS NULL THEN
284                 -- okay, no collision
285                 RETURN NEW;
286         ELSE
287                 -- collision; nip it in the bud
288                 RAISE EXCEPTION 'Colliding purchase orders: ordering_agency %, date %, name ''%''',
289                         NEW.ordering_agency, NEW.order_date, NEW.name;
290         END IF;
291 END;
292 $$ LANGUAGE PLPGSQL;
293
294 CREATE TRIGGER po_org_name_date_unique_trg
295   BEFORE INSERT OR UPDATE ON acq.purchase_order
296   FOR EACH ROW EXECUTE PROCEDURE acq.po_org_name_date_unique ();
297
298 CREATE TABLE acq.po_note (
299         id              SERIAL                          PRIMARY KEY,
300         purchase_order  INT                             NOT NULL REFERENCES acq.purchase_order (id) DEFERRABLE INITIALLY DEFERRED,
301         creator         INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
302         editor          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
303         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
304         edit_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
305         value           TEXT                            NOT NULL
306 );
307 CREATE INDEX po_note_po_idx ON acq.po_note (purchase_order);
308 CREATE INDEX acq_po_note_creator_idx  ON acq.po_note ( creator );
309 CREATE INDEX acq_po_note_editor_idx   ON acq.po_note ( editor );
310
311 CREATE TABLE acq.lineitem (
312         id                  BIGSERIAL                   PRIMARY KEY,
313         creator             INT                         NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
314         editor              INT                         NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
315         selector            INT                         NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
316         provider            INT                         REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED,
317         purchase_order      INT                         REFERENCES acq.purchase_order (id) DEFERRABLE INITIALLY DEFERRED,
318         picklist            INT                         REFERENCES acq.picklist (id) DEFERRABLE INITIALLY DEFERRED,
319         expected_recv_time  TIMESTAMP WITH TIME ZONE,
320         create_time         TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
321         edit_time           TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
322         marc                TEXT                        NOT NULL,
323         eg_bib_id           INT                         REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
324         source_label        TEXT,
325         item_count          INT                         NOT NULL DEFAULT 0,
326         state               TEXT                        NOT NULL DEFAULT 'new',
327     CONSTRAINT picklist_or_po CHECK (picklist IS NOT NULL OR purchase_order IS NOT NULL)
328 );
329 CREATE INDEX li_po_idx ON acq.lineitem (purchase_order);
330 CREATE INDEX li_pl_idx ON acq.lineitem (picklist);
331 CREATE INDEX li_creator_idx   ON acq.lineitem ( creator );
332 CREATE INDEX li_editor_idx    ON acq.lineitem ( editor );
333 CREATE INDEX li_selector_idx  ON acq.lineitem ( selector );
334
335 CREATE TABLE acq.lineitem_note (
336         id              SERIAL                          PRIMARY KEY,
337         lineitem        INT                             NOT NULL REFERENCES acq.lineitem (id) DEFERRABLE INITIALLY DEFERRED,
338         creator         INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
339         editor          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
340         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
341         edit_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
342         value           TEXT                            NOT NULL
343 );
344 CREATE INDEX li_note_li_idx ON acq.lineitem_note (lineitem);
345 CREATE INDEX li_note_creator_idx  ON acq.lineitem_note ( creator );
346 CREATE INDEX li_note_editor_idx   ON acq.lineitem_note ( editor );
347
348 CREATE TABLE acq.lineitem_detail (
349     id          BIGSERIAL       PRIMARY KEY,
350     lineitem    INT         NOT NULL REFERENCES acq.lineitem (id) DEFERRABLE INITIALLY DEFERRED,
351     fund        INT         REFERENCES acq.fund (id) DEFERRABLE INITIALLY DEFERRED,
352     fund_debit  INT         REFERENCES acq.fund_debit (id) DEFERRABLE INITIALLY DEFERRED,
353     eg_copy_id  BIGINT      REFERENCES asset.copy (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
354     barcode     TEXT,
355     cn_label    TEXT,
356     note        TEXT,
357     collection_code TEXT,
358     circ_modifier   TEXT    REFERENCES config.circ_modifier (code) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
359     owning_lib  INT         REFERENCES actor.org_unit (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
360     location    INT         REFERENCES asset.copy_location (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
361     recv_time   TIMESTAMP WITH TIME ZONE
362 );
363
364 CREATE INDEX li_detail_li_idx ON acq.lineitem_detail (lineitem);
365
366 CREATE TABLE acq.lineitem_attr_definition (
367         id              BIGSERIAL       PRIMARY KEY,
368         code            TEXT            NOT NULL,
369         description     TEXT            NOT NULL,
370         remove          TEXT            NOT NULL DEFAULT '',
371         ident           BOOL            NOT NULL DEFAULT FALSE
372 );
373
374 CREATE TABLE acq.lineitem_marc_attr_definition (
375         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
376         xpath           TEXT            NOT NULL
377 ) INHERITS (acq.lineitem_attr_definition);
378
379 CREATE TABLE acq.lineitem_provider_attr_definition (
380         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
381         xpath           TEXT            NOT NULL,
382         provider        INT     NOT NULL REFERENCES acq.provider (id) DEFERRABLE INITIALLY DEFERRED
383 ) INHERITS (acq.lineitem_attr_definition);
384
385 CREATE TABLE acq.lineitem_generated_attr_definition (
386         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
387         xpath           TEXT            NOT NULL
388 ) INHERITS (acq.lineitem_attr_definition);
389
390 CREATE TABLE acq.lineitem_usr_attr_definition (
391         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq'),
392         usr             INT     NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED
393 ) INHERITS (acq.lineitem_attr_definition);
394 CREATE INDEX li_usr_attr_def_usr_idx  ON acq.lineitem_usr_attr_definition ( usr );
395
396 CREATE TABLE acq.lineitem_local_attr_definition (
397         id              BIGINT  PRIMARY KEY DEFAULT NEXTVAL('acq.lineitem_attr_definition_id_seq')
398 ) INHERITS (acq.lineitem_attr_definition);
399
400 CREATE TABLE acq.lineitem_attr (
401         id              BIGSERIAL       PRIMARY KEY,
402         definition      BIGINT          NOT NULL,
403         lineitem        BIGINT          NOT NULL REFERENCES acq.lineitem (id) DEFERRABLE INITIALLY DEFERRED,
404         attr_type       TEXT            NOT NULL,
405         attr_name       TEXT            NOT NULL,
406         attr_value      TEXT            NOT NULL
407 );
408
409 CREATE INDEX li_attr_li_idx ON acq.lineitem_attr (lineitem);
410 CREATE INDEX li_attr_value_idx ON acq.lineitem_attr (attr_value);
411 CREATE INDEX li_attr_definition_idx ON acq.lineitem_attr (definition);
412
413
414 -- Seed data
415
416
417 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('title','Title of work','//*[@tag="245"]/*[contains("abcmnopr",@code)]');
418 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)]');
419 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('language','Language of work','//*[@tag="240"]/*[@code="l"][1]');
420 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('pagination','Pagination','//*[@tag="300"]/*[@code="a"][1]');
421 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('isbn','ISBN','//*[@tag="020"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
422 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath, remove ) VALUES ('issn','ISSN','//*[@tag="022"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
423 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('price','Price','//*[@tag="020" or @tag="022"]/*[@code="c"][1]');
424 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('identifier','Identifier','//*[@tag="001"]');
425 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('publisher','Publisher','//*[@tag="260"]/*[@code="b"][1]');
426 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('pubdate','Publication Date','//*[@tag="260"]/*[@code="c"][1]');
427 INSERT INTO acq.lineitem_marc_attr_definition ( code, description, xpath ) VALUES ('edition','Edition','//*[@tag="250"]/*[@code="a"][1]');
428
429 INSERT INTO acq.lineitem_local_attr_definition ( code, description ) VALUES ('estimated_price', 'Estimated Price');
430
431
432 CREATE TABLE acq.distribution_formula (
433         id              SERIAL PRIMARY KEY,
434         owner   INT NOT NULL
435                         REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
436         name    TEXT NOT NULL,
437         skip_count      INT NOT NULL DEFAULT 0,
438         CONSTRAINT acqdf_name_once_per_owner UNIQUE (name, owner)
439 );
440
441 CREATE TABLE acq.distribution_formula_entry (
442         id                      SERIAL PRIMARY KEY,
443         formula         INTEGER NOT NULL REFERENCES acq.distribution_formula(id)
444                                 ON DELETE CASCADE
445                                 DEFERRABLE INITIALLY DEFERRED,
446         position        INTEGER NOT NULL,
447         item_count      INTEGER NOT NULL,
448         owning_lib      INTEGER REFERENCES actor.org_unit(id)
449                                 DEFERRABLE INITIALLY DEFERRED,
450         location        INTEGER REFERENCES asset.copy_location(id),
451         CONSTRAINT acqdfe_lib_once_per_formula UNIQUE( formula, position ),
452         CONSTRAINT acqdfe_must_be_somewhere
453                                 CHECK( owning_lib IS NOT NULL OR location IS NOT NULL ) 
454 );
455
456 CREATE TABLE acq.fund_tag (
457         id              SERIAL PRIMARY KEY,
458         owner   INT NOT NULL
459                         REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
460         name    TEXT NOT NULL,
461         CONSTRAINT acqft_tag_once_per_owner UNIQUE (name, owner)
462 );
463
464 CREATE TABLE acq.fund_tag_map (
465         id                      SERIAL PRIMARY KEY,
466         fund            INTEGER NOT NULL REFERENCES acq.fund(id)
467                                 DEFERRABLE INITIALLY DEFERRED,
468         tag         INTEGER REFERENCES acq.fund_tag(id)
469                                 ON DELETE CASCADE
470                                 DEFERRABLE INITIALLY DEFERRED,
471         CONSTRAINT acqftm_fund_once_per_tag UNIQUE( fund, tag )
472 );
473
474 CREATE TABLE acq.fund_transfer (
475     id               SERIAL         PRIMARY KEY,
476     src_fund         INT            NOT NULL REFERENCES acq.fund( id )
477                                     DEFERRABLE INITIALLY DEFERRED,
478     src_amount       NUMERIC        NOT NULL,
479     dest_fund        INT            NOT NULL REFERENCES acq.fund( id )
480                                     DEFERRABLE INITIALLY DEFERRED,
481     dest_amount      NUMERIC        NOT NULL,
482     transfer_time    TIMESTAMPTZ    NOT NULL DEFAULT now(),
483     transfer_user    INT            NOT NULL REFERENCES actor.usr( id )
484                                     DEFERRABLE INITIALLY DEFERRED,
485     note             TEXT
486 );
487
488 CREATE INDEX acqftr_usr_idx
489 ON acq.fund_transfer( transfer_user );
490
491 COMMENT ON TABLE acq.fund_transfer IS $$
492 /*
493  * Copyright (C) 2009  Georgia Public Library Service
494  * Scott McKellar <scott@esilibrary.com>
495  *
496  * Fund Transfer
497  *
498  * Each row represents the transfer of money from a source fund
499  * to a destination fund.  There should be corresponding entries
500  * in acq.fund_allocation.  The purpose of acq.fund_transfer is
501  * to record how much money moved from which fund to which other
502  * fund.
503  *
504  * The presence of two amount fields, rather than one, reflects
505  * the possibility that the two funds are denominated in different
506  * currencies.  If they use the same currency type, the two
507  * amounts should be the same.
508  *
509  * ****
510  *
511  * This program is free software; you can redistribute it and/or
512  * modify it under the terms of the GNU General Public License
513  * as published by the Free Software Foundation; either version 2
514  * of the License, or (at your option) any later version.
515  *
516  * This program is distributed in the hope that it will be useful,
517  * but WITHOUT ANY WARRANTY; without even the implied warranty of
518  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
519  * GNU General Public License for more details.
520  */
521 $$;
522
523 CREATE TABLE acq.fiscal_calendar (
524         id              SERIAL         PRIMARY KEY,
525         name            TEXT           NOT NULL
526 );
527
528 -- Create a default calendar (though we don't specify its contents). 
529 -- Create a foreign key in actor.org_unit, initially pointing to
530 -- the default calendar.
531
532 INSERT INTO acq.fiscal_calendar (
533     name
534 ) VALUES (
535
536     'Default'
537 );
538
539 ALTER TABLE actor.org_unit
540 ADD COLUMN fiscal_calendar INT NOT NULL
541     REFERENCES acq.fiscal_calendar( id )
542     DEFERRABLE INITIALLY DEFERRED
543     DEFAULT 1;
544
545 CREATE TABLE acq.fiscal_year (
546         id              SERIAL         PRIMARY KEY,
547         calendar        INT            NOT NULL
548                                        REFERENCES acq.fiscal_calendar
549                                        ON DELETE CASCADE
550                                        DEFERRABLE INITIALLY DEFERRED,
551         year            INT            NOT NULL,
552         year_begin      TIMESTAMPTZ    NOT NULL,
553         year_end        TIMESTAMPTZ    NOT NULL,
554         CONSTRAINT acq_fy_logical_key  UNIQUE ( calendar, year ),
555     CONSTRAINT acq_fy_physical_key UNIQUE ( calendar, year_begin )
556 );
557
558 -- Functions
559
560 CREATE TYPE acq.flat_lineitem_holding_subfield AS (lineitem int, holding int, subfield text, data text);
561 CREATE OR REPLACE FUNCTION acq.extract_holding_attr_table (lineitem int, tag text) RETURNS SETOF acq.flat_lineitem_holding_subfield AS $$
562 DECLARE
563     counter INT;
564     lida    acq.flat_lineitem_holding_subfield%ROWTYPE;
565 BEGIN
566
567     SELECT  COUNT(*) INTO counter
568       FROM  xpath_table(
569                 'id',
570                 'marc',
571                 'acq.lineitem',
572                 '//*[@tag="' || tag || '"]',
573                 'id=' || lineitem
574             ) as t(i int,c text);
575
576     FOR i IN 1 .. counter LOOP
577         FOR lida IN
578             SELECT  * 
579               FROM  (   SELECT  id,i,t,v
580                           FROM  xpath_table(
581                                     'id',
582                                     'marc',
583                                     'acq.lineitem',
584                                     '//*[@tag="' || tag || '"][position()=' || i || ']/*/@code|' ||
585                                         '//*[@tag="' || tag || '"][position()=' || i || ']/*[@code]',
586                                     'id=' || lineitem
587                                 ) as t(id int,t text,v text)
588                     )x
589         LOOP
590             RETURN NEXT lida;
591         END LOOP;
592     END LOOP;
593
594     RETURN;
595 END;
596 $$ LANGUAGE PLPGSQL;
597
598 CREATE TYPE acq.flat_lineitem_detail AS (lineitem int, holding int, attr text, data text);
599 CREATE OR REPLACE FUNCTION acq.extract_provider_holding_data ( lineitem_i int ) RETURNS SETOF acq.flat_lineitem_detail AS $$
600 DECLARE
601     prov_i  INT;
602     tag_t   TEXT;
603     lida    acq.flat_lineitem_detail%ROWTYPE;
604 BEGIN
605     SELECT provider INTO prov_i FROM acq.lineitem WHERE id = lineitem_i;
606     IF NOT FOUND THEN RETURN; END IF;
607
608     SELECT holding_tag INTO tag_t FROM acq.provider WHERE id = prov_i;
609     IF NOT FOUND OR tag_t IS NULL THEN RETURN; END IF;
610
611     FOR lida IN
612         SELECT  lineitem_i,
613                 h.holding,
614                 a.name,
615                 h.data
616           FROM  acq.extract_holding_attr_table( lineitem_i, tag_t ) h
617                 JOIN acq.provider_holding_subfield_map a USING (subfield)
618           WHERE a.provider = prov_i
619     LOOP
620         RETURN NEXT lida;
621     END LOOP;
622
623     RETURN;
624 END;
625 $$ LANGUAGE PLPGSQL;
626
627 -- select * from acq.extract_provider_holding_data(699);
628
629 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$
630         SELECT public.extract_marc_field('acq.lineitem', $1, $2, $3);
631 $$ LANGUAGE SQL;
632
633 /*
634 CREATE OR REPLACE FUNCTION public.extract_bib_marc_field ( BIGINT, TEXT ) RETURNS TEXT AS $$
635         SELECT public.extract_marc_field('biblio.record_entry', $1, $2);
636 $$ LANGUAGE SQL;
637
638 CREATE OR REPLACE FUNCTION public.extract_authority_marc_field ( BIGINT, TEXT ) RETURNS TEXT AS $$
639         SELECT public.extract_marc_field('authority.record_entry', $1, $2);
640 $$ LANGUAGE SQL;
641 */
642 -- For example:
643 -- INSERT INTO acq.lineitem_provider_attr_definition ( provider, code, description, xpath ) VALUES (1,'price','Price','//*[@tag="020" or @tag="022"]/*[@code="a"][1]');
644
645 /*
646 Suggested vendor fields:
647         vendor_price
648         vendor_currency
649         vendor_avail
650         vendor_po
651         vendor_identifier
652 */
653
654 CREATE OR REPLACE FUNCTION public.ingest_acq_marc ( ) RETURNS TRIGGER AS $$
655 DECLARE
656         value           TEXT;
657         atype           TEXT;
658         prov            INT;
659         adef            RECORD;
660         xpath_string    TEXT;
661 BEGIN
662         FOR adef IN SELECT *,tableoid FROM acq.lineitem_attr_definition LOOP
663
664                 SELECT relname::TEXT INTO atype FROM pg_class WHERE oid = adef.tableoid;
665
666                 IF (atype NOT IN ('lineitem_usr_attr_definition','lineitem_local_attr_definition')) THEN
667                         IF (atype = 'lineitem_provider_attr_definition') THEN
668                                 SELECT provider INTO prov FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
669                                 CONTINUE WHEN NEW.provider IS NULL OR prov <> NEW.provider;
670                         END IF;
671                         
672                         IF (atype = 'lineitem_provider_attr_definition') THEN
673                                 SELECT xpath INTO xpath_string FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
674                         ELSIF (atype = 'lineitem_marc_attr_definition') THEN
675                                 SELECT xpath INTO xpath_string FROM acq.lineitem_marc_attr_definition WHERE id = adef.id;
676                         ELSIF (atype = 'lineitem_generated_attr_definition') THEN
677                                 SELECT xpath INTO xpath_string FROM acq.lineitem_generated_attr_definition WHERE id = adef.id;
678                         END IF;
679
680                         SELECT extract_acq_marc_field(id, xpath_string, adef.remove) INTO value FROM acq.lineitem WHERE id = NEW.id;
681
682                         IF (value IS NOT NULL AND value <> '') THEN
683                                 INSERT INTO acq.lineitem_attr (lineitem, definition, attr_type, attr_name, attr_value)
684                                         VALUES (NEW.id, adef.id, atype, adef.code, value);
685                         END IF;
686
687                 END IF;
688
689         END LOOP;
690
691         RETURN NULL;
692 END;
693 $$ LANGUAGE PLPGSQL;
694
695 CREATE OR REPLACE FUNCTION public.cleanup_acq_marc ( ) RETURNS TRIGGER AS $$
696 BEGIN
697         IF TG_OP = 'UPDATE' THEN
698                 DELETE FROM acq.lineitem_attr
699                         WHERE lineitem = OLD.id AND attr_type IN ('lineitem_provider_attr_definition', 'lineitem_marc_attr_definition','lineitem_generated_attr_definition');
700                 RETURN NEW;
701         ELSE
702                 DELETE FROM acq.lineitem_attr WHERE lineitem = OLD.id;
703                 RETURN OLD;
704         END IF;
705 END;
706 $$ LANGUAGE PLPGSQL;
707
708 CREATE TRIGGER cleanup_lineitem_trigger
709         BEFORE UPDATE OR DELETE ON acq.lineitem
710         FOR EACH ROW EXECUTE PROCEDURE public.cleanup_acq_marc();
711
712 CREATE TRIGGER ingest_lineitem_trigger
713         AFTER INSERT OR UPDATE ON acq.lineitem
714         FOR EACH ROW EXECUTE PROCEDURE public.ingest_acq_marc();
715
716 CREATE OR REPLACE FUNCTION acq.exchange_ratio ( from_ex TEXT, to_ex TEXT ) RETURNS NUMERIC AS $$
717 DECLARE
718     rat NUMERIC;
719 BEGIN
720     IF from_ex = to_ex THEN
721         RETURN 1.0;
722     END IF;
723
724     SELECT ratio INTO rat FROM acq.exchange_rate WHERE from_currency = from_ex AND to_currency = to_ex;
725
726     IF FOUND THEN
727         RETURN rat;
728     ELSE
729         SELECT ratio INTO rat FROM acq.exchange_rate WHERE from_currency = to_ex AND to_currency = from_ex;
730         IF FOUND THEN
731             RETURN 1.0/rat;
732         END IF;
733     END IF;
734
735     RETURN NULL;
736
737 END;
738 $$ LANGUAGE PLPGSQL;
739
740 CREATE OR REPLACE FUNCTION acq.exchange_ratio ( TEXT, TEXT, NUMERIC ) RETURNS NUMERIC AS $$
741     SELECT $3 * acq.exchange_ratio($1, $2);
742 $$ LANGUAGE SQL;
743
744 CREATE OR REPLACE FUNCTION acq.find_bad_fy()
745 /*
746         Examine the acq.fiscal_year table, comparing successive years.
747         Report any inconsistencies, i.e. years that overlap, have gaps
748     between them, or are out of sequence.
749 */
750 RETURNS SETOF RECORD AS $$
751 DECLARE
752         first_row  BOOLEAN;
753         curr_year  RECORD;
754         prev_year  RECORD;
755         return_rec RECORD;
756 BEGIN
757         first_row := true;
758         FOR curr_year in
759                 SELECT
760                         id,
761                         calendar,
762                         year,
763                         year_begin,
764                         year_end
765                 FROM
766                         acq.fiscal_year
767                 ORDER BY
768                         calendar,
769                         year_begin
770         LOOP
771                 --
772                 IF first_row THEN
773                         first_row := FALSE;
774                 ELSIF curr_year.calendar    = prev_year.calendar THEN
775                         IF curr_year.year_begin > prev_year.year_end THEN
776                                 -- This ugly kludge works around the fact that older
777                                 -- versions of PostgreSQL don't support RETURN QUERY SELECT
778                                 FOR return_rec IN SELECT
779                                         prev_year.id,
780                                         prev_year.year,
781                                         'Gap between fiscal years'::TEXT
782                                 LOOP
783                                         RETURN NEXT return_rec;
784                                 END LOOP;
785                         ELSIF curr_year.year_begin < prev_year.year_end THEN
786                                 FOR return_rec IN SELECT
787                                         prev_year.id,
788                                         prev_year.year,
789                                         'Overlapping fiscal years'::TEXT
790                                 LOOP
791                                         RETURN NEXT return_rec;
792                                 END LOOP;
793                         ELSIF curr_year.year < prev_year.year THEN
794                                 FOR return_rec IN SELECT
795                                         prev_year.id,
796                                         prev_year.year,
797                                         'Fiscal years out of order'::TEXT
798                                 LOOP
799                                         RETURN NEXT return_rec;
800                                 END LOOP;
801                         END IF;
802                 END IF;
803                 --
804                 prev_year := curr_year;
805         END LOOP;
806         --
807         RETURN;
808 END;
809 $$ LANGUAGE plpgsql;
810
811 CREATE OR REPLACE VIEW acq.funding_source_credit_total AS
812     SELECT  funding_source,
813             SUM(amount) AS amount
814       FROM  acq.funding_source_credit
815       GROUP BY 1;
816
817 CREATE OR REPLACE VIEW acq.funding_source_allocation_total AS
818     SELECT  funding_source,
819             SUM(a.amount)::NUMERIC(100,2) AS amount
820     FROM  acq.fund_allocation a
821     GROUP BY 1;
822
823 CREATE OR REPLACE VIEW acq.funding_source_balance AS
824     SELECT  COALESCE(c.funding_source, a.funding_source) AS funding_source,
825             SUM(COALESCE(c.amount,0.0) - COALESCE(a.amount,0.0))::NUMERIC(100,2) AS amount
826       FROM  acq.funding_source_credit_total c
827             FULL JOIN acq.funding_source_allocation_total a USING (funding_source)
828       GROUP BY 1;
829
830 CREATE OR REPLACE VIEW acq.fund_allocation_total AS
831     SELECT  fund,
832             SUM(a.amount * acq.exchange_ratio(s.currency_type, f.currency_type))::NUMERIC(100,2) AS amount
833     FROM acq.fund_allocation a
834          JOIN acq.fund f ON (a.fund = f.id)
835          JOIN acq.funding_source s ON (a.funding_source = s.id)
836     GROUP BY 1;
837
838 CREATE OR REPLACE VIEW acq.fund_debit_total AS
839     SELECT  id AS fund,
840             encumbrance,
841             SUM(amount) AS amount
842       FROM  acq.fund_debit 
843       GROUP BY 1,2;
844
845 CREATE OR REPLACE VIEW acq.fund_encumbrance_total AS
846     SELECT  fund,
847             SUM(amount) AS amount
848       FROM  acq.fund_debit_total
849       WHERE encumbrance IS TRUE
850       GROUP BY 1;
851
852 CREATE OR REPLACE VIEW acq.fund_spent_total AS
853     SELECT  fund,
854             SUM(amount) AS amount
855       FROM  acq.fund_debit_total
856       WHERE encumbrance IS FALSE
857       GROUP BY 1;
858
859 CREATE OR REPLACE VIEW acq.fund_combined_balance AS
860     SELECT  c.fund,
861             c.amount - COALESCE(d.amount,0.0) AS amount
862       FROM  acq.fund_allocation_total c
863             LEFT JOIN acq.fund_debit_total d USING (fund);
864
865 CREATE OR REPLACE VIEW acq.fund_spent_balance AS
866     SELECT  c.fund,
867             c.amount - COALESCE(d.amount,0.0) AS amount
868       FROM  acq.fund_allocation_total c
869             LEFT JOIN acq.fund_spent_total d USING (fund);
870
871 COMMIT;
872
873
874
875