]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/040.schema.asset.sql
LP#1587620: inconsistent copy counts between opac and staff client for peer bibs
[Evergreen.git] / Open-ILS / src / sql / Pg / 040.schema.asset.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18 DROP SCHEMA IF EXISTS asset CASCADE;
19
20 BEGIN;
21
22 CREATE SCHEMA asset;
23
24 CREATE TABLE asset.copy_location (
25         id              SERIAL  PRIMARY KEY,
26         name            TEXT    NOT NULL,
27         owning_lib      INT     NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
28         holdable        BOOL    NOT NULL DEFAULT TRUE,
29         hold_verify     BOOL    NOT NULL DEFAULT FALSE,
30         opac_visible    BOOL    NOT NULL DEFAULT TRUE,
31         circulate       BOOL    NOT NULL DEFAULT TRUE,
32         label_prefix    TEXT,
33         label_suffix    TEXT,
34         checkin_alert   BOOL    NOT NULL DEFAULT FALSE,
35         deleted         BOOL    NOT NULL DEFAULT FALSE,
36         url             TEXT
37 );
38 CREATE UNIQUE INDEX acl_name_once_per_lib ON asset.copy_location (name, owning_lib) WHERE deleted = FALSE OR deleted IS FALSE;
39
40 CREATE TABLE asset.copy_location_order
41 (
42         id              SERIAL           PRIMARY KEY,
43         location        INT              NOT NULL
44                                              REFERENCES asset.copy_location
45                                              ON DELETE CASCADE
46                                              DEFERRABLE INITIALLY DEFERRED,
47         org             INT              NOT NULL
48                                              REFERENCES actor.org_unit
49                                              ON DELETE CASCADE
50                                              DEFERRABLE INITIALLY DEFERRED,
51         position        INT              NOT NULL DEFAULT 0,
52         CONSTRAINT acplo_once_per_org UNIQUE ( location, org )
53 );
54
55 CREATE TABLE asset.copy_location_group (
56     id              SERIAL  PRIMARY KEY,
57     name            TEXT    NOT NULL, -- i18n
58     owner           INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
59     pos             INT     NOT NULL DEFAULT 0,
60     top             BOOL    NOT NULL DEFAULT FALSE,
61     opac_visible    BOOL    NOT NULL DEFAULT TRUE,
62     CONSTRAINT lgroup_once_per_owner UNIQUE (owner,name)
63 );
64
65 CREATE TABLE asset.copy_location_group_map (
66     id       SERIAL PRIMARY KEY,
67     location    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
68     lgroup      INT     NOT NULL REFERENCES asset.copy_location_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
69     CONSTRAINT  lgroup_once_per_group UNIQUE (lgroup,location)
70 );
71
72 CREATE TABLE asset.copy (
73         id              BIGSERIAL                       PRIMARY KEY,
74         circ_lib        INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
75         creator         BIGINT                          NOT NULL,
76         call_number     BIGINT                          NOT NULL,
77         editor          BIGINT                          NOT NULL,
78         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
79         edit_date       TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
80         copy_number     INT,
81         status          INT                             NOT NULL DEFAULT 0 REFERENCES config.copy_status (id) DEFERRABLE INITIALLY DEFERRED,
82         location        INT                             NOT NULL DEFAULT 1 REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED,
83         loan_duration   INT                             NOT NULL CHECK ( loan_duration IN (1,2,3) ),
84         fine_level      INT                             NOT NULL CHECK ( fine_level IN (1,2,3) ),
85         age_protect     INT,
86         circulate       BOOL                            NOT NULL DEFAULT TRUE,
87         deposit         BOOL                            NOT NULL DEFAULT FALSE,
88         ref             BOOL                            NOT NULL DEFAULT FALSE,
89         holdable        BOOL                            NOT NULL DEFAULT TRUE,
90         deposit_amount  NUMERIC(6,2)                    NOT NULL DEFAULT 0.00,
91         price           NUMERIC(8,2),
92         barcode         TEXT                            NOT NULL,
93         circ_modifier   TEXT,
94         circ_as_type    TEXT,
95         dummy_title     TEXT,
96         dummy_author    TEXT,
97         alert_message   TEXT,
98         opac_visible    BOOL                            NOT NULL DEFAULT TRUE,
99         deleted         BOOL                            NOT NULL DEFAULT FALSE,
100         floating        INT,
101         dummy_isbn      TEXT,
102         status_changed_time TIMESTAMP WITH TIME ZONE,
103         active_date TIMESTAMP WITH TIME ZONE,
104         mint_condition      BOOL        NOT NULL DEFAULT TRUE,
105     cost    NUMERIC(8,2)
106 );
107 CREATE UNIQUE INDEX copy_barcode_key ON asset.copy (barcode) WHERE deleted = FALSE OR deleted IS FALSE;
108 CREATE INDEX cp_cn_idx ON asset.copy (call_number);
109 CREATE INDEX cp_avail_cn_idx ON asset.copy (call_number);
110 CREATE INDEX cp_creator_idx  ON asset.copy ( creator );
111 CREATE INDEX cp_editor_idx   ON asset.copy ( editor );
112 CREATE INDEX cp_create_date  ON asset.copy (create_date);
113 CREATE INDEX cp_available_by_circ_lib_idx ON asset.copy (circ_lib) WHERE status IN (0,7);
114 CREATE RULE protect_copy_delete AS ON DELETE TO asset.copy DO INSTEAD UPDATE asset.copy SET deleted = TRUE WHERE OLD.id = asset.copy.id;
115
116 CREATE TABLE asset.copy_part_map (
117     id          SERIAL  PRIMARY KEY,
118     target_copy BIGINT  NOT NULL, -- points o asset.copy
119     part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
120 );
121 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
122
123 CREATE TABLE asset.opac_visible_copies (
124   id        BIGSERIAL primary key,
125   copy_id   BIGINT, -- copy id
126   record    BIGINT,
127   circ_lib  INTEGER
128 );
129 COMMENT ON TABLE asset.opac_visible_copies IS $$
130 Materialized view of copies that are visible in the OPAC, used by
131 search.query_parser_fts() to speed up OPAC visibility checks on large
132 databases.  Contents are maintained by a set of triggers.
133 $$;
134 CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
135 CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
136 CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
137
138 CREATE OR REPLACE FUNCTION asset.acp_status_changed()
139 RETURNS TRIGGER AS $$
140 BEGIN
141         IF NEW.status <> OLD.status AND NOT (NEW.status = 0 AND OLD.status = 7) THEN
142         NEW.status_changed_time := now();
143         IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
144             NEW.active_date := now();
145         END IF;
146     END IF;
147     RETURN NEW;
148 END;
149 $$ LANGUAGE plpgsql;
150
151 -- Need to check on initial create. Fast adds, manual edit of status at create, etc.
152 CREATE OR REPLACE FUNCTION asset.acp_created()
153 RETURNS TRIGGER AS $$
154 BEGIN
155     IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
156         NEW.active_date := now();
157     END IF;
158     IF NEW.status_changed_time IS NULL THEN
159         NEW.status_changed_time := now();
160     END IF;
161     RETURN NEW;
162 END;
163 $$ LANGUAGE plpgsql;
164
165 CREATE TRIGGER acp_status_changed_trig
166     BEFORE UPDATE ON asset.copy
167     FOR EACH ROW EXECUTE PROCEDURE asset.acp_status_changed();
168
169 CREATE TRIGGER acp_created_trig
170     BEFORE INSERT ON asset.copy
171     FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
172
173 CREATE OR REPLACE FUNCTION asset.acp_location_fixer()
174 RETURNS TRIGGER AS $$
175 DECLARE
176     new_copy_location INT;
177 BEGIN
178     IF (TG_OP = 'UPDATE') THEN
179         IF NEW.location = OLD.location AND NEW.call_number = OLD.call_number AND NEW.circ_lib = OLD.circ_lib THEN
180             RETURN NEW;
181         END IF;
182     END IF;
183     SELECT INTO new_copy_location acpl.id FROM asset.copy_location acpl JOIN actor.org_unit_ancestors_distance((SELECT owning_lib FROM asset.call_number WHERE id = NEW.call_number)) aouad ON acpl.owning_lib = aouad.id WHERE deleted IS FALSE AND name = (SELECT name FROM asset.copy_location WHERE id = NEW.location) ORDER BY distance LIMIT 1;
184     IF new_copy_location IS NULL THEN
185         SELECT INTO new_copy_location acpl.id FROM asset.copy_location acpl JOIN actor.org_unit_ancestors_distance(NEW.circ_lib) aouad ON acpl.owning_lib = aouad.id WHERE deleted IS FALSE AND name = (SELECT name FROM asset.copy_location WHERE id = NEW.location) ORDER BY distance LIMIT 1;
186     END IF;
187     IF new_copy_location IS NOT NULL THEN
188         NEW.location = new_copy_location;
189     END IF;
190     RETURN NEW;
191 END;
192 $$ LANGUAGE plpgsql;
193
194 CREATE TRIGGER acp_location_fixer_trig
195     BEFORE INSERT OR UPDATE OF location, call_number, circ_lib ON asset.copy
196     FOR EACH ROW EXECUTE PROCEDURE asset.acp_location_fixer();
197
198 CREATE TABLE asset.stat_cat_sip_fields (
199     field   CHAR(2) PRIMARY KEY,
200     name    TEXT    NOT NULL,
201     one_only BOOL    NOT NULL DEFAULT FALSE
202 );
203 COMMENT ON TABLE asset.stat_cat_sip_fields IS $$
204 Asset Statistical Category SIP Fields
205
206 Contains the list of valid SIP Field identifiers for
207 Statistical Categories.
208 $$;
209
210 CREATE TABLE asset.stat_cat_entry_transparency_map (
211         id                      BIGSERIAL       PRIMARY KEY,
212         stat_cat                INT             NOT NULL, -- needs ON DELETE CASCADE
213         stat_cat_entry          INT             NOT NULL, -- needs ON DELETE CASCADE
214         owning_transparency     INT             NOT NULL, -- needs ON DELETE CASCADE
215         CONSTRAINT scte_once_per_trans UNIQUE (owning_transparency,stat_cat)
216 );
217
218 CREATE TABLE asset.stat_cat (
219         id              SERIAL  PRIMARY KEY,
220         owner           INT     NOT NULL,
221         opac_visible    BOOL    NOT NULL DEFAULT FALSE,
222         name            TEXT    NOT NULL,
223         required        BOOL    NOT NULL DEFAULT FALSE,
224     sip_field   CHAR(2) REFERENCES asset.stat_cat_sip_fields(field) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
225     sip_format  TEXT,
226     checkout_archive    BOOL NOT NULL DEFAULT FALSE,
227         CONSTRAINT sc_once_per_owner UNIQUE (owner,name)
228 );
229
230 CREATE TABLE asset.stat_cat_entry (
231         id              SERIAL  PRIMARY KEY,
232         stat_cat        INT     NOT NULL,
233         owner           INT     NOT NULL,
234         value           TEXT    NOT NULL,
235         CONSTRAINT sce_once_per_owner UNIQUE (stat_cat,owner,value)
236 );
237
238 CREATE TABLE asset.stat_cat_entry_copy_map (
239         id              BIGSERIAL       PRIMARY KEY,
240         stat_cat        INT             NOT NULL,
241         stat_cat_entry  INT             NOT NULL,
242         owning_copy     BIGINT          NOT NULL,
243         CONSTRAINT sce_once_per_copy UNIQUE (owning_copy,stat_cat)
244 );
245 CREATE INDEX scecm_owning_copy_idx ON asset.stat_cat_entry_copy_map(owning_copy);
246
247 CREATE FUNCTION asset.stat_cat_check() RETURNS trigger AS $func$
248 DECLARE
249     sipfield asset.stat_cat_sip_fields%ROWTYPE;
250     use_count INT;
251 BEGIN
252     IF NEW.sip_field IS NOT NULL THEN
253         SELECT INTO sipfield * FROM asset.stat_cat_sip_fields WHERE field = NEW.sip_field;
254         IF sipfield.one_only THEN
255             SELECT INTO use_count count(id) FROM asset.stat_cat WHERE sip_field = NEW.sip_field AND id != NEW.id;
256             IF use_count > 0 THEN
257                 RAISE EXCEPTION 'Sip field cannot be used twice';
258             END IF;
259         END IF;
260     END IF;
261     RETURN NEW;
262 END;
263 $func$ LANGUAGE PLPGSQL;
264
265 CREATE TRIGGER asset_stat_cat_sip_update_trigger
266     BEFORE INSERT OR UPDATE ON asset.stat_cat FOR EACH ROW
267     EXECUTE PROCEDURE asset.stat_cat_check();
268
269 CREATE TABLE asset.copy_note (
270         id              BIGSERIAL                       PRIMARY KEY,
271         owning_copy     BIGINT                          NOT NULL,
272         creator         BIGINT                          NOT NULL,
273         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
274         pub             BOOL                            NOT NULL DEFAULT FALSE,
275         title           TEXT                            NOT NULL,
276         value           TEXT                            NOT NULL
277 );
278 CREATE INDEX asset_copy_note_creator_idx ON asset.copy_note ( creator );
279 CREATE INDEX asset_copy_note_owning_copy_idx ON asset.copy_note ( owning_copy );
280
281 CREATE TABLE asset.uri (
282     id  SERIAL  PRIMARY KEY,
283     href    TEXT    NOT NULL,
284     label   TEXT,
285     use_restriction TEXT,
286     active  BOOL    NOT NULL DEFAULT TRUE
287 );
288
289 CREATE TABLE asset.call_number_class (
290     id             bigserial     PRIMARY KEY,
291     name           TEXT          NOT NULL,
292     normalizer     TEXT          NOT NULL DEFAULT 'asset.normalize_generic',
293     field          TEXT          NOT NULL DEFAULT '050ab,055ab,060ab,070ab,080ab,082ab,086ab,088ab,090,092,096,098,099'
294 );
295 COMMENT ON TABLE asset.call_number_class IS $$
296 Defines the call number normalization database functions in the "normalizer"
297 column and the tag/subfield combinations to use to lookup the call number in
298 the "field" column for a given classification scheme. Tag/subfield combinations
299 are delimited by commas.
300 $$;
301
302 CREATE OR REPLACE FUNCTION asset.label_normalizer() RETURNS TRIGGER AS $func$
303 DECLARE
304     sortkey        TEXT := '';
305 BEGIN
306     sortkey := NEW.label_sortkey;
307
308     IF NEW.label_class IS NULL THEN
309             NEW.label_class := COALESCE(
310             (
311                 SELECT substring(value from E'\\d+')::integer
312                 FROM actor.org_unit_ancestor_setting('cat.default_classification_scheme', NEW.owning_lib)
313             ), 1
314         );
315     END IF;
316
317     EXECUTE 'SELECT ' || acnc.normalizer || '(' || 
318        quote_literal( NEW.label ) || ')'
319        FROM asset.call_number_class acnc
320        WHERE acnc.id = NEW.label_class
321        INTO sortkey;
322     NEW.label_sortkey = sortkey;
323     RETURN NEW;
324 END;
325 $func$ LANGUAGE PLPGSQL;
326
327 CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
328     # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
329     # thus could probably be considered a derived work, although nothing was
330     # directly copied - but to err on the safe side of providing attribution:
331     # Copyright (C) 2007 LibLime
332     # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
333     # Licensed under the GPL v2 or later
334
335     use strict;
336     use warnings;
337
338     # Converts the callnumber to uppercase
339     # Strips spaces from start and end of the call number
340     # Converts anything other than letters, digits, and periods into spaces
341     # Collapses multiple spaces into a single underscore
342     my $callnum = uc(shift);
343     $callnum =~ s/^\s//g;
344     $callnum =~ s/\s$//g;
345     # NOTE: this previously used underscores, but this caused sorting issues
346     # for the "before" half of page 0 on CN browse, sorting CNs containing a
347     # decimal before "whole number" CNs
348     $callnum =~ s/[^A-Z0-9_.]/ /g;
349     $callnum =~ s/ {2,}/ /g;
350
351     return $callnum;
352 $func$ LANGUAGE PLPERLU IMMUTABLE;
353
354 CREATE OR REPLACE FUNCTION asset.label_normalizer_dewey(TEXT) RETURNS TEXT AS $func$
355     # Derived from the Koha C4::ClassSortRoutine::Dewey module
356     # Copyright (C) 2007 LibLime
357     # Licensed under the GPL v2 or later
358
359     use strict;
360     use warnings;
361
362     my $init = uc(shift);
363     $init =~ s/^\s+//;
364     $init =~ s/\s+$//;
365     $init =~ s!/!!g;
366     $init =~ s/^([\p{IsAlpha}]+)/$1 /;
367     my @tokens = split /\.|\s+/, $init;
368     my $digit_group_count = 0;
369     my $first_digit_group_idx;
370     for (my $i = 0; $i <= $#tokens; $i++) {
371         if ($tokens[$i] =~ /^\d+$/) {
372             $digit_group_count++;
373             if ($digit_group_count == 1) {
374                 $first_digit_group_idx = $i;
375             }
376             if (2 == $digit_group_count) {
377                 $tokens[$i] = sprintf("%-15.15s", $tokens[$i]);
378                 $tokens[$i] =~ tr/ /0/;
379             }
380         }
381     }
382     # Pad the first digit_group if there was only one
383     if (1 == $digit_group_count) {
384         $tokens[$first_digit_group_idx] .= '_000000000000000'
385     }
386     my $key = join("_", @tokens);
387     $key =~ s/[^\p{IsAlnum}_]//g;
388
389     return $key;
390
391 $func$ LANGUAGE PLPERLU IMMUTABLE;
392
393
394 CREATE OR REPLACE FUNCTION asset.label_normalizer_lc(TEXT) RETURNS TEXT AS $func$
395     use strict;
396     use warnings;
397
398     # Library::CallNumber::LC is currently hosted at http://code.google.com/p/library-callnumber-lc/
399     # The author hopes to upload it to CPAN some day, which would make our lives easier
400     use Library::CallNumber::LC;
401
402     my $callnum = Library::CallNumber::LC->new(shift);
403     return $callnum->normalize();
404
405 $func$ LANGUAGE PLPERLU IMMUTABLE;
406
407 INSERT INTO asset.call_number_class (name, normalizer, field) VALUES 
408     ('Generic', 'asset.label_normalizer_generic', '050ab,055ab,060ab,070ab,080ab,082ab,086ab,088ab,090,092,096,098,099'),
409     ('Dewey (DDC)', 'asset.label_normalizer_dewey', '080ab,082ab,092abef'),
410     ('Library of Congress (LC)', 'asset.label_normalizer_lc', '050ab,055ab,090abef')
411 ;
412
413 CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
414 BEGIN
415     NEW.label_sortkey := REGEXP_REPLACE(
416         evergreen.lpad_number_substrings(
417             naco_normalize(NEW.label),
418             '0',
419             10
420         ),
421         E'\\s+',
422         '',
423         'g'
424     );
425     RETURN NEW;
426 END;
427 $$ LANGUAGE PLPGSQL;
428
429 CREATE TABLE asset.call_number_prefix (
430         id                      SERIAL   PRIMARY KEY,
431         owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
432         label               TEXT                NOT NULL, -- i18n
433         label_sortkey   TEXT
434 );
435 CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
436 CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
437 CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
438
439 CREATE TABLE asset.call_number_suffix (
440         id                      SERIAL   PRIMARY KEY,
441         owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
442         label               TEXT                NOT NULL, -- i18n
443         label_sortkey   TEXT
444 );
445 CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
446 CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
447 CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
448
449 CREATE TABLE asset.call_number (
450         id              bigserial PRIMARY KEY,
451         creator         BIGINT                          NOT NULL,
452         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
453         editor          BIGINT                          NOT NULL,
454         edit_date       TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
455         record          bigint                          NOT NULL,
456         owning_lib      INT                                 NOT NULL,
457         label           TEXT                            NOT NULL,
458         deleted         BOOL                            NOT NULL DEFAULT FALSE,
459         prefix          INT                                 NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
460         suffix          INT                                 NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED,
461         label_class     BIGINT                          NOT NULL
462                                                         REFERENCES asset.call_number_class(id)
463                                                         DEFERRABLE INITIALLY DEFERRED,
464         label_sortkey   TEXT
465 );
466 CREATE INDEX asset_call_number_record_idx ON asset.call_number (record);
467 CREATE INDEX asset_call_number_creator_idx ON asset.call_number (creator);
468 CREATE INDEX asset_call_number_editor_idx ON asset.call_number (editor);
469 CREATE INDEX asset_call_number_dewey_idx ON asset.call_number (public.call_number_dewey(label));
470 CREATE INDEX asset_call_number_upper_label_id_owning_lib_idx ON asset.call_number (oils_text_as_bytea(label),id,owning_lib);
471 CREATE INDEX asset_call_number_label_sortkey ON asset.call_number(oils_text_as_bytea(label_sortkey));
472 CREATE UNIQUE INDEX asset_call_number_label_once_per_lib ON asset.call_number (record, owning_lib, label, prefix, suffix) WHERE deleted = FALSE OR deleted IS FALSE;
473 CREATE INDEX asset_call_number_label_sortkey_browse ON asset.call_number(oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib) WHERE deleted IS FALSE OR deleted = FALSE;
474 CREATE RULE protect_cn_delete AS ON DELETE TO asset.call_number DO INSTEAD UPDATE asset.call_number SET deleted = TRUE WHERE OLD.id = asset.call_number.id;
475 CREATE TRIGGER asset_label_sortkey_trigger
476     BEFORE UPDATE OR INSERT ON asset.call_number
477     FOR EACH ROW EXECUTE PROCEDURE asset.label_normalizer();
478
479 CREATE TABLE asset.uri_call_number_map (
480     id          BIGSERIAL   PRIMARY KEY,
481     uri         INT         NOT NULL REFERENCES asset.uri (id),
482     call_number INT         NOT NULL REFERENCES asset.call_number (id),
483     CONSTRAINT uri_cn_once UNIQUE (uri,call_number)
484 );
485 CREATE INDEX asset_uri_call_number_map_cn_idx ON asset.uri_call_number_map (call_number);
486
487 CREATE TABLE asset.call_number_note (
488         id              BIGSERIAL                       PRIMARY KEY,
489         call_number     BIGINT                          NOT NULL,
490         creator         BIGINT                          NOT NULL,
491         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
492         pub             BOOL                            NOT NULL DEFAULT FALSE,
493         title           TEXT                            NOT NULL,
494         value           TEXT                            NOT NULL
495 );
496 CREATE INDEX asset_call_number_note_creator_idx ON asset.call_number_note ( creator );
497
498 CREATE TABLE asset.copy_template (
499         id             SERIAL   PRIMARY KEY,
500         owning_lib     INT      NOT NULL
501                                 REFERENCES actor.org_unit (id)
502                                 DEFERRABLE INITIALLY DEFERRED,
503         creator        BIGINT   NOT NULL
504                                 REFERENCES actor.usr (id)
505                                 DEFERRABLE INITIALLY DEFERRED,
506         editor         BIGINT   NOT NULL
507                                 REFERENCES actor.usr (id)
508                                 DEFERRABLE INITIALLY DEFERRED,
509         create_date    TIMESTAMP WITH TIME ZONE    DEFAULT NOW(),
510         edit_date      TIMESTAMP WITH TIME ZONE    DEFAULT NOW(),
511         name           TEXT     NOT NULL,
512         -- columns above this point are attributes of the template itself
513         -- columns after this point are attributes of the copy this template modifies/creates
514         circ_lib       INT      REFERENCES actor.org_unit (id)
515                                 DEFERRABLE INITIALLY DEFERRED,
516         status         INT      REFERENCES config.copy_status (id)
517                                 DEFERRABLE INITIALLY DEFERRED,
518         location       INT      REFERENCES asset.copy_location (id)
519                                 DEFERRABLE INITIALLY DEFERRED,
520         loan_duration  INT      CONSTRAINT valid_loan_duration CHECK (
521                                     loan_duration IS NULL OR loan_duration IN (1,2,3)),
522         fine_level     INT      CONSTRAINT valid_fine_level CHECK (
523                                     fine_level IS NULL OR loan_duration IN (1,2,3)),
524         age_protect    INT,
525         circulate      BOOL,
526         deposit        BOOL,
527         ref            BOOL,
528         holdable       BOOL,
529         deposit_amount NUMERIC(6,2),
530         price          NUMERIC(8,2),
531         circ_modifier  TEXT,
532         circ_as_type   TEXT,
533         alert_message  TEXT,
534         opac_visible   BOOL,
535         floating       INT,
536         mint_condition BOOL
537 );
538
539 CREATE TABLE asset.copy_vis_attr_cache (
540     id              BIGSERIAL   PRIMARY KEY,
541     record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
542     target_copy     BIGINT      NOT NULL,
543     vis_attr_vector INT[]
544 );
545 CREATE INDEX copy_vis_attr_cache_record_idx ON asset.copy_vis_attr_cache (record);
546 CREATE INDEX copy_vis_attr_cache_copy_idx ON asset.copy_vis_attr_cache (target_copy);
547
548 CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
549 DECLARE
550     ans RECORD;
551     trans INT;
552 BEGIN
553     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
554
555     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
556         RETURN QUERY
557         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
558              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
559              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
560         SELECT  ans.depth,
561                 ans.id,
562                 COUNT( av.id ),
563                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
564                 COUNT( av.id ),
565                 trans
566           FROM  mask,
567                 available_statuses,
568                 org_list,
569                 asset.copy_vis_attr_cache av
570                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
571                 JOIN asset.call_number cn ON (cp.call_number = cn.id AND not cn.deleted)
572           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
573           GROUP BY 1,2,6;
574
575         IF NOT FOUND THEN
576             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
577         END IF;
578
579     END LOOP;
580
581     RETURN;
582 END;
583 $f$ LANGUAGE PLPGSQL;
584
585 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
586 DECLARE
587     ans RECORD;
588     trans INT;
589 BEGIN
590     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
591
592     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
593         RETURN QUERY
594         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
595              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
596              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
597         SELECT  -1,
598                 ans.id,
599                 COUNT( av.id ),
600                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
601                 COUNT( av.id ),
602                 trans
603           FROM  mask,
604                 org_list,
605                 asset.copy_vis_attr_cache av
606                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
607           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
608           GROUP BY 1,2,6;
609
610         IF NOT FOUND THEN
611             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
612         END IF;
613
614     END LOOP;   
615                 
616     RETURN;     
617 END;            
618 $f$ LANGUAGE PLPGSQL;
619
620 CREATE OR REPLACE FUNCTION asset.staff_ou_record_copy_count(org integer, rid bigint)
621  RETURNS TABLE(depth integer, org_unit integer, visible bigint, available bigint, unshadow bigint, transcendant integer)
622  LANGUAGE plpgsql
623 AS $function$
624 DECLARE
625     ans RECORD;
626     trans INT;
627 BEGIN
628     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
629
630     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
631         RETURN QUERY
632         WITH available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
633             cp AS(
634                 SELECT  cp.id,
635                         (cp.status = ANY (available_statuses.ids))::INT as available,
636                         (cl.opac_visible AND cp.opac_visible)::INT as opac_visible
637                   FROM
638                         available_statuses,
639                         actor.org_unit_descendants(ans.id) d
640                         JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
641                         JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
642                         JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
643             ),
644             peer AS (
645                 select  cp.id,
646                         (cp.status = ANY  (available_statuses.ids))::INT as available,
647                         (cl.opac_visible AND cp.opac_visible)::INT as opac_visible
648                 FROM
649                         available_statuses,
650                         actor.org_unit_descendants(ans.id) d
651                         JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
652                         JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
653                         JOIN biblio.peer_bib_copy_map bp ON (bp.peer_record = rid AND bp.target_copy = cp.id)
654             )
655         select ans.depth, ans.id, count(id), sum(x.available::int), sum(x.opac_visible::int), trans
656         from ((select * from cp) union (select * from peer)) x
657         group by 1,2,6;
658
659         IF NOT FOUND THEN
660             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
661         END IF;
662
663     END LOOP;
664     RETURN;
665 END;
666 $function$;
667
668 CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
669 DECLARE
670     ans RECORD;
671     trans INT;
672 BEGIN
673     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
674
675     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
676         RETURN QUERY
677         SELECT  -1,
678                 ans.id,
679                 COUNT( cp.id ),
680                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
681                 SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
682                 trans
683           FROM
684                 actor.org_unit_descendants(ans.id) d
685                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
686                 JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
687                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
688           GROUP BY 1,2,6;
689
690         IF NOT FOUND THEN
691             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
692         END IF;
693
694     END LOOP;
695
696     RETURN;
697 END;
698 $f$ LANGUAGE PLPGSQL;
699
700 CREATE OR REPLACE FUNCTION asset.record_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
701 BEGIN
702     IF staff IS TRUE THEN
703         IF place > 0 THEN
704             RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
705         ELSE
706             RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
707         END IF;
708     ELSE
709         IF place > 0 THEN
710             RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
711         ELSE
712             RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
713         END IF;
714     END IF;
715
716     RETURN;
717 END;
718 $f$ LANGUAGE PLPGSQL;
719
720 CREATE OR REPLACE FUNCTION asset.record_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
721 BEGIN
722     PERFORM 1
723         FROM
724             asset.copy acp
725             JOIN asset.call_number acn ON acp.call_number = acn.id
726             JOIN asset.copy_location acpl ON acp.location = acpl.id
727             JOIN config.copy_status ccs ON acp.status = ccs.id
728         WHERE
729             acn.record = rid
730             AND acp.holdable = true
731             AND acpl.holdable = true
732             AND ccs.holdable = true
733             AND acp.deleted = false
734             AND acpl.deleted = false
735             AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
736         LIMIT 1;
737     IF FOUND THEN
738         RETURN true;
739     END IF;
740     RETURN FALSE;
741 END;
742 $f$ LANGUAGE PLPGSQL;
743
744 CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
745 DECLARE
746     ans RECORD;
747     trans INT;
748 BEGIN
749     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
750
751     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
752         RETURN QUERY
753         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
754              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
755              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
756         SELECT  ans.depth,
757                 ans.id,
758                 COUNT( av.id ),
759                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
760                 COUNT( av.id ),
761                 trans
762           FROM  mask,
763                 org_list,
764                 available_statuses,
765                 asset.copy_vis_attr_cache av
766                 JOIN asset.copy cp ON (cp.id = av.target_copy)
767                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
768           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
769           GROUP BY 1,2,6;
770
771         IF NOT FOUND THEN
772             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
773         END IF;
774
775     END LOOP;
776
777     RETURN;
778 END;
779 $f$ LANGUAGE PLPGSQL;
780
781 CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
782 DECLARE
783     ans RECORD;
784     trans INT;
785 BEGIN
786     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
787
788     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
789         RETURN QUERY
790         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
791              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
792              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
793         SELECT  -1,
794                 ans.id,
795                 COUNT( av.id ),
796                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
797                 COUNT( av.id ),
798                 trans
799           FROM  mask,
800                 org_list,
801                 available_statuses,
802                 asset.copy_vis_attr_cache av
803                 JOIN asset.copy cp ON (cp.id = av.target_copy)
804                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
805           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
806           GROUP BY 1,2,6;
807
808         IF NOT FOUND THEN
809             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
810         END IF;
811
812     END LOOP;   
813                 
814     RETURN;     
815 END;            
816 $f$ LANGUAGE PLPGSQL;
817
818 CREATE OR REPLACE FUNCTION asset.staff_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
819 DECLARE         
820     ans RECORD; 
821     trans INT;
822 BEGIN
823     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
824
825     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
826         RETURN QUERY
827         SELECT  ans.depth,
828                 ans.id,
829                 COUNT( cp.id ),
830                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
831                 COUNT( cp.id ),
832                 trans
833           FROM
834                 actor.org_unit_descendants(ans.id) d
835                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
836                 JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
837                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
838           GROUP BY 1,2,6;
839
840         IF NOT FOUND THEN
841             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
842         END IF;
843
844     END LOOP;
845
846     RETURN;
847 END;
848 $f$ LANGUAGE PLPGSQL;
849
850 CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
851 DECLARE
852     ans RECORD;
853     trans INT;
854 BEGIN
855     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
856
857     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
858         RETURN QUERY
859         SELECT  -1,
860                 ans.id,
861                 COUNT( cp.id ),
862                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
863                 COUNT( cp.id ),
864                 trans
865           FROM
866                 actor.org_unit_descendants(ans.id) d
867                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
868                 JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
869                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
870           GROUP BY 1,2,6;
871
872         IF NOT FOUND THEN
873             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
874         END IF;
875
876     END LOOP;
877
878     RETURN;
879 END;
880 $f$ LANGUAGE PLPGSQL;
881
882 CREATE OR REPLACE FUNCTION asset.metarecord_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
883 BEGIN
884     IF staff IS TRUE THEN
885         IF place > 0 THEN
886             RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
887         ELSE
888             RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
889         END IF;
890     ELSE
891         IF place > 0 THEN
892             RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
893         ELSE
894             RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
895         END IF;
896     END IF;
897
898     RETURN;
899 END;
900 $f$ LANGUAGE PLPGSQL;
901
902 CREATE OR REPLACE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
903 BEGIN
904     PERFORM 1
905         FROM
906             asset.copy acp
907             JOIN asset.call_number acn ON acp.call_number = acn.id
908             JOIN asset.copy_location acpl ON acp.location = acpl.id
909             JOIN config.copy_status ccs ON acp.status = ccs.id
910             JOIN metabib.metarecord_source_map mmsm ON acn.record = mmsm.source
911         WHERE
912             mmsm.metarecord = rid
913             AND acp.holdable = true
914             AND acpl.holdable = true
915             AND ccs.holdable = true
916             AND acp.deleted = false
917             AND acpl.deleted = false
918             AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
919         LIMIT 1;
920     IF FOUND THEN
921         RETURN true;
922     END IF;
923     RETURN FALSE;
924 END;
925 $f$ LANGUAGE PLPGSQL;
926
927 CREATE OR REPLACE FUNCTION asset.autogenerate_placeholder_barcode ( ) RETURNS TRIGGER AS $f$
928 BEGIN
929         IF NEW.barcode LIKE '@@%' THEN
930                 NEW.barcode := '@@' || NEW.id;
931         END IF;
932         RETURN NEW;
933 END;
934 $f$ LANGUAGE PLPGSQL;
935
936 CREATE TRIGGER autogenerate_placeholder_barcode
937         BEFORE INSERT OR UPDATE ON asset.copy
938         FOR EACH ROW EXECUTE PROCEDURE asset.autogenerate_placeholder_barcode();
939
940 CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
941 DECLARE
942     copy_id BIGINT;
943 BEGIN
944     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
945     PERFORM * FROM asset.copy WHERE id = copy_id;
946     IF NOT FOUND THEN
947         RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
948     END IF;
949     RETURN NULL;
950 END;
951 $F$ LANGUAGE PLPGSQL;
952
953 CREATE TABLE asset.copy_tag (
954     id              SERIAL PRIMARY KEY,
955     tag_type        TEXT REFERENCES config.copy_tag_type (code)
956                     ON UPDATE CASCADE ON DELETE CASCADE,
957     label           TEXT NOT NULL,
958     value           TEXT NOT NULL,
959     index_vector    tsvector NOT NULL,
960     staff_note      TEXT,
961     pub             BOOLEAN DEFAULT TRUE,
962     owner           INTEGER NOT NULL REFERENCES actor.org_unit (id)
963 );
964
965 CREATE INDEX asset_copy_tag_label_idx
966     ON asset.copy_tag (label);
967 CREATE INDEX asset_copy_tag_label_lower_idx
968     ON asset.copy_tag (evergreen.lowercase(label));
969 CREATE INDEX asset_copy_tag_index_vector_idx
970     ON asset.copy_tag
971     USING GIN(index_vector);
972 CREATE INDEX asset_copy_tag_tag_type_idx
973     ON asset.copy_tag (tag_type);
974 CREATE INDEX asset_copy_tag_owner_idx
975     ON asset.copy_tag (owner);
976
977 CREATE OR REPLACE FUNCTION asset.set_copy_tag_value () RETURNS TRIGGER AS $$
978 BEGIN
979     IF NEW.value IS NULL THEN
980         NEW.value = NEW.label;        
981     END IF;
982
983     RETURN NEW;
984 END;
985 $$ LANGUAGE 'plpgsql';
986
987 -- name of following trigger chosen to ensure it runs first
988 CREATE TRIGGER asset_copy_tag_do_value
989     BEFORE INSERT OR UPDATE ON asset.copy_tag
990     FOR EACH ROW EXECUTE PROCEDURE asset.set_copy_tag_value();
991 CREATE TRIGGER asset_copy_tag_fti_trigger
992     BEFORE UPDATE OR INSERT ON asset.copy_tag
993     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
994
995 CREATE TABLE asset.copy_tag_copy_map (
996     id              BIGSERIAL PRIMARY KEY,
997     copy            BIGINT,
998     tag             INTEGER REFERENCES asset.copy_tag (id)
999                     ON UPDATE CASCADE ON DELETE CASCADE
1000 );
1001
1002 CREATE INDEX asset_copy_tag_copy_map_copy_idx
1003     ON asset.copy_tag_copy_map (copy);
1004 CREATE INDEX asset_copy_tag_copy_map_tag_idx
1005     ON asset.copy_tag_copy_map (tag);
1006
1007 CREATE OR REPLACE FUNCTION asset.copy_state (cid BIGINT) RETURNS TEXT AS $$
1008 DECLARE
1009     last_circ_stop      TEXT;
1010     the_copy        asset.copy%ROWTYPE;
1011 BEGIN
1012
1013     SELECT * INTO the_copy FROM asset.copy WHERE id = cid;
1014     IF NOT FOUND THEN RETURN NULL; END IF;
1015
1016     IF the_copy.status = 3 THEN -- Lost
1017         RETURN 'LOST';
1018     ELSIF the_copy.status = 4 THEN -- Missing
1019         RETURN 'MISSING';
1020     ELSIF the_copy.status = 14 THEN -- Damaged
1021         RETURN 'DAMAGED';
1022     ELSIF the_copy.status = 17 THEN -- Lost and paid
1023         RETURN 'LOST_AND_PAID';
1024     END IF;
1025
1026     SELECT stop_fines INTO last_circ_stop
1027       FROM  action.circulation
1028       WHERE target_copy = cid AND checkin_time IS NULL
1029       ORDER BY xact_start DESC LIMIT 1;
1030
1031     IF FOUND THEN
1032         IF last_circ_stop IN (
1033             'CLAIMSNEVERCHECKEDOUT',
1034             'CLAIMSRETURNED',
1035             'LONGOVERDUE'
1036         ) THEN
1037             RETURN last_circ_stop;
1038         END IF;
1039     END IF;
1040
1041     RETURN 'NORMAL';
1042 END;
1043 $$ LANGUAGE PLPGSQL;
1044
1045 CREATE TYPE config.copy_alert_type_state AS ENUM (
1046     'NORMAL',
1047     'LOST',
1048     'LOST_AND_PAID',
1049     'MISSING',
1050     'DAMAGED',
1051     'CLAIMSRETURNED',
1052     'LONGOVERDUE',
1053     'CLAIMSNEVERCHECKEDOUT'
1054 );
1055
1056 CREATE TYPE config.copy_alert_type_event AS ENUM (
1057     'CHECKIN',
1058     'CHECKOUT'
1059 );
1060
1061 CREATE TABLE config.copy_alert_type (
1062     id      serial  primary key, -- reserve 1-100 for system
1063     scope_org   int not null references actor.org_unit (id) on delete cascade,
1064     active      bool    not null default true,
1065     name        text    not null unique,
1066     state       config.copy_alert_type_state,
1067     event       config.copy_alert_type_event,
1068     in_renew    bool,
1069     invert_location bool    not null default false,
1070     at_circ     bool,
1071     at_owning   bool,
1072     next_status int[]
1073 );
1074 SELECT SETVAL('config.copy_alert_type_id_seq'::TEXT, 100);
1075
1076 CREATE TABLE actor.copy_alert_suppress (
1077     id          serial primary key,
1078     org         int not null references actor.org_unit (id) on delete cascade,
1079     alert_type  int not null references config.copy_alert_type (id) on delete cascade
1080 );
1081
1082 CREATE TABLE asset.copy_alert (
1083     id      bigserial   primary key,
1084     alert_type  int     not null references config.copy_alert_type (id) on delete cascade,
1085     copy        bigint  not null,
1086     temp        bool    not null default false,
1087     create_time timestamptz not null default now(),
1088     create_staff    bigint  not null references actor.usr (id) on delete set null,
1089     note        text,
1090     ack_time    timestamptz,
1091     ack_staff   bigint references actor.usr (id) on delete set null
1092 );
1093
1094 CREATE VIEW asset.active_copy_alert AS
1095     SELECT  *
1096       FROM  asset.copy_alert
1097       WHERE ack_time IS NULL;
1098
1099 COMMIT;
1100