Skip to content

Answer how long a spool sat and what is sitting idle, from the ledger that exists to say so - #119

Merged
dimmus merged 3 commits into
mainfrom
claude/store-measures
Sep 25, 2026
Merged

dimmus merged 3 commits into
mainfrom
claude/store-measures

Conversation

@dimmus

@dimmus dimmus commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Issue #35, the turnover and dead stock rows. Stacked on #118 (which is stacked on #117); merge those first and this diff shrinks to its own commit.

What it computes. contexts/inventory/store_measures.py joins each lot to three instants from material_movements — its stock.received row, its first outbound row, its last row of any kind — and folds:

  • «Оборачиваемость»: mean days between arrival and first outbound, per family, over lots that left the shelf. Lots still there are counted beside the mean, never inside it.
  • «Залежалое»: lots in stock with mass left and no movement for idle_days (60 by default), costed as purchase_price × remaining / initial only where receiving recorded a price. Unpriced lots are counted and add nothing to the total, rather than being costed at nought.

The money split. GET /store/turnover is grams and days under the router's VIEW_PRODUCTION; GET /store/dead-stock sits behind VIEW_FINANCIALS on top, refused whole to an operator. StoreMeasures.tsx never requests it without the permission — the screen test asserts that in the network log — and draws nothing in its place.

Verified

  • Backend: 4 unit tests, 2 API tests, the existing store API tests; six gates each exit=0; docs gates pass (DESIGN-KIT §2.4 now says what is built and what is owed: drying state, stocktake, the non-filament classes).
  • Frontend, from the lockfile: tsc --build --force, lint, test (36 files, 333 passed), build each exit=0; API client regenerated.
  • Not re-run locally: the full backend suite (28 min); CI's backend job runs it.

Two traps recorded in HANDOFF: record_movement does not flush and next_sequence reads the ledger, so two rows before one flush claim the same rung; and lint-imports reported a broken contract while another process was writing files and kept all six when re-run alone.

🤖 Generated with Claude Code

dimmus and others added 2 commits September 25, 2026 15:59
…with (#36)

A parcel ended at `packaging_tasks.shipped_at` with a carrier code and
nothing else: no zone, no promise, no arrival, so no «В срок» and no
«Точность» anybody measured. New context `contexts/logistics` (migration
0028: `shipments`, `shipment_events`).

A shipment opens at the API edge when a parcel ships, pinning the zone and
its transit days from the order's own rate snapshot - a zone edited
afterwards moves the next promise and never this one (ADR-0020, applied to
a promise). An order with no pinned rates, or a postcode no zone claims,
gets a transit time and no promise: null, never nought days. Every change of
state is an event and the status is what the last event says; `delivered_at`
is written by the `delivered` event alone, an arrival before the dispatch is
refused, and a closed parcel takes no more events.

The scorecards count what was measured. «В срок» is over delivered parcels
that carried a promise, with the counts beside the share; «Точность» groups
by the pinned promise, so a zone whose transit days changed mid-window is two
rows rather than one blurred average; «Повреждений» is recorded events, and
the foot says so. Not served, each with its reason in the code: «Средняя
цена» and «Что и почём» (money behind a production gate), «Оценка» (no chosen
weights), «Отгрузка сегодня» (the packaging board's), «Куда» (on the order).

`LogisticsPage.tsx` draws the lanes, the shipment with its history, the
carriers and the delivery accuracy. DESIGN-KIT §1 now reads twenty-one of
twenty-one, and the screen-inventory gate gained its zero case.

Verified: 14 unit + 5 API tests (the ship route opens the shipment; an
operator records the delivery and a customer may not; no money on the
schemas); `alembic upgrade`, `check` (no drift), `downgrade`, re-`upgrade`
on the dev database; six backend gates each exit=0; referential-integrity,
schema-contract, migration and docs gates pass. Frontend from the lockfile:
`tsc --build --force`, lint, test (35 files, 330 passed), build, and a
regenerated API client, each exit=0; five screen tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… that exists to say so (#35)

Issue #35's second slice: «Оборачиваемость» and «Залежалое». Both are the
question a location overwritten in place destroys and the movement ledger was
written to keep - so they are folds over `material_movements`, never a column
somebody maintains.

Turnover is the mean days between a lot's `stock.received` row and its first
outbound row (mounted, issued or written off; a cell-to-cell move is not
outbound), per family, over lots that *left* the shelf. The ones still there
are counted beside the mean and never inside it, so an untouched shelf does
not read as a fast one. Dead stock is lots in stock with mass left and no
movement for the window, costed as purchase price pro rata to what remains
**only where receiving recorded a price**; unpriced lots are counted and add
nothing, rather than being costed at nought. Served as `GET /store/turnover`
under the router's VIEW_PRODUCTION and `GET /store/dead-stock` behind
VIEW_FINANCIALS on top - the `/jobs/variances` shape §2.4 asked for.
`StoreMeasures.tsx` never requests the money half without the permission; its
test reads the network log to prove it.

Verified: 4 unit tests (both folds and the three-instant read) and 2 API tests
(the floor may read turnover and is refused dead stock; the manager gets it
costed); the existing store API tests pass; six backend gates each exit=0;
docs gates pass with DESIGN-KIT §2.4 updated. Frontend from the lockfile:
`tsc --build --force`, lint, test (36 files, 333 passed), build, and a
regenerated API client, each exit=0; three screen tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dimmus
dimmus force-pushed the claude/store-measures branch from 9cea5d0 to 29a79fd Compare September 25, 2026 10:59
@dimmus
dimmus merged commit 5416d4a into main Sep 25, 2026
7 checks passed
@dimmus
dimmus deleted the claude/store-measures branch September 25, 2026 12:03
dimmus added a commit that referenced this pull request Sep 26, 2026
… read the two settings nothing read (#35) (#120)

`inventory.require_drying` and `inventory.drying_valid_hours` have been in the
settings catalogue since the settings screen was built, and nothing read either.
This is the read.

One nullable column, `material_lots.dried_at` (migration 0029), written only when
a spool comes out of the dryer. The state is computed at read time from that
instant, the clock and the two settings: not_required (PLA, or the rule off),
drying, unknown (never marked — not measured, so not lapsed), dry with hours
left, expired. Never stored, so a spool nobody looked at cannot go on reading as
dry, and shortening the window on the settings screen shortens every mark; the
API test edits the setting mid-test to prove it.

Two writes, `POST /store/lots/{id}/dry` and `/dried`, both ledger rows. The
second is refused for a spool that was never sent, which is what keeps
`dried_at` meaning what its name says. The spool keeps its cell in the dryer so
the map does not offer its slot to anybody.

`GET /store/cells/{address}` serves `StoredLot` — the lot, its family, its
receipt and the state — and `CellDetail.tsx` draws the kit's «Сушка» column
with «не сушилась» and «просрочена» kept apart. «Высушена» is offered for
anything physically in the dryer even after the rule is switched off, or the
spool would be stuck there.

Also corrects two comments that described the store before #119 landed, and
extends the docs gate's number-word table past thirty.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant