This document defines the physical data model for GableLBM. We prioritize data integrity, unit-awareness, and a "double-entry" ledger for all asset movements (Inventory, Cash, Receivables).
- Always UUID: All primary keys are
UUID v4. - Atomic Units: Physical quantities are stored in
decimal(19,4)to prevent rounding errors. - Double-Entry Everything: Inventory isn't just "updated"; it's moved from
FromLocationtoToLocation. - Unit Awareness: Every quantity must be paired with a
UOMID.
The Inventory module handles the complexity of LBM products, conversions (Piece to MBF), and multi-yard logistics.
erDiagram
PRODUCT ||--o{ STOCK_QUANT : "current levels"
PRODUCT ||--o{ INVENTORY_MOVE_LINE : "moved"
UOM_FAMILY ||--|{ UOM : "contains"
PRODUCT ||--|| UOM : "base unit"
LOCATION ||--o{ STOCK_QUANT : "holds"
INVENTORY_MOVE ||--|{ INVENTORY_MOVE_LINE : "contains"
LOCATION ||--o{ INVENTORY_MOVE : "from/to"
PRODUCT {
uuid id PK
string sku "unique"
string name
uuid base_uom_id FK
string category
decimal weight_lbs
decimal volume_bf
decimal average_cost
jsonb metadata "custom attributes"
}
UOM_FAMILY {
uuid id PK
string name "e.g. Length, Volume"
}
UOM {
uuid id PK
uuid family_id FK
string name "Piece, Bunk, MBF"
enum type "reference, bigger, smaller"
decimal ratio "relative to reference"
}
LOCATION {
uuid id PK
string name "Yard A, Bin 4"
enum type "physical, virtual, transit"
boolean allow_negative "sales floor vs storage"
}
STOCK_QUANT {
uuid id PK
uuid product_id FK
uuid location_id FK
decimal qty_on_hand
decimal qty_reserved
timestamp inbound_date
}
INVENTORY_MOVE {
uuid id PK
string reference "PO# or Order#"
enum move_type "receipt, delivery, internal, reman"
uuid from_location_id FK
uuid to_location_id FK
enum state "draft, confirmed, done"
timestamp created_at
}
INVENTORY_MOVE_LINE {
uuid id PK
uuid move_id FK
uuid product_id FK
uuid uom_id FK
decimal qty
}
-- Core Types
CREATE TYPE move_state AS ENUM ('draft', 'confirmed', 'done', 'cancelled');
CREATE TYPE location_type AS ENUM ('physical', 'virtual', 'transit', 'production');
-- Products
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sku VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
base_uom_id UUID NOT NULL,
category VARCHAR(100),
weight_lbs DECIMAL(19,4) DEFAULT 0,
volume_bf DECIMAL(19,4) DEFAULT 0,
average_cost DECIMAL(19,4) DEFAULT 0,
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- UOM System
CREATE TABLE uom_families (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL
);
CREATE TABLE uoms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
family_id UUID REFERENCES uom_families(id),
name VARCHAR(50) NOT NULL,
ratio DECIMAL(19,8) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Locations & Stock
CREATE TABLE locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
type location_type DEFAULT 'physical',
parent_id UUID REFERENCES locations(id),
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE stock_quant (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES products(id),
location_id UUID REFERENCES locations(id),
qty_on_hand DECIMAL(19,4) NOT NULL DEFAULT 0,
qty_reserved DECIMAL(19,4) NOT NULL DEFAULT 0,
lot_number VARCHAR(100),
inbound_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(product_id, location_id, lot_number)
);
-- The Double-Entry Engine
CREATE TABLE inventory_moves (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reference VARCHAR(255),
move_type VARCHAR(50),
from_location_id UUID REFERENCES locations(id),
to_location_id UUID REFERENCES locations(id),
state move_state DEFAULT 'draft',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE inventory_move_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
move_id UUID REFERENCES inventory_moves(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
uom_id UUID REFERENCES uoms(id),
qty DECIMAL(19,4) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);The Sales module implements the core pricing waterfall and order management flows.
erDiagram
CUSTOMER ||--o{ CUSTOMER_JOB : "has multiple"
CUSTOMER ||--o{ QUOTE : "requests"
CUSTOMER_JOB ||--o{ QUOTE : "linked to"
PRICE_LEVEL ||--o{ CUSTOMER : "default level"
QUOTE ||--|{ QUOTE_LINE : "contains"
QUOTE ||--o{ SALES_ORDER : "converts to"
SALES_ORDER ||--|{ SALES_ORDER_LINE : "contains"
PRODUCT ||--o{ PRICE_RULE : "target"
CUSTOMER ||--o{ PRICE_RULE : "target"
PRODUCT ||--o{ QUOTE_LINE : "quoted"
PRODUCT ||--o{ SALES_ORDER_LINE : "sold"
CUSTOMER {
uuid id PK
string name
string account_number
uuid price_level_id FK
decimal credit_limit
decimal balance_due
}
CUSTOMER_JOB {
uuid id PK
uuid customer_id FK
string name "e.g. Smith Deck"
decimal budget_limit
boolean job_is_taxable
}
PRICE_LEVEL {
uuid id PK
string name "Retail, Level 1"
decimal multiplier "e.g. 0.85"
}
PRICE_RULE {
uuid id PK
uuid product_id FK "nullable"
uuid category_id FK "nullable"
uuid customer_id FK "nullable"
uuid job_id FK "nullable"
enum type "NetPrice, DiscountPercentage"
decimal value
decimal min_qty
timestamp start_date
timestamp end_date
}
QUOTE {
uuid id PK
uuid customer_id FK
uuid job_id FK
uuid root_quote_id "versioning"
enum state "draft, sent, accepted, expired"
decimal total_amount
timestamp expires_at
}
QUOTE_LINE {
uuid id PK
uuid quote_id FK
uuid product_id FK
decimal qty
uuid uom_id FK
decimal unit_price
decimal current_cost "locked at quote time"
}
SALES_ORDER {
uuid id PK
uuid customer_id FK
uuid job_id FK
uuid quote_id FK "optional"
enum state "pending, confirmed, delivered"
decimal total_amount
timestamp created_at
}
SALES_ORDER_LINE {
uuid id PK
uuid order_id FK
uuid product_id FK
decimal qty
uuid uom_id FK
decimal unit_price
decimal line_total
}
-- Pricing
CREATE TABLE price_levels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
multiplier DECIMAL(19,4) NOT NULL DEFAULT 1.0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Customers
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
account_number VARCHAR(50) UNIQUE NOT NULL,
price_level_id UUID REFERENCES price_levels(id),
credit_limit DECIMAL(19,4) DEFAULT 0,
balance_due DECIMAL(19,4) DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE customer_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES customers(id),
name VARCHAR(255) NOT NULL,
budget_limit DECIMAL(19,4) DEFAULT 0,
job_is_taxable BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- The Pricing Waterfall
CREATE TABLE price_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES products(id),
customer_id UUID REFERENCES customers(id),
job_id UUID REFERENCES customer_jobs(id),
type VARCHAR(50) NOT NULL,
value DECIMAL(19,4) NOT NULL,
min_qty DECIMAL(19,4) DEFAULT 0,
start_date TIMESTAMP WITH TIME ZONE,
end_date TIMESTAMP WITH TIME ZONE,
priority INTEGER DEFAULT 0
);
-- Quotes & Orders
CREATE TABLE quotes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES customers(id),
job_id UUID REFERENCES customer_jobs(id),
root_quote_id UUID REFERENCES quotes(id),
state VARCHAR(50) DEFAULT 'draft',
total_amount DECIMAL(19,4) DEFAULT 0,
expires_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE quote_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
quote_id UUID REFERENCES quotes(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
uom_id UUID REFERENCES uoms(id),
qty DECIMAL(19,4) NOT NULL,
unit_price DECIMAL(19,4) NOT NULL,
current_cost DECIMAL(19,4) NOT NULL
);
CREATE TABLE sales_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES customers(id),
job_id UUID REFERENCES customer_jobs(id),
quote_id UUID REFERENCES quotes(id),
state VARCHAR(50) DEFAULT 'pending',
total_amount DECIMAL(19,4) DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE sales_order_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID REFERENCES sales_orders(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
uom_id UUID REFERENCES uoms(id),
qty DECIMAL(19,4) NOT NULL,
unit_price DECIMAL(19,4) NOT NULL,
line_total DECIMAL(19,4) NOT NULL
);The Finance module handles AR, Invoicing, and a strict double-entry ledger for all financial transactions.
erDiagram
SALES_ORDER ||--o{ INVOICE : "invoiced"
CUSTOMER ||--o{ INVOICE : "billed to"
CUSTOMER_JOB ||--o{ INVOICE : "job linkage"
INVOICE ||--|{ INVOICE_LINE : "contains"
INVOICE ||--o{ PAYMENT : "paid by"
CUSTOMER_JOB ||--o{ LIEN_NOTICE : "tracked"
LEDGER_ENTRY ||--|{ LEDGER_LINE : "double-entry"
CHART_OF_ACCOUNTS ||--o{ LEDGER_LINE : "account link"
INVOICE {
uuid id PK
uuid customer_id FK
uuid job_id FK
uuid sales_order_id FK
string invoice_number "unique"
timestamp invoice_date
timestamp due_date
decimal subtotal
decimal tax_total
decimal total_amount
decimal balance_remaining
enum state "open, paid, void"
}
INVOICE_LINE {
uuid id PK
uuid invoice_id FK
uuid product_id FK
decimal qty
decimal unit_price
decimal line_total
}
PAYMENT {
uuid id PK
uuid customer_id FK
decimal amount
enum method "check, cash, credit, e-payment"
string reference "check# or transaction_id"
timestamp payment_date
jsonb application_map "split across invoices"
}
LIEN_NOTICE {
uuid id PK
uuid job_id FK
timestamp notice_sent_at
timestamp expires_at
enum state "active, released, expired"
string document_url "link to PDF"
}
CHART_OF_ACCOUNTS {
uuid id PK
string code "e.g. 1200-AR"
string name "Accounts Receivable"
enum account_type "asset, liability, equity, income, expense"
}
LEDGER_ENTRY {
uuid id PK
string reference "Invoice# or Payment#"
timestamp posted_at
string memo
}
LEDGER_LINE {
uuid id PK
uuid ledger_entry_id FK
uuid account_id FK
decimal debit
decimal credit
}
-- Chart of Accounts
CREATE TABLE chart_of_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
account_type VARCHAR(50) NOT NULL,
is_active BOOLEAN DEFAULT TRUE
);
-- Invoices
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES customers(id),
job_id UUID REFERENCES customer_jobs(id),
sales_order_id UUID REFERENCES sales_orders(id),
invoice_number VARCHAR(50) UNIQUE NOT NULL,
invoice_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
due_date TIMESTAMP WITH TIME ZONE,
subtotal DECIMAL(19,4) NOT NULL,
tax_total DECIMAL(19,4) NOT NULL,
total_amount DECIMAL(19,4) NOT NULL,
balance_remaining DECIMAL(19,4) NOT NULL,
state VARCHAR(50) DEFAULT 'open',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE invoice_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID REFERENCES invoices(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
qty DECIMAL(19,4) NOT NULL,
unit_price DECIMAL(19,4) NOT NULL,
line_total DECIMAL(19,4) NOT NULL
);
-- Payments
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES customers(id),
amount DECIMAL(19,4) NOT NULL,
payment_method VARCHAR(50),
reference_number VARCHAR(255),
payment_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
application_map JSONB
);
-- Construction Lien Tracking
CREATE TABLE lien_notices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID REFERENCES customer_jobs(id),
notice_sent_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE,
state VARCHAR(50) DEFAULT 'active',
document_url VARCHAR(2048)
);
-- Double-Entry Ledger
CREATE TABLE ledger_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reference VARCHAR(255),
posted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
memo TEXT
);
CREATE TABLE ledger_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ledger_entry_id UUID REFERENCES ledger_entries(id) ON DELETE CASCADE,
account_id UUID REFERENCES chart_of_accounts(id),
debit DECIMAL(19,4) NOT NULL DEFAULT 0,
credit DECIMAL(19,4) NOT NULL DEFAULT 0
);