Two inventory signals that look useful and aren't

Lead time isn’t recoverable from the arrival side at all, which I think makes the separation less delicate than it sounds. An arrival stamp is half the measurement. The other half is the day someone placed the order, and that lives on the PO rather than in inventory, so a transfer can’t shorten a supplier lead time you’re computing correctly. It has no order date to subtract from.

Where it does bite is anything built on arrivals alone. Time from reorder point to stock landing, say. A transfer closes that in a day or two and an overseas supplier takes weeks, so mixing them plans you against a response time the supplier can’t hit.

If you’re reading raw movements with no event labels there’s a cheap test for it.

A transfer conserves. Stock leaves A and the same quantity turns up at B within a day or two, so the total across locations doesn’t move, where a supplier receipt is net new. Sum the locations and the arrivals that raise the total are the ones you can time.

Right on lead time — I was sloppy. If you’re subtracting an order date from an arrival, the order date only exists on a PO, so a transfer has nothing to pair with and can’t corrupt the number. The failure I described only exists in the arrivals-only version, which is the one you named.

The conservation test is neat. Two things survive it in the wrong direction.

In-transit duration. “The same quantity turns up at B within a day or two” holds for a van between two shops. It doesn’t for a 3PL to a store, or anything crossing a border — stock leaves A, sits in transit a fortnight, then lands. Summed across locations the total drops when it leaves and rises when it arrives, so a slow transfer reads as shrinkage followed by a supplier receipt. The window you allow for the offsetting movement is doing a lot of work.

Net new isn’t only suppliers. A customer return restocking raises the total. So does a cycle count correcting upward, or found stock after a stocktake. All net new, none of them anything you’d want inside a lead time. In apparel the returns one isn’t a rounding error.

Neither is a reason not to use it — with no event labels it’s still the cheapest thing available. But it separates conserved from net-new, which is a slightly different question from transfer versus supplier receipt.

Fair on both, and conserved versus net new is the more honest name for it. I was describing the test by what I wanted it to mean.

Returns you can get back, cheaply. A restock on a refund is tied to the order it came off, and the reconstruction is already reading orders, so it’s the one net-new event that turns up labeled. Subtract refund restocks before you sum the locations and the apparel case mostly goes away. Cycle counts and found stock don’t have that anchor, and I’d just carry them as noise.

In transit I’d fix by matching instead of by widening the window. A slow transfer still leaves a departure somewhere. Same SKU, same quantity, nothing ordered. Returns and counts and found stock give you a lone increase with nothing leaving anywhere, so instead of requiring the offsetting movement inside a day or two, look back from each arrival for an unmatched decrease of that exact quantity. Quantity is a better fingerprint than timing.

That breaks on a consolidated shipment that gets split, or received in parts, because then the quantities don’t line up at either end. At which point you’re guessing again.

On the duplicate SKU finding, there is a second version of it that is invisible from outside and worse on the inside, which is the barcode field. SKUs at least get typed by one person with one convention. Barcodes arrive from suppliers, from CSV imports, and from staff reading them off a box, and Shopify does not validate the field at all. It will store a 12 character string in a field that should hold a 13 digit EAN, and it will store the same barcode on two different variants.

Two checks I would run on a catalogue before trusting any inventory number from it. Check digit validity is the first one. A UPC or EAN has a computed last digit, so a barcode that fails the check was mistyped or truncated somewhere upstream and will not scan at a till. Duplicate barcodes across variants is the second, and that is the one that makes a scan at receiving credit the wrong line and puts the counts out with no error appearing anywhere.

Both have the same shape as your updated_at problem in one way. Nothing fails loudly. The label prints, the number looks reasonable, and you find out when a customer is standing at the counter.

@_daniel’s two checks are the right two, and I can add a third from this week, because it just cost a merchant an afternoon.

A shop with about 7,600 variants, a lot of them carrying short 8-digit codes in the barcode field. I ran the check digit on a sample: half failed EAN-8. So far, exactly your first case. But the half that passed were no better in practice, because the number printed on the label under the bars was a different one entirely, 13 digits, from the supplier’s own labelling. The stored code was an internal code that happened to be 8 digits long. A checksum can’t see that. The only thing that can is a scanner in someone’s hand, at the shelf, saying “not found”.

Which is why I think the third check isn’t a query at all. It’s a workflow: when a scan doesn’t match, let the person holding the box pick the item and save the scanned code onto it right there. One pass through the stockroom and the field is clean, and it stays clean because the next scan matches on its own. Your duplicate case needs the opposite reflex: when a code matches two lines, refuse and ask, never pick the first row. That one silently credits the wrong variant and the count looks fine.

Disclosure: I build Binly, and both of those behaviours went in this week after that exact merchant. The checksum query is still worth running first; it tells you how bad the field is before anyone walks the shelves.

One caveat to the 12-versus-13-digit example: 12 digits can be a valid UPC/GTIN-12. It is a mismatch if the supplier says that particular item has an EAN-13, but length alone should not send a mixed UPC/EAN catalog into the error bucket.

I’d separate the checks into expected identifier type, format/check digit, duplicate mapping, and physical-label match. Keep the original value as text through the CSV/spreadsheet step so leading zeroes survive. If it is an internal identifier rather than a GTIN, classify it separately; don’t change its digits just to make an EAN test pass.

That also gives staff a more useful exception list: “expected EAN-13, received 12 digits” is actionable; “bad barcode” may not be.

GS1’s explanation of GTIN formats distinguishes UPC/GTIN-12 from EAN/GTIN-13.

Disclosure: I develop CatalogDelta. This is AI-assisted data-checking guidance, not a claim to have inspected anyone’s store or labels.

Three of you have converged on something from different directions, and one of it lines up with a decision we made last week for a reason I couldn’t have articulated as well as you just did.

On refuse-and-ask: we shipped exactly that in a scan-to-receive flow last week — when a scanned code matches two lines, stop and ask rather than take the likelier one. I chose it because picking wrong is silent and picking nothing is loud, which felt right but was a hunch. “That one silently credits the wrong variant and the count looks fine” is the same conclusion with a merchant’s afternoon behind it. Two people landing on the same reflex independently is better evidence than either of us having a view.

The learn-on-scan half we don’t do, and I think it’s the better answer. We generate internal barcodes in bulk for items missing one, which fixes the blank field and does nothing about the wrong-but-present field — which, per your 7,600 variants, is the bigger problem. Bulk generation can’t know the stored 8-digit code isn’t what’s printed under the bars. A person at the shelf can.

One caution where your workflow meets CheneyCHAN’s taxonomy, because I think the failure sits between them. If a mismatched scan saves whatever was scanned, you’ll sometimes enshrine a carton code or a shelf-label code instead of the item’s GTIN — and it’s then clean-looking, unique and wrong, which is worse than blank because nothing flags it again. That wants the classification at the moment of save rather than as a later audit: is this a GTIN of the expected type or an internal identifier, and stored as the latter if so.

And the leading-zeroes point is the same bug family as one I documented on the other Stocky thread this week — a cost of 1,250.00 imports as 1, because parseFloat stops at the comma. Same shape as a barcode losing a leading zero in a spreadsheet step: the value survives, stays plausible, and is silently different. Neither errors. That’s the recurring one across all of this.