How does Fishbowl cost a cycle count under FIFO?
A customer asked us a simple question last month: “if we do a cycle count and find some inventory we thought was lost, does Fishbowl cost it at what we originally paid?”
We were halfway through building an inventory-correction tool for them, and that question wasn’t in our spec. They run FIFO. They assumed a cycle count of “found” units would restore the cost of the original receipt. We thought that was probably right too — but we’d never specifically validated it. So we went looking.
The answer turned out to be more interesting than either of us expected, and worth writing down for anyone else on FIFO who’s about to assume the same thing we did.
This is against Fishbowl 2025.3.
Short answer: Fishbowl doesn’t restore the original receipt cost on cycle-up. It creates a new cost layer dated today, priced at the current top of the FIFO queue. That’s rarely the cost you’d expect — and the rest of this piece is about why, and what it does to your books over time.
What we expected, and what we found
We expected this: when you cycle-up a part by 5 units, those 5 units come back into stock costed at whatever they cost when you originally received them — like reaching back to the receipt that put them in your warehouse the first time.
What Fishbowl actually does: it creates a brand new cost layer, dated today, costed at the current top of the FIFO queue — which is the oldest still-open layer’s unit cost. The original receipt is not consulted. There’s no lookup, no “where did these units come from.” Just: new layer, today’s date, current top-of-FIFO price.
If your oldest open FIFO layer is $8/ea, your cycle-up adds units at $8/ea. If the part has sold completely down and there are no open layers, Fishbowl falls back to the most recent fulfilled layer’s cost — silently. That fulfilled layer could be three years old. No warning, no flag, nothing in the audit trail saying “we picked a stale cost.” It just uses it.
Cycle-down is more intuitive, but worth saying out loud: it consumes the oldest open layer first, walks forward through layers until it’s used the full quantity, and the GL impact is the real weighted cost of those consumed layers. So if your queue is [$8 × 20, then $14 × 5] and you cycle 5 units down, the GL takes a hit of $40 — not $70. That’s correct FIFO. It also surprises a lot of operators who expected the loss to reflect “what we just bought.”
The asymmetry
Same magnitude in different directions, different absolute numbers in the GL.
| Queue (oldest first) | On-hand | Cycle to | Δ | Cost selected | GL impact |
|---|---|---|---|---|---|
[$8 × 3 open, $14 × 22 open] |
25 | 30 | +5 | $8 (top-of-FIFO) | +$40 |
[$8 × 3 open, $14 × 22 open] |
25 | 20 | −5 | $8 × 3 + $14 × 2 (consumed) | −$52 |
Same count change. Different GL impact. The cycle-up grabs the top of the queue; the cycle-down walks through it. This isn’t a Fishbowl bug — it’s how layer-based costing works. But controllers don’t always expect their +5 and their −5 to post at different amounts, and the explanation only makes sense once you see the layers.
The bigger problem is what happens over time. If you cycle-up regularly to correct chronic miscounts — found units, misplaced receipts, off-paperwork stock — every adjustment adds a new layer at the current top-of-FIFO cost.
Months later, the oldest cheap layers you started with are quietly displaced by newer layers minted at higher cycle-up costs. Your inventory value drifts toward current cost regardless of when the units were physically acquired. Whether that’s good or bad depends on the story you want your books to tell, but most people don’t realize it’s happening.
“But we use lot tracking — doesn’t that fix this?”
This was the next question from our customer, and the answer is no, with one wrinkle.
Lot tracking changes how Fishbowl identifies the physical inventory (which tag holds which lot, what expiration date, etc.). It does not change how cost layers work. Cost layers are still per-part. There is no layer-per-lot. There is no link between a lot number and a specific cost layer — that link doesn’t exist anywhere in the data model.
So if you have Lot ABC123 sitting on a tag and you cycle that lot up by 5 units, Fishbowl picks up the change on the tag (lot stays correct, no problem there) but the cost layer it creates is just “this part, 5 units, current top-of-FIFO cost, today.” No lot attribution. The lot information lives on the tag; the cost information lives on the layer; the two never share a key.
The wrinkle: when you have multiple tags for the same lot — from migrations or race conditions in receiving — cycle counts pick one tag to absorb the change in a non-deterministic order. That’s a separate problem, and one we’ll write up separately. But the cost-layer answer is independent: whichever tag absorbs the qty change, the cost layer affected is the same part-wide FIFO layer.
The short version: lot tracking doesn’t protect FIFO customers from the cycle-up cost surprise. A lot-tracked FIFO customer is in exactly the same position on cost as an unlot-tracked FIFO customer.
What if we weren’t on FIFO?
Worth contrasting because some of the surprises here are FIFO-specific.
| Method | Cycle UP | Cycle DOWN | Asymmetric? |
|---|---|---|---|
| FIFO | New layer at top-of-FIFO cost | Consumes oldest layers first | Yes |
| Average | Adds qty × current avg to the pool | Removes at the current running average | No |
| Standard | At the part’s standard cost | At the part’s standard cost; variance posts separately | No |
Average and Standard don’t have the asymmetry because they don’t carry a queue of dated layers. There’s one number per part, applied in both directions. The asymmetry — and the drift toward current cost from repeated cycle-ups — is a feature of layer-based costing.
This is not a reason on its own to leave FIFO. It is one input among many if you’re already weighing a costing-method review.
What we ended up telling our customer
A few things, all of them practical:
- “You cannot get Fishbowl to restore the original receipt cost on a cycle-up via the CSV importer.” That path passes a null unit cost into the cycle operation, which forces top-of-FIFO selection. If they need a specific historical cost on a “found inventory” adjustment, they have to use the REST inventory endpoint and pass the unit cost explicitly. The CSV is insufficient.
- “If you cycle a sold-out part back up to fix a counting error, look at what cost Fishbowl picked.” The stale-fulfilled-layer fallback is the most likely place to get a misleading number, and it happens silently.
- “Repeated cycle-ups will drift your inventory value toward current cost over time.” Not a Fishbowl bug. Just a thing that happens, that nobody talks about, and that the books quietly reflect.
- “If a count matches exactly, nothing is written.” No audit row, no inventory log, no record that the count happened. If they need proof-of-cycle-counting for compliance, they need to track it outside Fishbowl — the system only records deltas.
The customer’s specific question — “will Fishbowl restore the original cost?” — turned out to be the wrong question. The right question was “what cost will Fishbowl assign to the found units, and are we okay with that being current top-of-FIFO rather than the original receipt?”
For their setup, the answer was “we’re okay with it as long as we know it’s happening.” Which is the answer for most operators we’ve worked with on this once they actually see what’s going on under the hood.
Here’s the query we use for this
If you’re on Fishbowl FIFO and any of this is news to you, the fastest sanity check is to look at what the last twelve months of cycle counts have actually done to your cost layers. The pattern shows up in the data fast — usually within a few minutes of running the query against your Fishbowl MySQL database.
This is the diagnostic query we run when we’re picking up a new Fishbowl customer on FIFO and want to know whether routine cycle counting has been quietly drifting their inventory value. It returns one row per cost layer touched by a cycle count in the last twelve months — created by a cycle-up or consumed by a cycle-down — with the dollar impact of each.
SELECT
il.id AS inventoryLogId,
il.eventDate,
p.num AS partNum,
p.description AS partDescription,
ilt.name AS eventType, -- 'adj:inc' = cycle-up; 'adj:dec' = cycle-down
il.changeQty AS eventChangeQty, -- the cycle-count delta
il.info AS note,
su.initials AS userInitials,
cl.id AS costLayerId,
cl.dateCreated AS layerCreatedAt,
cl.orgQty AS layerOrgQty,
cl.orgTotalCost AS layerOrgTotalCost,
ROUND(cl.orgTotalCost / NULLIF(cl.orgQty, 0), 6) AS layerUnitCost,
iltocl.qty AS layerQtyTouched, -- + = layer created; − = layer consumed
ROUND(iltocl.qty * (cl.orgTotalCost / NULLIF(cl.orgQty, 0)), 2)
AS layerValueImpact, -- signed $ impact on inventory value
CASE
WHEN il.typeId = 64 THEN 'CREATED (cycle-up)'
WHEN il.typeId = 65 THEN 'CONSUMED (cycle-down)'
END AS layerDirection
FROM inventorylog il
JOIN inventorylogtype ilt ON ilt.id = il.typeId
JOIN inventorylogtocostlayer iltocl ON iltocl.inventoryLogId = il.id
JOIN costlayer cl ON cl.id = iltocl.costLayerId
JOIN part p ON p.id = il.partId
LEFT JOIN sysuser su ON su.id = il.userId
WHERE il.typeId IN (64, 65)
AND il.eventDate >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
ORDER BY il.eventDate DESC, il.id, cl.id;
Read-only. Safe to run on a production Fishbowl MySQL instance.
How to read the output:
layerValueImpact > 0— that cycle-up added dollars to inventory value at the listedlayerUnitCost. If the layer was created today andlayerUnitCostis an old number, that’s the top-of-FIFO selection in action.layerValueImpact < 0— that cycle-down removed dollars at the consumed layer’s cost. Multiple rows for the sameinventoryLogIdmean the cycle-down spanned more than one layer.- Sort by
ABS(layerValueImpact)to find the largest single adjustments. These are usually the ones worth investigating manually — was this really a count discrepancy, or a process problem upstream? - Sum
layerValueImpactper part to see net drift. If it’s materially positive on a part, routine cycle counts have been inflating that part’s value; materially negative means deflating.
Two things to know before running it:
- The query excludes zero-delta cycle counts (where the count matched on-hand exactly). Those are real events but they touch no cost layer, so they don’t show up in the layer audit table. If you want a count of “we cycled and confirmed” events for compliance purposes, that’s a different query against
inventorylogalone. layerUnitCostis the layer’s creation-time unit cost. For most parts that’s the same as the unit cost at consumption. For parts with landed-cost reconciliations on the original receipt, the reconciled value is reflected — still the right number for “what did this post at.”
If the output of this query tells a “drift in one direction” story, that’s worth knowing before the next physical count. If it tells a “small, oscillating, no real pattern” story, the FIFO asymmetry isn’t moving your numbers materially and you can file it away.