You cannot report your way out of a wrong number. — A case study
A physical movement becomes a record, in the same motion.
Before
The record was made after the work, from memory, in a batch at the end of a shift. Batch numbers and serials — the details that make the position meaningful — were exactly the details hardest to recall an hour later, so they were the first things approximated and the first things wrong.
Now
Recording is a by-product of the movement rather than a task that follows it. The batch and the serial are captured at the point of contact, when the item is physically in someone's hands and the label is right there, because that is the only moment the information is free to collect.
- Capture at the point of contact, not at the end of the shift
- Batch and serial recorded while the item is in hand
- The floor tool kept deliberately thin, so it is never the slow option
- A movement that physically happened is always recordable
A pile of movements becomes a position.
Before
"How many do we have" returned one number, and when it was wrong there was no way to find out where it went wrong. A count that disagreed with the system was resolved by overwriting the system, which fixed the display and destroyed the only evidence of what had actually happened.
Now
The position is built from the movements, so it can be asked properly: how many, in which batches, expiring when, and which specific serials. When a physical count disagrees, the difference is recorded as its own event with a reason attached rather than silently corrected — which turns the discrepancies from noise into the most useful data in the system.
- Position derived from the movement history, by batch and by serial
- A count is a set of identified units, not a single total
- A discrepancy is an event with a reason, never a silent overwrite
- Expiring stock allocated first, with overrides recorded rather than blocked
A position becomes a number somebody will sign.
Before
Reports were assembled from the current balance, which meant every one of them was a claim about now, made from a figure that was wrong in an unknown direction. Asking what the stock had been worth at the end of last month was not a query — it was an archaeology project, and its answer was a negotiation.
Now
Because the position is reconstructed from movements, a report can be asked as at a date rather than only as of now — month-end valuation included. Management reads the same records the floor is writing rather than a copy that is a day behind, and the discrepancy report is the one that pays for the system: it says where the process is leaking, not merely that it is.
- Reports as at any date, rebuilt from the movement history
- Management and floor read one dataset, not an operational store and a stale mirror
- Discrepancies reported by reason, location and pattern
- Expiry exposed as its own report, before the stock is written off
The brief was reporting: management needed figures they could act on. The obstacle was that the figures would have been confidently wrong, because the recorded stock position and the physical shelf had not agreed for a long time. So the reporting system had to begin at the other end — with how a movement gets recorded on the floor — because a dashboard built over bad counts does not surface a problem, it launders one.
Management wanted reports, and they were right to. What they could not do was act on them. Every figure the existing process produced was a summary of a stock position that nobody trusted, and the people who knew it was wrong were the ones being asked to sign off decisions made from it.
The counts drifted for an ordinary reason. Recording a movement was a separate act from making it — you moved the stock, and then, later, when there was a moment, you wrote down what you had moved. Later means from memory, in bulk, at the end of a shift, by someone who has moved a hundred other things since. That is not carelessness; it is what any process produces when the record is a second job rather than a by-product of the first.
Underneath that sat a harder problem the summaries were hiding. This stock is tracked in batches with expiry dates and, separately, by individual serial number — so "how many do we have" is not a question with a number for an answer. Two hundred units, of which some expire next month and some next year, and each of which is individually identified, is four or five different operational facts wearing one total. A report that gives the total has answered a question nobody was actually asking.
And the two groups using the system wanted opposite things. Floor staff need recording to be nearly instant, because every additional second is another reason to do it later and another source of drift. Management needs depth — by batch, by expiry, by serial, as at a date. Building one interface that serves both is the standard way these systems fail: either the floor tool grows heavy enough to be worked around, or the reports stay too shallow to be worth reading.
The decisions
What was chosen, why it was chosen, and what it cost.
Two interfaces over one dataset: a thin one for the floor, a deep one for management.
The two audiences want opposite things, and the usual compromise fails in both directions — a floor tool heavy enough to satisfy management gets worked around, and a report shallow enough to build from a simple floor tool is not worth reading. Splitting the surfaces lets each be genuinely good at its own job.
The cost: Two front ends to build and keep in step, and a permanent standing temptation to add just one more field to the floor tool. Every one of those would be individually reasonable and collectively fatal, so the thinness has to be defended as a feature rather than treated as a stage.
The floor tool does not block a movement it believes is impossible.
This is the cause of the original problem, not merely a nicety. When the system refuses to record what somebody is physically holding, they do not stop moving the stock — they stop telling the system, and the record starts drifting from reality in a way nothing will detect. The shelf is the authority; the software's disagreement is information, not a veto.
The cost: Impossible states can now be recorded — negative stock, a serial in two places — and the system has to represent and resolve them rather than assume they cannot exist. Considerably more work than rejecting the input, and it is the work that makes the data honest.
A discrepancy is an event with a reason. Corrections never overwrite silently.
When a count disagrees with the system, the difference is the single most valuable thing the warehouse produces — it is the process telling you where it leaks. Overwriting the balance fixes the number on the screen and throws away the only evidence of what happened.
The cost: It costs time at exactly the tedious moment, and it asks floor staff to attribute a cause they often genuinely do not know. "Unknown" had to be a first-class, blame-free answer, or the reasons become noise invented to clear a form.
A count is a set of identified units, not a total.
With batches and serials, a single number is a summary that conceals the question people actually have. Forty units expiring next week and forty expiring next year are not eighty of anything you can plan with, and a serialised unit is not interchangeable with its neighbour by definition.
The cost: Every screen now has to decide which summary it is showing and say so, and the honest answer is longer than the convenient one. Some views are slower to read than a single big number would have been — which is the correct trade, but it is a trade.
Allocation is first-expired-first-out, and an override is recorded rather than forbidden.
With dated stock the natural policy is not first-in-first-out but first-expired-first-out, or you write off the back of the shelf while shipping the front. But real operations have real exceptions, and a policy that cannot be overridden is one that gets defeated physically instead.
The cost: Overrides have to be recorded, reportable and unembarrassing to use, which means accepting that the policy will be broken and designing for it. A system that only supports the ideal path is one whose data stops describing the actual warehouse.
Reports are answerable as at a date, not only as of now.
A month-end valuation is a claim about a moment that has already passed, and a current balance cannot make it. Keeping the movements means any historical position can be rebuilt, and a figure signed in March still reconciles in September.
The cost: Reconstructing a position over a long history is expensive, so it needs periodic snapshots to read forward from — and snapshots are a second representation of the same truth, which has to be rebuildable and verifiable against the log rather than trusted.
Management reads the operational data, not a nightly copy of it.
A reporting mirror that is a day behind produces the specific failure this engagement existed to end: two numbers, both defensible, neither agreeing, and a meeting about which is right. One dataset means the disagreement is impossible rather than merely discouraged.
The cost: Reporting load lands on the store the floor depends on, so a heavy query is now an operational risk. Read paths have to be shaped and indexed deliberately, and the expensive reports have to be prevented from being run casually at the busiest hour.
Recording is a by-product of the movement, never a task after it.
Every second between the physical act and the record is a chance for the record not to happen, and any delay at all means it is written from memory. Batch numbers and serials are precisely the details memory loses first, and they are the details the whole reporting layer depends on.
The cost: It makes the system dependent on labelling and hardware being right on the floor — an unreadable label or a flat scanner battery is now an operational stoppage rather than an inconvenience. That dependency has to be owned by the warehouse, not assumed by the software.
Built on
- Next.js
- Both surfaces — the thin floor tool and the management reporting.
- Supabase
- Postgres, auth, and the access rules that separate the floor from the reports.
- PostgreSQL
- The movement log, batches, serials, snapshots and the derived position.
- TypeScript
- Shared across capture, position and reporting, so a movement means one thing throughout.
Talks to
- Scheduled reports & exports
- Finance
- Purchasing
What changed
- The recorded position and the physical shelf are reconciled continuously, not at a count
- "How many do we have" is answerable by batch, by expiry and by serial
- Recording happens as the stock moves, rather than from memory afterwards
- A discrepancy is an event with a reason, so the pattern of them is reportable
- The floor tool never blocks a movement that physically happened
- Management reads the same records the floor is writing, not a stale copy
- A valuation can be produced as at a date, not only as of now
- Expiring stock is allocated first, and an override is recorded rather than prevented
A dashboard over bad data is worse than no dashboard.
Without it, people know they are guessing and act accordingly. With it, they are just as wrong and considerably more confident. If there is a number your team quietly works around, tell us what you would do differently if you trusted it — and you leave the first conversation with a scope, a budget and a ship date.
