300 lines
8.0 KiB
Plaintext
300 lines
8.0 KiB
Plaintext
// ============================================================================
|
|
// PROCUREMENT SCHEMA v2.4 — Final Enterprise Architecture
|
|
// ============================================================================
|
|
|
|
// ---------- ENUMS ----------
|
|
|
|
Enum quote_status {
|
|
offered
|
|
accepted
|
|
arrived
|
|
rejected
|
|
expired
|
|
}
|
|
|
|
Enum termsheet_status {
|
|
offered
|
|
accepted
|
|
rejected
|
|
}
|
|
|
|
Enum material_status {
|
|
active
|
|
discontinued
|
|
draft
|
|
}
|
|
|
|
Enum attribute_data_type {
|
|
number
|
|
integer
|
|
string
|
|
enum
|
|
boolean
|
|
date
|
|
}
|
|
|
|
Enum discount_type {
|
|
none
|
|
percent
|
|
fixed
|
|
}
|
|
|
|
Enum item_decision_status {
|
|
pending
|
|
chosen
|
|
rejected
|
|
}
|
|
|
|
Enum quote_event_type {
|
|
offered
|
|
accepted
|
|
rejected
|
|
arrived
|
|
note
|
|
edited
|
|
expired
|
|
}
|
|
|
|
// ============================================================================
|
|
// CATALOG DOMAIN
|
|
// ============================================================================
|
|
|
|
Table units {
|
|
id integer [pk]
|
|
code varchar [not null, unique]
|
|
name_fa varchar
|
|
dimension varchar [note: 'length, mass, pressure, area, volume, voltage, count, ratio, time, currency']
|
|
si_factor decimal [not null, default: 1.0, note: 'Universal multiply factor to convert to base SI unit within same dimension']
|
|
is_base boolean [default: false]
|
|
}
|
|
|
|
Table brands {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
name text [not null]
|
|
country text
|
|
created_at timestamptz [not null, default: `now()`]
|
|
|
|
indexes {
|
|
(name, country) [unique]
|
|
}
|
|
}
|
|
|
|
Table material_categories {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
slug text [not null, unique]
|
|
name text [not null]
|
|
description text
|
|
is_active boolean [not null, default: true]
|
|
created_at timestamptz [not null, default: `now()`]
|
|
updated_at timestamptz [not null, default: `now()`]
|
|
}
|
|
|
|
// Global Attribute Dictionary
|
|
Table attribute_definitions {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
attribute_key text [not null, unique]
|
|
label text [not null]
|
|
data_type attribute_data_type [not null]
|
|
unit_id integer [ref: > units.id]
|
|
enum_values jsonb [note: 'Allowed options for enum types']
|
|
created_at timestamptz [not null, default: `now()`]
|
|
}
|
|
|
|
// Category-Attribute Junction (Scopes global attributes & sets category rules)
|
|
Table category_attributes {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
category_id uuid [not null, ref: > material_categories.id]
|
|
attribute_definition_id uuid [not null, ref: > attribute_definitions.id]
|
|
is_required boolean [not null, default: false]
|
|
is_filterable boolean [not null, default: false]
|
|
conditional_rules jsonb [note: 'UI Wizard rules e.g. {"when": {"is_colored": true}, "then": {"required": ["color"]}}']
|
|
sort_order smallint [not null, default: 0]
|
|
|
|
indexes {
|
|
(category_id, attribute_definition_id) [unique]
|
|
}
|
|
}
|
|
|
|
// Abstract Base Products / Materials
|
|
Table materials {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
category_id uuid [not null, ref: > material_categories.id]
|
|
name text [not null]
|
|
canonical_unit_id integer [not null, ref: > units.id]
|
|
status material_status [not null, default: `'active'`]
|
|
created_at timestamptz [not null, default: `now()`]
|
|
updated_at timestamptz [not null, default: `now()`]
|
|
|
|
indexes {
|
|
(category_id, name) [unique]
|
|
}
|
|
}
|
|
|
|
// M:N Brand <-> Material Junction
|
|
Table brand_materials {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
brand_id uuid [not null, ref: > brands.id]
|
|
material_id uuid [not null, ref: > materials.id]
|
|
catalog_code text
|
|
is_active boolean [not null, default: true]
|
|
created_at timestamptz [not null, default: `now()`]
|
|
|
|
indexes {
|
|
(brand_id, material_id) [unique]
|
|
}
|
|
}
|
|
|
|
// Material & Brand Specific Packaging / Unit Overrides
|
|
Table material_units {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
material_id uuid [not null, ref: > materials.id]
|
|
brand_material_id uuid [ref: > brand_materials.id, note: 'NULL = generic material fallback; set = brand-specific packaging override']
|
|
unit_id integer [not null, ref: > units.id]
|
|
conversion_factor "numeric(18,6)" [not null, note: 'Multiplier to convert to canonical_unit_id']
|
|
is_default boolean [not null, default: false]
|
|
|
|
indexes {
|
|
(material_id, brand_material_id, unit_id) [unique]
|
|
}
|
|
}
|
|
|
|
// Typed Specs Source of Truth (EAV)
|
|
Table material_attribute_values {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
material_id uuid [not null, ref: > materials.id]
|
|
attribute_definition_id uuid [not null, ref: > attribute_definitions.id]
|
|
value_number "numeric(18,6)"
|
|
value_string text
|
|
value_boolean boolean
|
|
value_date date
|
|
|
|
indexes {
|
|
(material_id, attribute_definition_id) [unique]
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// QUOTATION & PROCUREMENT DOMAIN
|
|
// ============================================================================
|
|
|
|
Table suppliers {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
name text [not null]
|
|
location text
|
|
phone text
|
|
contact_person text
|
|
tax_id text
|
|
created_at timestamptz [not null, default: `now()`]
|
|
}
|
|
|
|
Table projects {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
name text [not null]
|
|
description text
|
|
start_date date
|
|
created_at timestamptz [not null, default: `now()`]
|
|
}
|
|
|
|
Table quotes {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
supplier_id uuid [not null, ref: > suppliers.id]
|
|
project_id uuid [ref: > projects.id]
|
|
quote_date timestamptz [not null, default: `now()`]
|
|
expiry_date date
|
|
accepted_date timestamptz
|
|
arrived_date timestamptz
|
|
status quote_status [not null, default: `'offered'`]
|
|
is_official boolean [not null, default: true, note: 'True = formal binding quote; False = informal price estimate / draft']
|
|
notes text
|
|
created_at timestamptz [not null, default: `now()`]
|
|
updated_at timestamptz [not null, default: `now()`]
|
|
}
|
|
Table quote_events {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
quote_id uuid [not null, ref: > quotes.id]
|
|
event_type quote_event_type [not null]
|
|
payload jsonb [not null, default: `'{}'`]
|
|
occurred_at timestamptz [not null, default: `now()`]
|
|
actor text
|
|
}
|
|
|
|
Table line_items {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
quote_id uuid [not null, ref: > quotes.id]
|
|
material_id uuid [not null, ref: > materials.id]
|
|
brand_material_id uuid [ref: > brand_materials.id, note: 'Optional: set when a specific branded product is quoted']
|
|
unit_id integer [not null, ref: > units.id]
|
|
quantity "numeric(18,4)" [not null]
|
|
notes text
|
|
created_at timestamptz [not null, default: `now()`]
|
|
}
|
|
|
|
// Historical Snapshot: Freezes material specs at quote generation time
|
|
Table line_item_specs {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
line_item_id uuid [not null, ref: > line_items.id]
|
|
attribute_definition_id uuid [not null, ref: > attribute_definitions.id]
|
|
value_number "numeric(18,6)"
|
|
value_string text
|
|
value_boolean boolean
|
|
value_date date
|
|
unit_id_at_time integer [ref: > units.id]
|
|
|
|
indexes {
|
|
(line_item_id, attribute_definition_id) [unique]
|
|
}
|
|
}
|
|
|
|
Table termsheets {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
quote_id uuid [not null, ref: > quotes.id]
|
|
label text [not null]
|
|
status termsheet_status [not null, default: `'offered'`]
|
|
discount_type discount_type [not null, default: `'none'`]
|
|
discount_value "numeric(18,4)" [not null, default: 0]
|
|
created_at timestamptz [not null, default: `now()`]
|
|
}
|
|
|
|
Table termsheet_schedule_steps {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
termsheet_id uuid [not null, ref: > termsheets.id]
|
|
step_no smallint [not null]
|
|
due_date date [not null]
|
|
amount "numeric(18,2)" [not null]
|
|
note text
|
|
|
|
indexes {
|
|
(termsheet_id, step_no) [unique]
|
|
}
|
|
}
|
|
|
|
Table item_termsheets {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
line_item_id uuid [not null, ref: > line_items.id]
|
|
termsheet_id uuid [not null, ref: > termsheets.id]
|
|
unit_price "numeric(18,4)" [not null]
|
|
min_order_qty "numeric(18,4)"
|
|
total_price "numeric(18,2)" [note: 'Calculated at query/view time: unit_price * line_items.quantity']
|
|
|
|
indexes {
|
|
(line_item_id, termsheet_id) [unique]
|
|
}
|
|
}
|
|
|
|
Table item_decisions {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
line_item_id uuid [not null, unique, ref: - line_items.id]
|
|
chosen_item_termsheet_id uuid [ref: > item_termsheets.id]
|
|
status item_decision_status [not null, default: `'pending'`]
|
|
adjusted_unit_price "numeric(18,4)"
|
|
adjusted_notes text
|
|
decided_at timestamptz
|
|
}
|
|
|
|
Table item_receipts {
|
|
id uuid [pk, default: `gen_random_uuid()`]
|
|
item_decision_id uuid [not null, unique, ref: - item_decisions.id]
|
|
received_at timestamptz [not null, default: `now()`]
|
|
received_qty "numeric(18,4)" [not null]
|
|
on_time boolean
|
|
quality_note text} |