refactor(catalog): reduce the category taxonomy to a supplier label

Categories are kept only as the label each supplier carries (suppliers.category_id
+ the material_categories list it points at). Everything that treated category as
a shared key across materials, inventory, quotes and unit conversions is removed —
including two features that were never populated (0 rows on every volume).

Removed (backend):
- routers category_attributes, category_ref_units, unit_conversions + registration
- ?category_id= filters on /materials, /inventory and /quotes (the /suppliers one
  stays — it filters suppliers by their own label)
- GET /inventory/materials/options: required a category and had no caller
- the material-payload category derivation (expand_service.category_by_material,
  the inventory serializer lookup) and the material category on /pm/on-the-way;
  the wire keys stay, now always null, so the payload shape is unchanged
- startup DDL: DROP the empty category_attributes / category_reference_units /
  unit_conversions tables (idempotent) and their CREATE statements; seed.py
  truncate list updated

Removed (frontend):
- the category-keyed conversion path: refUnitForCategory / findConversion /
  refConverted, the categoryUnits + unitConversions store slices, api clients,
  hooks and the "≈ converted quantity" line on quote detail and project pages
  (it needed rows in those tables to render, and there were none)
- material.supplier_category and its read sites (the edit modal grew one
  yesterday to restore labels; the label lives on the supplier now)
- the inventory page's category chips, the row filter and the «دسته» column,
  plus the lookup's categories fetch and catName helper

Kept: supplier label + its list and picker, /suppliers?category_id=, the global
standard_conversions feature and the units page (material_units conversions).

Verified against a restore of the production dump: startup DDL drops exactly the
three tables, suppliers keep their label (3/3), material_categories still serves
its 8 rows, the removed endpoints 404, and materials/inventory/quotes/units/
standard-conversions all return 200 with quote line items intact.
This commit is contained in:
ZCode
2026-09-20 19:44:23 +00:00
parent 2fd72fecf7
commit b469238153
29 changed files with 73 additions and 947 deletions
+16 -19
View File
@@ -18,28 +18,9 @@ logger = logging.getLogger(__name__)
_DASHBOARD_DDL = [
# One reference unit per category (units.html «واحد مرجع هر دسته»)
"""
CREATE TABLE IF NOT EXISTS category_reference_units (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
category_id UUID NOT NULL UNIQUE REFERENCES material_categories(id) ON DELETE CASCADE,
ref_unit TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
""",
# Brand-scoped / category-scoped conversions: 1 from_unit = factor × to_unit.
# UNIQUE must treat brand NULL as a concrete value (PG treats NULLs as distinct).
"""
CREATE TABLE IF NOT EXISTS unit_conversions (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
category_id UUID NOT NULL REFERENCES material_categories(id) ON DELETE CASCADE,
brand TEXT,
from_unit TEXT NOT NULL,
to_unit TEXT NOT NULL,
factor NUMERIC(18,6) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
""",
"""
CREATE UNIQUE INDEX IF NOT EXISTS uq_unit_conversion
ON unit_conversions (category_id, COALESCE(brand, ''), from_unit, to_unit)
""",
@@ -413,6 +394,22 @@ async def ensure_v26(engine: AsyncEngine) -> None:
""")
async def ensure_drop_category_tables(engine: AsyncEngine) -> None:
"""v2.7 follow-up: the category taxonomy is reduced to a supplier label.
These three tables backed features that were never populated (0 rows on every
volume): per-category reference units and conversion factors (the
per-reference-unit price line) and the category -> attribute mapping. Dropping
them is idempotent and loses no data. material_categories itself stays — every
supplier carries one as its label.
"""
async with engine.begin() as conn:
await conn.execute(text(
"DROP TABLE IF EXISTS category_attributes, category_reference_units, unit_conversions CASCADE"
))
logger.info("category taxonomy tables dropped (attributes / ref units / conversions)")
async def ensure_audit_table(engine: AsyncEngine) -> None:
"""v2.7: general activity trail (audit_log). Idempotent; safe on any volume."""
ddl = """