The Rules That Never Bend — stock-domain invariants¶
What this covers. This is the quick-reference contract for the whole restaurant-inventory domain: the handful of rules that must hold at all times, no matter how a number was produced or which path produced it. Every other chapter in this bible teaches one area in depth; this one collects the load-bearing rules from all of them into one place you can scan. Each entry is a bold Rule, a one-line WHY, and — where a number makes it click — a small worked example. If a proposed change would break any rule below, the change is wrong, not the rule.
How to read this¶
These are invariants, not features — properties that are true on every code path, every day, regardless of who or what produced a value. They are deliberately terse. A newcomer should be able to read the bold lines and know what must never break; the WHY lines say why the system behaves as it does; the worked example is the proof.
A few rules are independence rules, written "X ⊥ Y" and read "X is independent of Y." They sound abstract, but each one closes a specific, expensive class of mistake — valuing an item by whether it happens to appear in a recipe; re-pricing stock just for moving between your own buildings; routing money off a label a customer can rename. The independence is the rule.
A note on words, used precisely throughout. A warehouse is the set of storage locations valued and closed together — a restaurant's back-of-house store, a bar store, a shared commissary; stock sits and is counted in the storage locations inside it (the walk-in, the back room, the prep room), and is valued by the warehouse around them. A restaurant is the business — the operation whose profit-and-loss the food cost lands on, with its own local clock and business date. They are deliberately different things; a single warehouse can serve several restaurants, and one restaurant can draw from several warehouses. "Kitchen" here only ever names a station on a dish or a specific warehouse — never the business and never "the stock location" in the abstract.
1. On-hand is a SUM, never a stored number¶
Rule. How much of an item is on hand is derived — it is the running total of every stock movement into and out of the real places you keep stock, as of a date. A movement's quantity is unsigned; the sign is structural — the same quantity counts out of the movement's from-location and into its to-location, so a location's total is the sum over the movements that touch it, signed by which side of each movement it sits on. The total is computed at read time (a checkpoint at the last closed period plus the moves since), never read from some mutable "quantity on hand" field that a write might forget to update.
WHY. A stored number drifts from its own history the first time any write forgets to update it; a total computed straight from the journal of movements cannot lie about that journal.
This is the perpetual-inventory spine — perpetual inventory means on-hand and value are kept continuously live off a journal of movements rather than counted up once a month. The same rule covers value: an item's worth is its weighted-average cost (defined in rule 8), also derived, never a stored "stock value" column. The "as of a date" read is a checkpoint plus a bounded tail (sum forward from the last closed period), so a years-old item is not re-added from the beginning of time — but the authoritative answer is always the journal, never a cache treated as truth.
Worked example. Flour: receive 25 kg (a movement from the supplier into the walk-in), consume 6 kg over a week (six movements from the walk-in to the customer, behind dish sales), waste 0.5 kg (a movement from the walk-in to scrap). On-hand = 25 − 6 − 0.5 = 18.5 kg, computed as a sum when someone asks. No row anyone updated says "18.5"; if one did, and one of the consume movements had failed to decrement it, the stored number and the journal would silently disagree and nobody could say which was right.
2. The journal is immutable; the paperwork has a lifecycle¶
Rule. A posted stock movement is never edited or deleted — a mistake is corrected by a contra-movement, an equal and opposite movement that leaves both the error and its fix on the record. That is different from the document layer, which comes in two species, told apart by where the document came from.
Paperwork a person authors — goods receipts, stock counts, transfers, waste write-offs, production runs — is a process: a draft is freely editable and moves no stock; posting asserts the document as a whole and emits the immutable movements (the stock count is the one exception: its posting freezes an observation and emits none — the differences post later, as a separate gesture, rule 3); amending posts contra-movements and opens an editable copy; cancelling posts the contra-movements alone. A production session takes the plainest correction of all: reverse it and record the corrected session as a new document. Why a draft: the person who writes a sheet is often not the person who signs it off, and a half-written document must be able to sit overnight without touching the books.
Paperwork that arrives — sales tickets fed in by a till or a delivery platform — is a fact: nobody is editing it, so there is nothing to draft. Capture is assertion, each line posts its movements the moment it can, a line that cannot post yet waits in quarantine, and the only correction is to reverse the document's movements and record a corrected one.
WHY. Editing a posted movement destroys the audit trail and silently breaks every number already computed from it; correcting-by-contra keeps both the error and its correction, so the books are always reconstructable.
This is the load-bearing split between the immutable journal and the mutable paperwork. The event — a movement that happened — is frozen forever; the paperwork — the operator's editable draft — is mutable right up until it posts. Corrections inside an open period are easy (reverse and re-edit); corrections after a period has closed flow to the next open period as prior-period adjustments (rule 11). Either way the trail keeps both rows — never a quiet overwrite. Value obeys the same discipline as quantity: when value must change with no goods moving — the on-hand effect of an invoice arriving after its delivery posted, a negative-on-hand correction, the closing re-spread — the change is recorded as an explicit, append-only valuation entry tied to its source document, never as a quiet rewrite of a posted movement's value.
Worked example. A goods receipt posts 25 kg of flour at €0.80/kg,
then the operator notices it was really 24 kg. You do not change the movement
from 25 to 24. You post a contra-movement (−1 kg at the same value) and the journal
reads +25, −1, net 24, with both rows and their timestamps intact. An auditor a
year later sees exactly what happened and when.
3. The count wins — as an observation, then a gesture¶
Rule. When the book and a physical count disagree, the count is the truth. A stock count is a snapshot of absolute quantity, not a change: posting it freezes the observation ("the walk-in holds 18.0 kg") and moves nothing. The book is brought to agreement by a separate human gesture — posting differences — which emits the overage and shortage movements for exactly the surviving gap. At close, those corrections are dated the period's last day, and the closing weighted-average cost re-spreads the period's value over the counted quantity — the book yields to the count, never the other way round.
WHY. The running book is only a claim assembled from the movements someone remembered to record; the count is the one event that sees what the book cannot — theft, spoilage, over-portioning, a mis-keyed invoice — so when the two disagree, reality (the count) is authoritative. And counting and adjudicating are different jobs done by different people at different times, so the observation and the correction are separate acts.
The closing re-spread — value that should remain over quantity that actually remains — is exactly how shrinkage lands in cost of goods sold. The close binds one full count: the earliest posted on or after the period's last day; the count keeps its own honest date, and the corrections carry the period's. Counting is not only a close-time event: a partial, mid-period cycle count trues the book on the spot — its differences post dated the count's own day, at the current average — while the close still demands a complete count per warehouse. And cost enters the books through exactly two doors: a priced goods receipt, or the single opening count that starts a warehouse's stock-keeping — the operator's counted quantities at asserted costs, to which each pool is settled on the opening date. The opening is also the one count whose silence speaks: it asserts a book instead of verifying one, so an item left off it is asserted at zero and whatever value the untracked era had it worth is extinguished at the boundary — where a full count, verifying a book that already exists, still demands a row for every item that book knows about. After that a count never carries a cost: an overage on a live pool enters at the running average (found stock was already paid for), and an overage on an empty pool enters at zero — value is never born without a priced document. Implements are the strict case of the same rule: a count may record more pans than the book claims — the sighting is never refused — but the surplus cannot be reconciled into stock: converting it is declined, and a period will not close over it, because an implement's whole cost leaves as an expense the day the unit breaks, and a pan that joined the pool at zero would one day write off value nobody paid for. The found units wait for the receipt that bought them, which lifts the quantity and prices it in one gesture.
Worked example. The book says 18.5 kg flour on hand; the count records 18.0 kg and moves nothing. The closer checks the journals, accepts the gap, and a 0.5 kg shortage posts (a movement from the counted storage location to stock loss); the close re-spreads the period's flour value over 18.0 kg, not 18.5. The 0.5 kg of unexplained shrinkage flows into cost of goods sold automatically — nobody had to hand-write a single "write-down."
4. The base unit is frozen the moment an item has history¶
Rule. Every item has one base unit — the single dimension it is measured and costed in (pieces, or grams, or milliliters). It is chosen once and locked the instant any stock movement references the item; after the first posting it can never change.
WHY. The base unit is the denominator of every stored quantity and every unit cost for that item; changing it would silently re-interpret every historical number already in the journal.
Nothing enters the journal except after being converted to the base unit at the moment of posting, so all the downstream math — totals, costs, variance — works in plain base-unit decimals and never has to think about units again. If grams quietly became milliliters after a thousand movements had been stored "in grams," all thousand numbers — and every cost derived from them — would now mean something different, with no way to fix it, because the system deliberately does not convert between mass and volume (it would need a density it does not model). So the anchor is frozen the moment it acquires history.
5. Value is per warehouse; profit-and-loss is per restaurant¶
Rule. An item's value — its weighted-average cost, one shared cost pool — is computed per company, per warehouse, per item: you value stock where it physically sits. Cost of goods sold, by contrast, is attributed per restaurant (a profit center — the business unit a margin belongs to): it is derived and stamped onto the consumption movements, and a restaurant never owns a stock pool of its own.
WHY. "What is a kilogram of flour worth right now?" and "which restaurant earned the margin on that pasta?" are different questions with different correct answers at different grains — value follows the building that holds (and paid for) the stock; margin follows the restaurant that sold it.
A profit center is the unit you measure profitability against — here, the restaurant. Warehouse and restaurant relate many-to-many ("draws-from / supplies"): one commissary can feed several restaurants, and one restaurant can pull from several warehouses. That link governs eligibility and routing — which warehouses a restaurant's sales may deplete, which restaurants a warehouse may supply — not remote draws: what a commissary preps for a restaurant is transferred into that restaurant's own warehouse, and a sale always depletes a place the goods are actually in. A per-restaurant profit-and-loss is a roll-up of the consumption attributed to it, never a second place value is measured; "company-wide" valuation is only the special case of a single warehouse.
Worked example. A shared commissary preps a batch of base sauce and holds it at €0.85/kg — one place, one pool, one weighted-average. Restaurants A and B are each supplied by transfer: the sauce moves into each restaurant's own warehouse, carrying the commissary's €0.85 cost with it (value follows the goods, warehouse to warehouse). When either restaurant sells a dish, the consumption depletes its own warehouse — never reaching back into the commissary's — and the cost-of-goods euro is stamped to A's or B's profit-and-loss respectively (margin follows the restaurant). One commissary's cost, two profit-and-loss statements, no double-counting — and every warehouse's count stays truthful at both ends, because stock is only ever consumed where it actually sits.
6. Behavior routes off stable policy fields, never a category name¶
Rule. Every accounting, movement, and tracking decision branches on stable, enumerated policy fields on the item — how it is treated for accounting, whether it is lot- or serial-tracked, what its recipe role is — and the facts derived from those. It never branches on the item's category, which is a name and an id a customer controls.
WHY. A category is a user-extensible, localized label — the same category reads "Packaging" in English and "Упаковка" in Russian, and a customer can add their own — so any behavior keyed on a category name or id is keyed on data the customer can edit; renaming or re-localizing it would silently re-route money.
The accounting treatment is one of four: perpetually tracked (on the stock journal, counted, valued — flour, chicken); expensed on receipt (written off when it arrives, never carried as on-hand — salt, cleaning supplies); expensed on write-off (durable smallwares — pans, knives — pooled at cost and expensed the day each one dies); or capitalized and depreciated (durable assets — an oven, a delivery van — handled in fixed-asset accounting). That treatment is resolved once, persisted on the item, and locked once the item has transaction history — so behavior always keys off a stable choice, never a label on a dropdown. (The treatment also routes what a goods-receipt line does — into stock, into the implements pool, straight to expense, or into the asset register — there is no separate "procurable" gate.)
Worked example. A salt sachet is expensed on receipt with the ingredient recipe role; a takeaway clamshell is stock-tracked with the packaging role. A customer renames the category "Consumables" to "Disposables" and adds a "Compostables" category of their own. Because behavior reads the items' policy facts and not the category, not one accounting decision moves — the salt is still expensed, the clamshell still counted and rolling into packaging cost, no matter what the labels say.
7. Valuation ⊥ recipe role¶
Rule. How an item's money flows (its accounting treatment) and what it may be inside a recipe (its recipe role) are independent axes. An expense-on-receipt item (oil, salt) is not carried on-hand, counted, or valued — yet, carrying the ingredient role, it is a first-class recipe ingredient, costed in recipes at a reference price (its last purchase, or a standard) and left out of counts and actual-usage variance, where it counts toward theoretical usage only.
WHY. Gating recipe-membership on "is it stocked?" would either force every recipe ingredient to be perpetually counted (absurd for a pinch of salt) or bar cheap staples from recipes entirely — both wrong, because the two concerns are simply different questions.
Independent does not mean ungated. Recipe-membership is gated by the role: an ingredient line may point only at an ingredient (an atom that never decomposes) or a producible item (a made thing with its own recipe); a packaging row only at a packaging item. Consumables, implements, and fixed assets carry no role and can never be picked onto a recipe — which is what turns "gloves never reach cost of goods" into a structural guarantee rather than a convention. Food attributes stay ungated: any item can carry nutrition or allergens, including resale goods like a bottle of cola.
Worked example. A vinaigrette recipe uses 200 g romaine (stock-tracked, role ingredient: counted, valued at weighted-average cost, contributes to actual-usage variance) and 15 g salt (expensed on receipt, role ingredient: never counted, costed at its last-purchase reference price, contributes only to theoretical cost). Both are ordinary recipe ingredients; the role is what lets each sit on the recipe, and the treatment decides how each is costed and reconciled.
8. Quantity ⊥ value¶
Rule. A movement can change quantity without changing total value, and the costing layer changes independently of where units physically sit. Moving stock between two storage locations of one warehouse touches value not at all — they share the pool — and moving it between two of your own warehouses leaves the company's total untouched; the weighted-average cost simply re-blends at the receiving warehouse.
WHY. Quantity and value are two different facts about the same stock — carrying a kilogram from one of your buildings to another does not create or destroy a euro of inventory value — so the value layer must be decoupled from the quantity layer, or every internal transfer would fabricate or vaporize money.
Weighted-average cost (WAC) means every unit of an item in a warehouse shares one blended price, re-averaged on each receipt. Conceptually there are distinct layers — the event (a movement happened), the lot detail (which batch), the on-hand (derived quantity), and the value — and they stay distinct even where they share one journal.
Worked example. Transfer 10 kg flour from the commissary (€0.85/kg) to the bar store. Quantity moves: −10 kg at the commissary, +10 kg at the bar store. Total value across the company is unchanged. The bar store's weighted-average cost re-blends to absorb the incoming 10 kg at €0.85; no value was created by the act of moving it. A difference in cost between two warehouses is a re-blend at the destination, not a gain or a loss.
9. Stock is valued net of recoverable tax¶
Rule. The value that feeds an item's weighted-average cost is net of any input tax the business gets back. Every goods receipt carries two money figures: the gross total (what you owe and tie out against the supplier's invoice) and the net capitalized amount (what actually feeds the journal and the cost). The cost reads the net figure, never the gross.
WHY. Recoverable tax is money the business reclaims from the tax authority, not a cost of the goods — capitalizing it into inventory overstates cost of goods sold, and food-cost percentage, by the tax rate.
The value the receipt movement carries into stock is the landed cost of each line: the goods, plus freight and handling allocated across the lines, plus that line's non-recoverable tax. Tax is a fact of the line, never a header pot: each receipt line carries its own rate and its own recoverability, because one invoice can mix rates (a Portuguese invoice with 6%, 13%, and 23% lines) and mix taxability (a US resale-exempt food line next to taxed supplies) — spreading one header tax figure across the document by value would mis-cost every line. Two tax regimes, one path: where input tax is recoverable (the EU, Portugal first), it becomes a receivable and inventory is valued net of it; where it is not (the US, where purchases for resale are exempt), the recoverable portion is zero and net simply equals cost.
Worked example (EU, 23% recoverable VAT). Receive flour: subtotal €100, tax €23, invoice total €123. The gross figure is 123 (what you owe and reconcile against the supplier invoice). The net figure is 100 (the €23 is reclaimed from the tax authority). The weighted-average cost blends in €100, not €123 — or every dish made from this flour would carry a 23% phantom cost. Same flour, US, exempt for resale: the recoverable portion is 0, so net = gross = 100 — identical math, no special case.
10. A sale explodes the recipe as it was on the sale's own date¶
Rule. Costing a sale unpacks the recipe as it stood on the sale's business date — the textbook name is exploding a bill of materials (a bill of materials is the full ingredient breakdown of a made item). The system resolves the recipe version in force on that date, walks its tree down to raw ingredients, and pins that version's identity onto every consumption movement so the costing can always be reproduced.
WHY. A recipe edited after a sale must not retroactively change what that sale consumed or cost — historical costing has to be reproducible — so the explosion binds to the version that was active then, not the version active now.
Each recipe version is a separate, immutable record with a unique activation date; only drafts are editable. A recipe ingredient points either at an item — whose own recipe, if it has one, is resolved by the same date at explosion — or at one specific pinned version of a nested sub-recipe, frozen until the parent is re-versioned. Either way, "explode the recipe as of date D" is deterministic: the same date always yields the same tree. Activation may even be backdated (a new version's effective date must come strictly after the latest active version's, but may lie in the past) — and even then, consumption already posted is never re-exploded; the pinned version protects history.
Worked example. A pasta dish used recipe v3 (active from 2026-05-01) until v4 raised the cheese from 30 g to 40 g (active from 2026-06-01). A sale on 2026-05-20 explodes against v3 (30 g cheese) and pins v3 onto its consumption movement; a sale on 2026-06-05 explodes against v4 (40 g). Re-running May's cost of goods next year still uses v3 — the answer never moves.
11. The detail must tie out to the total at every close¶
Rule. At every close, the sum of the detail must equal the controlling total: the individual movement rows for a period, added up, must equal the period's controlling balance, within a small tolerance. Residuals below tolerance are zeroed; anything above is a hard close-blocker.
WHY. The parts must equal the whole, or the books are internally inconsistent and no period total can be trusted — the tie-out is the system catching its own arithmetic before it ships a wrong profit-and-loss.
The tolerance is a per-currency configuration default — a tenth of a currency unit out of the box: tiny money residuals and tiny quantity residuals are zeroed as rounding; anything larger blocks the close until it is explained. A sale that arrives after its period has closed is not edited back into it — that would undo a finished reconciliation — but posted as a prior-period adjustment in the next open period, preserving the closed period's tie-out. The system enumerates every blocked row (so nothing hides); the agent only diagnoses and ranks; the operator approves; the deterministic path posts.
Worked example. A period's flour detail sums to €1,402.06 while the controlling balance reads €1,402.10 — a €0.04 gap, under tolerance, so it is zeroed as rounding and the close proceeds. The next period a sold dish turns out to have no costed recipe; its detail can't be valued, so it surfaces as a named, listed close-blocker, not a silent zero — the close waits until a recipe version covering the sale's date is activated (activation may be backdated for exactly this case; posted history never re-explodes).
12. Sales deplete stock immediately, and the same sale never depletes twice¶
Rule. Stock behind a sale is depleted the instant the sale comes through the door, not deferred to a month-end batch: each sale line immediately posts storage location → customer movements, stamped with the selling restaurant (the profit center) and the warehouse the route resolved to (the dish's station as the restaurant maps it, then a per-item override, then the restaurant's default — always one of the restaurant's own warehouses) — dish lines after the recipe is exploded, raw-item lines one-for-one. And the front door is idempotent: every sale carries its source's own unforgeable id, and a repeat of the same sale is recognized and skipped, so a re-sent order never depletes stock twice.
WHY (immediate). Perpetual inventory means the book reflects reality continuously; deferring depletion to the close would leave on-hand and food cost wrong all month and would collapse the count-versus-book variance the close depends on. WHY (idempotent). Tills and delivery platforms retry and replay; without that id, one re-sent batch would deplete the same flour twice and silently corrupt cost of goods.
A sale line points at one of two things: a dish (date-correct recipe explosion, rule 10) or a stocked item sold directly (selling raw stock as-is — a 1:1 storage location → customer movement at weighted-average cost, no explosion). A comped plate is a sale at €0 — the guest transaction happened, so it posts exactly like a paid sale, cost without revenue. Consumption that was never guest-facing — a staff meal, a recipe test — is internal consumption, its own document, and its destination comes from its consumption category: an eaten category posts to the customer side (a person ate the food), a used-up category posts to scrap (the ingredients were consumed by the process, not eaten by a guest); same movement shape, the category carries the difference. A sale that names a dish the system can't recognize is quarantined — held, unposted, and flagged for someone to map the name to a real dish — never silently costed at zero; a sold dish with no costed recipe on its date is a close-blocker, not a €0 line.
Worked example. A till sends order #4417 (2× pasta). On arrival it is recorded
once under the source id 4417; the two pasta lines explode (date-correct) into leaf
consumption movements posted immediately, stamped with the selling restaurant and the
routed consumption warehouse. The till retries the same batch an hour later: id 4417 already
exists, so it is skipped. On-hand reflects the sale once, in real time.
13. The book never refuses a fact¶
Rule. A document that records something that already happened — a sale, a transfer, a write-off, a production batch — posts no matter what the book says is on hand. The issue is valued at the pool's own implied price (the running average, or the deficit price a negative pool already carries; the latest receipt stands in when the pool has no price at all), and the pool may run negative between reconciliations. The guard sits at the close, not the door: a warehouse whose book is negative will not close until the missing receipt, or the count that explains the gap, has arrived.
WHY. The book is a claim assembled from delayed, imperfect paperwork — receipts post days late, journals are filled by different hands — while the person holding the goods is reporting reality. Refusing the record puts nothing back in the walk-in; it only hides the fact and makes two books wrong at once (the source still claims stock that left, the destination never learns what arrived). A recorded deficit is a visible debt the next receipt or count settles.
The same tolerance runs at every grain beneath the pool. The lot detail attributes an issue to the lots it can still see and lets the remainder pass unattributed; a storage location inside the warehouse may read negative when moves outran the paperwork. Each heals at its own reconciliation: the pool by the lifting receipt or the count, the location by the count's relocation, the lot by the receipt that names it.
Worked example. The book says 6 kg of flour at the commissary; the cook has already carried 10 kg to the bar store and records the transfer. It posts: the commissary's pool goes to −4 kg at its €0.85 average, the bar store re-blends +10 kg at €0.85. Tomorrow the overdue 25 kg receipt posts and lifts the deficit — or the next count finds where the paper went wrong. Had the record been refused, the commissary would still claim 6 kg it does not have, and the bar store would be selling flour its book never received.
14. Food attributes never touch on-hand or value¶
Rule. Nutrition, allergens, and shelf-life post no stock movement and never change on-hand or weighted-average cost. The journal meets food safety at exactly one point — a tracked lot's expiry date (rule 17).
WHY. "What is this food, and how long does it keep?" and "how much do I have, and what did it cost?" are different domains; coupling them is how a system grows a nutrition-staleness bug and a fridge-only shelf-life afterthought — keeping food attributes off the journal keeps both sides correct.
Food attributes ride the same recipe-tree roll-up that costing uses (rule 16), not the journal of movements.
15. A made item keeps no longer than its shortest-lived component¶
Rule. The advisory shelf-life cap of a made item is the minimum over its recipe-tree components of their remaining life — the most-restrictive-component rule — and it is computed on the shared roll-up, not merely asserted in prose.
WHY. A dish cannot be safe for longer than its fastest-spoiling sub-recipe; a long-lived dressing must not extend a short-lived cooked-protein component — so the cap has to be derived from the tree, not declared.
Worked example. A grain bowl uses a cooked-chicken sub-recipe good for 3 days and a
dressing sub-recipe good for 7. The bowl's advisory cap is min(3, 7) = 3 days. If
the chicken sub-recipe's own recipe changes and its cap drops to 2, the next roll-up
drops the bowl to 2 — through the very same invalidation that refreshes nutrition
(rule 16).
16. The recipe-tree roll-ups share one explosion and one invalidation¶
Rule. Cost, nutrition, allergens, and the shelf-life cap are all produced by one walk of the recipe tree and refreshed by one ancestor-invalidation; no attribute gets a private recompute path of its own. Each computed profile stamps its provenance — its source, the recipe version it was computed from, and when.
WHY. A second, separate recompute path is exactly how nutrition goes stale — it fails to fire when a child sub-recipe changes, and a dish shows calories from an old version of a component. One spine plus one invalidation makes a "one attribute stale while the others are fresh" state structurally impossible, and the version stamp makes any staleness detectable.
17. Tracked lots are consumed earliest-expiry-first, and expiry = arrival + shelf-life¶
Rule. A lot-tracked batch's expiry is its arrival or production date plus its shelf-life for the item's default storage condition (the use-by date is the hard one), stamped when the goods are received. Consumption issues the earliest-expiring lot first — FEFO (first-expiry-first-out), not first-in-first-out. A lapsed lot leaves as an ordinary waste write-off, tagged expired.
WHY. Fungible food needs no per-lot cost layers — a warehouse runs one blended weighted-average — but it does need expiry-correct issue order and recall traceability; FEFO on a lot's expiry delivers both, without the per-layer cost fiction of first-in-first-out.
This is the operational half of "shelf-life": the lot, its expiry, the order stock is issued in, and the write-off when a batch lapses. Quantity and value still live at the movement level (one blended weighted-average); the lot, its expiry, and its traceability live one layer down.
Worked example. Two flour lots: Lot-A expires the 5th, Lot-B the 12th. A 10 kg issue draws Lot-A first (FEFO) — say 6 kg from Lot-A, 4 kg from Lot-B — emptying the soonest-to-expire batch first. Whatever is left in Lot-A past the 5th is past its use-by and leaves as an ordinary spoilage write-off, reason expired, valued at the warehouse's weighted-average cost.
18. Food-safety rules are data, not buried numbers¶
Rule. The shelf-life policy — which combinations of risk flags map to how many days, each rule citing its food-safety source — is versioned, shared data keyed by jurisdiction, never numbers hard-baked into the logic. And the language model that reads a recipe is forbidden from emitting a day number: it only assigns risk flags; the deterministic policy table turns flags into days.
WHY. Food codes differ by region and change over time; baking them in forces a code change to tune a single day-cap and blocks a second jurisdiction outright. Keeping the language model out of the number keeps the safety math auditable and testable — the same two-stage discipline costing uses to keep the model out of the money math.
Worked example. A cooked-chicken sub-recipe is classified with the flags cooked-protein and rapidly-chilled; the model never says "4 days." The policy table — first-match-wins, most-restrictive-first — maps that flag combination to a recommended and a maximum number of days, each rule citing its source. A jurisdiction with a stricter code ships a different version of the table; not a line of logic changes.
See also¶
- Item model & policies — the stable policy fields every behavior routes off, and why a category name must never decide accounting (rules 6, 7).
- Units of measure — the base unit, conversion at the posting edge, and base-unit immutability (rule 4).
- Costing & valuation — weighted-average cost, the landed-cost waterfall, net-versus-gross value, and quantity/value decoupling (rules 8, 9, 11).
- Scope & locations — company, warehouse, storage location, restaurant, and channel; the warehouse↔restaurant many-to-many; commissary supply by transfer; value-versus-profit grain; sales routing (rules 5, 12).
- Procurement — the pre-journal purchase order, the three-way match on the gross total, and the stocked gate that reorder pars filter on.
- Glossary — the kept vocabulary: recipe version and activation date, goods receipt, unit, base unit, restaurant, company-scoping.
- Stock ledger — the double-entry journal of movements, on-hand as a sum, correct-by-contra, valuation entries, and virtual locations (rules 1, 2, 8).
- Recipes & production — date-correct bill-of-materials explosion, prep items, emergent edible-portion yield, and the production consume-and-yield pair (rules 7, 10).
- Food attributes — the recipe-tree roll-up of nutrition/allergens/shelf-life and the lot-expiry → FEFO → expiry-write-off seam (rules 14–18).
- Period close — count-wins, posting differences, cycle counts, the opening count, the close self-check, deterministic blockers, and prior-period adjustments (rules 3, 11).