update: Major Overhaul to all subsystems

This commit is contained in:
2026-03-07 11:32:18 +02:00
parent 810e81b323
commit c62188fda6
107 changed files with 20414 additions and 929 deletions

View File

@@ -76,6 +76,102 @@ SCHEMA_STATEMENTS = [
)""",
"CREATE INDEX IF NOT EXISTS idx_mfg_audit_time ON mfg_audit_log(timestamp)",
"CREATE INDEX IF NOT EXISTS idx_mfg_audit_action ON mfg_audit_log(action)",
# Active device alerts (current state, not history)
"""CREATE TABLE IF NOT EXISTS device_alerts (
device_serial TEXT NOT NULL,
subsystem TEXT NOT NULL,
state TEXT NOT NULL,
message TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (device_serial, subsystem)
)""",
"CREATE INDEX IF NOT EXISTS idx_device_alerts_serial ON device_alerts(device_serial)",
# CRM communications log
"""CREATE TABLE IF NOT EXISTS crm_comms_log (
id TEXT PRIMARY KEY,
customer_id TEXT,
type TEXT NOT NULL,
mail_account TEXT,
direction TEXT NOT NULL,
subject TEXT,
body TEXT,
body_html TEXT,
attachments TEXT NOT NULL DEFAULT '[]',
ext_message_id TEXT,
from_addr TEXT,
to_addrs TEXT,
logged_by TEXT,
occurred_at TEXT NOT NULL,
created_at TEXT NOT NULL
)""",
"CREATE INDEX IF NOT EXISTS idx_crm_comms_customer ON crm_comms_log(customer_id, occurred_at)",
# CRM media references
"""CREATE TABLE IF NOT EXISTS crm_media (
id TEXT PRIMARY KEY,
customer_id TEXT,
order_id TEXT,
filename TEXT NOT NULL,
nextcloud_path TEXT NOT NULL,
mime_type TEXT,
direction TEXT,
tags TEXT NOT NULL DEFAULT '[]',
uploaded_by TEXT,
created_at TEXT NOT NULL
)""",
"CREATE INDEX IF NOT EXISTS idx_crm_media_customer ON crm_media(customer_id)",
"CREATE INDEX IF NOT EXISTS idx_crm_media_order ON crm_media(order_id)",
# CRM sync state (last email sync timestamp, etc.)
"""CREATE TABLE IF NOT EXISTS crm_sync_state (
key TEXT PRIMARY KEY,
value TEXT
)""",
# CRM Quotations
"""CREATE TABLE IF NOT EXISTS crm_quotations (
id TEXT PRIMARY KEY,
quotation_number TEXT UNIQUE NOT NULL,
title TEXT,
subtitle TEXT,
customer_id TEXT NOT NULL,
language TEXT NOT NULL DEFAULT 'en',
status TEXT NOT NULL DEFAULT 'draft',
order_type TEXT,
shipping_method TEXT,
estimated_shipping_date TEXT,
global_discount_label TEXT,
global_discount_percent REAL NOT NULL DEFAULT 0,
vat_percent REAL NOT NULL DEFAULT 24,
shipping_cost REAL NOT NULL DEFAULT 0,
shipping_cost_discount REAL NOT NULL DEFAULT 0,
install_cost REAL NOT NULL DEFAULT 0,
install_cost_discount REAL NOT NULL DEFAULT 0,
extras_label TEXT,
extras_cost REAL NOT NULL DEFAULT 0,
comments TEXT NOT NULL DEFAULT '[]',
subtotal_before_discount REAL NOT NULL DEFAULT 0,
global_discount_amount REAL NOT NULL DEFAULT 0,
new_subtotal REAL NOT NULL DEFAULT 0,
vat_amount REAL NOT NULL DEFAULT 0,
final_total REAL NOT NULL DEFAULT 0,
nextcloud_pdf_path TEXT,
nextcloud_pdf_url TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)""",
"""CREATE TABLE IF NOT EXISTS crm_quotation_items (
id TEXT PRIMARY KEY,
quotation_id TEXT NOT NULL,
product_id TEXT,
description TEXT,
unit_type TEXT NOT NULL DEFAULT 'pcs',
unit_cost REAL NOT NULL DEFAULT 0,
discount_percent REAL NOT NULL DEFAULT 0,
quantity REAL NOT NULL DEFAULT 1,
line_total REAL NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (quotation_id) REFERENCES crm_quotations(id)
)""",
"CREATE INDEX IF NOT EXISTS idx_crm_quotations_customer ON crm_quotations(customer_id)",
"CREATE INDEX IF NOT EXISTS idx_crm_quotation_items_quotation ON crm_quotation_items(quotation_id, sort_order)",
]
@@ -86,6 +182,65 @@ async def init_db():
for stmt in SCHEMA_STATEMENTS:
await _db.execute(stmt)
await _db.commit()
# Migrations: add columns that may not exist in older DBs
_migrations = [
"ALTER TABLE crm_comms_log ADD COLUMN body_html TEXT",
"ALTER TABLE crm_comms_log ADD COLUMN mail_account TEXT",
"ALTER TABLE crm_comms_log ADD COLUMN from_addr TEXT",
"ALTER TABLE crm_comms_log ADD COLUMN to_addrs TEXT",
"ALTER TABLE crm_comms_log ADD COLUMN is_important INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE crm_comms_log ADD COLUMN is_read INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE crm_quotation_items ADD COLUMN vat_percent REAL NOT NULL DEFAULT 24",
"ALTER TABLE crm_quotations ADD COLUMN quick_notes TEXT NOT NULL DEFAULT '{}'",
"ALTER TABLE crm_quotations ADD COLUMN client_org TEXT",
"ALTER TABLE crm_quotations ADD COLUMN client_name TEXT",
"ALTER TABLE crm_quotations ADD COLUMN client_location TEXT",
"ALTER TABLE crm_quotations ADD COLUMN client_phone TEXT",
"ALTER TABLE crm_quotations ADD COLUMN client_email TEXT",
]
for m in _migrations:
try:
await _db.execute(m)
await _db.commit()
except Exception:
pass # column already exists
# Migration: drop NOT NULL on crm_comms_log.customer_id if it exists.
# SQLite doesn't support ALTER COLUMN, so we check via table_info and
# rebuild the table if needed.
rows = await _db.execute_fetchall("PRAGMA table_info(crm_comms_log)")
for row in rows:
# row: (cid, name, type, notnull, dflt_value, pk)
if row[1] == "customer_id" and row[3] == 1: # notnull=1
logger.info("Migrating crm_comms_log: removing NOT NULL from customer_id")
await _db.execute("ALTER TABLE crm_comms_log RENAME TO crm_comms_log_old")
await _db.execute("""CREATE TABLE crm_comms_log (
id TEXT PRIMARY KEY,
customer_id TEXT,
type TEXT NOT NULL,
mail_account TEXT,
direction TEXT NOT NULL,
subject TEXT,
body TEXT,
body_html TEXT,
attachments TEXT NOT NULL DEFAULT '[]',
ext_message_id TEXT,
from_addr TEXT,
to_addrs TEXT,
logged_by TEXT,
occurred_at TEXT NOT NULL,
created_at TEXT NOT NULL
)""")
await _db.execute("""INSERT INTO crm_comms_log
SELECT id, customer_id, type, NULL, direction, subject, body, body_html,
attachments, ext_message_id, from_addr, to_addrs, logged_by,
occurred_at, created_at
FROM crm_comms_log_old""")
await _db.execute("DROP TABLE crm_comms_log_old")
await _db.execute("CREATE INDEX IF NOT EXISTS idx_crm_comms_customer ON crm_comms_log(customer_id, occurred_at)")
await _db.commit()
logger.info("Migration complete: crm_comms_log.customer_id is now nullable")
break
logger.info(f"SQLite database initialized at {settings.sqlite_db_path}")
@@ -252,3 +407,37 @@ async def purge_loop():
await purge_old_data()
except Exception as e:
logger.error(f"Purge failed: {e}")
# --- Device Alerts ---
async def upsert_alert(device_serial: str, subsystem: str, state: str,
message: str | None = None):
db = await get_db()
await db.execute(
"""INSERT INTO device_alerts (device_serial, subsystem, state, message, updated_at)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT(device_serial, subsystem)
DO UPDATE SET state=excluded.state, message=excluded.message,
updated_at=excluded.updated_at""",
(device_serial, subsystem, state, message),
)
await db.commit()
async def delete_alert(device_serial: str, subsystem: str):
db = await get_db()
await db.execute(
"DELETE FROM device_alerts WHERE device_serial = ? AND subsystem = ?",
(device_serial, subsystem),
)
await db.commit()
async def get_alerts(device_serial: str) -> list:
db = await get_db()
rows = await db.execute_fetchall(
"SELECT * FROM device_alerts WHERE device_serial = ? ORDER BY updated_at DESC",
(device_serial,),
)
return [dict(r) for r in rows]