Deep Dive

How does Fishbowl cost a cycle count under FIFO?

Israel LopezMay 20, 20266 min read

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 listed layerUnitCost. If the layer was created today and layerUnitCost is 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 same inventoryLogId mean 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 layerValueImpact per 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 inventorylog alone.
  • layerUnitCost is 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.