The Mark-purchased dialog gains a "Scan receipt…" button: attach a receipt
photo/PDF and the model reads it, fills the unit price each item actually cost,
and offers to save those to the price book — so inventory spend and future
estimates reflect what you really paid.
- driver.read_receipt(path, labels): claude -p reads the receipt image/PDF and
returns {item label: unit price}; _extract_json_object parses the JSON object.
- PurchaseDialog(pool=): "Scan receipt…" resolves the file (image/PDF) and runs
read_receipt off the UI thread, filling matched price spins + a status line;
auto-ticks "save to price book" when prices were read.
- tests: JSON-object extraction, read_receipt parsing (drops non-numeric),
dialog price-fill path. 230 pass.
Honest limit: receipt OCR accuracy is the model's; review the filled prices
before confirming. Live read needs your machine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. Offcut reuse was lost on optimize: Find better layout / Best of 100 / Try
alternative now pass available=self._available(); reoptimize seeds preserve
owned/source so a locked offcut stays owned (not silently bought).
2. Inventory is now species-aware end to end: purchase/consume/adjust/on_hand/
available_stock and record_build key by (stock, material); plan_consumption
and Mark-purchased group by species; PurchaseDialog shows species and prices
at the species rate; price-book save backs out the multiplier to the SPF base.
A spruce on-hand no longer satisfies an oak cut.
3. Cross-species placement is now invalid: validate_cut_plan and the GUI drop
path reject an oak cut on a spruce piece.
4. Yield is bought-only and consistent: _score divides bought-used by bought-area
(owned offcuts excluded); the Shopping tab's yield matches.
tests: locked-reopt keeps owned offcut, species-aware on-hand, cross-species
validate, yield excludes owned, optimize preserves the offcut toggle. 203 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the ledger into the BOM window via three workflows (Codex's
workflow-first UX), plus an offcut-consuming planner.
- StockPiece gains owned/source; build_cut_plan(available=) seeds the packer
from owned offcuts (reusing the seeded-packing) so they're consumed before
buying. Score reports stock_count = pieces to BUY (offcuts free) + owned_count;
prices.estimate and the Buy list exclude owned pieces.
- BOM header: "Build units", an opt-in "Use shop offcuts" toggle, and
"Mark purchased…" / "Record build…" buttons.
- PurchaseDialog: confirm qty + price each, opt-in "save prices to price book".
- RecordBuildDialog: shows consumed stock + each offcut with Keep/Burn/Trash/
Ignore before committing (the moment to correct reality).
- Ledger.record_build takes per-offcut dispositions; used offcuts are consumed
from the bin; build cost snapshot logged.
- tests: offcut toggle drops buy-count to 0, record-build writes the ledger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New shop-packet output: a printable cost estimate driven by the active
CutPlan's buy-counts × a curated, editable price book (HST 15%).
- prices.py: DEFAULT_PRICES seeded with real Kent (New Brunswick) shelf
prices per buy-unit (lumber = 8' stick, plywood = 4x8 sheet); persisted to
$XDG_CONFIG_HOME/woodshop/prices.json (defaults + saved overrides).
estimate() -> CostEstimate (lines/subtotal/tax/total/missing); lumber price
scales with stick length; unknown stock is flagged, never invented.
- BOM window: Cost tab with "Edit prices…" (PriceEditDialog), "Refresh from
Kent…", and Print.
- fetch_kent_prices() + scripts/fetch_kent_prices.py: best-effort refresh.
Kent renders prices client-side (not in HTML), so it tries a static parse
then Playwright if installed — honest that it may need updating.
- tests: estimate math, per-sheet plywood, stick-length scaling, missing-price
flagging, save/load roundtrip, corrupt-file fallback, JSON-LD parse, cost
tab render + price edit persistence. 153 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex findings:
1. reoptimize sent unlocked plywood to fresh sheets whenever any sheet
placement was locked, instead of packing into free space on the locked
sheet — so locking one of two panels that share a sheet split them onto
two sheets. Added _free_rects_sheet (guillotine subtraction carving free
rectangles around locked panels) + _pack_plywood_seeded, and refactored
_pack_plywood_guillotine onto a shared _guillotine_pack core that accepts
seeded sheets. reoptimize now uses it for the plywood branch.
2. "Best of 100" only tried the ~6 STRATEGIES when locks existed. The locked
path now runs strategies + shuffle restarts up to 100 attempts via
reoptimize, matching the label.
Tests: plywood lock keeps both panels on one sheet; locked Best-of-100 stays valid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_drop_piece looked up plan.item(item.pid), but item.pid is a Placement id (pl2)
while CutPlan.item() expects a CutItem id (ci2) — every drop raised StopIteration
before validate/revert could run. Use the already-found placement's item_id
(plan.item(p.item_id)) for the stock-compat check and message.
Added tests/test_bom_window.py (offscreen QGraphicsScene): drop-overlap reverts
without crashing, drop-onto-incompatible-stock reverts, and a valid move commits.
128 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>