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>
82 lines
3.9 KiB
Python
82 lines
3.9 KiB
Python
"""
|
|
Migration: Add description_aliases JSON column to ingredient_sources table.
|
|
Stores alternative descriptions that map to the same ingredient source.
|
|
Also backfills missing product codes on existing line items.
|
|
"""
|
|
import asyncio
|
|
from sqlalchemy import text
|
|
from database import engine
|
|
|
|
|
|
async def migrate():
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("""
|
|
ALTER TABLE ingredient_sources
|
|
ADD COLUMN IF NOT EXISTS description_aliases JSON DEFAULT '[]'
|
|
"""))
|
|
print("+ Added description_aliases column to ingredient_sources")
|
|
|
|
# Backfill missing product codes on existing line items.
|
|
# For every ingredient_source that has both a product_code and description_pattern,
|
|
# find line items from the same supplier with no product_code where the first-line
|
|
# description matches, and fill in the code + ingredient_id.
|
|
# This is idempotent — re-running updates 0 rows if already backfilled.
|
|
result = await conn.execute(text("""
|
|
UPDATE line_items li
|
|
SET product_code = src.product_code,
|
|
ingredient_id = COALESCE(li.ingredient_id, src.ingredient_id)
|
|
FROM ingredient_sources src, invoices inv
|
|
WHERE inv.id = li.invoice_id
|
|
AND inv.supplier_id = src.supplier_id
|
|
AND inv.kitchen_id = src.kitchen_id
|
|
AND src.product_code IS NOT NULL
|
|
AND src.product_code != ''
|
|
AND src.description_pattern IS NOT NULL
|
|
AND src.description_pattern != ''
|
|
AND (li.product_code IS NULL OR li.product_code = '')
|
|
AND LOWER(TRIM(split_part(li.description, E'\\n', 1)))
|
|
= LOWER(TRIM(src.description_pattern))
|
|
"""))
|
|
count = result.rowcount
|
|
if count > 0:
|
|
print(f"+ Backfilled product_code on {count} line items (from ingredient sources)")
|
|
else:
|
|
print("+ No line items needed product_code backfill (from ingredient sources)")
|
|
|
|
# Second pass: backfill from sibling line items.
|
|
# If any line item from a supplier has a product_code, and other line items from
|
|
# the same supplier have the same first-line description but no product_code,
|
|
# copy the code across. This handles the OCR-missed-code case (e.g. Bramleys
|
|
# butter 283) without needing ingredient sources to exist yet.
|
|
result2 = await conn.execute(text("""
|
|
UPDATE line_items li
|
|
SET product_code = known.code
|
|
FROM (
|
|
SELECT DISTINCT ON (inv.supplier_id, LOWER(TRIM(split_part(li2.description, E'\\n', 1))))
|
|
inv.supplier_id,
|
|
LOWER(TRIM(split_part(li2.description, E'\\n', 1))) AS norm_desc,
|
|
li2.product_code AS code
|
|
FROM line_items li2
|
|
JOIN invoices inv ON inv.id = li2.invoice_id
|
|
WHERE li2.product_code IS NOT NULL
|
|
AND li2.product_code != ''
|
|
ORDER BY inv.supplier_id,
|
|
LOWER(TRIM(split_part(li2.description, E'\\n', 1))),
|
|
li2.id DESC
|
|
) known, invoices inv2
|
|
WHERE inv2.id = li.invoice_id
|
|
AND inv2.supplier_id = known.supplier_id
|
|
AND (li.product_code IS NULL OR li.product_code = '')
|
|
AND LOWER(TRIM(split_part(li.description, E'\\n', 1))) = known.norm_desc
|
|
"""))
|
|
count2 = result2.rowcount
|
|
if count2 > 0:
|
|
print(f"+ Backfilled product_code on {count2} line items (from sibling line items)")
|
|
else:
|
|
print("+ No line items needed product_code backfill (from siblings)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Running migration: add_description_aliases")
|
|
asyncio.run(migrate())
|
|
print("Migration complete!")
|