[{"data":1,"prerenderedAt":1947},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup":1912,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup":1921,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup":1926,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup":1927},{"id":4,"title":5,"body":6,"dateModified":1878,"datePublished":1878,"description":1879,"extension":1880,"faq":1881,"keyword":1890,"kind":1891,"meta":1892,"navigation":97,"path":1901,"seo":1902,"slug":1905,"stem":1906,"topics":1907,"type":1910,"__hash__":1911},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup\u002Findex.md","Build a Bill of Materials Cost Rollup",{"type":7,"value":8,"toc":1866},"minimark",[9,29,34,62,65,69,473,476,590,594,871,878,882,1103,1110,1307,1311,1592,1595,1740,1744,1755,1759,1794,1798,1801,1805,1811,1817,1823,1829,1833,1862],[10,11,12,13,17,18,22,23,28],"p",{},"A manufactured product's cost is the cost of everything that goes into it, and everything that goes into its sub-assemblies, all the way down to purchased parts — plus the labour and overhead of each assembly step on the way up. That structure lives in a ",[14,15,16],"em",{},"bill of materials"," (BOM): a list of parent–component pairs with the quantity of each component per parent. ERP systems calculate the rollup, but the BOM and the costs are often exported to Excel for a pricing review, a quote or a what-if, where nested ",[19,20,21],"code",{},"SUMPRODUCT"," formulas become unmanageable beyond two levels. A short recursive function in Python handles any depth. This page reads a three-level BOM and a cost list from Excel, rolls costs up from purchased parts to finished products including scrap allowances and each assembly's own labour and overhead, checks for loops and parts with no cost, lists where each part is used, and calculates the effect of a supplier price increase on every finished product. It belongs to ",[24,25,27],"a",{"href":26},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[19,42,43],{"__ignoreMap":40},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," pandas",[44,60,61],{"class":54}," openpyxl\n",[10,63,64],{},"Two sheets: the BOM, one row per parent and component with the quantity per parent and a scrap allowance — the extra percentage consumed because some parts are lost or damaged in assembly; and the item costs, where a purchased part's cost is its buying price and an assembly's cost is the labour and overhead of assembling it. Two bicycles share wheels and bolts but differ in frame and drivetrain.",[30,66,68],{"id":67},"step-1-load-the-bom-and-check-it-before-costing","Step 1 — Load the BOM and check it before costing",[35,70,74],{"className":71,"code":72,"language":73,"meta":40,"style":40},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nbom = pd.read_excel(\"bom.xlsx\")\nitems = pd.read_excel(\"item_costs.xlsx\").set_index(\"Item\")\n\nchildren = {p: list(zip(g[\"Component\"], g[\"Qty per\"], g[\"Scrap %\"])) for p, g in bom.groupby(\"Parent\")}\nall_items = set(bom[\"Parent\"]) | set(bom[\"Component\"])\nno_cost = sorted(all_items - set(items.index))\npurchased_with_children = sorted(set(children) & set(items.index[items[\"Type\"] == \"purchased\"]))\n\ndef find_loop(node, path=()):\n    if node in path:\n        return path[path.index(node):] + (node,)\n    for comp, _, _ in children.get(node, []):\n        loop = find_loop(comp, path + (node,))\n        if loop:\n            return loop\n    return None\n\nloops = [l for l in (find_loop(p) for p in children) if l]\nprint(\"items with no cost:\", no_cost, \"| purchased items with a BOM:\", purchased_with_children, \"| loops:\", loops)\n","python",[19,75,76,92,99,117,138,143,202,233,255,296,301,319,333,348,362,378,387,396,405,410,447],{"__ignoreMap":40},[44,77,78,82,86,89],{"class":46,"line":47},[44,79,81],{"class":80},"s-kum","import",[44,83,85],{"class":84},"skGVy"," pandas ",[44,87,88],{"class":80},"as",[44,90,91],{"class":84}," pd\n",[44,93,95],{"class":46,"line":94},2,[44,96,98],{"emptyLinePlaceholder":97},true,"\n",[44,100,102,105,108,111,114],{"class":46,"line":101},3,[44,103,104],{"class":84},"bom ",[44,106,107],{"class":80},"=",[44,109,110],{"class":84}," pd.read_excel(",[44,112,113],{"class":54},"\"bom.xlsx\"",[44,115,116],{"class":84},")\n",[44,118,120,123,125,127,130,133,136],{"class":46,"line":119},4,[44,121,122],{"class":84},"items ",[44,124,107],{"class":80},[44,126,110],{"class":84},[44,128,129],{"class":54},"\"item_costs.xlsx\"",[44,131,132],{"class":84},").set_index(",[44,134,135],{"class":54},"\"Item\"",[44,137,116],{"class":84},[44,139,141],{"class":46,"line":140},5,[44,142,98],{"emptyLinePlaceholder":97},[44,144,146,149,151,154,158,161,164,167,170,173,176,178,181,184,187,190,193,196,199],{"class":46,"line":145},6,[44,147,148],{"class":84},"children ",[44,150,107],{"class":80},[44,152,153],{"class":84}," {p: ",[44,155,157],{"class":156},"sP0c6","list",[44,159,160],{"class":84},"(",[44,162,163],{"class":156},"zip",[44,165,166],{"class":84},"(g[",[44,168,169],{"class":54},"\"Component\"",[44,171,172],{"class":84},"], g[",[44,174,175],{"class":54},"\"Qty per\"",[44,177,172],{"class":84},[44,179,180],{"class":54},"\"Scrap %\"",[44,182,183],{"class":84},"])) ",[44,185,186],{"class":80},"for",[44,188,189],{"class":84}," p, g ",[44,191,192],{"class":80},"in",[44,194,195],{"class":84}," bom.groupby(",[44,197,198],{"class":54},"\"Parent\"",[44,200,201],{"class":84},")}\n",[44,203,205,208,210,213,216,218,221,224,226,228,230],{"class":46,"line":204},7,[44,206,207],{"class":84},"all_items ",[44,209,107],{"class":80},[44,211,212],{"class":156}," set",[44,214,215],{"class":84},"(bom[",[44,217,198],{"class":54},[44,219,220],{"class":84},"]) ",[44,222,223],{"class":80},"|",[44,225,212],{"class":156},[44,227,215],{"class":84},[44,229,169],{"class":54},[44,231,232],{"class":84},"])\n",[44,234,236,239,241,244,247,250,252],{"class":46,"line":235},8,[44,237,238],{"class":84},"no_cost ",[44,240,107],{"class":80},[44,242,243],{"class":156}," sorted",[44,245,246],{"class":84},"(all_items ",[44,248,249],{"class":80},"-",[44,251,212],{"class":156},[44,253,254],{"class":84},"(items.index))\n",[44,256,258,261,263,265,267,270,273,276,278,281,284,287,290,293],{"class":46,"line":257},9,[44,259,260],{"class":84},"purchased_with_children ",[44,262,107],{"class":80},[44,264,243],{"class":156},[44,266,160],{"class":84},[44,268,269],{"class":156},"set",[44,271,272],{"class":84},"(children) ",[44,274,275],{"class":80},"&",[44,277,212],{"class":156},[44,279,280],{"class":84},"(items.index[items[",[44,282,283],{"class":54},"\"Type\"",[44,285,286],{"class":84},"] ",[44,288,289],{"class":80},"==",[44,291,292],{"class":54}," \"purchased\"",[44,294,295],{"class":84},"]))\n",[44,297,299],{"class":46,"line":298},10,[44,300,98],{"emptyLinePlaceholder":97},[44,302,304,307,311,314,316],{"class":46,"line":303},11,[44,305,306],{"class":80},"def",[44,308,310],{"class":309},"s_Opv"," find_loop",[44,312,313],{"class":84},"(node, path",[44,315,107],{"class":80},[44,317,318],{"class":84},"()):\n",[44,320,322,325,328,330],{"class":46,"line":321},12,[44,323,324],{"class":80},"    if",[44,326,327],{"class":84}," node ",[44,329,192],{"class":80},[44,331,332],{"class":84}," path:\n",[44,334,336,339,342,345],{"class":46,"line":335},13,[44,337,338],{"class":80},"        return",[44,340,341],{"class":84}," path[path.index(node):] ",[44,343,344],{"class":80},"+",[44,346,347],{"class":84}," (node,)\n",[44,349,351,354,357,359],{"class":46,"line":350},14,[44,352,353],{"class":80},"    for",[44,355,356],{"class":84}," comp, _, _ ",[44,358,192],{"class":80},[44,360,361],{"class":84}," children.get(node, []):\n",[44,363,365,368,370,373,375],{"class":46,"line":364},15,[44,366,367],{"class":84},"        loop ",[44,369,107],{"class":80},[44,371,372],{"class":84}," find_loop(comp, path ",[44,374,344],{"class":80},[44,376,377],{"class":84}," (node,))\n",[44,379,381,384],{"class":46,"line":380},16,[44,382,383],{"class":80},"        if",[44,385,386],{"class":84}," loop:\n",[44,388,390,393],{"class":46,"line":389},17,[44,391,392],{"class":80},"            return",[44,394,395],{"class":84}," loop\n",[44,397,399,402],{"class":46,"line":398},18,[44,400,401],{"class":80},"    return",[44,403,404],{"class":156}," None\n",[44,406,408],{"class":46,"line":407},19,[44,409,98],{"emptyLinePlaceholder":97},[44,411,413,416,418,421,423,426,428,431,433,436,438,441,444],{"class":46,"line":412},20,[44,414,415],{"class":84},"loops ",[44,417,107],{"class":80},[44,419,420],{"class":84}," [l ",[44,422,186],{"class":80},[44,424,425],{"class":84}," l ",[44,427,192],{"class":80},[44,429,430],{"class":84}," (find_loop(p) ",[44,432,186],{"class":80},[44,434,435],{"class":84}," p ",[44,437,192],{"class":80},[44,439,440],{"class":84}," children) ",[44,442,443],{"class":80},"if",[44,445,446],{"class":84}," l]\n",[44,448,450,453,455,458,461,464,467,470],{"class":46,"line":449},21,[44,451,452],{"class":156},"print",[44,454,160],{"class":84},[44,456,457],{"class":54},"\"items with no cost:\"",[44,459,460],{"class":84},", no_cost, ",[44,462,463],{"class":54},"\"| purchased items with a BOM:\"",[44,465,466],{"class":84},", purchased_with_children, ",[44,468,469],{"class":54},"\"| loops:\"",[44,471,472],{"class":84},", loops)\n",[10,474,475],{},"Before any cost is calculated, three checks protect the result. Every item in the BOM must have a cost record; a missing one would silently count as zero and make products look cheaper than they are. An item marked as purchased should not also have components, which usually means a part has been made in-house and its type was never updated. And the BOM must contain no loops — an assembly that, through some chain of components, contains itself — because a recursive rollup would never finish. The loop search follows each parent down through its components and reports the first cycle it finds as a readable path. This BOM passes all three.",[477,478,486,487,486,491,486,495,486,503,486,513,486,520,486,525,486,529,486,534,486,539,486,544,486,548,486,552,486,555,486,560,486,565,486,569,486,573,486,576,486,581,486,586],"svg",{"viewBox":479,"role":480,"ariaLabelledBy":481,"xmlns":484,"style":485},"84 0 592 376","img",[482,483],"bom-tree-t","bom-tree-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[488,489,490],"title",{"id":482},"The standard bike's structure",[492,493,494],"desc",{"id":483},"The standard bike is made of an aluminium frame, two wheels, a drive kit and twelve bolts, plus its own assembly labour. Each wheel is a rim, 32 spokes, a hub and a tyre plus wheel-building labour. The drive kit is a chain, a crank and four bolts plus its own labour.",[496,497],"rect",{"x":498,"y":499,"width":500,"height":501,"fill":502},"84","0","592","376","#ffffff",[496,504],{"x":505,"y":506,"width":507,"height":508,"rx":509,"fill":510,"stroke":511,"style":512},"100.0","16.0","560.0","59.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[514,515,519],"text",{"x":516,"y":517,"style":518},"380.0","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","BIKE-STD",[514,521,524],{"x":516,"y":522,"style":523},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","+ £28.00 assembly",[46,526],{"x1":516,"y1":527,"x2":516,"y2":528,"stroke":511,"style":512},"80.0","102.0",[530,531],"polygon",{"points":532,"fill":533},"380.0,106.0 375.0,97.0 385.0,97.0","#5b5cf0",[496,535],{"x":505,"y":536,"width":507,"height":508,"rx":509,"fill":537,"stroke":538,"style":512},"111.0","#e7ebef","var(--line,#cdd5e6)",[514,540,543],{"x":516,"y":541,"style":542},"136.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","FRAME-AL ×1 · DRIVE-KIT ×1 · BOLT-M6 ×12",[514,545,547],{"x":516,"y":546,"style":523},"156.0","purchased and sub-assembly",[46,549],{"x1":516,"y1":550,"x2":516,"y2":551,"stroke":511,"style":512},"175.0","197.0",[530,553],{"points":554,"fill":533},"380.0,201.0 375.0,192.0 385.0,192.0",[496,556],{"x":505,"y":557,"width":507,"height":508,"rx":509,"fill":558,"stroke":559,"style":512},"206.0","#fdefd8","var(--gold,#b4740a)",[514,561,564],{"x":516,"y":562,"style":563},"231.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","WHEEL-26 ×2",[514,566,568],{"x":516,"y":567,"style":523},"251.0","+ £6.50 each to build",[46,570],{"x1":516,"y1":571,"x2":516,"y2":572,"stroke":511,"style":512},"270.0","292.0",[530,574],{"points":575,"fill":533},"380.0,296.0 375.0,287.0 385.0,287.0",[496,577],{"x":505,"y":578,"width":507,"height":508,"rx":509,"fill":579,"stroke":580,"style":512},"301.0","#d9f4f1","var(--teal,#0f9488)",[514,582,585],{"x":516,"y":583,"style":584},"326.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","RIM · SPOKE ×32 · HUB · TYRE",[514,587,589],{"x":516,"y":588,"style":523},"346.0","purchased parts",[30,591,593],{"id":592},"step-2-roll-the-costs-up","Step 2 — Roll the costs up",[35,595,597],{"className":71,"code":596,"language":73,"meta":40,"style":40},"from functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef rolled_cost(item):\n    own = float(items.loc[item, \"Cost\"])\n    if item not in children:\n        return own                                                    # a purchased part\n    materials = sum(qty * (1 + scrap) * rolled_cost(comp) for comp, qty, scrap in children[item])\n    return round(own + materials, 4)\n\nrollup = pd.DataFrame({\"Item\": sorted(children), \"Own cost\": [items.loc[i, \"Cost\"] for i in sorted(children)],\n                       \"Rolled cost\": [rolled_cost(i) for i in sorted(children)]})\nrollup[\"Materials\"] = (rollup[\"Rolled cost\"] - rollup[\"Own cost\"]).round(2)\nprint(rollup.to_string(index=False))\n",[19,598,599,612,616,634,644,662,678,689,732,752,756,799,818,853],{"__ignoreMap":40},[44,600,601,604,607,609],{"class":46,"line":47},[44,602,603],{"class":80},"from",[44,605,606],{"class":84}," functools ",[44,608,81],{"class":80},[44,610,611],{"class":84}," lru_cache\n",[44,613,614],{"class":46,"line":94},[44,615,98],{"emptyLinePlaceholder":97},[44,617,618,621,623,627,629,632],{"class":46,"line":101},[44,619,620],{"class":309},"@lru_cache",[44,622,160],{"class":84},[44,624,626],{"class":625},"sa561","maxsize",[44,628,107],{"class":80},[44,630,631],{"class":156},"None",[44,633,116],{"class":84},[44,635,636,638,641],{"class":46,"line":119},[44,637,306],{"class":80},[44,639,640],{"class":309}," rolled_cost",[44,642,643],{"class":84},"(item):\n",[44,645,646,649,651,654,657,660],{"class":46,"line":140},[44,647,648],{"class":84},"    own ",[44,650,107],{"class":80},[44,652,653],{"class":156}," float",[44,655,656],{"class":84},"(items.loc[item, ",[44,658,659],{"class":54},"\"Cost\"",[44,661,232],{"class":84},[44,663,664,666,669,672,675],{"class":46,"line":145},[44,665,324],{"class":80},[44,667,668],{"class":84}," item ",[44,670,671],{"class":80},"not",[44,673,674],{"class":80}," in",[44,676,677],{"class":84}," children:\n",[44,679,680,682,685],{"class":46,"line":204},[44,681,338],{"class":80},[44,683,684],{"class":84}," own                                                    ",[44,686,688],{"class":687},"s-wDw","# a purchased part\n",[44,690,691,694,696,699,702,705,708,711,714,717,719,722,724,727,729],{"class":46,"line":235},[44,692,693],{"class":84},"    materials ",[44,695,107],{"class":80},[44,697,698],{"class":156}," sum",[44,700,701],{"class":84},"(qty ",[44,703,704],{"class":80},"*",[44,706,707],{"class":84}," (",[44,709,710],{"class":156},"1",[44,712,713],{"class":80}," +",[44,715,716],{"class":84}," scrap) ",[44,718,704],{"class":80},[44,720,721],{"class":84}," rolled_cost(comp) ",[44,723,186],{"class":80},[44,725,726],{"class":84}," comp, qty, scrap ",[44,728,192],{"class":80},[44,730,731],{"class":84}," children[item])\n",[44,733,734,736,739,742,744,747,750],{"class":46,"line":257},[44,735,401],{"class":80},[44,737,738],{"class":156}," round",[44,740,741],{"class":84},"(own ",[44,743,344],{"class":80},[44,745,746],{"class":84}," materials, ",[44,748,749],{"class":156},"4",[44,751,116],{"class":84},[44,753,754],{"class":46,"line":298},[44,755,98],{"emptyLinePlaceholder":97},[44,757,758,761,763,766,768,771,774,777,780,783,785,787,789,792,794,796],{"class":46,"line":303},[44,759,760],{"class":84},"rollup ",[44,762,107],{"class":80},[44,764,765],{"class":84}," pd.DataFrame({",[44,767,135],{"class":54},[44,769,770],{"class":84},": ",[44,772,773],{"class":156},"sorted",[44,775,776],{"class":84},"(children), ",[44,778,779],{"class":54},"\"Own cost\"",[44,781,782],{"class":84},": [items.loc[i, ",[44,784,659],{"class":54},[44,786,286],{"class":84},[44,788,186],{"class":80},[44,790,791],{"class":84}," i ",[44,793,192],{"class":80},[44,795,243],{"class":156},[44,797,798],{"class":84},"(children)],\n",[44,800,801,804,807,809,811,813,815],{"class":46,"line":321},[44,802,803],{"class":54},"                       \"Rolled cost\"",[44,805,806],{"class":84},": [rolled_cost(i) ",[44,808,186],{"class":80},[44,810,791],{"class":84},[44,812,192],{"class":80},[44,814,243],{"class":156},[44,816,817],{"class":84},"(children)]})\n",[44,819,820,823,826,828,830,833,836,838,840,843,845,848,851],{"class":46,"line":335},[44,821,822],{"class":84},"rollup[",[44,824,825],{"class":54},"\"Materials\"",[44,827,286],{"class":84},[44,829,107],{"class":80},[44,831,832],{"class":84}," (rollup[",[44,834,835],{"class":54},"\"Rolled cost\"",[44,837,286],{"class":84},[44,839,249],{"class":80},[44,841,842],{"class":84}," rollup[",[44,844,779],{"class":54},[44,846,847],{"class":84},"]).round(",[44,849,850],{"class":156},"2",[44,852,116],{"class":84},[44,854,855,857,860,863,865,868],{"class":46,"line":350},[44,856,452],{"class":156},[44,858,859],{"class":84},"(rollup.to_string(",[44,861,862],{"class":625},"index",[44,864,107],{"class":80},[44,866,867],{"class":156},"False",[44,869,870],{"class":84},"))\n",[10,872,873,874,877],{},"The rollup is a recursive function: the cost of a purchased part is its price; the cost of an assembly is its own labour and overhead plus, for each component, the quantity per assembly, increased by the scrap allowance, times the component's own rolled cost. Because the function calls itself for each component, it works to any depth, and ",[19,875,876],{},"lru_cache"," makes sure each item is costed once even when it appears in many assemblies — the wheel, used in both bikes, is calculated a single time. The result for each assembly separates its own cost from the materials it consumes. A wheel comes to £47.93 — rim, hub, tyre and 32 spokes with a 3% allowance for broken spokes, plus £6.50 to build — and the standard bike to £220.27, of which the two wheels are £95.87, more than two fifths. The pro bike, with its carbon frame and crank, comes to £557.77.",[30,879,881],{"id":880},"step-3-where-is-each-part-used","Step 3 — Where is each part used?",[35,883,885],{"className":71,"code":884,"language":73,"meta":40,"style":40},"from collections import defaultdict\n\ndef explode(item, qty=1.0, out=None):\n    out = defaultdict(float) if out is None else out\n    for comp, per, scrap in children.get(item, []):\n        need = qty * per * (1 + scrap)\n        if comp in children:\n            explode(comp, need, out)\n        else:\n            out[comp] += need\n    return out\n\nfinished = [\"BIKE-STD\", \"BIKE-PRO\"]\nwhere_used = pd.DataFrame({f: explode(f) for f in finished}).fillna(0).round(3)\nprint(where_used.to_string())\n",[19,886,887,899,903,928,961,973,999,1010,1015,1023,1034,1040,1044,1066,1096],{"__ignoreMap":40},[44,888,889,891,894,896],{"class":46,"line":47},[44,890,603],{"class":80},[44,892,893],{"class":84}," collections ",[44,895,81],{"class":80},[44,897,898],{"class":84}," defaultdict\n",[44,900,901],{"class":46,"line":94},[44,902,98],{"emptyLinePlaceholder":97},[44,904,905,907,910,913,915,918,921,923,925],{"class":46,"line":101},[44,906,306],{"class":80},[44,908,909],{"class":309}," explode",[44,911,912],{"class":84},"(item, qty",[44,914,107],{"class":80},[44,916,917],{"class":156},"1.0",[44,919,920],{"class":84},", out",[44,922,107],{"class":80},[44,924,631],{"class":156},[44,926,927],{"class":84},"):\n",[44,929,930,933,935,938,941,944,946,949,952,955,958],{"class":46,"line":119},[44,931,932],{"class":84},"    out ",[44,934,107],{"class":80},[44,936,937],{"class":84}," defaultdict(",[44,939,940],{"class":156},"float",[44,942,943],{"class":84},") ",[44,945,443],{"class":80},[44,947,948],{"class":84}," out ",[44,950,951],{"class":80},"is",[44,953,954],{"class":156}," None",[44,956,957],{"class":80}," else",[44,959,960],{"class":84}," out\n",[44,962,963,965,968,970],{"class":46,"line":140},[44,964,353],{"class":80},[44,966,967],{"class":84}," comp, per, scrap ",[44,969,192],{"class":80},[44,971,972],{"class":84}," children.get(item, []):\n",[44,974,975,978,980,983,985,988,990,992,994,996],{"class":46,"line":145},[44,976,977],{"class":84},"        need ",[44,979,107],{"class":80},[44,981,982],{"class":84}," qty ",[44,984,704],{"class":80},[44,986,987],{"class":84}," per ",[44,989,704],{"class":80},[44,991,707],{"class":84},[44,993,710],{"class":156},[44,995,713],{"class":80},[44,997,998],{"class":84}," scrap)\n",[44,1000,1001,1003,1006,1008],{"class":46,"line":204},[44,1002,383],{"class":80},[44,1004,1005],{"class":84}," comp ",[44,1007,192],{"class":80},[44,1009,677],{"class":84},[44,1011,1012],{"class":46,"line":235},[44,1013,1014],{"class":84},"            explode(comp, need, out)\n",[44,1016,1017,1020],{"class":46,"line":257},[44,1018,1019],{"class":80},"        else",[44,1021,1022],{"class":84},":\n",[44,1024,1025,1028,1031],{"class":46,"line":298},[44,1026,1027],{"class":84},"            out[comp] ",[44,1029,1030],{"class":80},"+=",[44,1032,1033],{"class":84}," need\n",[44,1035,1036,1038],{"class":46,"line":303},[44,1037,401],{"class":80},[44,1039,960],{"class":84},[44,1041,1042],{"class":46,"line":321},[44,1043,98],{"emptyLinePlaceholder":97},[44,1045,1046,1049,1051,1054,1057,1060,1063],{"class":46,"line":335},[44,1047,1048],{"class":84},"finished ",[44,1050,107],{"class":80},[44,1052,1053],{"class":84}," [",[44,1055,1056],{"class":54},"\"BIKE-STD\"",[44,1058,1059],{"class":84},", ",[44,1061,1062],{"class":54},"\"BIKE-PRO\"",[44,1064,1065],{"class":84},"]\n",[44,1067,1068,1071,1073,1076,1078,1081,1083,1086,1088,1091,1094],{"class":46,"line":350},[44,1069,1070],{"class":84},"where_used ",[44,1072,107],{"class":80},[44,1074,1075],{"class":84}," pd.DataFrame({f: explode(f) ",[44,1077,186],{"class":80},[44,1079,1080],{"class":84}," f ",[44,1082,192],{"class":80},[44,1084,1085],{"class":84}," finished}).fillna(",[44,1087,499],{"class":156},[44,1089,1090],{"class":84},").round(",[44,1092,1093],{"class":156},"3",[44,1095,116],{"class":84},[44,1097,1098,1100],{"class":46,"line":364},[44,1099,452],{"class":156},[44,1101,1102],{"class":84},"(where_used.to_string())\n",[10,1104,1105,1106,1109],{},"The same tree read the other way gives the ",[14,1107,1108],{},"where-used"," view: for each finished product, the total quantity of every purchased part it consumes, after multiplying quantities down every level. A standard bike uses 64 spokes through its two wheels, and, through the frame fixings and the drive kit, sixteen bolts plus scrap. The table answers the questions purchasing asks — which products does this part go into, and how many do we need for a production run — and it is the input for the price-change analysis that follows.",[477,1111,486,1117,486,1120,486,1123,486,1128,486,1134,486,1138,486,1144,486,1148,486,1152,486,1156,486,1160,486,1164,486,1167,486,1170,486,1174,486,1176,486,1178,486,1180,486,1183,486,1186,486,1189,486,1191,486,1195,486,1197,486,1200,486,1202,486,1204,486,1207,486,1210,486,1212,486,1215,486,1217,486,1220,486,1222,486,1224,486,1227,486,1230,486,1232,486,1235,486,1237,486,1240,486,1242,486,1244,486,1247,486,1251,486,1253,486,1256,486,1258,486,1260,486,1262,486,1265,486,1268,486,1272,486,1274,486,1277,486,1279,486,1281,486,1283,486,1285,486,1288,486,1292,486,1294,486,1297,486,1299,486,1302,486,1304],{"viewBox":1112,"role":480,"ariaLabelledBy":1113,"xmlns":484,"style":1116},"176 0 407 252",[1114,1115],"bom-where-used-t","bom-where-used-d","width:100%;max-width:407px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[488,1118,1119],{"id":1114},"Purchased parts per finished bike",[492,1121,1122],{"id":1115},"Each bike uses two rims, hubs and tyres and 65.92 spokes including the 3 percent breakage allowance. The standard bike uses an aluminium frame and a standard crank, the pro bike a carbon frame and carbon crank. Both use 16.8 bolts including scrap and one chain.",[496,1124],{"x":1125,"y":499,"width":1126,"height":1127,"fill":502},"176","407","252",[496,1129],{"x":1130,"y":506,"width":1131,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"192.6","32.0","24.0","stroke-width:1px",[496,1135],{"x":1136,"y":506,"width":1137,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"224.6","173.8",[514,1139,1143],{"x":1140,"y":1141,"style":1142},"311.6","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[496,1145],{"x":1146,"y":506,"width":1147,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"398.5","84.2",[514,1149,1151],{"x":1150,"y":1141,"style":1142},"440.6","B",[496,1153],{"x":1154,"y":506,"width":1155,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"482.7","84.6",[514,1157,1159],{"x":1158,"y":1141,"style":1142},"525.0","C",[496,1161],{"x":1130,"y":1162,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"40.0","28.0",[514,1165,710],{"x":1166,"y":508,"style":1142},"208.6",[496,1168],{"x":1136,"y":1162,"width":1137,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},"stroke-width:1.5px",[514,1171,1173],{"x":1140,"y":508,"style":1172},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Part",[496,1175],{"x":1146,"y":1162,"width":1147,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1177,519],{"x":1150,"y":508,"style":1172},[496,1179],{"x":1154,"y":1162,"width":1155,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1181,1182],{"x":1158,"y":508,"style":1172},"BIKE-PRO",[496,1184],{"x":1130,"y":1185,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"68.0",[514,1187,850],{"x":1166,"y":1188,"style":1142},"87.0",[496,1190],{"x":1136,"y":1185,"width":1137,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1192,1194],{"x":1140,"y":1188,"style":1193},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","SPOKE",[496,1196],{"x":1146,"y":1185,"width":1147,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1198,1199],{"x":1150,"y":1188,"style":1193},"65.92",[496,1201],{"x":1154,"y":1185,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1203,1199],{"x":1158,"y":1188,"style":1193},[496,1205],{"x":1130,"y":1206,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"96.0",[514,1208,1093],{"x":1166,"y":1209,"style":1142},"115.0",[496,1211],{"x":1136,"y":1206,"width":1137,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1213,1214],{"x":1140,"y":1209,"style":1193},"BOLT-M6",[496,1216],{"x":1146,"y":1206,"width":1147,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1218,1219],{"x":1150,"y":1209,"style":1193},"16.8",[496,1221],{"x":1154,"y":1206,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1223,1219],{"x":1158,"y":1209,"style":1193},[496,1225],{"x":1130,"y":1226,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"124.0",[514,1228,749],{"x":1166,"y":1229,"style":1142},"143.0",[496,1231],{"x":1136,"y":1226,"width":1137,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1233,1234],{"x":1140,"y":1229,"style":1193},"RIM-26 \u002F HUB \u002F TYRE-26",[496,1236],{"x":1146,"y":1226,"width":1147,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1238,1239],{"x":1150,"y":1229,"style":1193},"2 each",[496,1241],{"x":1154,"y":1226,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1243,1239],{"x":1158,"y":1229,"style":1193},[496,1245],{"x":1130,"y":1246,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"152.0",[514,1248,1250],{"x":1166,"y":1249,"style":1142},"171.0","5",[496,1252],{"x":1136,"y":1246,"width":1137,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1254,1255],{"x":1140,"y":1249,"style":1193},"FRAME-AL",[496,1257],{"x":1146,"y":1246,"width":1147,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1259,710],{"x":1150,"y":1249,"style":1193},[496,1261],{"x":1154,"y":1246,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1263,1264],{"x":1158,"y":1249,"style":1193},"—",[496,1266],{"x":1130,"y":1267,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"180.0",[514,1269,1271],{"x":1166,"y":1270,"style":1142},"199.0","6",[496,1273],{"x":1136,"y":1267,"width":1137,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1275,1276],{"x":1140,"y":1270,"style":1193},"FRAME-CF",[496,1278],{"x":1146,"y":1267,"width":1147,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1280,1264],{"x":1150,"y":1270,"style":1193},[496,1282],{"x":1154,"y":1267,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1284,710],{"x":1158,"y":1270,"style":1193},[496,1286],{"x":1130,"y":1287,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},"208.0",[514,1289,1291],{"x":1166,"y":1290,"style":1142},"227.0","7",[496,1293],{"x":1136,"y":1287,"width":1137,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1295,1296],{"x":1140,"y":1290,"style":1193},"CRANK \u002F CRANK-CF",[496,1298],{"x":1146,"y":1287,"width":1147,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1300,1301],{"x":1150,"y":1290,"style":1193},"1 \u002F —",[496,1303],{"x":1154,"y":1287,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1305,1306],{"x":1158,"y":1290,"style":1193},"— \u002F 1",[30,1308,1310],{"id":1309},"step-4-what-a-supplier-price-increase-does","Step 4 — What a supplier price increase does",[35,1312,1314],{"className":71,"code":1313,"language":73,"meta":40,"style":40},"def product_costs(price_changes=None):\n    rolled_cost.cache_clear()\n    original = items[\"Cost\"].copy()\n    for item, pct in (price_changes or {}).items():\n        items.loc[item, \"Cost\"] = original[item] * (1 + pct)\n    result = {f: rolled_cost(f) for f in finished}\n    items[\"Cost\"] = original\n    rolled_cost.cache_clear()\n    return result\n\nbase = product_costs()\nscenario = product_costs({\"SPOKE\": 0.25, \"RIM-26\": 0.10, \"TYRE-26\": 0.08})\nimpact = pd.DataFrame({\"Current\": base, \"After increase\": scenario})\nimpact[\"Change\"] = impact[\"After increase\"] - impact[\"Current\"]\nimpact[\"Change %\"] = (impact[\"Change\"] \u002F impact[\"Current\"] * 100).round(2)\nprint(impact.round(2).to_string())\n",[19,1315,1316,1332,1337,1352,1370,1395,1414,1428,1432,1439,1443,1453,1494,1515,1542,1580],{"__ignoreMap":40},[44,1317,1318,1320,1323,1326,1328,1330],{"class":46,"line":47},[44,1319,306],{"class":80},[44,1321,1322],{"class":309}," product_costs",[44,1324,1325],{"class":84},"(price_changes",[44,1327,107],{"class":80},[44,1329,631],{"class":156},[44,1331,927],{"class":84},[44,1333,1334],{"class":46,"line":94},[44,1335,1336],{"class":84},"    rolled_cost.cache_clear()\n",[44,1338,1339,1342,1344,1347,1349],{"class":46,"line":101},[44,1340,1341],{"class":84},"    original ",[44,1343,107],{"class":80},[44,1345,1346],{"class":84}," items[",[44,1348,659],{"class":54},[44,1350,1351],{"class":84},"].copy()\n",[44,1353,1354,1356,1359,1361,1364,1367],{"class":46,"line":119},[44,1355,353],{"class":80},[44,1357,1358],{"class":84}," item, pct ",[44,1360,192],{"class":80},[44,1362,1363],{"class":84}," (price_changes ",[44,1365,1366],{"class":80},"or",[44,1368,1369],{"class":84}," {}).items():\n",[44,1371,1372,1375,1377,1379,1381,1384,1386,1388,1390,1392],{"class":46,"line":140},[44,1373,1374],{"class":84},"        items.loc[item, ",[44,1376,659],{"class":54},[44,1378,286],{"class":84},[44,1380,107],{"class":80},[44,1382,1383],{"class":84}," original[item] ",[44,1385,704],{"class":80},[44,1387,707],{"class":84},[44,1389,710],{"class":156},[44,1391,713],{"class":80},[44,1393,1394],{"class":84}," pct)\n",[44,1396,1397,1400,1402,1405,1407,1409,1411],{"class":46,"line":145},[44,1398,1399],{"class":84},"    result ",[44,1401,107],{"class":80},[44,1403,1404],{"class":84}," {f: rolled_cost(f) ",[44,1406,186],{"class":80},[44,1408,1080],{"class":84},[44,1410,192],{"class":80},[44,1412,1413],{"class":84}," finished}\n",[44,1415,1416,1419,1421,1423,1425],{"class":46,"line":204},[44,1417,1418],{"class":84},"    items[",[44,1420,659],{"class":54},[44,1422,286],{"class":84},[44,1424,107],{"class":80},[44,1426,1427],{"class":84}," original\n",[44,1429,1430],{"class":46,"line":235},[44,1431,1336],{"class":84},[44,1433,1434,1436],{"class":46,"line":257},[44,1435,401],{"class":80},[44,1437,1438],{"class":84}," result\n",[44,1440,1441],{"class":46,"line":298},[44,1442,98],{"emptyLinePlaceholder":97},[44,1444,1445,1448,1450],{"class":46,"line":303},[44,1446,1447],{"class":84},"base ",[44,1449,107],{"class":80},[44,1451,1452],{"class":84}," product_costs()\n",[44,1454,1455,1458,1460,1463,1466,1468,1471,1473,1476,1478,1481,1483,1486,1488,1491],{"class":46,"line":321},[44,1456,1457],{"class":84},"scenario ",[44,1459,107],{"class":80},[44,1461,1462],{"class":84}," product_costs({",[44,1464,1465],{"class":54},"\"SPOKE\"",[44,1467,770],{"class":84},[44,1469,1470],{"class":156},"0.25",[44,1472,1059],{"class":84},[44,1474,1475],{"class":54},"\"RIM-26\"",[44,1477,770],{"class":84},[44,1479,1480],{"class":156},"0.10",[44,1482,1059],{"class":84},[44,1484,1485],{"class":54},"\"TYRE-26\"",[44,1487,770],{"class":84},[44,1489,1490],{"class":156},"0.08",[44,1492,1493],{"class":84},"})\n",[44,1495,1496,1499,1501,1503,1506,1509,1512],{"class":46,"line":335},[44,1497,1498],{"class":84},"impact ",[44,1500,107],{"class":80},[44,1502,765],{"class":84},[44,1504,1505],{"class":54},"\"Current\"",[44,1507,1508],{"class":84},": base, ",[44,1510,1511],{"class":54},"\"After increase\"",[44,1513,1514],{"class":84},": scenario})\n",[44,1516,1517,1520,1523,1525,1527,1530,1532,1534,1536,1538,1540],{"class":46,"line":350},[44,1518,1519],{"class":84},"impact[",[44,1521,1522],{"class":54},"\"Change\"",[44,1524,286],{"class":84},[44,1526,107],{"class":80},[44,1528,1529],{"class":84}," impact[",[44,1531,1511],{"class":54},[44,1533,286],{"class":84},[44,1535,249],{"class":80},[44,1537,1529],{"class":84},[44,1539,1505],{"class":54},[44,1541,1065],{"class":84},[44,1543,1544,1546,1549,1551,1553,1556,1558,1560,1563,1565,1567,1569,1571,1574,1576,1578],{"class":46,"line":364},[44,1545,1519],{"class":84},[44,1547,1548],{"class":54},"\"Change %\"",[44,1550,286],{"class":84},[44,1552,107],{"class":80},[44,1554,1555],{"class":84}," (impact[",[44,1557,1522],{"class":54},[44,1559,286],{"class":84},[44,1561,1562],{"class":80},"\u002F",[44,1564,1529],{"class":84},[44,1566,1505],{"class":54},[44,1568,286],{"class":84},[44,1570,704],{"class":80},[44,1572,1573],{"class":156}," 100",[44,1575,1090],{"class":84},[44,1577,850],{"class":156},[44,1579,116],{"class":84},[44,1581,1582,1584,1587,1589],{"class":46,"line":380},[44,1583,452],{"class":156},[44,1585,1586],{"class":84},"(impact.round(",[44,1588,850],{"class":156},[44,1590,1591],{"class":84},").to_string())\n",[10,1593,1594],{},"When a supplier announces price increases, the question is what they do to the cost of each finished product. Applying the new prices to the cost table and rerunning the rollup answers it exactly, through every level of the BOM; the cache is cleared before and after so the scenario neither uses stale values nor leaves the changed prices behind. A 25% rise in spokes, 10% in rims and 8% in tyres adds the same amount to both bikes, because they share the wheel, but as a percentage it matters far more to the cheaper standard bike than to the pro. Both bikes go up by £7.43, but that is 3.4% on the standard bike and only 1.3% on the pro. That difference is what a pricing review needs to see: whether the standard bike's selling price still covers the higher cost.",[477,1596,486,1602,486,1605,486,1608,486,1613,486,1616,486,1619,486,1622,486,1626,486,1629,486,1633,486,1636,486,1640,486,1644,486,1648,486,1652,486,1654,486,1657,486,1659,486,1662,486,1664,486,1667,486,1669,486,1672,486,1674,486,1677,486,1679,486,1682,486,1684,486,1686,486,1688,486,1690,486,1692,486,1695,486,1697,486,1700,486,1702,486,1705,486,1709,486,1713,486,1715,486,1717,486,1719,486,1721,486,1723,486,1726,486,1728,486,1731,486,1733,486,1735,486,1737],{"viewBox":1597,"role":480,"ariaLabelledBy":1598,"xmlns":484,"style":1601},"132 0 496 140",[1599,1600],"bom-impact-t","bom-impact-d","width:100%;max-width:496px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[488,1603,1604],{"id":1599},"Effect of the supplier increases",[492,1606,1607],{"id":1600},"Spokes up 25 percent, rims up 10 percent and tyres up 8 percent add 7.43 pounds to each bike because both use the same wheels. The standard bike's cost rises 3.38 percent from 220.27 to 227.71 pounds; the pro bike's rises 1.33 percent from 557.77 to 565.21 pounds.",[496,1609],{"x":1610,"y":499,"width":1611,"height":1612,"fill":502},"132","496","140",[496,1614],{"x":1615,"y":506,"width":1131,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"148.1",[496,1617],{"x":1618,"y":506,"width":1155,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"180.1",[514,1620,1143],{"x":1621,"y":1141,"style":1142},"222.4",[496,1623],{"x":1624,"y":506,"width":1625,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"264.7","75.9",[514,1627,1151],{"x":1628,"y":1141,"style":1142},"302.7",[496,1630],{"x":1631,"y":506,"width":1632,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"340.6","111.9",[514,1634,1159],{"x":1635,"y":1141,"style":1142},"396.6",[496,1637],{"x":1638,"y":506,"width":1639,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"452.5","71.8",[514,1641,1643],{"x":1642,"y":1141,"style":1142},"488.4","D",[496,1645],{"x":1646,"y":506,"width":1647,"height":1132,"rx":499,"fill":537,"stroke":538,"style":1133},"524.3","87.6",[514,1649,1651],{"x":1650,"y":1141,"style":1142},"568.1","E",[496,1653],{"x":1615,"y":1162,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},[514,1655,710],{"x":1656,"y":508,"style":1142},"164.1",[496,1658],{"x":1618,"y":1162,"width":1155,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1660,1661],{"x":1621,"y":508,"style":1172},"Product",[496,1663],{"x":1624,"y":1162,"width":1625,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1665,1666],{"x":1628,"y":508,"style":1172},"Current",[496,1668],{"x":1631,"y":1162,"width":1632,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1670,1671],{"x":1635,"y":508,"style":1172},"After increase",[496,1673],{"x":1638,"y":1162,"width":1639,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1675,1676],{"x":1642,"y":508,"style":1172},"Change",[496,1678],{"x":1646,"y":1162,"width":1647,"height":1163,"rx":499,"fill":510,"stroke":511,"style":1169},[514,1680,1681],{"x":1650,"y":508,"style":1172},"Change %",[496,1683],{"x":1615,"y":1185,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},[514,1685,850],{"x":1656,"y":1188,"style":1142},[496,1687],{"x":1618,"y":1185,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1689,519],{"x":1621,"y":1188,"style":1193},[496,1691],{"x":1624,"y":1185,"width":1625,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1693,1694],{"x":1628,"y":1188,"style":1193},"£220.27",[496,1696],{"x":1631,"y":1185,"width":1632,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1698,1699],{"x":1635,"y":1188,"style":1193},"£227.71",[496,1701],{"x":1638,"y":1185,"width":1639,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1703,1704],{"x":1642,"y":1188,"style":1193},"+£7.43",[496,1706],{"x":1646,"y":1185,"width":1647,"height":1163,"rx":499,"fill":1707,"stroke":1708,"style":1169},"#fce9e9","var(--danger,#dc2626)",[514,1710,1712],{"x":1650,"y":1188,"style":1711},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","3.38%",[496,1714],{"x":1615,"y":1206,"width":1131,"height":1163,"rx":499,"fill":537,"stroke":538,"style":1133},[514,1716,1093],{"x":1656,"y":1209,"style":1142},[496,1718],{"x":1618,"y":1206,"width":1155,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1720,1182],{"x":1621,"y":1209,"style":1193},[496,1722],{"x":1624,"y":1206,"width":1625,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1724,1725],{"x":1628,"y":1209,"style":1193},"£557.77",[496,1727],{"x":1631,"y":1206,"width":1632,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1729,1730],{"x":1635,"y":1209,"style":1193},"£565.21",[496,1732],{"x":1638,"y":1206,"width":1639,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1734,1704],{"x":1642,"y":1209,"style":1193},[496,1736],{"x":1646,"y":1206,"width":1647,"height":1163,"rx":499,"fill":502,"stroke":538,"style":1133},[514,1738,1739],{"x":1650,"y":1209,"style":1193},"1.33%",[30,1741,1743],{"id":1742},"writing-the-rollup-back-to-excel","Writing the rollup back to Excel",[10,1745,1746,1747,1750,1751,1754],{},"The rolled costs, the where-used table and the scenario belong in one workbook for the pricing review: a sheet per view, with the rolled cost of every assembly next to its own cost and materials, so a reviewer can follow any figure down one level at a time. It is worth adding an ",[14,1748,1749],{},"indented BOM"," sheet too — each finished product exploded into its full tree, one row per line with the level shown by indentation and the extended quantity and cost per line — because that is the view engineers and buyers recognise from their ERP. The recursive ",[19,1752,1753],{},"explode"," function produces it with one more column for the level. Writing costs as values rather than formulas keeps the workbook a snapshot of the rollup at the date it was run, which is what a standard cost review needs.",[30,1756,1758],{"id":1757},"common-pitfalls","Common pitfalls",[1760,1761,1762,1770,1776,1782,1788],"ul",{},[1763,1764,1765,1769],"li",{},[1766,1767,1768],"strong",{},"Costing before checking."," Missing costs count as zero and loops never finish; check first.",[1763,1771,1772,1775],{},[1766,1773,1774],{},"Forgetting scrap allowances."," Small percentages on high-quantity parts add up.",[1763,1777,1778,1781],{},[1766,1779,1780],{},"Recomputing shared sub-assemblies."," Cache the rolled cost so each item is calculated once.",[1763,1783,1784,1787],{},[1766,1785,1786],{},"Only one level in Excel formulas."," Use recursion for BOMs of any depth.",[1763,1789,1790,1793],{},[1766,1791,1792],{},"What-if changes left in place."," Restore the original prices after a scenario.",[30,1795,1797],{"id":1796},"conclusion","Conclusion",[10,1799,1800],{},"A bill of materials cost rollup reads the parent–component pairs and item costs, checks for missing costs, mislabelled purchased parts and loops, and then rolls costs up recursively: each assembly's own labour and overhead plus each component's quantity, scrap allowance and rolled cost. The same tree gives the where-used quantities of every purchased part per finished product and, by rerunning the rollup with new prices, the exact effect of a supplier price increase on each product.",[30,1802,1804],{"id":1803},"frequently-asked-questions","Frequently asked questions",[10,1806,1807,1810],{},[1766,1808,1809],{},"How do I calculate a multi-level BOM cost in Python?","\nWrite a recursive function that returns a part's price for purchased items and, for assemblies, their own cost plus the sum of component quantities times component costs.",[10,1812,1813,1816],{},[1766,1814,1815],{},"How do I include scrap in a BOM cost rollup?","\nMultiply each component's quantity by one plus its scrap percentage before multiplying by its cost.",[10,1818,1819,1822],{},[1766,1820,1821],{},"How do I detect loops in a bill of materials?","\nFollow each parent down through its components while tracking the path; if an item appears on its own path, there is a loop.",[10,1824,1825,1828],{},[1766,1826,1827],{},"How do I see the effect of a component price change on finished products?","\nChange the component's price, rerun the rollup for each finished product, compare with the current costs and restore the original price.",[30,1830,1832],{"id":1831},"related","Related",[1760,1834,1835,1841,1848,1855],{},[1763,1836,1837,1838,1840],{},"Up one level: ",[24,1839,27],{"href":26}," — finance and operations reports.",[1763,1842,1843,1847],{},[24,1844,1846],{"href":1845},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Froll-up-totals-through-a-parent-child-hierarchy\u002F","Roll Up Totals Through a Parent-Child Hierarchy"," — the same recursion for reporting lines.",[1763,1849,1850,1854],{},[24,1851,1853],{"href":1852},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-weighted-average-cost-inventory-valuation\u002F","Calculate Weighted Average Cost Inventory Valuation"," — the cost of parts in stock.",[1763,1856,1857,1861],{},[24,1858,1860],{"href":1859},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fmodel-the-impact-of-a-price-increase\u002F","Model the Impact of a Price Increase"," — the selling-price side.",[1863,1864,1865],"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 .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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":40,"searchDepth":94,"depth":94,"links":1867},[1868,1869,1870,1871,1872,1873,1874,1875,1876,1877],{"id":32,"depth":94,"text":33},{"id":67,"depth":94,"text":68},{"id":592,"depth":94,"text":593},{"id":880,"depth":94,"text":881},{"id":1309,"depth":94,"text":1310},{"id":1742,"depth":94,"text":1743},{"id":1757,"depth":94,"text":1758},{"id":1796,"depth":94,"text":1797},{"id":1803,"depth":94,"text":1804},{"id":1831,"depth":94,"text":1832},"2026-10-02","Calculate the standard cost of every assembled product from a multi-level bill of materials in Excel: walk the parent–component tree recursively with quantities, add each assembly's own labour and overhead, detect loops and components with no cost, show where a part is used, and work out what a supplier price increase does to every finished product — all with pandas and plain Python.","md",[1882,1884,1886,1888],{"q":1809,"a":1883},"Write a recursive function that returns a part's price for purchased items and, for assemblies, their own cost plus the sum of component quantities times component costs.",{"q":1815,"a":1885},"Multiply each component's quantity by one plus its scrap percentage before multiplying by its cost.",{"q":1821,"a":1887},"Follow each parent down through its components while tracking the path; if an item appears on its own path, there is a loop.",{"q":1827,"a":1889},"Change the component's price, rerun the rollup for each finished product, compare with the current costs and restore the original price.","bom cost rollup python",null,{"breadcrumb":1893},[1894,1896,1899],{"name":1895,"item":1562},"Home",{"name":1897,"item":1898},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":1900,"item":26},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup",{"title":1903,"description":1904},"Build a Bill of Materials Cost Rollup with Python","Multi-level BOM cost rollup with pandas: recursive costing with quantities and scrap, labour and overhead per assembly, loop and missing-cost checks, where-used lists and the impact of a component price change.","build-a-bill-of-materials-cost-rollup","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup\u002Findex",[1908,1909],"report-layouts","joins-and-lookups","how-to","f7S-vmxBahx0tZlsg9eie1L1zsuL-0xZ_CaTMuZRc9w",[1913,1917],{"title":1914,"path":1915,"stem":1916,"children":-1},"Build a Bank Reconciliation Report with Python and Excel","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report\u002Findex",{"title":1918,"path":1919,"stem":1920,"children":-1},"Build a Break-Even Analysis in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-break-even-analysis-in-excel-with-python","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-break-even-analysis-in-excel-with-python\u002Findex",[1922,1924],{"path":1923,"title":1900},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":1925,"title":1897},"\u002Fautomating-reporting-workflows",[],[1928,1932,1935,1938,1941,1944],{"path":1929,"title":1930,"kind":1931},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":1933,"title":1934,"kind":1910},"\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":1936,"title":1937,"kind":1910},"\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":1939,"title":1940,"kind":1910},"\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":1942,"title":1943,"kind":1910},"\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":1945,"title":1946,"kind":1910},"\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",1790972119238]