[{"data":1,"prerenderedAt":2457},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report":2422,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report":2431,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report":2436,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report":2437},{"id":4,"title":5,"body":6,"dateModified":2388,"datePublished":2388,"description":2389,"extension":2390,"faq":2391,"keyword":2400,"kind":2401,"meta":2402,"navigation":112,"path":2411,"seo":2412,"slug":2415,"stem":2416,"topics":2417,"type":2420,"__hash__":2421},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report\u002Findex.md","Build a Product Margin Report",{"type":7,"value":8,"toc":2375},"minimark",[9,24,29,58,77,81,84,443,446,450,457,626,633,760,764,767,987,1002,1132,1136,1139,1394,1397,1478,1482,1485,1779,1782,1786,2254,2261,2265,2300,2304,2310,2314,2320,2326,2332,2338,2342,2371],[10,11,12,13,17,18,23],"p",{},"Revenue reports say what sold; margin reports say what was worth selling. A product with healthy list prices can lose money once discounts, returns and shipping are counted, and a big customer can be the least profitable one. The data for a margin report is usually all in Excel exports — sales lines from the order system, standard costs from the ERP, freight invoices from the carrier — but combining them correctly takes care: costs change during the year, so each sale must be costed at the cost that applied ",[14,15,16],"em",{},"on its date","; freight arrives as one invoice per shipment and must be shared across the products in it; and returns must reduce both revenue and cost. This page builds the report with pandas and writes it to Excel. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl\n",[10,59,60,61,64,65,68,69,72,73,76],{},"Four inputs: ",[37,62,63],{},"sales_lines.xlsx"," (order, date, customer, SKU, quantity, list price and line discount) for August and September 2026; ",[37,66,67],{},"standard_costs.xlsx"," with each product's standard cost and the date it took effect — two products got more expensive on 1 September — plus unit weight; ",[37,70,71],{},"freight.xlsx"," with the carrier charge per order; and ",[37,74,75],{},"returns.xlsx"," with returned quantities.",[25,78,80],{"id":79},"step-1-net-revenue-after-discounts-and-returns","Step 1 — Net revenue after discounts and returns",[10,82,83],{},"Start with what the customer actually pays for what they keep:",[30,85,89],{"className":86,"code":87,"language":88,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nlines = pd.read_excel(\"sales_lines.xlsx\")\nreturns = pd.read_excel(\"returns.xlsx\")\n\nlines = lines.merge(returns, on=[\"Order\", \"SKU\"], how=\"left\").fillna({\"Qty returned\": 0})\nlines[\"Net qty\"] = lines[\"Qty\"] - lines[\"Qty returned\"]\nlines[\"Net price\"] = (lines[\"List price\"] * (1 - lines[\"Discount %\"] \u002F 100)).round(2)\nlines[\"Revenue\"] = (lines[\"Net qty\"] * lines[\"Net price\"]).round(2)\nprint(lines[[\"Order\", \"SKU\", \"Qty\", \"Qty returned\", \"Net price\", \"Revenue\"]].head(8).to_string(index=False))\nprint(\"revenue:\", round(lines[\"Revenue\"].sum(), 2), \"| discounts given:\",\n      round((lines[\"Net qty\"] * lines[\"List price\"]).sum() - lines[\"Revenue\"].sum(), 2))\n","python",[37,90,91,107,114,132,147,152,207,239,292,323,374,409],{"__ignoreMap":35},[40,92,93,97,101,104],{"class":42,"line":43},[40,94,96],{"class":95},"s-kum","import",[40,98,100],{"class":99},"skGVy"," pandas ",[40,102,103],{"class":95},"as",[40,105,106],{"class":99}," pd\n",[40,108,110],{"class":42,"line":109},2,[40,111,113],{"emptyLinePlaceholder":112},true,"\n",[40,115,117,120,123,126,129],{"class":42,"line":116},3,[40,118,119],{"class":99},"lines ",[40,121,122],{"class":95},"=",[40,124,125],{"class":99}," pd.read_excel(",[40,127,128],{"class":50},"\"sales_lines.xlsx\"",[40,130,131],{"class":99},")\n",[40,133,135,138,140,142,145],{"class":42,"line":134},4,[40,136,137],{"class":99},"returns ",[40,139,122],{"class":95},[40,141,125],{"class":99},[40,143,144],{"class":50},"\"returns.xlsx\"",[40,146,131],{"class":99},[40,148,150],{"class":42,"line":149},5,[40,151,113],{"emptyLinePlaceholder":112},[40,153,155,157,159,162,166,168,171,174,177,180,183,186,188,191,194,197,200,204],{"class":42,"line":154},6,[40,156,119],{"class":99},[40,158,122],{"class":95},[40,160,161],{"class":99}," lines.merge(returns, ",[40,163,165],{"class":164},"sa561","on",[40,167,122],{"class":95},[40,169,170],{"class":99},"[",[40,172,173],{"class":50},"\"Order\"",[40,175,176],{"class":99},", ",[40,178,179],{"class":50},"\"SKU\"",[40,181,182],{"class":99},"], ",[40,184,185],{"class":164},"how",[40,187,122],{"class":95},[40,189,190],{"class":50},"\"left\"",[40,192,193],{"class":99},").fillna({",[40,195,196],{"class":50},"\"Qty returned\"",[40,198,199],{"class":99},": ",[40,201,203],{"class":202},"sP0c6","0",[40,205,206],{"class":99},"})\n",[40,208,210,213,216,219,221,224,227,229,232,234,236],{"class":42,"line":209},7,[40,211,212],{"class":99},"lines[",[40,214,215],{"class":50},"\"Net qty\"",[40,217,218],{"class":99},"] ",[40,220,122],{"class":95},[40,222,223],{"class":99}," lines[",[40,225,226],{"class":50},"\"Qty\"",[40,228,218],{"class":99},[40,230,231],{"class":95},"-",[40,233,223],{"class":99},[40,235,196],{"class":50},[40,237,238],{"class":99},"]\n",[40,240,242,244,247,249,251,254,257,259,262,265,268,271,273,276,278,281,284,287,290],{"class":42,"line":241},8,[40,243,212],{"class":99},[40,245,246],{"class":50},"\"Net price\"",[40,248,218],{"class":99},[40,250,122],{"class":95},[40,252,253],{"class":99}," (lines[",[40,255,256],{"class":50},"\"List price\"",[40,258,218],{"class":99},[40,260,261],{"class":95},"*",[40,263,264],{"class":99}," (",[40,266,267],{"class":202},"1",[40,269,270],{"class":95}," -",[40,272,223],{"class":99},[40,274,275],{"class":50},"\"Discount %\"",[40,277,218],{"class":99},[40,279,280],{"class":95},"\u002F",[40,282,283],{"class":202}," 100",[40,285,286],{"class":99},")).round(",[40,288,289],{"class":202},"2",[40,291,131],{"class":99},[40,293,295,297,300,302,304,306,308,310,312,314,316,319,321],{"class":42,"line":294},9,[40,296,212],{"class":99},[40,298,299],{"class":50},"\"Revenue\"",[40,301,218],{"class":99},[40,303,122],{"class":95},[40,305,253],{"class":99},[40,307,215],{"class":50},[40,309,218],{"class":99},[40,311,261],{"class":95},[40,313,223],{"class":99},[40,315,246],{"class":50},[40,317,318],{"class":99},"]).round(",[40,320,289],{"class":202},[40,322,131],{"class":99},[40,324,326,329,332,334,336,338,340,342,344,346,348,350,352,354,357,360,363,366,368,371],{"class":42,"line":325},10,[40,327,328],{"class":202},"print",[40,330,331],{"class":99},"(lines[[",[40,333,173],{"class":50},[40,335,176],{"class":99},[40,337,179],{"class":50},[40,339,176],{"class":99},[40,341,226],{"class":50},[40,343,176],{"class":99},[40,345,196],{"class":50},[40,347,176],{"class":99},[40,349,246],{"class":50},[40,351,176],{"class":99},[40,353,299],{"class":50},[40,355,356],{"class":99},"]].head(",[40,358,359],{"class":202},"8",[40,361,362],{"class":99},").to_string(",[40,364,365],{"class":164},"index",[40,367,122],{"class":95},[40,369,370],{"class":202},"False",[40,372,373],{"class":99},"))\n",[40,375,377,379,382,385,387,390,393,395,398,400,403,406],{"class":42,"line":376},11,[40,378,328],{"class":202},[40,380,381],{"class":99},"(",[40,383,384],{"class":50},"\"revenue:\"",[40,386,176],{"class":99},[40,388,389],{"class":202},"round",[40,391,392],{"class":99},"(lines[",[40,394,299],{"class":50},[40,396,397],{"class":99},"].sum(), ",[40,399,289],{"class":202},[40,401,402],{"class":99},"), ",[40,404,405],{"class":50},"\"| discounts given:\"",[40,407,408],{"class":99},",\n",[40,410,412,415,418,420,422,424,426,428,431,433,435,437,439,441],{"class":42,"line":411},12,[40,413,414],{"class":202},"      round",[40,416,417],{"class":99},"((lines[",[40,419,215],{"class":50},[40,421,218],{"class":99},[40,423,261],{"class":95},[40,425,223],{"class":99},[40,427,256],{"class":50},[40,429,430],{"class":99},"]).sum() ",[40,432,231],{"class":95},[40,434,223],{"class":99},[40,436,299],{"class":50},[40,438,397],{"class":99},[40,440,289],{"class":202},[40,442,373],{"class":99},[10,444,445],{},"Returns are matched on order and SKU and reduce the quantity before anything is priced or costed, so a returned drill takes out both its revenue and, in the next step, its cost. The total discount given is worth printing on its own: it is often the single biggest lever on margin, and seeing it as one number tends to start a useful conversation with the sales team.",[25,447,449],{"id":448},"step-2-the-standard-cost-in-force-on-each-sale-date","Step 2 — The standard cost in force on each sale date",[10,451,452,453,456],{},"Costs changed on 1 September, so August sales must use the old cost and September sales the new one. ",[37,454,455],{},"merge_asof"," picks, for each sale, the most recent cost record on or before its date:",[30,458,460],{"className":86,"code":459,"language":88,"meta":35,"style":35},"costs = pd.read_excel(\"standard_costs.xlsx\")\nlines = pd.merge_asof(lines.sort_values(\"Date\"), costs.sort_values(\"Effective from\"),\n                      left_on=\"Date\", right_on=\"Effective from\", by=\"SKU\", direction=\"backward\")\nassert lines[\"Std cost\"].notna().all(), \"sale before any cost record\"\nlines[\"COGS\"] = (lines[\"Net qty\"] * lines[\"Std cost\"]).round(2)\nprint(lines[[\"Date\", \"SKU\", \"Std cost\"]].drop_duplicates([\"SKU\", \"Std cost\"]).sort_values([\"SKU\", \"Date\"]).to_string(index=False))\n",[37,461,462,476,497,536,552,581],{"__ignoreMap":35},[40,463,464,467,469,471,474],{"class":42,"line":43},[40,465,466],{"class":99},"costs ",[40,468,122],{"class":95},[40,470,125],{"class":99},[40,472,473],{"class":50},"\"standard_costs.xlsx\"",[40,475,131],{"class":99},[40,477,478,480,482,485,488,491,494],{"class":42,"line":109},[40,479,119],{"class":99},[40,481,122],{"class":95},[40,483,484],{"class":99}," pd.merge_asof(lines.sort_values(",[40,486,487],{"class":50},"\"Date\"",[40,489,490],{"class":99},"), costs.sort_values(",[40,492,493],{"class":50},"\"Effective from\"",[40,495,496],{"class":99},"),\n",[40,498,499,502,504,506,508,511,513,515,517,520,522,524,526,529,531,534],{"class":42,"line":116},[40,500,501],{"class":164},"                      left_on",[40,503,122],{"class":95},[40,505,487],{"class":50},[40,507,176],{"class":99},[40,509,510],{"class":164},"right_on",[40,512,122],{"class":95},[40,514,493],{"class":50},[40,516,176],{"class":99},[40,518,519],{"class":164},"by",[40,521,122],{"class":95},[40,523,179],{"class":50},[40,525,176],{"class":99},[40,527,528],{"class":164},"direction",[40,530,122],{"class":95},[40,532,533],{"class":50},"\"backward\"",[40,535,131],{"class":99},[40,537,538,541,543,546,549],{"class":42,"line":134},[40,539,540],{"class":95},"assert",[40,542,223],{"class":99},[40,544,545],{"class":50},"\"Std cost\"",[40,547,548],{"class":99},"].notna().all(), ",[40,550,551],{"class":50},"\"sale before any cost record\"\n",[40,553,554,556,559,561,563,565,567,569,571,573,575,577,579],{"class":42,"line":149},[40,555,212],{"class":99},[40,557,558],{"class":50},"\"COGS\"",[40,560,218],{"class":99},[40,562,122],{"class":95},[40,564,253],{"class":99},[40,566,215],{"class":50},[40,568,218],{"class":99},[40,570,261],{"class":95},[40,572,223],{"class":99},[40,574,545],{"class":50},[40,576,318],{"class":99},[40,578,289],{"class":202},[40,580,131],{"class":99},[40,582,583,585,587,589,591,593,595,597,600,602,604,606,609,611,613,615,618,620,622,624],{"class":42,"line":154},[40,584,328],{"class":202},[40,586,331],{"class":99},[40,588,487],{"class":50},[40,590,176],{"class":99},[40,592,179],{"class":50},[40,594,176],{"class":99},[40,596,545],{"class":50},[40,598,599],{"class":99},"]].drop_duplicates([",[40,601,179],{"class":50},[40,603,176],{"class":99},[40,605,545],{"class":50},[40,607,608],{"class":99},"]).sort_values([",[40,610,179],{"class":50},[40,612,176],{"class":99},[40,614,487],{"class":50},[40,616,617],{"class":99},"]).to_string(",[40,619,365],{"class":164},[40,621,122],{"class":95},[40,623,370],{"class":202},[40,625,373],{"class":99},[10,627,628,629,23],{},"Drills sold in August are costed at 71.00 and those sold in September at 78.00; hoses move from 13.50 to 16.00; saws, whose cost did not change, use 52.00 throughout. Using a single current cost for the whole period would understate August's margin, overstate the change between the months, and make it impossible to tell whether margin fell because prices dropped or costs rose. The assertion catches a sale dated before a product's first cost record. As-of joins are explained in ",[19,630,632],{"href":631},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmatch-rows-by-nearest-date-with-merge-asof\u002F","Match Rows by Nearest Date with merge_asof",[634,635,643,644,643,648,643,652,643,659,643,666,643,676,643,683,643,688,643,692,643,696,643,702,643,708,643,712,643,714,643,716,643,721,643,726,643,729,643,731,643,733,643,736,643,740,643,743,643,745,643,747,643,750,643,754,643,756,643,758],"svg",{"viewBox":636,"role":637,"ariaLabelledBy":638,"xmlns":641,"style":642},"4 0 752 211","img",[639,640],"pm-cost-t","pm-cost-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[645,646,647],"title",{"id":639},"Which cost a drill sale gets",[649,650,651],"desc",{"id":640},"The drill's standard cost is 71.00 from 1 January and 78.00 from 1 September 2026. Sales on 4 and 12 August take 71.00. Sales on 2 and 15 September take 78.00, because merge_asof picks the latest cost on or before each sale date.",[653,654],"rect",{"x":655,"y":203,"width":656,"height":657,"fill":658},"4","752","211","#ffffff",[42,660],{"x1":661,"y1":662,"x2":663,"y2":662,"stroke":664,"style":665},"76.0","105.5","684.0","var(--muted,#5b6780)","stroke-width:2.5px",[653,667],{"x":668,"y":669,"width":670,"height":671,"rx":672,"fill":673,"stroke":674,"style":675},"20.0","16.0","136.0","57.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[677,678,682],"text",{"x":679,"y":680,"style":681},"88.0","40.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1 Jan",[677,684,687],{"x":679,"y":685,"style":686},"59.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","cost 71.00",[42,689],{"x1":679,"y1":690,"x2":679,"y2":662,"stroke":664,"style":691},"73.5","stroke-width:1.5px",[693,694],"circle",{"cx":679,"cy":662,"r":695,"fill":664,"stroke":658,"style":675},"7.0",[653,697],{"x":698,"y":699,"width":670,"height":671,"rx":672,"fill":700,"stroke":701,"style":675},"166.0","137.5","#f0f4ff","var(--brand,#5b5cf0)",[677,703,707],{"x":704,"y":705,"style":706},"234.0","162.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","4 Aug · SO-1",[677,709,711],{"x":704,"y":710,"style":686},"180.5","costed at 71.00",[42,713],{"x1":704,"y1":662,"x2":704,"y2":699,"stroke":664,"style":691},[693,715],{"cx":704,"cy":662,"r":695,"fill":701,"stroke":658,"style":675},[653,717],{"x":718,"y":669,"width":670,"height":671,"rx":672,"fill":719,"stroke":720,"style":675},"312.0","#fdefd8","var(--gold,#b4740a)",[677,722,725],{"x":723,"y":680,"style":724},"380.0","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","1 Sep",[677,727,728],{"x":723,"y":685,"style":686},"cost 78.00",[42,730],{"x1":723,"y1":690,"x2":723,"y2":662,"stroke":664,"style":691},[693,732],{"cx":723,"cy":662,"r":695,"fill":720,"stroke":658,"style":675},[653,734],{"x":735,"y":699,"width":670,"height":671,"rx":672,"fill":700,"stroke":701,"style":675},"458.0",[677,737,739],{"x":738,"y":705,"style":706},"526.0","2 Sep · SO-4",[677,741,742],{"x":738,"y":710,"style":686},"costed at 78.00",[42,744],{"x1":738,"y1":662,"x2":738,"y2":699,"stroke":664,"style":691},[693,746],{"cx":738,"cy":662,"r":695,"fill":701,"stroke":658,"style":675},[653,748],{"x":749,"y":669,"width":670,"height":671,"rx":672,"fill":700,"stroke":701,"style":675},"604.0",[677,751,753],{"x":752,"y":680,"style":706},"672.0","15 Sep · SO-6",[677,755,742],{"x":752,"y":685,"style":686},[42,757],{"x1":752,"y1":690,"x2":752,"y2":662,"stroke":664,"style":691},[693,759],{"cx":752,"cy":662,"r":695,"fill":701,"stroke":658,"style":675},[25,761,763],{"id":762},"step-3-allocate-freight-by-weight","Step 3 — Allocate freight by weight",[10,765,766],{},"Freight is invoiced per order, but the margin report is per product. Share each order's freight across its lines in proportion to their shipped weight — a heavy saw carries more of the cost than a light hose:",[30,768,770],{"className":86,"code":769,"language":88,"meta":35,"style":35},"freight = pd.read_excel(\"freight.xlsx\")\nlines[\"Weight\"] = lines[\"Qty\"] * lines[\"Weight kg\"]                  # what was shipped, before returns\nlines[\"Order weight\"] = lines.groupby(\"Order\")[\"Weight\"].transform(\"sum\")\nlines = lines.merge(freight, on=\"Order\", how=\"left\", validate=\"many_to_one\")\nlines[\"Freight alloc\"] = (lines[\"Freight\"] * lines[\"Weight\"] \u002F lines[\"Order weight\"]).round(2)\ncheck = lines.groupby(\"Order\")[\"Freight alloc\"].sum().round(2) - freight.set_index(\"Order\")[\"Freight\"]\nprint(\"allocated = invoiced per order:\", (check.abs() \u003C= 0.02).all(), \"| total freight:\", lines[\"Freight alloc\"].sum())\n",[37,771,772,786,817,846,881,919,955],{"__ignoreMap":35},[40,773,774,777,779,781,784],{"class":42,"line":43},[40,775,776],{"class":99},"freight ",[40,778,122],{"class":95},[40,780,125],{"class":99},[40,782,783],{"class":50},"\"freight.xlsx\"",[40,785,131],{"class":99},[40,787,788,790,793,795,797,799,801,803,805,807,810,813],{"class":42,"line":109},[40,789,212],{"class":99},[40,791,792],{"class":50},"\"Weight\"",[40,794,218],{"class":99},[40,796,122],{"class":95},[40,798,223],{"class":99},[40,800,226],{"class":50},[40,802,218],{"class":99},[40,804,261],{"class":95},[40,806,223],{"class":99},[40,808,809],{"class":50},"\"Weight kg\"",[40,811,812],{"class":99},"]                  ",[40,814,816],{"class":815},"s-wDw","# what was shipped, before returns\n",[40,818,819,821,824,826,828,831,833,836,838,841,844],{"class":42,"line":116},[40,820,212],{"class":99},[40,822,823],{"class":50},"\"Order weight\"",[40,825,218],{"class":99},[40,827,122],{"class":95},[40,829,830],{"class":99}," lines.groupby(",[40,832,173],{"class":50},[40,834,835],{"class":99},")[",[40,837,792],{"class":50},[40,839,840],{"class":99},"].transform(",[40,842,843],{"class":50},"\"sum\"",[40,845,131],{"class":99},[40,847,848,850,852,855,857,859,861,863,865,867,869,871,874,876,879],{"class":42,"line":134},[40,849,119],{"class":99},[40,851,122],{"class":95},[40,853,854],{"class":99}," lines.merge(freight, ",[40,856,165],{"class":164},[40,858,122],{"class":95},[40,860,173],{"class":50},[40,862,176],{"class":99},[40,864,185],{"class":164},[40,866,122],{"class":95},[40,868,190],{"class":50},[40,870,176],{"class":99},[40,872,873],{"class":164},"validate",[40,875,122],{"class":95},[40,877,878],{"class":50},"\"many_to_one\"",[40,880,131],{"class":99},[40,882,883,885,888,890,892,894,897,899,901,903,905,907,909,911,913,915,917],{"class":42,"line":149},[40,884,212],{"class":99},[40,886,887],{"class":50},"\"Freight alloc\"",[40,889,218],{"class":99},[40,891,122],{"class":95},[40,893,253],{"class":99},[40,895,896],{"class":50},"\"Freight\"",[40,898,218],{"class":99},[40,900,261],{"class":95},[40,902,223],{"class":99},[40,904,792],{"class":50},[40,906,218],{"class":99},[40,908,280],{"class":95},[40,910,223],{"class":99},[40,912,823],{"class":50},[40,914,318],{"class":99},[40,916,289],{"class":202},[40,918,131],{"class":99},[40,920,921,924,926,928,930,932,934,937,939,942,944,947,949,951,953],{"class":42,"line":154},[40,922,923],{"class":99},"check ",[40,925,122],{"class":95},[40,927,830],{"class":99},[40,929,173],{"class":50},[40,931,835],{"class":99},[40,933,887],{"class":50},[40,935,936],{"class":99},"].sum().round(",[40,938,289],{"class":202},[40,940,941],{"class":99},") ",[40,943,231],{"class":95},[40,945,946],{"class":99}," freight.set_index(",[40,948,173],{"class":50},[40,950,835],{"class":99},[40,952,896],{"class":50},[40,954,238],{"class":99},[40,956,957,959,961,964,967,970,973,976,979,982,984],{"class":42,"line":209},[40,958,328],{"class":202},[40,960,381],{"class":99},[40,962,963],{"class":50},"\"allocated = invoiced per order:\"",[40,965,966],{"class":99},", (check.abs() ",[40,968,969],{"class":95},"\u003C=",[40,971,972],{"class":202}," 0.02",[40,974,975],{"class":99},").all(), ",[40,977,978],{"class":50},"\"| total freight:\"",[40,980,981],{"class":99},", lines[",[40,983,887],{"class":50},[40,985,986],{"class":99},"].sum())\n",[10,988,989,992,993,996,997,1001],{},[37,990,991],{},"groupby(...).transform(\"sum\")"," puts each order's total weight on every one of its lines, so the share is a simple division. Allocation uses the weight ",[14,994,995],{},"shipped",", before returns, because the freight was paid on what left the warehouse. The check confirms every order's allocated freight adds back to its invoice within rounding. Weight is one reasonable driver; value or volume are others, and the same three lines of code work with any of them — ",[19,998,1000],{"href":999},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments\u002F","Allocate Shared Costs Across Departments"," discusses choosing a driver.",[634,1003,643,1008,643,1011,643,1014,643,1017,643,1021,643,1032,643,1038,643,1043,643,1047,643,1052,643,1057,643,1060,643,1067,643,1073,643,1077,643,1081,643,1084,643,1087,643,1094,643,1099,643,1102,643,1105,643,1109,643,1112,643,1117,643,1124,643,1129],{"viewBox":1004,"role":637,"ariaLabelledBy":1005,"xmlns":641,"style":642},"4 0 752 142",[1006,1007],"pm-flow-t","pm-flow-d",[645,1009,1010],{"id":1006},"What goes into each line's margin",[649,1012,1013],{"id":1007},"Each sales line starts from net revenue after discount and returns, subtracts cost of goods at the standard cost in force on the sale date, and subtracts its share of the order's freight allocated by weight, leaving the line margin that is summed by product and customer.",[653,1015],{"x":655,"y":203,"width":656,"height":1016,"fill":658},"142",[653,1018],{"x":668,"y":669,"width":1019,"height":1020,"rx":672,"fill":700,"stroke":701,"style":675},"153.0","110.5",[1022,1023,1025],"g",{"transform":1024},"translate(84.5,29.0) scale(1.000)",[1026,1027],"path",{"d":1028,"fill":1029,"stroke":1030,"style":1031},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[677,1033,1037],{"x":1034,"y":1035,"style":1036},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Net revenue",[677,1039,1042],{"x":1034,"y":1040,"style":1041},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","qty kept × discounted",[677,1044,1046],{"x":1034,"y":1045,"style":1041},"112.5","price",[42,1048],{"x1":1049,"y1":1050,"x2":1051,"y2":1050,"stroke":701,"style":675},"178.0","71.2","200.0",[1053,1054],"polygon",{"points":1055,"fill":1056},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[653,1058],{"x":1059,"y":669,"width":1019,"height":1020,"rx":672,"fill":673,"stroke":674,"style":675},"209.0",[1022,1061,1063],{"transform":1062},"translate(273.5,37.8) scale(1.000)",[1026,1064],{"d":1065,"fill":1029,"stroke":1066,"style":1031},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--text,#172033)",[677,1068,1072],{"x":1069,"y":1070,"style":1071},"285.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","− COGS",[677,1074,1076],{"x":1069,"y":1075,"style":1041},"103.8","dated standard cost",[42,1078],{"x1":1079,"y1":1050,"x2":1080,"y2":1050,"stroke":701,"style":675},"367.0","389.0",[1053,1082],{"points":1083,"fill":1056},"393.0,71.2 384.0,76.2 384.0,66.2",[653,1085],{"x":1086,"y":669,"width":1019,"height":1020,"rx":672,"fill":719,"stroke":720,"style":675},"398.0",[1022,1088,1090],{"transform":1089},"translate(462.5,29.0) scale(1.000)",[1026,1091],{"d":1092,"fill":1029,"stroke":1093,"style":1031},"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","var(--gold-ink,#7a4e06)",[677,1095,1098],{"x":1096,"y":1035,"style":1097},"474.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","− Freight",[677,1100,1101],{"x":1096,"y":1040,"style":1041},"share of order, by",[677,1103,1104],{"x":1096,"y":1045,"style":1041},"weight",[42,1106],{"x1":1107,"y1":1050,"x2":1108,"y2":1050,"stroke":701,"style":675},"556.0","578.0",[1053,1110],{"points":1111,"fill":1056},"582.0,71.2 573.0,76.2 573.0,66.2",[653,1113],{"x":1114,"y":669,"width":1019,"height":1020,"rx":672,"fill":1115,"stroke":1116,"style":675},"587.0","#d9f4f1","var(--teal,#0f9488)",[1022,1118,1120],{"transform":1119},"translate(651.5,37.8) scale(1.000)",[1026,1121],{"d":1122,"fill":1029,"stroke":1123,"style":1031},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--teal-ink,#0b6157)",[677,1125,1128],{"x":1126,"y":1070,"style":1127},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","= Margin",[677,1130,1131],{"x":1126,"y":1075,"style":1041},"by product, customer",[25,1133,1135],{"id":1134},"step-4-margin-by-product-customer-and-line","Step 4 — Margin by product, customer and line",[10,1137,1138],{},"With revenue, cost of goods and freight on every line, margins are group sums:",[30,1140,1142],{"className":86,"code":1141,"language":88,"meta":35,"style":35},"lines[\"Margin\"] = lines[\"Revenue\"] - lines[\"COGS\"] - lines[\"Freight alloc\"]\n\ndef margin_table(by):\n    g = lines.groupby(by)[[\"Revenue\", \"COGS\", \"Freight alloc\", \"Margin\"]].sum()\n    g[\"Margin %\"] = (g[\"Margin\"] \u002F g[\"Revenue\"] * 100).round(1)\n    return g.sort_values(\"Margin %\")\n\nby_product = margin_table(\"SKU\")\nby_customer = margin_table(\"Customer\")\nprint(by_product.round(2).to_string())\nprint(by_customer.round(2).to_string())\nlosers = lines[lines[\"Margin\"] \u003C 0][[\"Order\", \"Date\", \"Customer\", \"SKU\", \"Net qty\", \"Discount %\", \"Margin\"]]\nprint(losers.to_string(index=False))\n",[37,1143,1144,1177,1181,1193,1220,1259,1271,1275,1289,1303,1315,1326,1378],{"__ignoreMap":35},[40,1145,1146,1148,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175],{"class":42,"line":43},[40,1147,212],{"class":99},[40,1149,1150],{"class":50},"\"Margin\"",[40,1152,218],{"class":99},[40,1154,122],{"class":95},[40,1156,223],{"class":99},[40,1158,299],{"class":50},[40,1160,218],{"class":99},[40,1162,231],{"class":95},[40,1164,223],{"class":99},[40,1166,558],{"class":50},[40,1168,218],{"class":99},[40,1170,231],{"class":95},[40,1172,223],{"class":99},[40,1174,887],{"class":50},[40,1176,238],{"class":99},[40,1178,1179],{"class":42,"line":109},[40,1180,113],{"emptyLinePlaceholder":112},[40,1182,1183,1186,1190],{"class":42,"line":116},[40,1184,1185],{"class":95},"def",[40,1187,1189],{"class":1188},"s_Opv"," margin_table",[40,1191,1192],{"class":99},"(by):\n",[40,1194,1195,1198,1200,1203,1205,1207,1209,1211,1213,1215,1217],{"class":42,"line":134},[40,1196,1197],{"class":99},"    g ",[40,1199,122],{"class":95},[40,1201,1202],{"class":99}," lines.groupby(by)[[",[40,1204,299],{"class":50},[40,1206,176],{"class":99},[40,1208,558],{"class":50},[40,1210,176],{"class":99},[40,1212,887],{"class":50},[40,1214,176],{"class":99},[40,1216,1150],{"class":50},[40,1218,1219],{"class":99},"]].sum()\n",[40,1221,1222,1225,1228,1230,1232,1235,1237,1239,1241,1244,1246,1248,1250,1252,1255,1257],{"class":42,"line":149},[40,1223,1224],{"class":99},"    g[",[40,1226,1227],{"class":50},"\"Margin %\"",[40,1229,218],{"class":99},[40,1231,122],{"class":95},[40,1233,1234],{"class":99}," (g[",[40,1236,1150],{"class":50},[40,1238,218],{"class":99},[40,1240,280],{"class":95},[40,1242,1243],{"class":99}," g[",[40,1245,299],{"class":50},[40,1247,218],{"class":99},[40,1249,261],{"class":95},[40,1251,283],{"class":202},[40,1253,1254],{"class":99},").round(",[40,1256,267],{"class":202},[40,1258,131],{"class":99},[40,1260,1261,1264,1267,1269],{"class":42,"line":154},[40,1262,1263],{"class":95},"    return",[40,1265,1266],{"class":99}," g.sort_values(",[40,1268,1227],{"class":50},[40,1270,131],{"class":99},[40,1272,1273],{"class":42,"line":209},[40,1274,113],{"emptyLinePlaceholder":112},[40,1276,1277,1280,1282,1285,1287],{"class":42,"line":241},[40,1278,1279],{"class":99},"by_product ",[40,1281,122],{"class":95},[40,1283,1284],{"class":99}," margin_table(",[40,1286,179],{"class":50},[40,1288,131],{"class":99},[40,1290,1291,1294,1296,1298,1301],{"class":42,"line":294},[40,1292,1293],{"class":99},"by_customer ",[40,1295,122],{"class":95},[40,1297,1284],{"class":99},[40,1299,1300],{"class":50},"\"Customer\"",[40,1302,131],{"class":99},[40,1304,1305,1307,1310,1312],{"class":42,"line":325},[40,1306,328],{"class":202},[40,1308,1309],{"class":99},"(by_product.round(",[40,1311,289],{"class":202},[40,1313,1314],{"class":99},").to_string())\n",[40,1316,1317,1319,1322,1324],{"class":42,"line":376},[40,1318,328],{"class":202},[40,1320,1321],{"class":99},"(by_customer.round(",[40,1323,289],{"class":202},[40,1325,1314],{"class":99},[40,1327,1328,1331,1333,1336,1338,1340,1343,1346,1349,1351,1353,1355,1357,1359,1361,1363,1365,1367,1369,1371,1373,1375],{"class":42,"line":411},[40,1329,1330],{"class":99},"losers ",[40,1332,122],{"class":95},[40,1334,1335],{"class":99}," lines[lines[",[40,1337,1150],{"class":50},[40,1339,218],{"class":99},[40,1341,1342],{"class":95},"\u003C",[40,1344,1345],{"class":202}," 0",[40,1347,1348],{"class":99},"][[",[40,1350,173],{"class":50},[40,1352,176],{"class":99},[40,1354,487],{"class":50},[40,1356,176],{"class":99},[40,1358,1300],{"class":50},[40,1360,176],{"class":99},[40,1362,179],{"class":50},[40,1364,176],{"class":99},[40,1366,215],{"class":50},[40,1368,176],{"class":99},[40,1370,275],{"class":50},[40,1372,176],{"class":99},[40,1374,1150],{"class":50},[40,1376,1377],{"class":99},"]]\n",[40,1379,1381,1383,1386,1388,1390,1392],{"class":42,"line":1380},13,[40,1382,328],{"class":202},[40,1384,1385],{"class":99},"(losers.to_string(",[40,1387,365],{"class":164},[40,1389,122],{"class":95},[40,1391,370],{"class":202},[40,1393,373],{"class":99},[10,1395,1396],{},"Margin percentage is computed from the summed margin and revenue of each group — never by averaging line percentages. The tables tell a clear story: hoses are the weakest product at 7.2%, and Zenith Logistics, by far the biggest customer by revenue, earns the lowest margin at 17.1%, because it buys hoses at 25–30% off and its orders are heavy to ship. The loss-making line is the one to take to the sales manager: Zenith's 28 September hose order, at 30% off, loses €51 once the higher September cost and its share of freight are counted.",[634,1398,643,1404,643,1407,643,1410,643,1414,643,1420,643,1425,643,1429,643,1434,643,1437,643,1441,643,1444,643,1448,643,1451,643,1455,643,1458,643,1462,643,1464,643,1468,643,1473],{"viewBox":1399,"role":637,"ariaLabelledBy":1400,"xmlns":641,"style":1403},"4 0 743 180",[1401,1402],"pm-customers-t","pm-customers-d","width:100%;max-width:743px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[645,1405,1406],{"id":1401},"Gross margin % by customer after freight",[649,1408,1409],{"id":1402},"Oakridge Farms, Haddad & Sons and Café Señor, buying at small or no discounts, earn margins of 34 to 37 percent after freight. Zenith Logistics, the largest customer by revenue, earns 17.1 percent because of deep discounts on hoses and heavy freight.",[653,1411],{"x":655,"y":203,"width":1412,"height":1413,"fill":658},"743","180",[653,1415],{"x":668,"y":669,"width":1416,"height":1417,"rx":1418,"fill":673,"stroke":674,"style":1419},"130.5","28.0","7","stroke-width:1px",[677,1421,1424],{"x":1422,"y":1423,"style":681},"85.2","34.5","Oakridge Farms",[653,1426],{"x":1427,"y":669,"width":1428,"height":1417,"rx":1418,"fill":1115,"stroke":1116,"style":691},"160.5","521.7",[677,1430,1433],{"x":1431,"y":1423,"style":1432},"690.2","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","37.0%",[653,1435],{"x":668,"y":1436,"width":1416,"height":1417,"rx":1418,"fill":673,"stroke":674,"style":1419},"56.0",[677,1438,1440],{"x":1422,"y":1439,"style":681},"74.5","Haddad & Sons",[653,1442],{"x":1427,"y":1436,"width":1443,"height":1417,"rx":1418,"fill":1115,"stroke":1116,"style":691},"507.6",[677,1445,1447],{"x":1446,"y":1439,"style":1432},"676.1","36.0%",[653,1449],{"x":668,"y":1450,"width":1416,"height":1417,"rx":1418,"fill":673,"stroke":674,"style":1419},"96.0",[677,1452,1454],{"x":1422,"y":1453,"style":681},"114.5","Café Señor",[653,1456],{"x":1427,"y":1450,"width":1457,"height":1417,"rx":1418,"fill":1115,"stroke":1116,"style":691},"480.8",[677,1459,1461],{"x":1460,"y":1453,"style":1432},"649.3","34.1%",[653,1463],{"x":668,"y":670,"width":1416,"height":1417,"rx":1418,"fill":673,"stroke":674,"style":1419},[677,1465,1467],{"x":1422,"y":1466,"style":681},"154.5","Zenith Logistics",[653,1469],{"x":1427,"y":670,"width":1470,"height":1417,"rx":1418,"fill":1471,"stroke":1472,"style":691},"241.1","#fce9e9","var(--danger,#dc2626)",[677,1474,1477],{"x":1475,"y":1466,"style":1476},"409.6","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","17.1%",[25,1479,1481],{"id":1480},"step-5-why-did-margin-change-price-versus-cost","Step 5 — Why did margin change? Price versus cost",[10,1483,1484],{},"Margin per unit moved between August and September. Split the change for each product into the part caused by selling prices and the part caused by costs:",[30,1486,1488],{"className":86,"code":1487,"language":88,"meta":35,"style":35},"lines[\"Month\"] = lines[\"Date\"].dt.to_period(\"M\").astype(str)\nunit = lines.groupby([\"SKU\", \"Month\"]).apply(\n    lambda g: pd.Series({\"Price\": g[\"Revenue\"].sum() \u002F g[\"Net qty\"].sum(),\n                         \"Cost\": (g[\"COGS\"].sum() + g[\"Freight alloc\"].sum()) \u002F g[\"Net qty\"].sum(),\n                         \"Qty\": g[\"Net qty\"].sum()}), include_groups=False).unstack(\"Month\")\nbridge = pd.DataFrame({\n    \"Margin\u002Funit Aug\": unit[\"Price\"][\"2026-08\"] - unit[\"Cost\"][\"2026-08\"],\n    \"Price effect\": unit[\"Price\"][\"2026-09\"] - unit[\"Price\"][\"2026-08\"],\n    \"Cost effect\": -(unit[\"Cost\"][\"2026-09\"] - unit[\"Cost\"][\"2026-08\"]),\n})\nbridge[\"Margin\u002Funit Sep\"] = bridge[\"Margin\u002Funit Aug\"] + bridge[\"Price effect\"] + bridge[\"Cost effect\"]\nprint(bridge.round(2).to_string())\n",[37,1489,1490,1519,1538,1566,1596,1622,1632,1665,1693,1726,1730,1768],{"__ignoreMap":35},[40,1491,1492,1494,1497,1499,1501,1503,1505,1508,1511,1514,1517],{"class":42,"line":43},[40,1493,212],{"class":99},[40,1495,1496],{"class":50},"\"Month\"",[40,1498,218],{"class":99},[40,1500,122],{"class":95},[40,1502,223],{"class":99},[40,1504,487],{"class":50},[40,1506,1507],{"class":99},"].dt.to_period(",[40,1509,1510],{"class":50},"\"M\"",[40,1512,1513],{"class":99},").astype(",[40,1515,1516],{"class":202},"str",[40,1518,131],{"class":99},[40,1520,1521,1524,1526,1529,1531,1533,1535],{"class":42,"line":109},[40,1522,1523],{"class":99},"unit ",[40,1525,122],{"class":95},[40,1527,1528],{"class":99}," lines.groupby([",[40,1530,179],{"class":50},[40,1532,176],{"class":99},[40,1534,1496],{"class":50},[40,1536,1537],{"class":99},"]).apply(\n",[40,1539,1540,1543,1546,1549,1552,1554,1557,1559,1561,1563],{"class":42,"line":116},[40,1541,1542],{"class":95},"    lambda",[40,1544,1545],{"class":99}," g: pd.Series({",[40,1547,1548],{"class":50},"\"Price\"",[40,1550,1551],{"class":99},": g[",[40,1553,299],{"class":50},[40,1555,1556],{"class":99},"].sum() ",[40,1558,280],{"class":95},[40,1560,1243],{"class":99},[40,1562,215],{"class":50},[40,1564,1565],{"class":99},"].sum(),\n",[40,1567,1568,1571,1574,1576,1578,1581,1583,1585,1588,1590,1592,1594],{"class":42,"line":134},[40,1569,1570],{"class":50},"                         \"Cost\"",[40,1572,1573],{"class":99},": (g[",[40,1575,558],{"class":50},[40,1577,1556],{"class":99},[40,1579,1580],{"class":95},"+",[40,1582,1243],{"class":99},[40,1584,887],{"class":50},[40,1586,1587],{"class":99},"].sum()) ",[40,1589,280],{"class":95},[40,1591,1243],{"class":99},[40,1593,215],{"class":50},[40,1595,1565],{"class":99},[40,1597,1598,1601,1603,1605,1608,1611,1613,1615,1618,1620],{"class":42,"line":149},[40,1599,1600],{"class":50},"                         \"Qty\"",[40,1602,1551],{"class":99},[40,1604,215],{"class":50},[40,1606,1607],{"class":99},"].sum()}), ",[40,1609,1610],{"class":164},"include_groups",[40,1612,122],{"class":95},[40,1614,370],{"class":202},[40,1616,1617],{"class":99},").unstack(",[40,1619,1496],{"class":50},[40,1621,131],{"class":99},[40,1623,1624,1627,1629],{"class":42,"line":154},[40,1625,1626],{"class":99},"bridge ",[40,1628,122],{"class":95},[40,1630,1631],{"class":99}," pd.DataFrame({\n",[40,1633,1634,1637,1640,1642,1645,1648,1650,1652,1655,1658,1660,1662],{"class":42,"line":209},[40,1635,1636],{"class":50},"    \"Margin\u002Funit Aug\"",[40,1638,1639],{"class":99},": unit[",[40,1641,1548],{"class":50},[40,1643,1644],{"class":99},"][",[40,1646,1647],{"class":50},"\"2026-08\"",[40,1649,218],{"class":99},[40,1651,231],{"class":95},[40,1653,1654],{"class":99}," unit[",[40,1656,1657],{"class":50},"\"Cost\"",[40,1659,1644],{"class":99},[40,1661,1647],{"class":50},[40,1663,1664],{"class":99},"],\n",[40,1666,1667,1670,1672,1674,1676,1679,1681,1683,1685,1687,1689,1691],{"class":42,"line":241},[40,1668,1669],{"class":50},"    \"Price effect\"",[40,1671,1639],{"class":99},[40,1673,1548],{"class":50},[40,1675,1644],{"class":99},[40,1677,1678],{"class":50},"\"2026-09\"",[40,1680,218],{"class":99},[40,1682,231],{"class":95},[40,1684,1654],{"class":99},[40,1686,1548],{"class":50},[40,1688,1644],{"class":99},[40,1690,1647],{"class":50},[40,1692,1664],{"class":99},[40,1694,1695,1698,1700,1702,1705,1707,1709,1711,1713,1715,1717,1719,1721,1723],{"class":42,"line":294},[40,1696,1697],{"class":50},"    \"Cost effect\"",[40,1699,199],{"class":99},[40,1701,231],{"class":95},[40,1703,1704],{"class":99},"(unit[",[40,1706,1657],{"class":50},[40,1708,1644],{"class":99},[40,1710,1678],{"class":50},[40,1712,218],{"class":99},[40,1714,231],{"class":95},[40,1716,1654],{"class":99},[40,1718,1657],{"class":50},[40,1720,1644],{"class":99},[40,1722,1647],{"class":50},[40,1724,1725],{"class":99},"]),\n",[40,1727,1728],{"class":42,"line":325},[40,1729,206],{"class":99},[40,1731,1732,1735,1738,1740,1742,1745,1748,1750,1752,1754,1757,1759,1761,1763,1766],{"class":42,"line":376},[40,1733,1734],{"class":99},"bridge[",[40,1736,1737],{"class":50},"\"Margin\u002Funit Sep\"",[40,1739,218],{"class":99},[40,1741,122],{"class":95},[40,1743,1744],{"class":99}," bridge[",[40,1746,1747],{"class":50},"\"Margin\u002Funit Aug\"",[40,1749,218],{"class":99},[40,1751,1580],{"class":95},[40,1753,1744],{"class":99},[40,1755,1756],{"class":50},"\"Price effect\"",[40,1758,218],{"class":99},[40,1760,1580],{"class":95},[40,1762,1744],{"class":99},[40,1764,1765],{"class":50},"\"Cost effect\"",[40,1767,238],{"class":99},[40,1769,1770,1772,1775,1777],{"class":42,"line":411},[40,1771,328],{"class":202},[40,1773,1774],{"class":99},"(bridge.round(",[40,1776,289],{"class":202},[40,1778,1314],{"class":99},[10,1780,1781],{},"Per unit, the change in margin is exactly the change in price minus the change in cost, so the two effects add up to the difference between the months. For drills, the price effect is negative — September's big Zenith order carried a 20% discount — and the cost effect is negative too, from the standard cost rise. Hoses show both effects at once and end September earning five cents a unit. Separating the two answers the question a margin report always provokes: is it our pricing or our costs?",[25,1783,1785],{"id":1784},"step-6-write-the-report","Step 6 — Write the report",[30,1787,1789],{"className":86,"code":1788,"language":88,"meta":35,"style":35},"from openpyxl.formatting.rule import CellIsRule\nfrom openpyxl.styles import Font, PatternFill\n\nwith pd.ExcelWriter(\"product_margin_2026-09.xlsx\", engine=\"openpyxl\") as xw:\n    by_product.round(2).to_excel(xw, sheet_name=\"By product\")\n    by_customer.round(2).to_excel(xw, sheet_name=\"By customer\")\n    bridge.round(2).to_excel(xw, sheet_name=\"Price vs cost\")\n    losers.to_excel(xw, sheet_name=\"Loss-making lines\", index=False)\n    lines.drop(columns=[\"Effective from\", \"Order weight\"]).to_excel(xw, sheet_name=\"Lines\", index=False)\n    for name in (\"By product\", \"By customer\"):\n        ws = xw.sheets[name]\n        last = ws.max_row\n        ws.conditional_formatting.add(f\"F2:F{last}\", CellIsRule(operator=\"lessThan\", formula=[\"15\"],\n                                      fill=PatternFill(\"solid\", fgColor=\"FFC7CE\")))\n        ws.conditional_formatting.add(f\"F2:F{last}\", CellIsRule(operator=\"greaterThanOrEqual\", formula=[\"30\"],\n                                      fill=PatternFill(\"solid\", fgColor=\"C6EFCE\")))\n    for ws in xw.book.worksheets:\n        for c in ws[1]:\n            c.font = Font(bold=True)\n        ws.column_dimensions[\"A\"].width = 18\nprint(\"total margin:\", round(lines[\"Margin\"].sum(), 2), \"on revenue\", round(lines[\"Revenue\"].sum(), 2))\n",[37,1790,1791,1804,1816,1820,1848,1868,1886,1904,1926,1964,1986,1996,2006,2055,2082,2121,2143,2156,2175,2196,2213],{"__ignoreMap":35},[40,1792,1793,1796,1799,1801],{"class":42,"line":43},[40,1794,1795],{"class":95},"from",[40,1797,1798],{"class":99}," openpyxl.formatting.rule ",[40,1800,96],{"class":95},[40,1802,1803],{"class":99}," CellIsRule\n",[40,1805,1806,1808,1811,1813],{"class":42,"line":109},[40,1807,1795],{"class":95},[40,1809,1810],{"class":99}," openpyxl.styles ",[40,1812,96],{"class":95},[40,1814,1815],{"class":99}," Font, PatternFill\n",[40,1817,1818],{"class":42,"line":116},[40,1819,113],{"emptyLinePlaceholder":112},[40,1821,1822,1825,1828,1831,1833,1836,1838,1841,1843,1845],{"class":42,"line":134},[40,1823,1824],{"class":95},"with",[40,1826,1827],{"class":99}," pd.ExcelWriter(",[40,1829,1830],{"class":50},"\"product_margin_2026-09.xlsx\"",[40,1832,176],{"class":99},[40,1834,1835],{"class":164},"engine",[40,1837,122],{"class":95},[40,1839,1840],{"class":50},"\"openpyxl\"",[40,1842,941],{"class":99},[40,1844,103],{"class":95},[40,1846,1847],{"class":99}," xw:\n",[40,1849,1850,1853,1855,1858,1861,1863,1866],{"class":42,"line":149},[40,1851,1852],{"class":99},"    by_product.round(",[40,1854,289],{"class":202},[40,1856,1857],{"class":99},").to_excel(xw, ",[40,1859,1860],{"class":164},"sheet_name",[40,1862,122],{"class":95},[40,1864,1865],{"class":50},"\"By product\"",[40,1867,131],{"class":99},[40,1869,1870,1873,1875,1877,1879,1881,1884],{"class":42,"line":154},[40,1871,1872],{"class":99},"    by_customer.round(",[40,1874,289],{"class":202},[40,1876,1857],{"class":99},[40,1878,1860],{"class":164},[40,1880,122],{"class":95},[40,1882,1883],{"class":50},"\"By customer\"",[40,1885,131],{"class":99},[40,1887,1888,1891,1893,1895,1897,1899,1902],{"class":42,"line":209},[40,1889,1890],{"class":99},"    bridge.round(",[40,1892,289],{"class":202},[40,1894,1857],{"class":99},[40,1896,1860],{"class":164},[40,1898,122],{"class":95},[40,1900,1901],{"class":50},"\"Price vs cost\"",[40,1903,131],{"class":99},[40,1905,1906,1909,1911,1913,1916,1918,1920,1922,1924],{"class":42,"line":241},[40,1907,1908],{"class":99},"    losers.to_excel(xw, ",[40,1910,1860],{"class":164},[40,1912,122],{"class":95},[40,1914,1915],{"class":50},"\"Loss-making lines\"",[40,1917,176],{"class":99},[40,1919,365],{"class":164},[40,1921,122],{"class":95},[40,1923,370],{"class":202},[40,1925,131],{"class":99},[40,1927,1928,1931,1934,1936,1938,1940,1942,1944,1947,1949,1951,1954,1956,1958,1960,1962],{"class":42,"line":294},[40,1929,1930],{"class":99},"    lines.drop(",[40,1932,1933],{"class":164},"columns",[40,1935,122],{"class":95},[40,1937,170],{"class":99},[40,1939,493],{"class":50},[40,1941,176],{"class":99},[40,1943,823],{"class":50},[40,1945,1946],{"class":99},"]).to_excel(xw, ",[40,1948,1860],{"class":164},[40,1950,122],{"class":95},[40,1952,1953],{"class":50},"\"Lines\"",[40,1955,176],{"class":99},[40,1957,365],{"class":164},[40,1959,122],{"class":95},[40,1961,370],{"class":202},[40,1963,131],{"class":99},[40,1965,1966,1969,1972,1975,1977,1979,1981,1983],{"class":42,"line":325},[40,1967,1968],{"class":95},"    for",[40,1970,1971],{"class":99}," name ",[40,1973,1974],{"class":95},"in",[40,1976,264],{"class":99},[40,1978,1865],{"class":50},[40,1980,176],{"class":99},[40,1982,1883],{"class":50},[40,1984,1985],{"class":99},"):\n",[40,1987,1988,1991,1993],{"class":42,"line":376},[40,1989,1990],{"class":99},"        ws ",[40,1992,122],{"class":95},[40,1994,1995],{"class":99}," xw.sheets[name]\n",[40,1997,1998,2001,2003],{"class":42,"line":411},[40,1999,2000],{"class":99},"        last ",[40,2002,122],{"class":95},[40,2004,2005],{"class":99}," ws.max_row\n",[40,2007,2008,2011,2014,2017,2021,2024,2027,2030,2033,2036,2038,2041,2043,2046,2048,2050,2053],{"class":42,"line":1380},[40,2009,2010],{"class":99},"        ws.conditional_formatting.add(",[40,2012,2013],{"class":95},"f",[40,2015,2016],{"class":50},"\"F2:F",[40,2018,2020],{"class":2019},"sSjpA","{",[40,2022,2023],{"class":99},"last",[40,2025,2026],{"class":2019},"}",[40,2028,2029],{"class":50},"\"",[40,2031,2032],{"class":99},", CellIsRule(",[40,2034,2035],{"class":164},"operator",[40,2037,122],{"class":95},[40,2039,2040],{"class":50},"\"lessThan\"",[40,2042,176],{"class":99},[40,2044,2045],{"class":164},"formula",[40,2047,122],{"class":95},[40,2049,170],{"class":99},[40,2051,2052],{"class":50},"\"15\"",[40,2054,1664],{"class":99},[40,2056,2058,2061,2063,2066,2069,2071,2074,2076,2079],{"class":42,"line":2057},14,[40,2059,2060],{"class":164},"                                      fill",[40,2062,122],{"class":95},[40,2064,2065],{"class":99},"PatternFill(",[40,2067,2068],{"class":50},"\"solid\"",[40,2070,176],{"class":99},[40,2072,2073],{"class":164},"fgColor",[40,2075,122],{"class":95},[40,2077,2078],{"class":50},"\"FFC7CE\"",[40,2080,2081],{"class":99},")))\n",[40,2083,2085,2087,2089,2091,2093,2095,2097,2099,2101,2103,2105,2108,2110,2112,2114,2116,2119],{"class":42,"line":2084},15,[40,2086,2010],{"class":99},[40,2088,2013],{"class":95},[40,2090,2016],{"class":50},[40,2092,2020],{"class":2019},[40,2094,2023],{"class":99},[40,2096,2026],{"class":2019},[40,2098,2029],{"class":50},[40,2100,2032],{"class":99},[40,2102,2035],{"class":164},[40,2104,122],{"class":95},[40,2106,2107],{"class":50},"\"greaterThanOrEqual\"",[40,2109,176],{"class":99},[40,2111,2045],{"class":164},[40,2113,122],{"class":95},[40,2115,170],{"class":99},[40,2117,2118],{"class":50},"\"30\"",[40,2120,1664],{"class":99},[40,2122,2124,2126,2128,2130,2132,2134,2136,2138,2141],{"class":42,"line":2123},16,[40,2125,2060],{"class":164},[40,2127,122],{"class":95},[40,2129,2065],{"class":99},[40,2131,2068],{"class":50},[40,2133,176],{"class":99},[40,2135,2073],{"class":164},[40,2137,122],{"class":95},[40,2139,2140],{"class":50},"\"C6EFCE\"",[40,2142,2081],{"class":99},[40,2144,2146,2148,2151,2153],{"class":42,"line":2145},17,[40,2147,1968],{"class":95},[40,2149,2150],{"class":99}," ws ",[40,2152,1974],{"class":95},[40,2154,2155],{"class":99}," xw.book.worksheets:\n",[40,2157,2159,2162,2165,2167,2170,2172],{"class":42,"line":2158},18,[40,2160,2161],{"class":95},"        for",[40,2163,2164],{"class":99}," c ",[40,2166,1974],{"class":95},[40,2168,2169],{"class":99}," ws[",[40,2171,267],{"class":202},[40,2173,2174],{"class":99},"]:\n",[40,2176,2178,2181,2183,2186,2189,2191,2194],{"class":42,"line":2177},19,[40,2179,2180],{"class":99},"            c.font ",[40,2182,122],{"class":95},[40,2184,2185],{"class":99}," Font(",[40,2187,2188],{"class":164},"bold",[40,2190,122],{"class":95},[40,2192,2193],{"class":202},"True",[40,2195,131],{"class":99},[40,2197,2199,2202,2205,2208,2210],{"class":42,"line":2198},20,[40,2200,2201],{"class":99},"        ws.column_dimensions[",[40,2203,2204],{"class":50},"\"A\"",[40,2206,2207],{"class":99},"].width ",[40,2209,122],{"class":95},[40,2211,2212],{"class":202}," 18\n",[40,2214,2216,2218,2220,2223,2225,2227,2229,2231,2233,2235,2237,2240,2242,2244,2246,2248,2250,2252],{"class":42,"line":2215},21,[40,2217,328],{"class":202},[40,2219,381],{"class":99},[40,2221,2222],{"class":50},"\"total margin:\"",[40,2224,176],{"class":99},[40,2226,389],{"class":202},[40,2228,392],{"class":99},[40,2230,1150],{"class":50},[40,2232,397],{"class":99},[40,2234,289],{"class":202},[40,2236,402],{"class":99},[40,2238,2239],{"class":50},"\"on revenue\"",[40,2241,176],{"class":99},[40,2243,389],{"class":202},[40,2245,392],{"class":99},[40,2247,299],{"class":50},[40,2249,397],{"class":99},[40,2251,289],{"class":202},[40,2253,373],{"class":99},[10,2255,2256,2257,23],{},"Margin percentages under 15% are shaded red and those of 30% or more green, so weak products and customers stand out on each summary sheet. The Lines sheet keeps every computed column — net price, the cost used, allocated freight — so anyone can trace a figure in the summary back to individual sales. For a monthly pack, the headline margin feeds into ",[19,2258,2260],{"href":2259},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002F","Build a Monthly KPI Scorecard with Targets",[25,2262,2264],{"id":2263},"common-pitfalls","Common pitfalls",[2266,2267,2268,2276,2282,2288,2294],"ul",{},[2269,2270,2271,2275],"li",{},[2272,2273,2274],"strong",{},"One current cost for every sale."," Cost each sale at the standard cost in force on its date.",[2269,2277,2278,2281],{},[2272,2279,2280],{},"Ignoring returns."," Reduce quantity before computing both revenue and cost.",[2269,2283,2284,2287],{},[2272,2285,2286],{},"Freight left at order level."," Allocate it to lines with a driver such as weight.",[2269,2289,2290,2293],{},[2272,2291,2292],{},"Averaging margin percentages."," Divide summed margin by summed revenue.",[2269,2295,2296,2299],{},[2272,2297,2298],{},"Unexplained margin changes."," Separate price effects from cost effects.",[25,2301,2303],{"id":2302},"conclusion","Conclusion",[10,2305,2306,2307,2309],{},"A product margin report combines four sources line by line: net revenue after discounts and returns, the standard cost in force on each sale date via ",[37,2308,455],{},", freight allocated across each order's lines by weight, and the resulting margin. Group it by product and customer, list loss-making lines, split month-on-month changes into price and cost effects, and write it to Excel with colour-coded margins and the full line detail for audit.",[25,2311,2313],{"id":2312},"frequently-asked-questions","Frequently asked questions",[10,2315,2316,2319],{},[2272,2317,2318],{},"How do I calculate product margin from sales data in Python?","\nCompute net revenue per line after discounts and returns, subtract cost of goods at standard cost and any allocated freight, then group by product and divide summed margin by summed revenue.",[10,2321,2322,2325],{},[2272,2323,2324],{},"How do I use the cost that applied on each sale date?","\nUse pandas merge_asof between the sales lines and the dated cost table, matching by SKU with direction backward, so each sale takes the latest cost effective on or before its date.",[10,2327,2328,2331],{},[2272,2329,2330],{},"How do I allocate freight to products?","\nShare each order's freight across its lines in proportion to a driver such as shipped weight, using groupby transform to get each order's total weight.",[10,2333,2334,2337],{},[2272,2335,2336],{},"How do I explain a change in margin?","\nPer unit, the change in margin equals the change in net price minus the change in unit cost, so report the price effect and the cost effect separately.",[25,2339,2341],{"id":2340},"related","Related",[2266,2343,2344,2350,2357,2364],{},[2269,2345,2346,2347,2349],{},"Up one level: ",[19,2348,22],{"href":21}," — finance and operations reports in Excel.",[2269,2351,2352,2356],{},[19,2353,2355],{"href":2354},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-fifo-inventory-valuation-with-python\u002F","Calculate FIFO Inventory Valuation with Python"," — actual rather than standard costs.",[2269,2358,2359,2363],{},[19,2360,2362],{"href":2361},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcompare-supplier-quotes-with-python\u002F","Compare Supplier Quotes with Python"," — attacking the cost side.",[2269,2365,2366,2370],{},[19,2367,2369],{"href":2368},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-commission-report\u002F","Build a Sales Commission Report"," — paying on margin rather than revenue.",[2372,2373,2374],"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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":35,"searchDepth":109,"depth":109,"links":2376},[2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387],{"id":27,"depth":109,"text":28},{"id":79,"depth":109,"text":80},{"id":448,"depth":109,"text":449},{"id":762,"depth":109,"text":763},{"id":1134,"depth":109,"text":1135},{"id":1480,"depth":109,"text":1481},{"id":1784,"depth":109,"text":1785},{"id":2263,"depth":109,"text":2264},{"id":2302,"depth":109,"text":2303},{"id":2312,"depth":109,"text":2313},{"id":2340,"depth":109,"text":2341},"2026-10-02","Work out what each product and customer really earns from sales lines with pandas: net revenue after discounts and returns, the standard cost in force on each sale date with merge_asof, freight allocated by weight, gross margin by product, category and customer, loss-making lines, a price-versus-cost explanation of margin change, and an Excel report with margin colour coding.","md",[2392,2394,2396,2398],{"q":2318,"a":2393},"Compute net revenue per line after discounts and returns, subtract cost of goods at standard cost and any allocated freight, then group by product and divide summed margin by summed revenue.",{"q":2324,"a":2395},"Use pandas merge_asof between the sales lines and the dated cost table, matching by SKU with direction backward, so each sale takes the latest cost effective on or before its date.",{"q":2330,"a":2397},"Share each order's freight across its lines in proportion to a driver such as shipped weight, using groupby transform to get each order's total weight.",{"q":2336,"a":2399},"Per unit, the change in margin equals the change in net price minus the change in unit cost, so report the price effect and the cost effect separately.","build a product margin report",null,{"breadcrumb":2403},[2404,2406,2409],{"name":2405,"item":280},"Home",{"name":2407,"item":2408},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2410,"item":21},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report",{"title":2413,"description":2414},"Build a Product Margin Report in Excel with Python","Product and customer margin from sales lines: discounts and returns, dated standard costs with merge_asof, freight allocation, margin by product and customer, negative-margin lines, price and cost effects, Excel output.","build-a-product-margin-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-product-margin-report\u002Findex",[2418,2419],"report-layouts","pivots-and-summaries","how-to","utdqHLVl2tVVDduBT7WgZsg0ATuMyU2ionSdRPdrh88",[2423,2427],{"title":2424,"path":2425,"stem":2426,"children":-1},"Build a Price-Volume-Mix Analysis","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis\u002Findex",{"title":2428,"path":2429,"stem":2430,"children":-1},"Build a Production Yield and Scrap Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-production-yield-and-scrap-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-production-yield-and-scrap-report\u002Findex",[2432,2434],{"path":2433,"title":2410},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2435,"title":2407},"\u002Fautomating-reporting-workflows",[],[2438,2442,2445,2448,2451,2454],{"path":2439,"title":2440,"kind":2441},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2443,"title":2444,"kind":2420},"\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":2446,"title":2447,"kind":2420},"\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":2449,"title":2450,"kind":2420},"\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":2452,"title":2453,"kind":2420},"\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":2455,"title":2456,"kind":2420},"\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",1790972126131]