woodshop/MATERIALS_INVENTORY_PLAN.md

202 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Materials, Finish, Batch Builds & Shop Inventory — Plan (v2, reconciled)
Design plan for four related features, reconciled after a Codex review.
**STATUS: all 7 phases implemented (193 tests).** v2 changes vs v1 are marked **[v2]**.
Settled the two trailing questions: offcut reuse is **opt-in** (a toggle), and
recording a purchase **opt-in** saves prices to the price book.
## What changed in v2 (after Codex review)
- **[v2] Sanding never shrinks the design model.** Part dims = final/intended.
Sanding becomes a *manufacturing allowance* in the CutPlan (rough vs final),
not a scene mutation. (Codex #1.)
- **[v2] Rob's stock-reality refinement:** the allowance applies to dimensions we
actually **cut** — length always, width only for rips/sheet goods. Dimensional
lumber's fixed section (a 2x4 is 1.5×3.5 as delivered) is NOT padded. Final
dims remain the truth for fit in v1.
- **[v2] Finish is first-class** (`material` + `finish` enum + `finish_color`),
layered: stock shape → material/species → finish → visual → cost. A painted
pine board is still pine for buying/cutting. (Codex #2, #9.) Single enum now,
sheen later (simpler than Codex's kind+sheen split).
- **[v2] Inventory is an append-only event ledger**; current state is derived.
(Codex #5.) Kept deliberately lean — minimal events, no undo/branching, raw
events never shown to the user.
- **[v2] Offcuts are real stock pieces** behind one `AvailableStock` interface the
planner consumes (reusing the seeded-packing from lock-aware reopt). (Codex #6.)
- **[v2] Sequence reordered** per Codex: materials/finish first; allowance and
batch as CutPlan features; inventory last (model → workflows → window).
## Guiding principles
- Deterministic math, AI only for narrative. Easy, intuitive, useful — no
complexity for its own sake.
- Additive, backward-compatible serialization (loader filters by known fields).
- Physical state (stock, offcuts, builds) is **shop-wide**, in the data dir.
- Reuse existing machinery (`_pack_lumber_seeded`, `_pack_plywood_seeded`,
`_free_rects_sheet`).
## Data locations
- Scenes: `$XDG_DATA_HOME/woodshop/` (per project).
- Prices + estimate rates: `$XDG_CONFIG_HOME/woodshop/{prices,estimate}.json`.
- **New** ledger: `$XDG_DATA_HOME/woodshop/inventory.json` (shop-wide, append-only).
---
## Phase 1 — Material + finish fields + color resolver
**Goal:** parts carry a material and a finish; the viewer colors by them; sanded
raw wood reads lighter, painted boards show their color. Flat colors v1 (PyVista
textures are a future option).
**Data model (additive on `Part`):**
- `material: str = ""` — species/sheet: spruce, pine, oak, walnut, maple, birch,
mdf, spruce-ply. Default derived from stock (`lumber.py`).
- `finish: str = "raw"` — one of `raw | sanded | clear | stain | paint`.
- `finish_color: str = ""` — hex, for paint/stain.
- **[v2] Migration:** old scenes store `finishes: ["sanded"]`; `from_dict` maps a
non-empty list → `finish="sanded"` (or "paint" if a color is present).
**Color resolver (viewer), layered stock→material→finish→visual:**
- base = `MATERIAL_COLORS[material]` if set else positional palette (fallback).
- `paint``finish_color`; `stain` → blend base toward `finish_color`/darker;
`clear` → base slightly richer; `sanded` → base ~15% lighter; `raw` → base.
- **Per-part subtle tint** (±~5% lightness keyed by `_stable_hash(id)`) so
same-material boards stay distinguishable. Selection highlight unchanged.
**Scene ops / UI:** `scene.set_material(ref, m)`, `scene.set_finish(ref, kind,
color="")`. GUI: Paint button (QColorDialog) + Material & Finish dropdowns in the
inspector; works on selection/group. CLI/voice: `wood-material`, `wood-finish`/
`wood-paint`; voice color words → hex via a small name table.
**Tests:** resolver priority; sanded lightening; tint determinism; finish/material
persist + migrate; group ops.
---
## Phase 2 — Finish costs in the estimate
**Goal:** finish/paint cost folds into the project estimate by **finish kind ×
surface area**.
**Design:** `EstimateRates` gains per-kind $/sq ft (`sanded` = abrasives only,
`clear`, `stain`, `paint`). `project_estimate` adds a finish line per part using
its `finish` + finished surface area. Labor already has a sanding/finishing line;
extend its time to vary by finish kind (paint/stain take longer) — editable.
**Tests:** paint vs clear vs raw produce expected finish $; editable rates flow
through; zero for raw.
---
## Phase 3 — Manufacturing allowance in the CutPlan (rough vs final)
**Goal:** the cut plan distinguishes the size you **cut** from the **final** size,
so sanded/finished parts are cut slightly oversize where applicable.
**Design:**
- `CutItem` gains `final_length_in`, `final_width_in`; existing `length_in`/
`width_in` become the **rough cut** size.
- For a part whose `finish != raw`: `rough_length = final + sanding_allowance`;
`rough_width = final + sanding_allowance` **only** for sheet goods / ripped
widths. **[v2]** Dimensional lumber at full nominal width: rough_width = stock
width (no add); fixed section thickness unchanged.
- `ShopSettings.sanding_allowance_in` (already exists) default `1/32"`, editable.
- Cut list shows both: `Cut 24 1/16" × 3 9/16" → final 24" × 3 1/2"`.
- Nesting/packing uses the **rough** size (that's what you cut from stock).
**Tests:** finished part's rough length = final + allowance; lumber width not
padded; raw parts unchanged; cut list shows rough→final; packing uses rough.
**Open Q (deferred):** compensating joinery fit for material lost to sanding
(e.g. tenon thickness). v1 keeps final dims as the fit truth. Note as future.
---
## Phase 4 — Batch builds (quantity N)
**Goal:** estimate N identical units, nesting all units together so offcuts carry
across units → real per-unit cost.
**Design:**
- `build_cut_plan(..., quantity=1)` **[v2 confirmed]**: replicate **CutItems**,
not Parts — `ci_p3``ci_p3_u1, ci_p3_u2, …`. Label placements with unit #
("Unit 2 left leg") in the layout.
- `project_estimate(..., quantity=N)`: materials from the N-unit plan;
**setup labor once per batch**, per-operation time and consumables ×N. Report
**total + per-unit** (total / N).
- UI: a quantity spinner in the BOM header; every tab reflects it.
**Tests:** qty=2 uses ≤ 2× sticks (often fewer via shared offcuts); per-unit =
total/2; consumables/labor scale; setup once.
---
## Phase 5 — Inventory ledger model (event-sourced)
**Goal:** the source of truth for shop stock/offcuts/builds.
**Design [v2]:** append-only event log; derive current state (cache totals for
speed). Lean event set:
- `purchase {stock, qty, unit, price?, date}`
- `consume {stock, qty, build_id}`
- `create_offcut {offcut, build_id}`
- `discard {offcut_id, fate: burned|trashed}`
- `adjustment {…, reason}` (manual correction)
- `build_recorded {project, units, cost_snapshot, date}`
**`AvailableStock` interface [v2]:** standard stock and offcuts share one shape —
`{stock, material, length_in, width_in, is_sheet, source, bin?, usable,
reserved/consumed}`. The planner consumes both through this interface.
**Tests:** fold events → correct on-hand & offcut bin; cache matches fold;
adjustments apply; derived stats (units built, $ spent, waste split) correct.
---
## Phase 6 — Inventory workflows (the UX that matters)
**Workflow-first [v2], not spreadsheet-first:**
1. **Mark Purchased** — Shopping tab → "Add these to shop inventory?" → `purchase`
events. Optionally record price paid.
2. **Record Build** — confirmation FIRST: shows *Consumed* (e.g. 2 × 2x4 8') and
*Offcuts created* (18", 26", 12×24 ply); each offcut → **Keep / Trash / Burn /
Ignore**. On confirm: emit consume/create_offcut/discard/build_recorded.
3. **Use Shop Inventory** toggle in BOM — "Use available offcuts first" →
planner seeds from `AvailableStock` (reuses seeded-packing).
**Tests:** purchase adds on-hand; record-build deducts + creates offcuts + logs;
dispositions recorded; planner-with-offcuts uses ≤ as many new sticks.
---
## Phase 7 — Inventory window + stats (management view)
A top-level **Inventory** window (menu: *Shop → Inventory*) as a management/review
view, not the primary workflow. Tabs: On-hand, Offcut bin, Build history, Stats
(units built per project, $ spent, material used, waste % kept/burned/trashed).
Shop-wide.
---
## Locked sequence
1. Material + finish fields + color resolver
2. Finish costs in the estimate
3. Manufacturing allowance in CutPlan (rough vs final) — with the lumber-section caveat
4. Batch quantity in CutPlan
5. Inventory ledger model (event-sourced)
6. Purchase / Record-build / Use-offcuts workflows
7. Inventory window + stats
Each phase committed separately; phases 57 are the big block, built model-first.
## Settled decisions (formerly open questions)
- **Sanding:** rough-vs-final allowance on cut dims only; design dims stay final;
lumber section not padded. (Rob + Codex.)
- **Finish:** single `finish` enum + color now; sheen later.
- **Pricing:** ~~stock-based in v1~~ **now material-aware** — the planner groups
stock by (stock, species) so each bought piece is one material, and the price
book applies a per-species multiplier (SPF 1.0, oak 3.0, walnut 5.5, …),
editable in the price dialog. (B1 resolved.)
- **Batch labor:** setup once per batch, per-op ×N.
- **Inventory:** event-sourced source of truth; workflow-first UX; window second.
- **Still genuinely open:** auto-update price book from recorded purchase prices
(opt-in?); offcut reuse default-on vs opt-in (leaning opt-in toggle).