← All tasks

H4-P2 + H4-P3 — Attributes admin on Odoo-shape tables PROD · GATE 2

Second + third phases of the H4 Product/Variant rework, bundled into one Gate cycle. Lights up the seven new cafe.product_* tables that Slice P1 created dark (2026-05-19 earlier same day). The R10 plural tables stay authoritative for POS reads until H4-P4 ships the read-path cutover.

8/8 prod test · 51/51 regression green · 59/59 total. Commit 028ed27 on nix-cafe. New /cafe/settings/attributes admin with attribute + value + template + per-template attribute-line CRUD + Instant/Never create_variant picker. Cartesian variant generator verified end-to-end on prod (lumiere-coffee created an attribute via UI, wired a template line with 2 values, asserted 2 product_product rows + 2 combo rows in the DB).

What landed on prod

Code (nix-cafe 028ed27) — 5 new files (DAO, actions, page, client, backfill script) + 1 modified (settings-nav adds "Attributes (new)" entry). No migration in this slice (Slice P1 already created the 7 tables). No new dependencies.

Data backfillscripts/backfill-h4-p3-product-variant.ts ran against prod Supabase from local. Four idempotent passes mirror R10 + cafe.products into the singular tables, matched on natural keys:

[lumiere-coffee] [get-coffee] Attributes: + 0 created Attributes: + 0 created Attribute values: + 0 created Attribute values: + 0 created = 0 existing = 0 existing Templates: + 25 created Templates: + 9 created = 8 existing Default variants: + 25 created Default variants: + 9 created = 8 existing Template lines: + 0 created Template lines: + 0 created Value rels: + 0 created Value rels: + 0 created (Re-run: + 0 created across the board on all 3 tenants — idempotent.)

Final row counts on prod after backfill (verified via direct SQL probe):

cafe.product_template lumiere-coffee=25 get-coffee=9 demo=0 cafe.product_product lumiere-coffee=25 get-coffee=9 demo=0 cafe.product_attribute lumiere-coffee=1 get-coffee=0 demo=0 cafe.product_attribute_value lumiere-coffee=2 get-coffee=0 demo=0

End-to-end UI flow on lumiere-coffee (Starter)

Playwright drove the full create-attribute → add-values → create-template → wire-line → assert-variants flow with TAG-prefixed test data. All in-place visibility (Slice K pattern extended to this new client) — no page.goto() fallback needed.

1. Page loads — both sections render (attributes-section + templates-section) 2. Click "Add attribute" → AttrModal opens Fill name = "H4P3PROBE_Size" → click "Instant" radio pill → Save 3. AttrCard for H4P3PROBE_Size appears in-place with the "instant" badge 4. Click "Add value" → ValueModal → "H4P3PROBE_Small" → Save Value row appears in-place inside the expanded attribute card 5. Click "Add value" → ValueModal → "H4P3PROBE_Large" + priceExtra "0.50" → Save Value row appears in-place 6. Click "Add template" → TemplateModal → "H4P3PROBE_Latte" → Save TemplateCard appears in-place + auto-expands 7. In the template's detail section, pick H4P3PROBE_Size from the "Add line" select Line appears for our attribute 8. Click the chip for H4P3PROBE_Small + H4P3PROBE_Large (both turn green-selected) 9. Click "Save lines" → setTemplateAttributeLinesAction fires → DAO runs the Cartesian generator → 2 product_product rows inserted → 2 product_variant_combo rows (one per variant referencing its value) → template.has_variant flips true → DB probe confirms: 2 variants, codes "V001"+"V002", 2 combo rows 10. Cleanup: SQL DELETEs cascade everything (template→variants+combos+lines, attribute→values+line_value_rels)

Screenshots

Page loaded — both sections render
01 · Page loaded — both sections render
After Save lines — 2 variants generated
02 · After Save lines — 2 variants in the variants list
get-coffee page render — 9 templates
03 · get-coffee — 9 templates rendered (post-dedup)

Known data-quality finding on prod get-coffee

Duplicate product names on get-coffee → 9 templates from 17 products. The backfill collapses by (tenant, name). get-coffee's cafe.products has these dup-named rows:

"Odoo Polo" × 5 → 1 template (4 distinct cafe.products rows collapsed) "ESPRESSO" × 3 → 1 template (2 collapsed) "Ice Latte" × 2 → 1 template (1 collapsed) "Test Product" × 2 → 1 template (1 collapsed) ──────────────────────────────────────────────────────────────────── 17 active products → 9 templates + 9 default variants

These duplicates are presumably real Odoo variant rows that today share a name. The H4-P4 read-path cutover will need to either: (a) recognise duplicate-named products as variants of one template + emit multiple product_product rows; (b) treat them as distinct templates and de-dup by Odoo product.product.id instead of name; or (c) merge admin-side. Tracked as a P4 prereq, not a regression for P2+P3. POS reads still come from R10 plural until P4, so customer-facing menus are unaffected.

Checks — 8/8 prod

Raw: result.json

51/51 regression green — no behavioural regressions from this push. The POS register read path is unchanged (still hits R10 plural tables); only the new /cafe/settings/attributes route is affected.
test-phase1-prod.mjs11/11
test-phase2-sso-outdoor-prod.mjs6/6
test-phase2-cafe-multishop-prod.mjs (parallel on demo)6/6
test-m1-prod.mjs10/10
test-r7-prod.mjs14/14
test-r8-prod.mjs4/4
Plus prod feature test (above)8/8
Total59/59

What's next in the H4 arc

H4-P4 — POS read-path cutover. POS picker reads from cafe.product_template + product_product + product_variant_combo instead of R10 tables. cafe.products becomes a view or aliased. Must resolve the dup-name collision on get-coffee before this ships (otherwise 8 products disappear from the menu).

H4-P5 — drop the R10 plural tables once nothing reads them. Pure cleanup slice.

Files

New: nix-cafe/lib/db/product_attribute.ts · nix-cafe/lib/actions/product_attribute.ts · nix-cafe/app/(authed)/settings/attributes/page.tsx · nix-cafe/app/(authed)/settings/attributes/attributes-client.tsx · nix-cafe/scripts/backfill-h4-p3-product-variant.ts
Modified: nix-cafe/app/(authed)/settings/_components/settings-nav.tsx (one entry added)
Migrations: none (Slice P1 already created the 7 tables).
Dependencies: none.