Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)

FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-12 12:15:39 +00:00
commit 8d688b459d
10003 changed files with 1928395 additions and 0 deletions

View file

@ -0,0 +1,426 @@
"""
Migration script to add new invoice features:
- document_type, order_number columns on invoices
- duplicate_status, duplicate_of_id, related_document_id columns on invoices
- line_items table
Run this script once after deploying the new code.
"""
import asyncio
import logging
from sqlalchemy import text
from database import engine
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def run_migration():
"""Add new columns and tables for invoice features."""
# Always run all migrations - each has its own try/except to handle existing columns
logger.info("Running database migrations...")
# Run each migration in its own transaction
migrations = [
"ALTER TABLE invoices ADD COLUMN document_type VARCHAR(50) DEFAULT 'invoice'",
"ALTER TABLE invoices ADD COLUMN order_number VARCHAR(100)",
"ALTER TABLE invoices ADD COLUMN duplicate_status VARCHAR(50)",
"ALTER TABLE invoices ADD COLUMN duplicate_of_id INTEGER REFERENCES invoices(id)",
"ALTER TABLE invoices ADD COLUMN related_document_id INTEGER REFERENCES invoices(id)",
]
for sql in migrations:
try:
async with engine.begin() as conn:
await conn.execute(text(sql))
logger.info(f"Executed: {sql[:60]}...")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info(f"Column already exists, skipping")
else:
logger.warning(f"Migration warning: {e}")
# Create line_items table
create_line_items = """
CREATE TABLE IF NOT EXISTS line_items (
id SERIAL PRIMARY KEY,
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
description TEXT,
quantity NUMERIC(10, 3),
unit_price NUMERIC(10, 2),
amount NUMERIC(10, 2),
product_code VARCHAR(100),
line_number INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
try:
async with engine.begin() as conn:
await conn.execute(text(create_line_items))
logger.info("Created line_items table")
except Exception as e:
logger.warning(f"line_items table: {e}")
# Create index
try:
async with engine.begin() as conn:
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_line_items_invoice_id ON line_items(invoice_id)"
))
logger.info("Created index on line_items.invoice_id")
except Exception as e:
logger.warning(f"Index: {e}")
# Add aliases column to suppliers table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE suppliers ADD COLUMN aliases JSON DEFAULT '[]'"
))
logger.info("Added aliases column to suppliers")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("aliases column already exists")
else:
logger.warning(f"aliases column: {e}")
# Add net_total column to invoices table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE invoices ADD COLUMN net_total NUMERIC(10, 2)"
))
logger.info("Added net_total column to invoices")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("net_total column already exists")
else:
logger.warning(f"net_total column: {e}")
# Add is_non_stock column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN is_non_stock BOOLEAN DEFAULT FALSE"
))
logger.info("Added is_non_stock column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("is_non_stock column already exists")
else:
logger.warning(f"is_non_stock column: {e}")
# Add vendor_name column to invoices table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE invoices ADD COLUMN vendor_name VARCHAR(255)"
))
logger.info("Added vendor_name column to invoices")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("vendor_name column already exists")
else:
logger.warning(f"vendor_name column: {e}")
# Add ocr_raw_json column to invoices table (for storing full Azure response)
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE invoices ADD COLUMN ocr_raw_json TEXT"
))
logger.info("Added ocr_raw_json column to invoices")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("ocr_raw_json column already exists")
else:
logger.warning(f"ocr_raw_json column: {e}")
# Add supplier_match_type column to invoices table (for fuzzy matching)
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE invoices ADD COLUMN supplier_match_type VARCHAR(20)"
))
logger.info("Added supplier_match_type column to invoices")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("supplier_match_type column already exists")
else:
logger.warning(f"supplier_match_type column: {e}")
# Create field_mappings table
create_field_mappings = """
CREATE TABLE IF NOT EXISTS field_mappings (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
supplier_id INTEGER REFERENCES suppliers(id),
source_field VARCHAR(100) NOT NULL,
target_field VARCHAR(100) NOT NULL,
field_type VARCHAR(20) DEFAULT 'invoice',
transform VARCHAR(50) DEFAULT 'direct',
priority INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
try:
async with engine.begin() as conn:
await conn.execute(text(create_field_mappings))
logger.info("Created field_mappings table")
except Exception as e:
logger.warning(f"field_mappings table: {e}")
# Create index on field_mappings
try:
async with engine.begin() as conn:
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_field_mappings_kitchen_id ON field_mappings(kitchen_id)"
))
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_field_mappings_supplier_id ON field_mappings(supplier_id)"
))
logger.info("Created indexes on field_mappings")
except Exception as e:
logger.warning(f"field_mappings indexes: {e}")
# Add unit column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN unit VARCHAR(50)"
))
logger.info("Added unit column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("unit column already exists")
else:
logger.warning(f"unit column: {e}")
# Add order_quantity column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN order_quantity NUMERIC(10, 3)"
))
logger.info("Added order_quantity column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("order_quantity column already exists")
else:
logger.warning(f"order_quantity column: {e}")
# Add tax_rate column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN tax_rate VARCHAR(50)"
))
logger.info("Added tax_rate column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("tax_rate column already exists")
else:
logger.warning(f"tax_rate column: {e}")
# Add tax_amount column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN tax_amount NUMERIC(10, 2)"
))
logger.info("Added tax_amount column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("tax_amount column already exists")
else:
logger.warning(f"tax_amount column: {e}")
# Add raw_content column to line_items table (for pack size parsing)
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN raw_content TEXT"
))
logger.info("Added raw_content column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("raw_content column already exists")
else:
logger.warning(f"raw_content column: {e}")
# Add pack_quantity column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN pack_quantity INTEGER"
))
logger.info("Added pack_quantity column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("pack_quantity column already exists")
else:
logger.warning(f"pack_quantity column: {e}")
# Add unit_size column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN unit_size NUMERIC(10, 3)"
))
logger.info("Added unit_size column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("unit_size column already exists")
else:
logger.warning(f"unit_size column: {e}")
# Add unit_size_type column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN unit_size_type VARCHAR(10)"
))
logger.info("Added unit_size_type column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("unit_size_type column already exists")
else:
logger.warning(f"unit_size_type column: {e}")
# Add portions_per_unit column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN portions_per_unit INTEGER DEFAULT 1"
))
logger.info("Added portions_per_unit column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("portions_per_unit column already exists")
else:
logger.warning(f"portions_per_unit column: {e}")
# Add cost_per_item column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN cost_per_item NUMERIC(10, 4)"
))
logger.info("Added cost_per_item column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("cost_per_item column already exists")
else:
logger.warning(f"cost_per_item column: {e}")
# Add cost_per_portion column to line_items table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN cost_per_portion NUMERIC(10, 4)"
))
logger.info("Added cost_per_portion column to line_items")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("cost_per_portion column already exists")
else:
logger.warning(f"cost_per_portion column: {e}")
# Create product_definitions table for persistent portion/pack data
create_product_definitions = """
CREATE TABLE IF NOT EXISTS product_definitions (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
supplier_id INTEGER REFERENCES suppliers(id),
product_code VARCHAR(100),
description_pattern VARCHAR(255),
pack_quantity INTEGER,
unit_size NUMERIC(10, 3),
unit_size_type VARCHAR(10),
portions_per_unit INTEGER,
portion_description VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(kitchen_id, supplier_id, product_code)
)
"""
try:
async with engine.begin() as conn:
await conn.execute(text(create_product_definitions))
logger.info("Created product_definitions table")
except Exception as e:
logger.warning(f"product_definitions table: {e}")
# Create indexes on product_definitions
try:
async with engine.begin() as conn:
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_product_definitions_kitchen_id ON product_definitions(kitchen_id)"
))
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_product_definitions_supplier_id ON product_definitions(supplier_id)"
))
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_product_definitions_product_code ON product_definitions(product_code)"
))
logger.info("Created indexes on product_definitions")
except Exception as e:
logger.warning(f"product_definitions indexes: {e}")
# Remove default from portions_per_unit column (make it truly nullable)
# PostgreSQL doesn't have a simple ALTER COLUMN DROP DEFAULT, but we can change the default to NULL
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE line_items ALTER COLUMN portions_per_unit DROP DEFAULT"
))
logger.info("Removed default from portions_per_unit column")
except Exception as e:
logger.warning(f"portions_per_unit default removal: {e}")
# Add saved_by_user_id column to product_definitions table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE product_definitions ADD COLUMN saved_by_user_id INTEGER REFERENCES users(id)"
))
logger.info("Added saved_by_user_id column to product_definitions")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("saved_by_user_id column already exists")
else:
logger.warning(f"saved_by_user_id column: {e}")
# Add source_invoice_id column to product_definitions table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE product_definitions ADD COLUMN source_invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL"
))
logger.info("Added source_invoice_id column to product_definitions")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("source_invoice_id column already exists")
else:
logger.warning(f"source_invoice_id column: {e}")
# Add source_invoice_number column to product_definitions table
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE product_definitions ADD COLUMN source_invoice_number VARCHAR(100)"
))
logger.info("Added source_invoice_number column to product_definitions")
except Exception as e:
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
logger.info("source_invoice_number column already exists")
else:
logger.warning(f"source_invoice_number column: {e}")
logger.info("Migration completed successfully!")
if __name__ == "__main__":
asyncio.run(run_migration())