280 lines
14 KiB
SQL
280 lines
14 KiB
SQL
-- Multi-Tenant Ledger Schema
|
|
-- Supports SE (BAS/SIE4), US-DE (GAAP), US-TX (GAAP)
|
|
|
|
-- Company registry
|
|
CREATE TABLE IF NOT EXISTS boc_companies (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
legal_name TEXT NOT NULL,
|
|
org_number TEXT, -- Swedish org nr or US EIN
|
|
tax_id TEXT, -- EIN for US companies
|
|
jurisdiction TEXT NOT NULL, -- SE, US-DE, US-TX
|
|
company_type TEXT NOT NULL, -- AB, Inc, LLC, etc
|
|
address JSONB,
|
|
currency TEXT NOT NULL DEFAULT 'SEK',
|
|
fiscal_year_end DATE NOT NULL DEFAULT '12-31-2026',
|
|
accounting_std TEXT NOT NULL DEFAULT 'BAS', -- BAS, GAAP, IFRS
|
|
vat_registered BOOLEAN DEFAULT FALSE,
|
|
vat_number TEXT,
|
|
settings JSONB DEFAULT '{}',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Chart of accounts per company (BAS for SE, GAAP for US)
|
|
CREATE TABLE IF NOT EXISTS boc_chart_of_accounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
account_code TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
name_en TEXT, -- English name for US entities
|
|
account_type TEXT NOT NULL, -- asset, liability, equity, revenue, expense
|
|
account_subtype TEXT, -- current_asset, fixed_asset, current_liability, etc
|
|
parent_code TEXT,
|
|
vat_code TEXT, -- SE: 25, 12, 6, 0 | US: exempt
|
|
is_bank_account BOOLEAN DEFAULT FALSE,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER,
|
|
metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(company_id, account_code)
|
|
);
|
|
|
|
-- Standard BAS kontoplan (Sweden)
|
|
INSERT INTO boc_chart_of_accounts (company_id, account_code, name, name_en, account_type, account_subtype, vat_code, sort_order)
|
|
SELECT
|
|
c.id,
|
|
a.account_code,
|
|
a.name_sv,
|
|
a.name_en,
|
|
a.account_type,
|
|
a.account_subtype,
|
|
a.vat_code,
|
|
a.sort_order
|
|
FROM boc_companies c
|
|
CROSS JOIN (VALUES
|
|
('1000', 'Tillgångar', 'Assets', 'asset', 'header', NULL, 100),
|
|
('1221', 'Datorer och kringutrustning', 'Computer equipment', 'asset', 'fixed_asset', NULL, 1221),
|
|
('1227', 'Personbilar', 'Vehicles', 'asset', 'fixed_asset', NULL, 1227),
|
|
('1930', 'Företagskonto / affärskonto', 'Business account', 'asset', 'current_asset', NULL, 1930),
|
|
('2000', 'Skulder', 'Liabilities', 'liability', 'header', NULL, 2000),
|
|
('2510', 'Skatteskulder', 'Tax liabilities', 'liability', 'current_liability', NULL, 2510),
|
|
('2611', 'Utgående moms 25%', 'Output VAT 25%', 'liability', 'current_liability', '25', 2611),
|
|
('2640', 'Ingående moms', 'Input VAT', 'asset', 'current_asset', NULL, 2640),
|
|
('2990', 'Övriga skulder till närstående / ägaruttag', 'Related party debt / owner drawings', 'liability', 'current_liability', NULL, 2990),
|
|
('3000', 'Intäkter', 'Revenue', 'revenue', 'header', NULL, 3000),
|
|
('3001', 'Försäljning av tjänster', 'Service revenue', 'revenue', 'operating_revenue', '25', 3001),
|
|
('3010', 'Konsultarvode', 'Consulting fees', 'revenue', 'operating_revenue', '25', 3010),
|
|
('3020', 'Hyresintäkter', 'Rental income', 'revenue', 'operating_revenue', '25', 3020),
|
|
('3900', 'Övriga rörelseintäkter', 'Other operating income', 'revenue', 'other_revenue', '25', 3900),
|
|
('5000', 'Kostnader', 'Expenses', 'expense', 'header', NULL, 5000),
|
|
('5420', 'Personalrepresentation', 'Staff entertainment', 'expense', 'operating_expense', NULL, 5420),
|
|
('5460', 'Arbetskläder och skyddsmaterial', 'Work clothes and safety', 'expense', 'operating_expense', NULL, 5460),
|
|
('5612', 'Fordonsskatt', 'Vehicle tax', 'expense', 'operating_expense', NULL, 5612),
|
|
('5614', 'Bilförsäkring', 'Vehicle insurance', 'expense', 'operating_expense', NULL, 5614),
|
|
('5810', 'Resekostnader', 'Travel expenses', 'expense', 'operating_expense', NULL, 5810),
|
|
('5820', 'Biljettkostnader', 'Ticket expenses', 'expense', 'operating_expense', NULL, 5820),
|
|
('5900', 'Reklam och marknadsföring', 'Advertising and marketing', 'expense', 'operating_expense', '25', 5900),
|
|
('6071', 'Representation avdragsgill', 'Deductible entertainment', 'expense', 'operating_expense', NULL, 6071),
|
|
('6540', 'IT-tjänster, köpta', 'IT services purchased', 'expense', 'operating_expense', '25', 6540),
|
|
('6550', 'Programvarulicenser', 'Software licenses', 'expense', 'operating_expense', '25', 6550),
|
|
('7630', 'Friskvård', 'Wellness', 'expense', 'personnel_expense', NULL, 7630),
|
|
('8000', 'Finansiella poster', 'Financial items', 'expense', 'header', NULL, 8000),
|
|
('8910', 'Skatt på årets resultat', 'Income tax', 'expense', 'tax_expense', NULL, 8910)
|
|
) AS a(account_code, name_sv, name_en, account_type, account_subtype, vat_code, sort_order)
|
|
WHERE c.jurisdiction = 'SE';
|
|
|
|
-- Standard GAAP chart (US)
|
|
INSERT INTO boc_chart_of_accounts (company_id, account_code, name, name_en, account_type, account_subtype, sort_order)
|
|
SELECT
|
|
c.id,
|
|
a.account_code,
|
|
a.name_en,
|
|
a.name_en,
|
|
a.account_type,
|
|
a.account_subtype,
|
|
a.sort_order
|
|
FROM boc_companies c
|
|
CROSS JOIN (VALUES
|
|
('1000', 'Assets', 'asset', 'header', 100),
|
|
('1100', 'Cash and equivalents', 'asset', 'current_asset', 1100),
|
|
('1200', 'Accounts receivable', 'asset', 'current_asset', 1200),
|
|
('1500', 'Computer equipment', 'asset', 'fixed_asset', 1500),
|
|
('1600', 'Vehicles', 'asset', 'fixed_asset', 1600),
|
|
('2000', 'Liabilities', 'liability', 'header', 2000),
|
|
('2100', 'Accounts payable', 'liability', 'current_liability', 2100),
|
|
('2200', 'Accrued expenses', 'liability', 'current_liability', 2200),
|
|
('2300', 'Taxes payable', 'liability', 'current_liability', 2300),
|
|
('2500', 'Related party debt', 'liability', 'current_liability', 2500),
|
|
('3000', 'Equity', 'equity', 'header', 3000),
|
|
('3100', 'Common stock', 'equity', 'equity', 3100),
|
|
('3200', 'Retained earnings', 'equity', 'equity', 3200),
|
|
('3500', 'Owner drawings', 'equity', 'equity', 3500),
|
|
('4000', 'Revenue', 'revenue', 'header', 4000),
|
|
('4100', 'Service revenue', 'revenue', 'operating_revenue', 4100),
|
|
('4200', 'Consulting revenue', 'revenue', 'operating_revenue', 4200),
|
|
('4300', 'Rental income', 'revenue', 'operating_revenue', 4300),
|
|
('4900', 'Other income', 'revenue', 'other_revenue', 4900),
|
|
('5000', 'Expenses', 'expense', 'header', 5000),
|
|
('5100', 'Advertising', 'expense', 'operating_expense', 5100),
|
|
('5200', 'Travel and meals', 'expense', 'operating_expense', 5200),
|
|
('5300', 'IT services', 'expense', 'operating_expense', 5300),
|
|
('5400', 'Software licenses', 'expense', 'operating_expense', 5400),
|
|
('5500', 'Vehicle expenses', 'expense', 'operating_expense', 5500),
|
|
('5600', 'Insurance', 'expense', 'operating_expense', 5600),
|
|
('5700', 'Professional fees', 'expense', 'operating_expense', 5700),
|
|
('6000', 'Personnel', 'expense', 'header', 6000),
|
|
('6100', 'Salaries and wages', 'expense', 'personnel_expense', 6100),
|
|
('6200', 'Payroll taxes', 'expense', 'personnel_expense', 6200),
|
|
('6300', 'Benefits', 'expense', 'personnel_expense', 6300),
|
|
('7000', 'Taxes', 'expense', 'header', 7000),
|
|
('7100', 'Federal income tax', 'expense', 'tax_expense', 7100),
|
|
('7200', 'State income tax', 'expense', 'tax_expense', 7200)
|
|
) AS a(account_code, name_en, account_type, account_subtype, sort_order)
|
|
WHERE c.jurisdiction LIKE 'US-%';
|
|
|
|
-- Journal entries (universal, works for both BAS and GAAP)
|
|
CREATE TABLE IF NOT EXISTS boc_journal_entries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
entry_number TEXT NOT NULL,
|
|
entry_date DATE NOT NULL,
|
|
description TEXT NOT NULL,
|
|
reference TEXT,
|
|
source TEXT DEFAULT 'manual', -- manual, import, bank, payroll
|
|
source_id UUID,
|
|
attachments JSONB DEFAULT '[]',
|
|
is_reversed BOOLEAN DEFAULT FALSE,
|
|
reversed_by UUID REFERENCES boc_journal_entries(id),
|
|
status TEXT NOT NULL DEFAULT 'posted', -- draft, posted, reversed
|
|
posted_at TIMESTAMPTZ,
|
|
posted_by UUID REFERENCES boc_users(id),
|
|
created_by UUID REFERENCES boc_users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(company_id, entry_number)
|
|
);
|
|
|
|
-- Journal entry lines
|
|
CREATE TABLE IF NOT EXISTS boc_journal_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
entry_id UUID REFERENCES boc_journal_entries(id) ON DELETE CASCADE,
|
|
account_id UUID REFERENCES boc_chart_of_accounts(id) ON DELETE RESTRICT,
|
|
debit DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
credit DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
description TEXT,
|
|
project TEXT,
|
|
department TEXT,
|
|
vat_amount DECIMAL(15,2) DEFAULT 0,
|
|
vat_rate DECIMAL(5,2) DEFAULT 0,
|
|
metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Account balances per period
|
|
CREATE TABLE IF NOT EXISTS boc_period_balances (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
account_id UUID REFERENCES boc_chart_of_accounts(id) ON DELETE CASCADE,
|
|
fiscal_year INTEGER NOT NULL,
|
|
period INTEGER NOT NULL, -- 1-12 for month, 0 for year
|
|
opening_balance DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
closing_balance DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
total_debit DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
total_credit DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
currency TEXT NOT NULL DEFAULT 'SEK',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(company_id, account_id, fiscal_year, period)
|
|
);
|
|
|
|
-- VAT/GST tracking (SE: moms, US: sales tax if applicable)
|
|
CREATE TABLE IF NOT EXISTS boc_tax_reports (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
tax_type TEXT NOT NULL, -- VAT, GST, SalesTax
|
|
period_start DATE NOT NULL,
|
|
period_end DATE NOT NULL,
|
|
tax_in DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
tax_out DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
tax_payable DECIMAL(15,2) NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'draft',
|
|
filed_at TIMESTAMPTZ,
|
|
filed_with TEXT,
|
|
paid_at TIMESTAMPTZ,
|
|
metadata JSONB DEFAULT '{}',
|
|
created_by UUID REFERENCES boc_users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Payroll (works for both SE and US)
|
|
CREATE TABLE IF NOT EXISTS boc_payroll_entries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
employee_id UUID REFERENCES boc_employees(id),
|
|
period DATE NOT NULL,
|
|
gross_pay DECIMAL(15,2) NOT NULL,
|
|
net_pay DECIMAL(15,2) NOT NULL,
|
|
-- SE specific
|
|
tax_deduction DECIMAL(15,2) DEFAULT 0,
|
|
pension_contribution DECIMAL(15,2) DEFAULT 0,
|
|
-- US specific
|
|
federal_tax DECIMAL(15,2) DEFAULT 0,
|
|
state_tax DECIMAL(15,2) DEFAULT 0,
|
|
social_security DECIMAL(15,2) DEFAULT 0,
|
|
medicare DECIMAL(15,2) DEFAULT 0,
|
|
-- Employer contributions
|
|
employer_contribution DECIMAL(15,2) DEFAULT 0, -- SE: arbetsgivaravgift, US: FUTA + SUTA
|
|
benefits JSONB DEFAULT '{}',
|
|
status TEXT NOT NULL DEFAULT 'draft',
|
|
paid_at TIMESTAMPTZ,
|
|
journal_entry_id UUID REFERENCES boc_journal_entries(id),
|
|
metadata JSONB DEFAULT '{}',
|
|
created_by UUID REFERENCES boc_users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Intercompany transactions
|
|
CREATE TABLE IF NOT EXISTS boc_intercompany (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
from_company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
to_company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE,
|
|
entry_id UUID REFERENCES boc_journal_entries(id),
|
|
amount DECIMAL(15,2) NOT NULL,
|
|
currency TEXT NOT NULL,
|
|
description TEXT,
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
reconciled_at TIMESTAMPTZ,
|
|
metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Currency exchange rates
|
|
CREATE TABLE IF NOT EXISTS boc_exchange_rates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
from_currency TEXT NOT NULL,
|
|
to_currency TEXT NOT NULL,
|
|
rate DECIMAL(15,6) NOT NULL,
|
|
date DATE NOT NULL,
|
|
source TEXT DEFAULT 'manual',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(from_currency, to_currency, date)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX IF NOT EXISTS idx_companies_tenant ON boc_companies(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_coa_company ON boc_chart_of_accounts(company_id, account_code);
|
|
CREATE INDEX IF NOT EXISTS idx_journal_company_date ON boc_journal_entries(company_id, entry_date DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_journal_lines_entry ON boc_journal_lines(entry_id);
|
|
CREATE INDEX IF NOT EXISTS idx_journal_lines_account ON boc_journal_lines(account_id);
|
|
CREATE INDEX IF NOT EXISTS idx_balances_company_period ON boc_period_balances(company_id, fiscal_year, period);
|
|
CREATE INDEX IF NOT EXISTS idx_tax_reports_company ON boc_tax_reports(company_id, period_start, period_end);
|
|
CREATE INDEX IF NOT EXISTS idx_payroll_company_period ON boc_payroll_entries(company_id, period);
|
|
CREATE INDEX IF NOT EXISTS idx_intercompany_from ON boc_intercompany(from_company_id, status);
|
|
CREATE INDEX IF NOT EXISTS idx_intercompany_to ON boc_intercompany(to_company_id, status); |