Warehouse teams buy handhelds for a simple reason: the stock is not where the network is. Racking blocks Wi-Fi, cold rooms kill coverage, and a truckyard has no signal at all. So the device collects counts offline and syncs later, and that is exactly where inventory accuracy is won or lost.
The device is not the hard part. The import is.
Why batching is a design decision, not a limitation
Batch collection is a deliberate trade, and naming it honestly keeps the design sane.
- Offline collection guarantees the count completes, at the cost of arriving late.
- Late data means the ERP has moved on: receipts, sales and transfers happened between the count and the upload.
- A batch is not a snapshot of now; it is a set of observations taken over hours or days.
- The import therefore has to carry time, and it has to say what it intends: set, add, subtract or confirm.
- Systems that ignore this produce counts that are correct in the aisle and wrong in the ledger.
Three payload shapes, three different behaviours
Most import bugs come from mixing these three into one file and hoping the importer guesses.
- A count is an absolute statement: this location holds this quantity of this item at this time.
- An event is a movement: one unit moved from A to B, or was consumed, or was received.
- A delta is an adjustment: add or subtract this quantity, because the previous record was wrong.
- Counts and deltas cannot share a conflict rule, because one replaces truth and the other asserts a change.
- Choose per process: cycle counting suits counts, pick confirmations suit events, corrections suit deltas.
Land everything in staging first
The staging table is the single most valuable component in the pipeline, and it is usually skipped.
- Write every received record to staging with its raw payload, device id, operator and device timestamp.
- Validate in staging, not on the way in: a rejected record must still be visible for diagnosis.
- Never let the importer write stock directly from a file the user picked; the file is input, not authority.
- Keep staging rows after processing, with a status, so a dispute can be traced to the original scan.
- Treat staging as an inbox: unprocessed, processed, rejected, and manually resolved are distinct states.
Idempotency is the only defence against replay
A handheld that retries after a failed sync will send the same batch twice, and this is normal.
- Give every batch a deterministic identifier built from device, operator and time window.
- Give every record a record key that does not change when resent; a random UUID per attempt defeats this.
- Store the applied key in the database and reject duplicates with a clear status, not an error.
- Test the retry path deliberately: kill the sync midway, resend, and verify the stock is unchanged.
- If the pipeline cannot be idempotent, make the import a manual review step rather than an automatic write.
Conflict rules you can defend
A conflict exists when the ERP has changed since the observation, and every rule must be explicit.
- Compare the device timestamp against the last stock move for that product and location.
- If the ERP changed before the observation, accept the count, because it was measured later.
- If the ERP changed after the observation, flag it; the count may already be stale by hours.
- Never silently choose the larger or smaller value; a "most conservative wins" rule hides real losses.
- Send every flagged conflict to a short review queue with both values and the moves that caused them.
Time is the field everyone forgets
Device clocks drift, time zones differ, and a count without a timestamp cannot be reconciled.
- Store two timestamps: when the device observed the tag, and when the server received it.
- Force device time sync at the start of every session, and reject records with implausible timestamps.
- Keep the device clock in UTC and convert only for display; local time creates twice-yearly bugs.
- Use the observation time in conflict logic, and the receipt time only for audit.
- A count that arrives a week late is still data, but it should not overwrite a week of movements.
Validate before you write
The cheapest correction happens before the stock move is created.
- Check that the product, location and lot exist, and reject unknown barcodes rather than creating items.
- Check unit of measure conversions explicitly; a case-versus-piece mismatch is a silent hundredfold error.
- Check that the operator is authorised for that location, especially in multi-company setups.
- Check quantity plausibility against a configured band and route outliers to review instead of blocking.
- Record the validation result per record, so the rejection reason is available to the operator.
Tell the handheld what happened
An import that reports only a total is an import the field team will stop using.
- Return per-record status to the device on the next sync, keyed by the record key.
- Distinguish accepted, accepted with adjustment and rejected, with a one-line reason.
- Let the operator correct rejected records on the device instead of retyping them in the office.
- Show a per-session summary the supervisor can read without opening a spreadsheet.
- Watch the rejection reason distribution; a rising "unknown location" usually means a mapping problem.
A reconciliation report finance will accept
The count must be explainable, not just correct.
- Report the quantity before, the observed quantity, and the adjustment created, per line.
- Attribute each adjustment to a device, operator, session and time window.
- Separate adjustments caused by counting errors from those caused by unrecorded movements.
- Compare the value of adjustments against the period's inventory value, so drift is visible.
- Store the report immutably, because an auditor will ask for it months later.
Rollout order and what to test first
Sequence the rollout so the failures happen where they are cheap.
- Start with one location class and one device model, in shadow mode: import into staging, compare, do not write.
- Then enable writes for one product category with low value and high movement.
- Then widen to a full cycle count, keeping the manual recount as the tie-breaker.
- Test the ugly cases early: no signal, battery death mid-session, duplicate sync, wrong time zone.
- Publish a one-page operator procedure, because most "system errors" are actually sequence errors.






