FIFO costing needs lot layers with remaining quantity and unit cost. Apps Script can allocate sales against the oldest layers.
fifoCostCalculation skips sales that already have fifoCost, sorts layers by receivedAt, and depletes qtyRemaining as it allocates.
Insufficient stock throws with the sale id so ops can fix receipts before retrying.
This is an educational model — production ERP may need average cost, negative inventory policies, and locked periods.
Need this built? Hire a Google Apps Script developer →
Sheet / project setup
| Sheet | Column | Purpose |
|---|---|---|
| CostLayers | A lotId | Layer id |
| CostLayers | B sku | Item |
| CostLayers | C qtyRemaining | Mutated |
| CostLayers | D unitCost | Layer cost |
| CostLayers | E receivedAt | FIFO order |
| Sales | A saleId | Sale key |
| Sales | B sku | Item |
| Sales | C qty | Sold qty |
| Sales | D fifoCost | Output total cost |
What this script does
Allocates sale quantity across oldest positive layers and writes total cost.
Prerequisites
CostLayers receipts entered before sales costing; matching SKUs.
- Backup before run
- Do not re-run without restoring layers
- Dates parseable
Walkthrough
Two layers cost 10 then 12; sell 3 from first; expect fifoCost 30 and layer qty reduced.
Edge cases
Already-costed sales are skipped — clear column D to recompute only after restoring layers.
- Throws if not enough qty
- Cent rounding on total
Idempotency
Because layers mutate, the function skips sales with fifoCost set. Restoring a backup is required before full recomputes.
How to test
Partial layer consumption mid-lot.
Hardening for production
Write allocation audit rows (saleId, lotId, qty, cost).
Variations
LIFO by reversing sort; weighted average by total value/qty.
Full code: fifoCostCalculation()
Load CostLayers and uncosted Sales, then run fifoCostCalculation() once per batch.
/**
* FIFO unit cost: consume oldest layers first for a sale quantity.
*/
const LAYERS = "CostLayers"; // A lotId, B sku, C qtyRemaining, D unitCost, E receivedAt
const SALES = "Sales"; // A saleId, B sku, C qty, D fifoCost (out)
function fifoCostCalculation() {
const ss = SpreadsheetApp.getActive();
const layerSheet = ss.getSheetByName(LAYERS);
const layers = layerSheet.getDataRange().getValues();
// sort copy by receivedAt ascending for each sku during consume
const salesSheet = ss.getSheetByName(SALES);
const sales = salesSheet.getDataRange().getValues();
for (let s = 1; s < sales.length; s++) {
if (sales[s][3] !== "" && sales[s][3] != null) continue; // already costed
const sku = sales[s][1];
let need = Number(sales[s][2]) || 0;
let cost = 0;
// collect indexes for sku sorted by date
const idxs = [];
for (let i = 1; i < layers.length; i++) {
if (layers[i][1] === sku && Number(layers[i][2]) > 0) idxs.push(i);
}
idxs.sort(function (a, b) {
return new Date(layers[a][4]) - new Date(layers[b][4]);
});
for (let k = 0; k < idxs.length && need > 0; k++) {
const i = idxs[k];
const avail = Number(layers[i][2]);
const take = Math.min(avail, need);
cost += take * Number(layers[i][3]);
layers[i][2] = avail - take;
need -= take;
}
if (need > 0) throw new Error("Insufficient layers for sale " + sales[s][0]);
sales[s][3] = Math.round(cost * 100) / 100;
}
layerSheet.getRange(1, 1, layers.length, layers[0].length).setValues(layers);
salesSheet.getRange(1, 1, sales.length, sales[0].length).setValues(sales);
}- Line 4: Layer quantity reduced as sales allocate.
- Line 4: Sort key for oldest-first.
- Line 17: Skips sales with fifoCost filled.
- Line 34: Partial layer consumption.
- Line 39: Hard fail when stock layers run out.
- Line 43: Persists depleted layer quantities.
Deploy this example
- 01
Open Apps Script
In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.
- 02
Paste and save
Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.
- 03
Authorize once
Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.
- 04
Add the trigger
Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.
Before you run: FIFO costing
- 1Backup CostLayers
- 2Receipts posted with dates
- 3Sales qty and sku correct
- 4Understand skip-if-costed behavior
- 5Agree rounding to cents
- 6Plan audit trail if required
Frequently asked questions
Not after layers changed. Restore CostLayers and clear fifoCost first.
Compute total value/total qty per SKU and multiply — different function.
This sample throws; some teams allow provisional cost and true-up later.
Include location in the layer filter with sku.
Add positive layers or reverse allocations carefully.
Fine for modest layer counts; index layers by sku in an object of arrays for speed.
Simple clarity; optimize by pre-grouping layers per sku once.