[{"data":1,"prerenderedAt":2318},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python":2283,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python":2292,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python":2297,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python":2298},{"id":4,"title":5,"body":6,"dateModified":2249,"datePublished":2249,"description":2250,"extension":2251,"faq":2252,"keyword":2261,"kind":2262,"meta":2263,"navigation":197,"path":2272,"seo":2273,"slug":2276,"stem":2277,"topics":2278,"type":2281,"__hash__":2282},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python\u002Findex.md","Calculate FIFO Inventory Valuation with Python",{"type":7,"value":8,"toc":2234},"minimark",[9,19,24,56,62,162,166,169,373,376,380,391,920,1089,1092,1096,1099,1223,1227,1230,1469,1472,1578,1582,1585,1731,1734,1738,1741,1863,1866,1870,1873,2098,2102,2109,2113,2148,2152,2162,2166,2172,2178,2184,2190,2194,2230],[10,11,12,13,18],"p",{},"FIFO — first in, first out — assumes the oldest stock is sold first, so each sale is costed at the price of the earliest purchases still on hand, and the stock left at the end is valued at the most recent prices. It is simple to state and awkward in a spreadsheet: one sale can use up the tail of one purchase and part of the next, and every later cost depends on what earlier sales consumed. In Python it is a queue per product. This page is part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl",[35,54,55],{"class":45}," xlsxwriter\n",[10,57,58,61],{},[32,59,60],{},"stock_moves_july.xlsx"," lists the stock movements for July 2026 for two products: the opening stock with its cost, purchases with their unit cost, and sales with quantities only — the cost of a sale is what FIFO has to work out. In practice the purchases come from the purchase ledger and the sales from the sales or shipping system; combine them into one table of dated movements first.",[63,64,72,73,72,77,72,81,72,89,72,99,72,110,72,117,72,122,72,127,72,133,72,138,72,142,72,147,72,153,72,158],"svg",{"viewBox":65,"role":66,"ariaLabelledBy":67,"xmlns":70,"style":71},"64 0 632 331","img",[68,69],"ff-layers-t","ff-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:632px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[74,75,76],"title",{"id":68},"Cost layers for P-100, oldest at the bottom",[78,79,80],"desc",{"id":69},"Three layers of stock for product P-100. The opening stock of 120 at 4.10 is at the bottom and is consumed first, then the 6 July purchase of 200 at 4.35, then the 20 July purchase of 150 at 4.60 on top. Sales take units from the bottom layer upwards.",[82,83],"rect",{"x":84,"y":85,"width":86,"height":87,"fill":88},"64","0","632","331","#ffffff",[82,90],{"x":91,"y":92,"width":93,"height":94,"rx":95,"fill":96,"stroke":97,"style":98},"80.0","16.0","600.0","93.0","12","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[100,101,103],"g",{"transform":102},"translate(368.0,29.0) scale(1.000)",[104,105],"path",{"d":106,"fill":107,"stroke":108,"style":109},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","none","var(--gold-ink,#7a4e06)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[111,112,116],"text",{"x":113,"y":114,"style":115},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","20 Jul: 150 @ 4.60",[111,118,121],{"x":113,"y":119,"style":120},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","consumed last",[82,123],{"x":91,"y":124,"width":93,"height":94,"rx":95,"fill":125,"stroke":126,"style":98},"119.0","#d9f4f1","var(--teal,#0f9488)",[100,128,130],{"transform":129},"translate(368.0,132.0) scale(1.000)",[104,131],{"d":106,"fill":107,"stroke":132,"style":109},"var(--teal-ink,#0b6157)",[111,134,137],{"x":113,"y":135,"style":136},"178.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","6 Jul: 200 @ 4.35",[111,139,141],{"x":113,"y":140,"style":120},"198.0","consumed second",[82,143],{"x":91,"y":144,"width":93,"height":94,"rx":95,"fill":145,"stroke":146,"style":98},"222.0","#f0f4ff","var(--brand,#5b5cf0)",[100,148,150],{"transform":149},"translate(368.0,235.0) scale(1.000)",[104,151],{"d":106,"fill":107,"stroke":152,"style":109},"var(--brand-strong,#4338ca)",[111,154,157],{"x":113,"y":155,"style":156},"281.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Opening: 120 @ 4.10",[111,159,161],{"x":113,"y":160,"style":120},"301.0","consumed first",[20,163,165],{"id":164},"step-1-order-the-movements","Step 1 — Order the movements",[10,167,168],{},"FIFO depends on sequence, so sort by date, and within a day put receipts before sales, so that goods received in the morning are available to an afternoon sale. That is a policy choice; stating it in code makes it visible:",[25,170,174],{"className":171,"code":172,"language":173,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nmoves = pd.read_excel(\"stock_moves_july.xlsx\", parse_dates=[\"Date\"])\norder = {\"Opening\": 0, \"Purchase\": 1, \"Sale\": 2}\nmoves = moves.sort_values([\"Date\", \"Type\"], key=lambda s: s.map(order) if s.name == \"Type\" else s,\n                          kind=\"stable\").reset_index(drop=True)\nprint(moves.head(5).to_string(index=False))\n","python",[32,175,176,192,199,232,275,322,347],{"__ignoreMap":30},[35,177,178,182,186,189],{"class":37,"line":38},[35,179,181],{"class":180},"s-kum","import",[35,183,185],{"class":184},"skGVy"," pandas ",[35,187,188],{"class":180},"as",[35,190,191],{"class":184}," pd\n",[35,193,195],{"class":37,"line":194},2,[35,196,198],{"emptyLinePlaceholder":197},true,"\n",[35,200,202,205,208,211,214,217,221,223,226,229],{"class":37,"line":201},3,[35,203,204],{"class":184},"moves ",[35,206,207],{"class":180},"=",[35,209,210],{"class":184}," pd.read_excel(",[35,212,213],{"class":45},"\"stock_moves_july.xlsx\"",[35,215,216],{"class":184},", ",[35,218,220],{"class":219},"sa561","parse_dates",[35,222,207],{"class":180},[35,224,225],{"class":184},"[",[35,227,228],{"class":45},"\"Date\"",[35,230,231],{"class":184},"])\n",[35,233,235,238,240,243,246,249,252,254,257,259,262,264,267,269,272],{"class":37,"line":234},4,[35,236,237],{"class":184},"order ",[35,239,207],{"class":180},[35,241,242],{"class":184}," {",[35,244,245],{"class":45},"\"Opening\"",[35,247,248],{"class":184},": ",[35,250,85],{"class":251},"sP0c6",[35,253,216],{"class":184},[35,255,256],{"class":45},"\"Purchase\"",[35,258,248],{"class":184},[35,260,261],{"class":251},"1",[35,263,216],{"class":184},[35,265,266],{"class":45},"\"Sale\"",[35,268,248],{"class":184},[35,270,271],{"class":251},"2",[35,273,274],{"class":184},"}\n",[35,276,278,280,282,285,287,289,292,295,298,301,304,307,310,313,316,319],{"class":37,"line":277},5,[35,279,204],{"class":184},[35,281,207],{"class":180},[35,283,284],{"class":184}," moves.sort_values([",[35,286,228],{"class":45},[35,288,216],{"class":184},[35,290,291],{"class":45},"\"Type\"",[35,293,294],{"class":184},"], ",[35,296,297],{"class":219},"key",[35,299,300],{"class":180},"=lambda",[35,302,303],{"class":184}," s: s.map(order) ",[35,305,306],{"class":180},"if",[35,308,309],{"class":184}," s.name ",[35,311,312],{"class":180},"==",[35,314,315],{"class":45}," \"Type\"",[35,317,318],{"class":180}," else",[35,320,321],{"class":184}," s,\n",[35,323,325,328,330,333,336,339,341,344],{"class":37,"line":324},6,[35,326,327],{"class":219},"                          kind",[35,329,207],{"class":180},[35,331,332],{"class":45},"\"stable\"",[35,334,335],{"class":184},").reset_index(",[35,337,338],{"class":219},"drop",[35,340,207],{"class":180},[35,342,343],{"class":251},"True",[35,345,346],{"class":184},")\n",[35,348,350,353,356,359,362,365,367,370],{"class":37,"line":349},7,[35,351,352],{"class":251},"print",[35,354,355],{"class":184},"(moves.head(",[35,357,358],{"class":251},"5",[35,360,361],{"class":184},").to_string(",[35,363,364],{"class":219},"index",[35,366,207],{"class":180},[35,368,369],{"class":251},"False",[35,371,372],{"class":184},"))\n",[10,374,375],{},"A stable sort keeps the original order of movements with the same date and type, which matters when two sales happen on one day.",[20,377,379],{"id":378},"step-2-consume-layers-with-a-queue","Step 2 — Consume layers with a queue",[10,381,382,383,386,387,390],{},"Each SKU gets a ",[32,384,385],{},"deque"," of cost layers, ",[32,388,389],{},"[quantity, unit_cost]",". A receipt appends a layer at the back; a sale takes units from the front until its quantity is covered, splitting a layer if it only needs part of it. Each sale's cost is the sum over the layers it touched:",[25,392,394],{"className":171,"code":393,"language":173,"meta":30,"style":30},"from collections import deque, defaultdict\n\nclass NegativeStock(Exception):\n    pass\n\ndef fifo(moves):\n    layers = defaultdict(deque)\n    costed = []\n    for m in moves.itertuples():\n        stack = layers[m.SKU]\n        if m.Type in (\"Opening\", \"Purchase\"):\n            stack.append([m.Qty, m.UnitCost])\n            continue\n        need, cost, used = m.Qty, 0.0, []\n        while need > 0:\n            if not stack:\n                raise NegativeStock(f\"{m.Date:%Y-%m-%d} {m.SKU}: sale of {m.Qty} exceeds stock by {need}\")\n            qty, unit = stack[0]\n            take = min(qty, need)\n            cost += take * unit\n            used.append(f\"{take}@{unit:.2f}\")\n            need -= take\n            if take == qty:\n                stack.popleft()\n            else:\n                stack[0][0] -= take\n        costed.append({\"Date\": m.Date, \"SKU\": m.SKU, \"Qty\": m.Qty, \"COGS\": round(cost, 2),\n                       \"UnitCOGS\": round(cost \u002F m.Qty, 4), \"Layers used\": \" + \".join(used)})\n    return pd.DataFrame(costed), layers\n\nsales, layers = fifo(moves)\nprint(sales.to_string(index=False))\n",[32,395,396,409,413,430,435,439,451,461,472,487,504,526,532,538,555,573,585,662,677,691,709,743,755,767,773,781,801,844,879,888,893,904],{"__ignoreMap":30},[35,397,398,401,404,406],{"class":37,"line":38},[35,399,400],{"class":180},"from",[35,402,403],{"class":184}," collections ",[35,405,181],{"class":180},[35,407,408],{"class":184}," deque, defaultdict\n",[35,410,411],{"class":37,"line":194},[35,412,198],{"emptyLinePlaceholder":197},[35,414,415,418,421,424,427],{"class":37,"line":201},[35,416,417],{"class":180},"class",[35,419,420],{"class":41}," NegativeStock",[35,422,423],{"class":184},"(",[35,425,426],{"class":251},"Exception",[35,428,429],{"class":184},"):\n",[35,431,432],{"class":37,"line":234},[35,433,434],{"class":180},"    pass\n",[35,436,437],{"class":37,"line":277},[35,438,198],{"emptyLinePlaceholder":197},[35,440,441,444,448],{"class":37,"line":324},[35,442,443],{"class":180},"def",[35,445,447],{"class":446},"s_Opv"," fifo",[35,449,450],{"class":184},"(moves):\n",[35,452,453,456,458],{"class":37,"line":349},[35,454,455],{"class":184},"    layers ",[35,457,207],{"class":180},[35,459,460],{"class":184}," defaultdict(deque)\n",[35,462,464,467,469],{"class":37,"line":463},8,[35,465,466],{"class":184},"    costed ",[35,468,207],{"class":180},[35,470,471],{"class":184}," []\n",[35,473,475,478,481,484],{"class":37,"line":474},9,[35,476,477],{"class":180},"    for",[35,479,480],{"class":184}," m ",[35,482,483],{"class":180},"in",[35,485,486],{"class":184}," moves.itertuples():\n",[35,488,490,493,495,498,501],{"class":37,"line":489},10,[35,491,492],{"class":184},"        stack ",[35,494,207],{"class":180},[35,496,497],{"class":184}," layers[m.",[35,499,500],{"class":251},"SKU",[35,502,503],{"class":184},"]\n",[35,505,507,510,513,515,518,520,522,524],{"class":37,"line":506},11,[35,508,509],{"class":180},"        if",[35,511,512],{"class":184}," m.Type ",[35,514,483],{"class":180},[35,516,517],{"class":184}," (",[35,519,245],{"class":45},[35,521,216],{"class":184},[35,523,256],{"class":45},[35,525,429],{"class":184},[35,527,529],{"class":37,"line":528},12,[35,530,531],{"class":184},"            stack.append([m.Qty, m.UnitCost])\n",[35,533,535],{"class":37,"line":534},13,[35,536,537],{"class":180},"            continue\n",[35,539,541,544,546,549,552],{"class":37,"line":540},14,[35,542,543],{"class":184},"        need, cost, used ",[35,545,207],{"class":180},[35,547,548],{"class":184}," m.Qty, ",[35,550,551],{"class":251},"0.0",[35,553,554],{"class":184},", []\n",[35,556,558,561,564,567,570],{"class":37,"line":557},15,[35,559,560],{"class":180},"        while",[35,562,563],{"class":184}," need ",[35,565,566],{"class":180},">",[35,568,569],{"class":251}," 0",[35,571,572],{"class":184},":\n",[35,574,576,579,582],{"class":37,"line":575},16,[35,577,578],{"class":180},"            if",[35,580,581],{"class":180}," not",[35,583,584],{"class":184}," stack:\n",[35,586,588,591,594,597,600,604,607,610,613,616,618,621,623,626,629,631,634,636,638,641,643,646,648,651,653,656,658,660],{"class":37,"line":587},17,[35,589,590],{"class":180},"                raise",[35,592,593],{"class":184}," NegativeStock(",[35,595,596],{"class":180},"f",[35,598,599],{"class":45},"\"",[35,601,603],{"class":602},"sSjpA","{",[35,605,606],{"class":184},"m.Date",[35,608,609],{"class":180},":",[35,611,612],{"class":184},"%",[35,614,615],{"class":180},"Y-",[35,617,612],{"class":184},[35,619,620],{"class":180},"m-",[35,622,612],{"class":184},[35,624,625],{"class":180},"d",[35,627,628],{"class":602},"}",[35,630,242],{"class":602},[35,632,633],{"class":184},"m.",[35,635,500],{"class":251},[35,637,628],{"class":602},[35,639,640],{"class":45},": sale of ",[35,642,603],{"class":602},[35,644,645],{"class":184},"m.Qty",[35,647,628],{"class":602},[35,649,650],{"class":45}," exceeds stock by ",[35,652,603],{"class":602},[35,654,655],{"class":184},"need",[35,657,628],{"class":602},[35,659,599],{"class":45},[35,661,346],{"class":184},[35,663,665,668,670,673,675],{"class":37,"line":664},18,[35,666,667],{"class":184},"            qty, unit ",[35,669,207],{"class":180},[35,671,672],{"class":184}," stack[",[35,674,85],{"class":251},[35,676,503],{"class":184},[35,678,680,683,685,688],{"class":37,"line":679},19,[35,681,682],{"class":184},"            take ",[35,684,207],{"class":180},[35,686,687],{"class":251}," min",[35,689,690],{"class":184},"(qty, need)\n",[35,692,694,697,700,703,706],{"class":37,"line":693},20,[35,695,696],{"class":184},"            cost ",[35,698,699],{"class":180},"+=",[35,701,702],{"class":184}," take ",[35,704,705],{"class":180},"*",[35,707,708],{"class":184}," unit\n",[35,710,712,715,717,719,721,724,726,729,731,734,737,739,741],{"class":37,"line":711},21,[35,713,714],{"class":184},"            used.append(",[35,716,596],{"class":180},[35,718,599],{"class":45},[35,720,603],{"class":602},[35,722,723],{"class":184},"take",[35,725,628],{"class":602},[35,727,728],{"class":45},"@",[35,730,603],{"class":602},[35,732,733],{"class":184},"unit",[35,735,736],{"class":180},":.2f",[35,738,628],{"class":602},[35,740,599],{"class":45},[35,742,346],{"class":184},[35,744,746,749,752],{"class":37,"line":745},22,[35,747,748],{"class":184},"            need ",[35,750,751],{"class":180},"-=",[35,753,754],{"class":184}," take\n",[35,756,758,760,762,764],{"class":37,"line":757},23,[35,759,578],{"class":180},[35,761,702],{"class":184},[35,763,312],{"class":180},[35,765,766],{"class":184}," qty:\n",[35,768,770],{"class":37,"line":769},24,[35,771,772],{"class":184},"                stack.popleft()\n",[35,774,776,779],{"class":37,"line":775},25,[35,777,778],{"class":180},"            else",[35,780,572],{"class":184},[35,782,784,787,789,792,794,797,799],{"class":37,"line":783},26,[35,785,786],{"class":184},"                stack[",[35,788,85],{"class":251},[35,790,791],{"class":184},"][",[35,793,85],{"class":251},[35,795,796],{"class":184},"] ",[35,798,751],{"class":180},[35,800,754],{"class":184},[35,802,804,807,809,812,815,818,820,822,825,828,831,833,836,839,841],{"class":37,"line":803},27,[35,805,806],{"class":184},"        costed.append({",[35,808,228],{"class":45},[35,810,811],{"class":184},": m.Date, ",[35,813,814],{"class":45},"\"SKU\"",[35,816,817],{"class":184},": m.",[35,819,500],{"class":251},[35,821,216],{"class":184},[35,823,824],{"class":45},"\"Qty\"",[35,826,827],{"class":184},": m.Qty, ",[35,829,830],{"class":45},"\"COGS\"",[35,832,248],{"class":184},[35,834,835],{"class":251},"round",[35,837,838],{"class":184},"(cost, ",[35,840,271],{"class":251},[35,842,843],{"class":184},"),\n",[35,845,847,850,852,854,857,860,862,865,868,871,873,876],{"class":37,"line":846},28,[35,848,849],{"class":45},"                       \"UnitCOGS\"",[35,851,248],{"class":184},[35,853,835],{"class":251},[35,855,856],{"class":184},"(cost ",[35,858,859],{"class":180},"\u002F",[35,861,548],{"class":184},[35,863,864],{"class":251},"4",[35,866,867],{"class":184},"), ",[35,869,870],{"class":45},"\"Layers used\"",[35,872,248],{"class":184},[35,874,875],{"class":45},"\" + \"",[35,877,878],{"class":184},".join(used)})\n",[35,880,882,885],{"class":37,"line":881},29,[35,883,884],{"class":180},"    return",[35,886,887],{"class":184}," pd.DataFrame(costed), layers\n",[35,889,891],{"class":37,"line":890},30,[35,892,198],{"emptyLinePlaceholder":197},[35,894,896,899,901],{"class":37,"line":895},31,[35,897,898],{"class":184},"sales, layers ",[35,900,207],{"class":180},[35,902,903],{"class":184}," fifo(moves)\n",[35,905,907,909,912,914,916,918],{"class":37,"line":906},32,[35,908,352],{"class":251},[35,910,911],{"class":184},"(sales.to_string(",[35,913,364],{"class":219},[35,915,207],{"class":180},[35,917,369],{"class":251},[35,919,372],{"class":184},[63,921,72,927,72,930,72,933,72,938,72,945,72,951,72,955,72,959,72,963,72,967,72,970,72,975,72,977,72,980,72,982,72,985,72,990,72,995,72,997,72,1000,72,1002,72,1005,72,1009,72,1012,72,1016,72,1019,72,1023,72,1026,72,1028,72,1030,72,1032,72,1035,72,1037,72,1039,72,1041,72,1045,72,1047,72,1050,72,1052,72,1055,72,1061,72,1065,72,1070,72,1075,72,1083],{"viewBox":922,"role":66,"ariaLabelledBy":923,"xmlns":70,"style":926},"104 0 551 140",[924,925],"ff-split-t","ff-split-d","width:100%;max-width:551px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[74,928,929],{"id":924},"One sale, two layers",[78,931,932],{"id":925},"On the left, the P-100 layers before the 10 July sale, 50 units left at 4.10 and 200 at 4.35. The sale of 90 takes the 50 at 4.10 and 40 at 4.35. On the right, the layers after the sale, 160 at 4.35, with the sale costed at 379.00.",[82,934],{"x":935,"y":85,"width":936,"height":937,"fill":88},"104","551","140",[82,939],{"x":940,"y":941,"width":942,"height":943,"rx":85,"fill":145,"stroke":146,"style":944},"120.5","40.0","71.9","28.0","stroke-width:1.5px",[111,946,950],{"x":947,"y":948,"style":949},"156.4","59.0","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Layer",[82,952],{"x":953,"y":941,"width":954,"height":943,"rx":85,"fill":145,"stroke":146,"style":944},"192.4","46.8",[111,956,958],{"x":957,"y":948,"style":949},"215.8","Qty",[82,960],{"x":961,"y":941,"width":962,"height":943,"rx":85,"fill":145,"stroke":146,"style":944},"239.2","50.3",[111,964,966],{"x":965,"y":948,"style":949},"264.4","Cost",[82,968],{"x":940,"y":969,"width":942,"height":943,"rx":85,"fill":96,"stroke":97,"style":944},"68.0",[111,971,974],{"x":947,"y":972,"style":973},"87.0","font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Opening",[82,976],{"x":953,"y":969,"width":954,"height":943,"rx":85,"fill":96,"stroke":97,"style":944},[111,978,979],{"x":957,"y":972,"style":973},"50",[82,981],{"x":961,"y":969,"width":962,"height":943,"rx":85,"fill":96,"stroke":97,"style":944},[111,983,984],{"x":965,"y":972,"style":973},"4.10",[82,986],{"x":940,"y":987,"width":942,"height":943,"rx":85,"fill":88,"stroke":988,"style":989},"96.0","var(--line,#cdd5e6)","stroke-width:1px",[111,991,994],{"x":947,"y":992,"style":993},"115.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","6 Jul",[82,996],{"x":953,"y":987,"width":954,"height":943,"rx":85,"fill":96,"stroke":97,"style":944},[111,998,999],{"x":957,"y":992,"style":973},"200",[82,1001],{"x":961,"y":987,"width":962,"height":943,"rx":85,"fill":88,"stroke":988,"style":989},[111,1003,1004],{"x":965,"y":992,"style":993},"4.35",[82,1006],{"x":1007,"y":941,"width":1008,"height":943,"rx":85,"fill":145,"stroke":146,"style":944},"445.4","84.7",[111,1010,950],{"x":1011,"y":948,"style":949},"487.8",[82,1013],{"x":1014,"y":941,"width":1015,"height":943,"rx":85,"fill":145,"stroke":146,"style":944},"530.1","44.3",[111,1017,958],{"x":1018,"y":948,"style":949},"552.3",[82,1020],{"x":1021,"y":941,"width":1022,"height":943,"rx":85,"fill":145,"stroke":146,"style":944},"574.4","65.1",[111,1024,966],{"x":1025,"y":948,"style":949},"606.9",[82,1027],{"x":1007,"y":969,"width":1008,"height":943,"rx":85,"fill":88,"stroke":988,"style":989},[111,1029,994],{"x":1011,"y":972,"style":993},[82,1031],{"x":1014,"y":969,"width":1015,"height":943,"rx":85,"fill":88,"stroke":988,"style":989},[111,1033,1034],{"x":1018,"y":972,"style":993},"160",[82,1036],{"x":1021,"y":969,"width":1022,"height":943,"rx":85,"fill":88,"stroke":988,"style":989},[111,1038,1004],{"x":1025,"y":972,"style":993},[82,1040],{"x":1007,"y":987,"width":1008,"height":943,"rx":85,"fill":125,"stroke":126,"style":944},[111,1042,1044],{"x":1011,"y":992,"style":1043},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Sale COGS",[82,1046],{"x":1014,"y":987,"width":1015,"height":943,"rx":85,"fill":125,"stroke":126,"style":944},[111,1048,1049],{"x":1018,"y":992,"style":1043},"90",[82,1051],{"x":1021,"y":987,"width":1022,"height":943,"rx":85,"fill":125,"stroke":126,"style":944},[111,1053,1054],{"x":1025,"y":992,"style":1043},"379.00",[111,1056,1060],{"x":1057,"y":1058,"style":1059},"205.0","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before 10 Jul",[111,1062,1064],{"x":1063,"y":1058,"style":1059},"542.5","after 10 Jul",[37,1066],{"x1":1067,"y1":1068,"x2":1069,"y2":1068,"stroke":146,"style":98},"299.6","82.0","431.4",[1071,1072],"polygon",{"points":1073,"fill":1074},"435.4,82.0 426.4,87.0 426.4,77.0","#5b5cf0",[82,1076],{"x":1077,"y":1078,"width":1079,"height":1080,"rx":1081,"fill":88,"stroke":988,"style":1082},"321.1","49.0","92.7","22.0","11","stroke-width:1.2px",[111,1084,1088],{"x":1085,"y":1086,"style":1087},"367.5","64.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","sale of 90",[10,1090,1091],{},"Look at the P-100 sale on 10 July: 90 units, of which 50 came from the last of the opening stock at 4.10 and 40 from the 6 July purchase at 4.35, so its unit cost is between the two. That split is the whole point of FIFO, and the \"Layers used\" column makes it auditable.",[20,1093,1095],{"id":1094},"step-3-value-the-closing-stock","Step 3 — Value the closing stock",[10,1097,1098],{},"Whatever layers are left at the end of the period are the closing stock, valued at their own costs, which under FIFO are the most recent ones:",[25,1100,1102],{"className":171,"code":1101,"language":173,"meta":30,"style":30},"closing = pd.DataFrame(\n    [(sku, qty, unit, round(qty * unit, 2)) for sku, stack in layers.items() for qty, unit in stack],\n    columns=[\"SKU\", \"Qty\", \"UnitCost\", \"Value\"],\n)\nprint(closing.to_string(index=False))\nprint(closing.groupby(\"SKU\")[[\"Qty\", \"Value\"]].sum().to_string())\n",[32,1103,1104,1114,1155,1183,1187,1202],{"__ignoreMap":30},[35,1105,1106,1109,1111],{"class":37,"line":38},[35,1107,1108],{"class":184},"closing ",[35,1110,207],{"class":180},[35,1112,1113],{"class":184}," pd.DataFrame(\n",[35,1115,1116,1119,1121,1124,1126,1129,1131,1134,1137,1140,1142,1145,1147,1150,1152],{"class":37,"line":194},[35,1117,1118],{"class":184},"    [(sku, qty, unit, ",[35,1120,835],{"class":251},[35,1122,1123],{"class":184},"(qty ",[35,1125,705],{"class":180},[35,1127,1128],{"class":184}," unit, ",[35,1130,271],{"class":251},[35,1132,1133],{"class":184},")) ",[35,1135,1136],{"class":180},"for",[35,1138,1139],{"class":184}," sku, stack ",[35,1141,483],{"class":180},[35,1143,1144],{"class":184}," layers.items() ",[35,1146,1136],{"class":180},[35,1148,1149],{"class":184}," qty, unit ",[35,1151,483],{"class":180},[35,1153,1154],{"class":184}," stack],\n",[35,1156,1157,1160,1162,1164,1166,1168,1170,1172,1175,1177,1180],{"class":37,"line":201},[35,1158,1159],{"class":219},"    columns",[35,1161,207],{"class":180},[35,1163,225],{"class":184},[35,1165,814],{"class":45},[35,1167,216],{"class":184},[35,1169,824],{"class":45},[35,1171,216],{"class":184},[35,1173,1174],{"class":45},"\"UnitCost\"",[35,1176,216],{"class":184},[35,1178,1179],{"class":45},"\"Value\"",[35,1181,1182],{"class":184},"],\n",[35,1184,1185],{"class":37,"line":234},[35,1186,346],{"class":184},[35,1188,1189,1191,1194,1196,1198,1200],{"class":37,"line":277},[35,1190,352],{"class":251},[35,1192,1193],{"class":184},"(closing.to_string(",[35,1195,364],{"class":219},[35,1197,207],{"class":180},[35,1199,369],{"class":251},[35,1201,372],{"class":184},[35,1203,1204,1206,1209,1211,1214,1216,1218,1220],{"class":37,"line":324},[35,1205,352],{"class":251},[35,1207,1208],{"class":184},"(closing.groupby(",[35,1210,814],{"class":45},[35,1212,1213],{"class":184},")[[",[35,1215,824],{"class":45},[35,1217,216],{"class":184},[35,1219,1179],{"class":45},[35,1221,1222],{"class":184},"]].sum().to_string())\n",[20,1224,1226],{"id":1225},"step-4-reconcile","Step 4 — Reconcile",[10,1228,1229],{},"Cost is conserved: everything that came in, opening stock plus purchases, either went out as cost of goods sold or is still on the shelf. Check it per SKU, and check quantities the same way:",[25,1231,1233],{"className":171,"code":1232,"language":173,"meta":30,"style":30},"receipts = moves[moves[\"Type\"] != \"Sale\"].assign(Value=lambda d: d[\"Qty\"] * d[\"UnitCost\"])\ncheck = pd.DataFrame({\n    \"In value\": receipts.groupby(\"SKU\")[\"Value\"].sum(),\n    \"COGS\": sales.groupby(\"SKU\")[\"COGS\"].sum(),\n    \"Closing value\": closing.groupby(\"SKU\")[\"Value\"].sum(),\n})\ncheck[\"Difference\"] = (check[\"In value\"] - check[\"COGS\"] - check[\"Closing value\"]).round(2)\nqty_in = receipts.groupby(\"SKU\")[\"Qty\"].sum()\nqty_out = sales.groupby(\"SKU\")[\"Qty\"].sum()\ncheck[\"Qty check\"] = qty_in - qty_out == closing.groupby(\"SKU\")[\"Qty\"].sum()\nprint(check.round(2).to_string())\n",[32,1234,1235,1279,1289,1307,1323,1339,1344,1388,1407,1425,1457],{"__ignoreMap":30},[35,1236,1237,1240,1242,1245,1247,1249,1252,1255,1258,1261,1263,1266,1268,1270,1272,1275,1277],{"class":37,"line":38},[35,1238,1239],{"class":184},"receipts ",[35,1241,207],{"class":180},[35,1243,1244],{"class":184}," moves[moves[",[35,1246,291],{"class":45},[35,1248,796],{"class":184},[35,1250,1251],{"class":180},"!=",[35,1253,1254],{"class":45}," \"Sale\"",[35,1256,1257],{"class":184},"].assign(",[35,1259,1260],{"class":219},"Value",[35,1262,300],{"class":180},[35,1264,1265],{"class":184}," d: d[",[35,1267,824],{"class":45},[35,1269,796],{"class":184},[35,1271,705],{"class":180},[35,1273,1274],{"class":184}," d[",[35,1276,1174],{"class":45},[35,1278,231],{"class":184},[35,1280,1281,1284,1286],{"class":37,"line":194},[35,1282,1283],{"class":184},"check ",[35,1285,207],{"class":180},[35,1287,1288],{"class":184}," pd.DataFrame({\n",[35,1290,1291,1294,1297,1299,1302,1304],{"class":37,"line":201},[35,1292,1293],{"class":45},"    \"In value\"",[35,1295,1296],{"class":184},": receipts.groupby(",[35,1298,814],{"class":45},[35,1300,1301],{"class":184},")[",[35,1303,1179],{"class":45},[35,1305,1306],{"class":184},"].sum(),\n",[35,1308,1309,1312,1315,1317,1319,1321],{"class":37,"line":234},[35,1310,1311],{"class":45},"    \"COGS\"",[35,1313,1314],{"class":184},": sales.groupby(",[35,1316,814],{"class":45},[35,1318,1301],{"class":184},[35,1320,830],{"class":45},[35,1322,1306],{"class":184},[35,1324,1325,1328,1331,1333,1335,1337],{"class":37,"line":277},[35,1326,1327],{"class":45},"    \"Closing value\"",[35,1329,1330],{"class":184},": closing.groupby(",[35,1332,814],{"class":45},[35,1334,1301],{"class":184},[35,1336,1179],{"class":45},[35,1338,1306],{"class":184},[35,1340,1341],{"class":37,"line":324},[35,1342,1343],{"class":184},"})\n",[35,1345,1346,1349,1352,1354,1356,1359,1362,1364,1367,1370,1372,1374,1376,1378,1381,1384,1386],{"class":37,"line":349},[35,1347,1348],{"class":184},"check[",[35,1350,1351],{"class":45},"\"Difference\"",[35,1353,796],{"class":184},[35,1355,207],{"class":180},[35,1357,1358],{"class":184}," (check[",[35,1360,1361],{"class":45},"\"In value\"",[35,1363,796],{"class":184},[35,1365,1366],{"class":180},"-",[35,1368,1369],{"class":184}," check[",[35,1371,830],{"class":45},[35,1373,796],{"class":184},[35,1375,1366],{"class":180},[35,1377,1369],{"class":184},[35,1379,1380],{"class":45},"\"Closing value\"",[35,1382,1383],{"class":184},"]).round(",[35,1385,271],{"class":251},[35,1387,346],{"class":184},[35,1389,1390,1393,1395,1398,1400,1402,1404],{"class":37,"line":463},[35,1391,1392],{"class":184},"qty_in ",[35,1394,207],{"class":180},[35,1396,1397],{"class":184}," receipts.groupby(",[35,1399,814],{"class":45},[35,1401,1301],{"class":184},[35,1403,824],{"class":45},[35,1405,1406],{"class":184},"].sum()\n",[35,1408,1409,1412,1414,1417,1419,1421,1423],{"class":37,"line":474},[35,1410,1411],{"class":184},"qty_out ",[35,1413,207],{"class":180},[35,1415,1416],{"class":184}," sales.groupby(",[35,1418,814],{"class":45},[35,1420,1301],{"class":184},[35,1422,824],{"class":45},[35,1424,1406],{"class":184},[35,1426,1427,1429,1432,1434,1436,1439,1441,1444,1446,1449,1451,1453,1455],{"class":37,"line":489},[35,1428,1348],{"class":184},[35,1430,1431],{"class":45},"\"Qty check\"",[35,1433,796],{"class":184},[35,1435,207],{"class":180},[35,1437,1438],{"class":184}," qty_in ",[35,1440,1366],{"class":180},[35,1442,1443],{"class":184}," qty_out ",[35,1445,312],{"class":180},[35,1447,1448],{"class":184}," closing.groupby(",[35,1450,814],{"class":45},[35,1452,1301],{"class":184},[35,1454,824],{"class":45},[35,1456,1406],{"class":184},[35,1458,1459,1461,1464,1466],{"class":37,"line":506},[35,1460,352],{"class":251},[35,1462,1463],{"class":184},"(check.round(",[35,1465,271],{"class":251},[35,1467,1468],{"class":184},").to_string())\n",[10,1470,1471],{},"A difference other than rounding means a movement was skipped or double counted. Rounding each sale's cost to cents can leave a cent or two; if the ledger must match exactly, keep full precision and round only for display.",[63,1473,72,1479,72,1482,72,1485,72,1489,72,1494,72,1500,72,1505,72,1508,72,1513,72,1516,72,1519,72,1525,72,1529,72,1532,72,1536,72,1539,72,1542,72,1548,72,1552,72,1555,72,1559,72,1562,72,1565,72,1571,72,1575],{"viewBox":1474,"role":66,"ariaLabelledBy":1475,"xmlns":70,"style":1478},"4 0 752 125",[1476,1477],"ff-rec-t","ff-rec-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[74,1480,1481],{"id":1476},"Cost in equals cost out plus stock",[78,1483,1484],{"id":1477},"Opening stock value plus purchases equals cost of goods sold plus closing stock value. The reconciliation checks this per SKU; any difference beyond rounding means a missing or duplicated movement.",[82,1486],{"x":864,"y":85,"width":1487,"height":1488,"fill":88},"752","125",[82,1490],{"x":1491,"y":92,"width":1492,"height":94,"rx":95,"fill":1493,"stroke":988,"style":98},"20.0","153.0","#e7ebef",[100,1495,1497],{"transform":1496},"translate(84.5,29.0) scale(1.000)",[104,1498],{"d":106,"fill":107,"stroke":1499,"style":109},"var(--text,#172033)",[111,1501,1504],{"x":1502,"y":114,"style":1503},"96.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Opening value",[111,1506,1507],{"x":1502,"y":119,"style":120},"layers at start",[37,1509],{"x1":1510,"y1":1511,"x2":1512,"y2":1511,"stroke":146,"style":98},"178.0","62.5","200.0",[1071,1514],{"points":1515,"fill":1074},"204.0,62.5 195.0,67.5 195.0,57.5",[82,1517],{"x":1518,"y":92,"width":1492,"height":94,"rx":95,"fill":125,"stroke":126,"style":98},"209.0",[100,1520,1522],{"transform":1521},"translate(273.5,29.0) scale(1.000)",[104,1523],{"d":1524,"fill":107,"stroke":132,"style":109},"M6 3h9l4 4v14H6z M15 3v4h4",[111,1526,1528],{"x":1527,"y":114,"style":136},"285.5","+ Purchases",[111,1530,1531],{"x":1527,"y":119,"style":120},"new layers",[37,1533],{"x1":1534,"y1":1511,"x2":1535,"y2":1511,"stroke":146,"style":98},"367.0","389.0",[1071,1537],{"points":1538,"fill":1074},"393.0,62.5 384.0,67.5 384.0,57.5",[82,1540],{"x":1541,"y":92,"width":1492,"height":94,"rx":95,"fill":145,"stroke":146,"style":98},"398.0",[100,1543,1545],{"transform":1544},"translate(462.5,29.0) scale(1.000)",[104,1546],{"d":1547,"fill":107,"stroke":152,"style":109},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8",[111,1549,1551],{"x":1550,"y":114,"style":156},"474.5","− COGS",[111,1553,1554],{"x":1550,"y":119,"style":120},"layers consumed",[37,1556],{"x1":1557,"y1":1511,"x2":1558,"y2":1511,"stroke":146,"style":98},"556.0","578.0",[1071,1560],{"points":1561,"fill":1074},"582.0,62.5 573.0,67.5 573.0,57.5",[82,1563],{"x":1564,"y":92,"width":1492,"height":94,"rx":95,"fill":96,"stroke":97,"style":98},"587.0",[100,1566,1568],{"transform":1567},"translate(651.5,29.0) scale(1.000)",[104,1569],{"d":1570,"fill":107,"stroke":108,"style":109},"M5 12.5l4.5 4.5L19 7",[111,1572,1574],{"x":1573,"y":114,"style":115},"663.5","= Closing value",[111,1576,1577],{"x":1573,"y":119,"style":120},"layers left",[20,1579,1581],{"id":1580},"step-5-when-sales-exceed-stock","Step 5 — When sales exceed stock",[10,1583,1584],{},"In real data a sale sometimes appears before the receipt that covered it: a goods-received note keyed a day late, or a return processed as a negative purchase. FIFO cannot cost units that are not there, so the function stops and names the movement. Try it by moving a purchase after the sale that needs it:",[25,1586,1588],{"className":171,"code":1587,"language":173,"meta":30,"style":30},"broken = moves.copy()\nbroken.loc[(broken[\"SKU\"] == \"P-200\") & (broken[\"Date\"] == \"2026-07-14\"), \"Date\"] = pd.Timestamp(\"2026-07-25\")\nbroken = broken.sort_values([\"Date\", \"Type\"], key=lambda s: s.map(order) if s.name == \"Type\" else s, kind=\"stable\")\ntry:\n    fifo(broken)\nexcept NegativeStock as e:\n    print(\"stopped:\", e)\n",[32,1589,1590,1600,1648,1693,1700,1705,1718],{"__ignoreMap":30},[35,1591,1592,1595,1597],{"class":37,"line":38},[35,1593,1594],{"class":184},"broken ",[35,1596,207],{"class":180},[35,1598,1599],{"class":184}," moves.copy()\n",[35,1601,1602,1605,1607,1609,1611,1614,1617,1620,1623,1625,1627,1629,1632,1634,1636,1638,1640,1643,1646],{"class":37,"line":194},[35,1603,1604],{"class":184},"broken.loc[(broken[",[35,1606,814],{"class":45},[35,1608,796],{"class":184},[35,1610,312],{"class":180},[35,1612,1613],{"class":45}," \"P-200\"",[35,1615,1616],{"class":184},") ",[35,1618,1619],{"class":180},"&",[35,1621,1622],{"class":184}," (broken[",[35,1624,228],{"class":45},[35,1626,796],{"class":184},[35,1628,312],{"class":180},[35,1630,1631],{"class":45}," \"2026-07-14\"",[35,1633,867],{"class":184},[35,1635,228],{"class":45},[35,1637,796],{"class":184},[35,1639,207],{"class":180},[35,1641,1642],{"class":184}," pd.Timestamp(",[35,1644,1645],{"class":45},"\"2026-07-25\"",[35,1647,346],{"class":184},[35,1649,1650,1652,1654,1657,1659,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1681,1684,1687,1689,1691],{"class":37,"line":201},[35,1651,1594],{"class":184},[35,1653,207],{"class":180},[35,1655,1656],{"class":184}," broken.sort_values([",[35,1658,228],{"class":45},[35,1660,216],{"class":184},[35,1662,291],{"class":45},[35,1664,294],{"class":184},[35,1666,297],{"class":219},[35,1668,300],{"class":180},[35,1670,303],{"class":184},[35,1672,306],{"class":180},[35,1674,309],{"class":184},[35,1676,312],{"class":180},[35,1678,315],{"class":45},[35,1680,318],{"class":180},[35,1682,1683],{"class":184}," s, ",[35,1685,1686],{"class":219},"kind",[35,1688,207],{"class":180},[35,1690,332],{"class":45},[35,1692,346],{"class":184},[35,1694,1695,1698],{"class":37,"line":234},[35,1696,1697],{"class":180},"try",[35,1699,572],{"class":184},[35,1701,1702],{"class":37,"line":277},[35,1703,1704],{"class":184},"    fifo(broken)\n",[35,1706,1707,1710,1713,1715],{"class":37,"line":324},[35,1708,1709],{"class":180},"except",[35,1711,1712],{"class":184}," NegativeStock ",[35,1714,188],{"class":180},[35,1716,1717],{"class":184}," e:\n",[35,1719,1720,1723,1725,1728],{"class":37,"line":349},[35,1721,1722],{"class":251},"    print",[35,1724,423],{"class":184},[35,1726,1727],{"class":45},"\"stopped:\"",[35,1729,1730],{"class":184},", e)\n",[10,1732,1733],{},"Stopping is deliberate. Guessing a cost for missing units, or letting the layer go negative, produces a valuation that looks fine and is wrong. Fix the source data, or record the policy explicitly, for example costing the shortfall at the last known cost and flagging it for review.",[20,1735,1737],{"id":1736},"step-6-compare-with-weighted-average-cost","Step 6 — Compare with weighted average cost",[10,1739,1740],{},"Many businesses report on weighted average cost instead, and the difference between the two methods is often asked for. In a periodic weighted average, every unit available in the period costs the same: total value in divided by total units in:",[25,1742,1744],{"className":171,"code":1743,"language":173,"meta":30,"style":30},"avg_cost = (receipts.groupby(\"SKU\")[\"Value\"].sum() \u002F receipts.groupby(\"SKU\")[\"Qty\"].sum())\ncompare = pd.DataFrame({\n    \"FIFO COGS\": sales.groupby(\"SKU\")[\"COGS\"].sum(),\n    \"Avg cost COGS\": qty_out * avg_cost,\n    \"FIFO closing\": closing.groupby(\"SKU\")[\"Value\"].sum(),\n    \"Avg cost closing\": (qty_in - qty_out) * avg_cost,\n}).round(2)\nprint(compare.to_string())\n",[32,1745,1746,1778,1787,1802,1815,1830,1847,1856],{"__ignoreMap":30},[35,1747,1748,1751,1753,1756,1758,1760,1762,1765,1767,1769,1771,1773,1775],{"class":37,"line":38},[35,1749,1750],{"class":184},"avg_cost ",[35,1752,207],{"class":180},[35,1754,1755],{"class":184}," (receipts.groupby(",[35,1757,814],{"class":45},[35,1759,1301],{"class":184},[35,1761,1179],{"class":45},[35,1763,1764],{"class":184},"].sum() ",[35,1766,859],{"class":180},[35,1768,1397],{"class":184},[35,1770,814],{"class":45},[35,1772,1301],{"class":184},[35,1774,824],{"class":45},[35,1776,1777],{"class":184},"].sum())\n",[35,1779,1780,1783,1785],{"class":37,"line":194},[35,1781,1782],{"class":184},"compare ",[35,1784,207],{"class":180},[35,1786,1288],{"class":184},[35,1788,1789,1792,1794,1796,1798,1800],{"class":37,"line":201},[35,1790,1791],{"class":45},"    \"FIFO COGS\"",[35,1793,1314],{"class":184},[35,1795,814],{"class":45},[35,1797,1301],{"class":184},[35,1799,830],{"class":45},[35,1801,1306],{"class":184},[35,1803,1804,1807,1810,1812],{"class":37,"line":234},[35,1805,1806],{"class":45},"    \"Avg cost COGS\"",[35,1808,1809],{"class":184},": qty_out ",[35,1811,705],{"class":180},[35,1813,1814],{"class":184}," avg_cost,\n",[35,1816,1817,1820,1822,1824,1826,1828],{"class":37,"line":277},[35,1818,1819],{"class":45},"    \"FIFO closing\"",[35,1821,1330],{"class":184},[35,1823,814],{"class":45},[35,1825,1301],{"class":184},[35,1827,1179],{"class":45},[35,1829,1306],{"class":184},[35,1831,1832,1835,1838,1840,1843,1845],{"class":37,"line":324},[35,1833,1834],{"class":45},"    \"Avg cost closing\"",[35,1836,1837],{"class":184},": (qty_in ",[35,1839,1366],{"class":180},[35,1841,1842],{"class":184}," qty_out) ",[35,1844,705],{"class":180},[35,1846,1814],{"class":184},[35,1848,1849,1852,1854],{"class":37,"line":349},[35,1850,1851],{"class":184},"}).round(",[35,1853,271],{"class":251},[35,1855,346],{"class":184},[35,1857,1858,1860],{"class":37,"line":463},[35,1859,352],{"class":251},[35,1861,1862],{"class":184},"(compare.to_string())\n",[10,1864,1865],{},"With prices rising through the month, FIFO charges the older, cheaper costs to sales and leaves the newer, dearer ones in stock, so its cost of goods sold is lower and its closing stock higher than weighted average. When prices fall, the difference reverses.",[20,1867,1869],{"id":1868},"step-7-write-the-report","Step 7 — Write the report",[10,1871,1872],{},"Three sheets: sales with their FIFO cost and the layers used, the closing layers, and the reconciliation:",[25,1874,1876],{"className":171,"code":1875,"language":173,"meta":30,"style":30},"with pd.ExcelWriter(\"fifo_valuation_2026-07.xlsx\", engine=\"xlsxwriter\") as xw:\n    sales.to_excel(xw, sheet_name=\"Sales at FIFO cost\", index=False)\n    closing.to_excel(xw, sheet_name=\"Closing layers\", index=False)\n    check.join(compare).to_excel(xw, sheet_name=\"Reconciliation\")\n    money = xw.book.add_format({\"num_format\": \"#,##0.00\"})\n    xw.sheets[\"Sales at FIFO cost\"].set_column(\"A:A\", 18, xw.book.add_format({\"num_format\": \"yyyy-mm-dd\"}))\n    xw.sheets[\"Sales at FIFO cost\"].set_column(\"D:E\", 11, money)\n    xw.sheets[\"Sales at FIFO cost\"].set_column(\"F:F\", 30)\n    xw.sheets[\"Closing layers\"].set_column(\"C:D\", 11, money)\n    xw.sheets[\"Reconciliation\"].set_column(\"A:I\", 15, money)\nprint(\"written\")\n",[32,1877,1878,1906,1929,1951,1965,1985,2016,2034,2052,2069,2087],{"__ignoreMap":30},[35,1879,1880,1883,1886,1889,1891,1894,1896,1899,1901,1903],{"class":37,"line":38},[35,1881,1882],{"class":180},"with",[35,1884,1885],{"class":184}," pd.ExcelWriter(",[35,1887,1888],{"class":45},"\"fifo_valuation_2026-07.xlsx\"",[35,1890,216],{"class":184},[35,1892,1893],{"class":219},"engine",[35,1895,207],{"class":180},[35,1897,1898],{"class":45},"\"xlsxwriter\"",[35,1900,1616],{"class":184},[35,1902,188],{"class":180},[35,1904,1905],{"class":184}," xw:\n",[35,1907,1908,1911,1914,1916,1919,1921,1923,1925,1927],{"class":37,"line":194},[35,1909,1910],{"class":184},"    sales.to_excel(xw, ",[35,1912,1913],{"class":219},"sheet_name",[35,1915,207],{"class":180},[35,1917,1918],{"class":45},"\"Sales at FIFO cost\"",[35,1920,216],{"class":184},[35,1922,364],{"class":219},[35,1924,207],{"class":180},[35,1926,369],{"class":251},[35,1928,346],{"class":184},[35,1930,1931,1934,1936,1938,1941,1943,1945,1947,1949],{"class":37,"line":201},[35,1932,1933],{"class":184},"    closing.to_excel(xw, ",[35,1935,1913],{"class":219},[35,1937,207],{"class":180},[35,1939,1940],{"class":45},"\"Closing layers\"",[35,1942,216],{"class":184},[35,1944,364],{"class":219},[35,1946,207],{"class":180},[35,1948,369],{"class":251},[35,1950,346],{"class":184},[35,1952,1953,1956,1958,1960,1963],{"class":37,"line":234},[35,1954,1955],{"class":184},"    check.join(compare).to_excel(xw, ",[35,1957,1913],{"class":219},[35,1959,207],{"class":180},[35,1961,1962],{"class":45},"\"Reconciliation\"",[35,1964,346],{"class":184},[35,1966,1967,1970,1972,1975,1978,1980,1983],{"class":37,"line":277},[35,1968,1969],{"class":184},"    money ",[35,1971,207],{"class":180},[35,1973,1974],{"class":184}," xw.book.add_format({",[35,1976,1977],{"class":45},"\"num_format\"",[35,1979,248],{"class":184},[35,1981,1982],{"class":45},"\"#,##0.00\"",[35,1984,1343],{"class":184},[35,1986,1987,1990,1992,1995,1998,2000,2003,2006,2008,2010,2013],{"class":37,"line":324},[35,1988,1989],{"class":184},"    xw.sheets[",[35,1991,1918],{"class":45},[35,1993,1994],{"class":184},"].set_column(",[35,1996,1997],{"class":45},"\"A:A\"",[35,1999,216],{"class":184},[35,2001,2002],{"class":251},"18",[35,2004,2005],{"class":184},", xw.book.add_format({",[35,2007,1977],{"class":45},[35,2009,248],{"class":184},[35,2011,2012],{"class":45},"\"yyyy-mm-dd\"",[35,2014,2015],{"class":184},"}))\n",[35,2017,2018,2020,2022,2024,2027,2029,2031],{"class":37,"line":349},[35,2019,1989],{"class":184},[35,2021,1918],{"class":45},[35,2023,1994],{"class":184},[35,2025,2026],{"class":45},"\"D:E\"",[35,2028,216],{"class":184},[35,2030,1081],{"class":251},[35,2032,2033],{"class":184},", money)\n",[35,2035,2036,2038,2040,2042,2045,2047,2050],{"class":37,"line":463},[35,2037,1989],{"class":184},[35,2039,1918],{"class":45},[35,2041,1994],{"class":184},[35,2043,2044],{"class":45},"\"F:F\"",[35,2046,216],{"class":184},[35,2048,2049],{"class":251},"30",[35,2051,346],{"class":184},[35,2053,2054,2056,2058,2060,2063,2065,2067],{"class":37,"line":474},[35,2055,1989],{"class":184},[35,2057,1940],{"class":45},[35,2059,1994],{"class":184},[35,2061,2062],{"class":45},"\"C:D\"",[35,2064,216],{"class":184},[35,2066,1081],{"class":251},[35,2068,2033],{"class":184},[35,2070,2071,2073,2075,2077,2080,2082,2085],{"class":37,"line":489},[35,2072,1989],{"class":184},[35,2074,1962],{"class":45},[35,2076,1994],{"class":184},[35,2078,2079],{"class":45},"\"A:I\"",[35,2081,216],{"class":184},[35,2083,2084],{"class":251},"15",[35,2086,2033],{"class":184},[35,2088,2089,2091,2093,2096],{"class":37,"line":506},[35,2090,352],{"class":251},[35,2092,423],{"class":184},[35,2094,2095],{"class":45},"\"written\"",[35,2097,346],{"class":184},[20,2099,2101],{"id":2100},"scaling-up","Scaling up",[10,2103,2104,2105,2108],{},"The loop processes one movement at a time, which is fine for tens of thousands of rows per month. For millions of movements, process each SKU independently — they never interact — and carry the closing layers forward as next month's opening layers, stored as a small table of ",[32,2106,2107],{},"SKU, Qty, UnitCost"," rows, so each month only processes its own movements. That table is also what the auditors will ask for.",[20,2110,2112],{"id":2111},"common-pitfalls","Common pitfalls",[2114,2115,2116,2124,2130,2136,2142],"ul",{},[2117,2118,2119,2123],"li",{},[2120,2121,2122],"strong",{},"Unsorted movements."," FIFO is sequence dependent; sort by date and a documented same-day rule.",[2117,2125,2126,2129],{},[2120,2127,2128],{},"Averaging across layers."," A sale that spans two layers is costed at each layer's price for the units taken from it, not at an average.",[2117,2131,2132,2135],{},[2120,2133,2134],{},"Negative stock."," Stop and fix the data rather than costing units that do not exist.",[2117,2137,2138,2141],{},[2120,2139,2140],{},"Returns."," A customer return re-enters stock; decide whether at its original FIFO cost or as a new layer, and apply it consistently.",[2117,2143,2144,2147],{},[2120,2145,2146],{},"Rounding each step."," Round for display only, or the reconciliation drifts by cents.",[20,2149,2151],{"id":2150},"conclusion","Conclusion",[10,2153,2154,2155,2157,2158,2161],{},"Sort movements by date with receipts before sales on the same day, keep a ",[32,2156,385],{}," of ",[32,2159,2160],{},"[qty, unit_cost]"," layers per SKU, append receipts, and consume sales from the front, splitting layers as needed and recording which layers each sale used. Value the remaining layers as closing stock, reconcile value in against COGS plus closing stock, stop on sales that exceed stock, and compare with weighted average cost when asked.",[20,2163,2165],{"id":2164},"frequently-asked-questions","Frequently asked questions",[10,2167,2168,2171],{},[2120,2169,2170],{},"How do I calculate FIFO cost of goods sold in Python?","\nKeep a queue of purchase layers per product. For each sale, take units from the oldest layer first, moving to the next when it is used up, and add up quantity times unit cost for the units taken. The sum is the sale's cost of goods sold.",[10,2173,2174,2177],{},[2120,2175,2176],{},"How is closing inventory valued under FIFO?","\nAt the cost of the layers still in the queue after all sales, which are the most recent purchases. Multiply each remaining layer's quantity by its unit cost and add them up.",[10,2179,2180,2183],{},[2120,2181,2182],{},"What is the difference between FIFO and weighted average cost?","\nFIFO costs sales at the oldest prices and leaves recent prices in stock; weighted average uses one blended cost per unit for both. When prices rise, FIFO gives lower cost of goods sold and higher closing stock.",[10,2185,2186,2189],{},[2120,2187,2188],{},"What should happen when a sale exceeds the stock on hand?","\nTreat it as a data problem: usually a receipt was recorded late. Stop the calculation and report the movement, rather than inventing a cost.",[20,2191,2193],{"id":2192},"related","Related",[2114,2195,2196,2202,2209,2216,2223],{},[2117,2197,2198,2199,2201],{},"Up one level: ",[14,2200,17],{"href":16}," — the rest of the finance reports.",[2117,2203,2204,2208],{},[14,2205,2207],{"href":2206},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-inventory-reorder-report\u002F","Build an Inventory Reorder Report"," — the quantity side of the same stock.",[2117,2210,2211,2215],{},[14,2212,2214],{"href":2213},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger\u002F","Build a Profit and Loss Statement from a General Ledger"," — where cost of sales lands.",[2117,2217,2218,2222],{},[14,2219,2221],{"href":2220},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table\u002F","Convert Multi-Currency Transactions with an FX Rate Table"," — for purchases in other currencies.",[2117,2224,2225,2229],{},[14,2226,2228],{"href":2227},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas\u002F","Running Totals and Year-Over-Year Growth in pandas"," — the simpler cumulative case.",[2231,2232,2233],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":30,"searchDepth":194,"depth":194,"links":2235},[2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248],{"id":22,"depth":194,"text":23},{"id":164,"depth":194,"text":165},{"id":378,"depth":194,"text":379},{"id":1094,"depth":194,"text":1095},{"id":1225,"depth":194,"text":1226},{"id":1580,"depth":194,"text":1581},{"id":1736,"depth":194,"text":1737},{"id":1868,"depth":194,"text":1869},{"id":2100,"depth":194,"text":2101},{"id":2111,"depth":194,"text":2112},{"id":2150,"depth":194,"text":2151},{"id":2164,"depth":194,"text":2165},{"id":2192,"depth":194,"text":2193},"2026-09-29","Value stock and cost of goods sold first in, first out from purchase and sales ledgers: consume the oldest cost layers per SKU with a deque, cost each sale across several layers, value the layers left at the period end, stop on sales that exceed stock, compare with weighted average cost, and write the results to Excel with a reconciliation.","md",[2253,2255,2257,2259],{"q":2170,"a":2254},"Keep a queue of purchase layers per product. For each sale, take units from the oldest layer first, moving to the next when it is used up, and add up quantity times unit cost for the units taken. The sum is the sale's cost of goods sold.",{"q":2176,"a":2256},"At the cost of the layers still in the queue after all sales, which are the most recent purchases. Multiply each remaining layer's quantity by its unit cost and add them up.",{"q":2182,"a":2258},"FIFO costs sales at the oldest prices and leaves recent prices in stock; weighted average uses one blended cost per unit for both. When prices rise, FIFO gives lower cost of goods sold and higher closing stock.",{"q":2188,"a":2260},"Treat it as a data problem: usually a receipt was recorded late. Stop the calculation and report the movement, rather than inventing a cost.","calculate fifo inventory valuation",null,{"breadcrumb":2264},[2265,2267,2270],{"name":2266,"item":859},"Home",{"name":2268,"item":2269},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2271,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python",{"title":2274,"description":2275},"FIFO Inventory Valuation and COGS in Python (pandas)","Calculate FIFO cost of goods sold and closing inventory value in Python: purchase layers per SKU, sales consuming the oldest layers first, partial layers, negative stock checks, weighted average comparison, and an Excel report.","calculate-fifo-inventory-valuation-with-python","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python\u002Findex",[2279,2280],"report-layouts","pandas","how-to","1t0W7Oo5yP7AyK_jXrtekN_AcO3txZ_hkdioof01_Yo",[2284,2288],{"title":2285,"path":2286,"stem":2287,"children":-1},"Build an Overtime Report from Shift Data","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data\u002Findex",{"title":2289,"path":2290,"stem":2291,"children":-1},"Calculate Weighted Average Cost Inventory Valuation","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-weighted-average-cost-inventory-valuation","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-weighted-average-cost-inventory-valuation\u002Findex",[2293,2295],{"path":2294,"title":2271},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2296,"title":2268},"\u002Fautomating-reporting-workflows",[],[2299,2303,2306,2309,2312,2315],{"path":2300,"title":2301,"kind":2302},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2304,"title":2305,"kind":2281},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report","Add a Data Dictionary Sheet to an Excel Report",{"path":2307,"title":2308,"kind":2281},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard","Add a KPI Summary Block to an Excel Dashboard",{"path":2310,"title":2311,"kind":2281},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",{"path":2313,"title":2314,"kind":2281},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-slicer-style-filters-to-an-excel-dashboard","Add a Dropdown Filter That Drives an Excel Dashboard",{"path":2316,"title":2317,"kind":2281},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","Add a Summary Sheet to an Excel Report with Python",1790972127843]