[{"data":1,"prerenderedAt":2837},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report":2802,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report":2811,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report":2816,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report":2817},{"id":4,"title":5,"body":6,"dateModified":2768,"datePublished":2768,"description":2769,"extension":2770,"faq":2771,"keyword":2780,"kind":2781,"meta":2782,"navigation":103,"path":2791,"seo":2792,"slug":2795,"stem":2796,"topics":2797,"type":2800,"__hash__":2801},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report\u002Findex.md","Build a Project Budget Burn Report",{"type":7,"value":8,"toc":2756},"minimark",[9,19,24,53,68,72,75,502,513,517,525,782,785,935,939,942,1357,1368,1372,1375,1625,1628,1770,1774,1777,2493,2511,2641,2645,2684,2688,2691,2695,2701,2707,2713,2719,2723,2752],[10,11,12,13,18],"p",{},"Every project sponsor asks the same two questions: how much of the budget have we spent, and are we going to finish within it? \"We have spent 52% of the budget\" means little on its own — it is good news halfway through the timeline with 60% of the work done, and bad news with 30% done. A burn report puts the three numbers side by side: spend against a time-phased plan, how much work is actually complete, and a forecast of the final cost. The data is usually scattered — hours in a timesheet export, supplier invoices in an accounts payable list, the budget in a planning workbook — and pulling it together every week by hand is exactly the job to automate. This page builds the report with pandas, adds earned-value measures, and writes a workbook with a burn chart. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55,56,59,60,63,64,67],{},"Three inputs: ",[32,57,58],{},"project_plan.xlsx"," with each workstream's budget, planned start and end week and the project manager's percent-complete estimate; ",[32,61,62],{},"timesheets.xlsx"," with hours per person per day and their cost rate; and ",[32,65,66],{},"invoices.xlsx"," with supplier invoices tagged to a workstream. The project started on 6 July 2026, runs 16 weeks, and the report is at 27 September — the end of project week 11, counting the first week as week 0.",[20,69,71],{"id":70},"step-1-weekly-actuals-from-hours-and-invoices","Step 1 — Weekly actuals from hours and invoices",[10,73,74],{},"Labour cost is hours times rate; supplier costs come from invoices. Both are bucketed into project weeks and combined:",[25,76,80],{"className":77,"code":78,"language":79,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nplan = pd.read_excel(\"project_plan.xlsx\").set_index(\"Workstream\")\nts = pd.read_excel(\"timesheets.xlsx\")\ninv = pd.read_excel(\"invoices.xlsx\")\nstart = pd.Timestamp(\"2026-07-06\")\nas_of = pd.Timestamp(\"2026-09-27\")\n\nts[\"Cost\"] = ts[\"Hours\"] * ts[\"Rate\"]\nlabour = ts.assign(Week=(ts[\"Date\"] - start).dt.days \u002F\u002F 7).groupby([\"Week\", \"Workstream\"])[\"Cost\"].sum()\nsuppliers = inv.assign(Week=(inv[\"Date\"] - start).dt.days \u002F\u002F 7).groupby([\"Week\", \"Workstream\"])[\"Amount\"].sum()\n\nactual = (pd.concat([labour, suppliers], axis=1).fillna(0).sum(axis=1)\n            .unstack(\"Workstream\", fill_value=0).reindex(columns=plan.index, fill_value=0))\nactual = actual.reindex(range(int((as_of - start).days \u002F\u002F 7) + 1), fill_value=0)\nprint(actual.round(0).astype(int).tail(4).to_string())\nprint(\"spent to date:\", round(actual.to_numpy().sum()))\n","python",[32,81,82,98,105,129,144,159,175,190,195,228,285,330,335,371,408,459,484],{"__ignoreMap":30},[35,83,84,88,92,95],{"class":37,"line":38},[35,85,87],{"class":86},"s-kum","import",[35,89,91],{"class":90},"skGVy"," pandas ",[35,93,94],{"class":86},"as",[35,96,97],{"class":90}," pd\n",[35,99,101],{"class":37,"line":100},2,[35,102,104],{"emptyLinePlaceholder":103},true,"\n",[35,106,108,111,114,117,120,123,126],{"class":37,"line":107},3,[35,109,110],{"class":90},"plan ",[35,112,113],{"class":86},"=",[35,115,116],{"class":90}," pd.read_excel(",[35,118,119],{"class":45},"\"project_plan.xlsx\"",[35,121,122],{"class":90},").set_index(",[35,124,125],{"class":45},"\"Workstream\"",[35,127,128],{"class":90},")\n",[35,130,132,135,137,139,142],{"class":37,"line":131},4,[35,133,134],{"class":90},"ts ",[35,136,113],{"class":86},[35,138,116],{"class":90},[35,140,141],{"class":45},"\"timesheets.xlsx\"",[35,143,128],{"class":90},[35,145,147,150,152,154,157],{"class":37,"line":146},5,[35,148,149],{"class":90},"inv ",[35,151,113],{"class":86},[35,153,116],{"class":90},[35,155,156],{"class":45},"\"invoices.xlsx\"",[35,158,128],{"class":90},[35,160,162,165,167,170,173],{"class":37,"line":161},6,[35,163,164],{"class":90},"start ",[35,166,113],{"class":86},[35,168,169],{"class":90}," pd.Timestamp(",[35,171,172],{"class":45},"\"2026-07-06\"",[35,174,128],{"class":90},[35,176,178,181,183,185,188],{"class":37,"line":177},7,[35,179,180],{"class":90},"as_of ",[35,182,113],{"class":86},[35,184,169],{"class":90},[35,186,187],{"class":45},"\"2026-09-27\"",[35,189,128],{"class":90},[35,191,193],{"class":37,"line":192},8,[35,194,104],{"emptyLinePlaceholder":103},[35,196,198,201,204,207,209,212,215,217,220,222,225],{"class":37,"line":197},9,[35,199,200],{"class":90},"ts[",[35,202,203],{"class":45},"\"Cost\"",[35,205,206],{"class":90},"] ",[35,208,113],{"class":86},[35,210,211],{"class":90}," ts[",[35,213,214],{"class":45},"\"Hours\"",[35,216,206],{"class":90},[35,218,219],{"class":86},"*",[35,221,211],{"class":90},[35,223,224],{"class":45},"\"Rate\"",[35,226,227],{"class":90},"]\n",[35,229,231,234,236,239,243,245,248,251,253,256,259,262,266,269,272,275,277,280,282],{"class":37,"line":230},10,[35,232,233],{"class":90},"labour ",[35,235,113],{"class":86},[35,237,238],{"class":90}," ts.assign(",[35,240,242],{"class":241},"sa561","Week",[35,244,113],{"class":86},[35,246,247],{"class":90},"(ts[",[35,249,250],{"class":45},"\"Date\"",[35,252,206],{"class":90},[35,254,255],{"class":86},"-",[35,257,258],{"class":90}," start).dt.days ",[35,260,261],{"class":86},"\u002F\u002F",[35,263,265],{"class":264},"sP0c6"," 7",[35,267,268],{"class":90},").groupby([",[35,270,271],{"class":45},"\"Week\"",[35,273,274],{"class":90},", ",[35,276,125],{"class":45},[35,278,279],{"class":90},"])[",[35,281,203],{"class":45},[35,283,284],{"class":90},"].sum()\n",[35,286,288,291,293,296,298,300,303,305,307,309,311,313,315,317,319,321,323,325,328],{"class":37,"line":287},11,[35,289,290],{"class":90},"suppliers ",[35,292,113],{"class":86},[35,294,295],{"class":90}," inv.assign(",[35,297,242],{"class":241},[35,299,113],{"class":86},[35,301,302],{"class":90},"(inv[",[35,304,250],{"class":45},[35,306,206],{"class":90},[35,308,255],{"class":86},[35,310,258],{"class":90},[35,312,261],{"class":86},[35,314,265],{"class":264},[35,316,268],{"class":90},[35,318,271],{"class":45},[35,320,274],{"class":90},[35,322,125],{"class":45},[35,324,279],{"class":90},[35,326,327],{"class":45},"\"Amount\"",[35,329,284],{"class":90},[35,331,333],{"class":37,"line":332},12,[35,334,104],{"emptyLinePlaceholder":103},[35,336,338,341,343,346,349,351,354,357,360,363,365,367,369],{"class":37,"line":337},13,[35,339,340],{"class":90},"actual ",[35,342,113],{"class":86},[35,344,345],{"class":90}," (pd.concat([labour, suppliers], ",[35,347,348],{"class":241},"axis",[35,350,113],{"class":86},[35,352,353],{"class":264},"1",[35,355,356],{"class":90},").fillna(",[35,358,359],{"class":264},"0",[35,361,362],{"class":90},").sum(",[35,364,348],{"class":241},[35,366,113],{"class":86},[35,368,353],{"class":264},[35,370,128],{"class":90},[35,372,374,377,379,381,384,386,388,391,394,396,399,401,403,405],{"class":37,"line":373},14,[35,375,376],{"class":90},"            .unstack(",[35,378,125],{"class":45},[35,380,274],{"class":90},[35,382,383],{"class":241},"fill_value",[35,385,113],{"class":86},[35,387,359],{"class":264},[35,389,390],{"class":90},").reindex(",[35,392,393],{"class":241},"columns",[35,395,113],{"class":86},[35,397,398],{"class":90},"plan.index, ",[35,400,383],{"class":241},[35,402,113],{"class":86},[35,404,359],{"class":264},[35,406,407],{"class":90},"))\n",[35,409,411,413,415,418,421,424,427,430,432,435,437,439,442,445,448,451,453,455,457],{"class":37,"line":410},15,[35,412,340],{"class":90},[35,414,113],{"class":86},[35,416,417],{"class":90}," actual.reindex(",[35,419,420],{"class":264},"range",[35,422,423],{"class":90},"(",[35,425,426],{"class":264},"int",[35,428,429],{"class":90},"((as_of ",[35,431,255],{"class":86},[35,433,434],{"class":90}," start).days ",[35,436,261],{"class":86},[35,438,265],{"class":264},[35,440,441],{"class":90},") ",[35,443,444],{"class":86},"+",[35,446,447],{"class":264}," 1",[35,449,450],{"class":90},"), ",[35,452,383],{"class":241},[35,454,113],{"class":86},[35,456,359],{"class":264},[35,458,128],{"class":90},[35,460,462,465,468,470,473,475,478,481],{"class":37,"line":461},16,[35,463,464],{"class":264},"print",[35,466,467],{"class":90},"(actual.round(",[35,469,359],{"class":264},[35,471,472],{"class":90},").astype(",[35,474,426],{"class":264},[35,476,477],{"class":90},").tail(",[35,479,480],{"class":264},"4",[35,482,483],{"class":90},").to_string())\n",[35,485,487,489,491,494,496,499],{"class":37,"line":486},17,[35,488,464],{"class":264},[35,490,423],{"class":90},[35,492,493],{"class":45},"\"spent to date:\"",[35,495,274],{"class":90},[35,497,498],{"class":264},"round",[35,500,501],{"class":90},"(actual.to_numpy().sum()))\n",[10,503,504,505,508,509,512],{},"Week numbers are counted from the project start, so week 0 is the first week; this lines actuals up with the plan, which is expressed in project weeks too. ",[32,506,507],{},"reindex"," fills weeks with no cost with zeros so the cumulative series has no gaps. Combining the two sources with ",[32,510,511],{},"concat(..., axis=1)"," and summing across keeps labour and supplier costs side by side until the last moment, which is useful if the report later needs to show them separately.",[20,514,516],{"id":515},"step-2-a-time-phased-budget-to-compare-with","Step 2 — A time-phased budget to compare with",[10,518,519,520,524],{},"A budget total is not a plan. Spread each workstream's budget evenly across its planned weeks to get the ",[521,522,523],"em",{},"planned value"," — what should have been spent by each week:",[25,526,528],{"className":77,"code":527,"language":79,"meta":30,"style":30},"n_weeks = 16\nplanned = pd.DataFrame(0.0, index=range(n_weeks), columns=plan.index)\nfor ws, row in plan.iterrows():\n    weeks_span = range(int(row[\"Start week\"]), int(row[\"End week\"]) + 1)\n    planned.loc[list(weeks_span), ws] = row[\"Budget\"] \u002F len(weeks_span)\n\ncum_plan = planned.sum(axis=1).cumsum()\ncum_actual = actual.sum(axis=1).cumsum()\nweek_now = cum_actual.index[-1]\nprint(f\"week {week_now}: planned to date {cum_plan[week_now]:,.0f}, actual {cum_actual[week_now]:,.0f}, budget {plan['Budget'].sum():,.0f}\")\n",[32,529,530,540,572,586,625,655,659,678,696,712],{"__ignoreMap":30},[35,531,532,535,537],{"class":37,"line":38},[35,533,534],{"class":90},"n_weeks ",[35,536,113],{"class":86},[35,538,539],{"class":264}," 16\n",[35,541,542,545,547,550,553,555,558,560,562,565,567,569],{"class":37,"line":100},[35,543,544],{"class":90},"planned ",[35,546,113],{"class":86},[35,548,549],{"class":90}," pd.DataFrame(",[35,551,552],{"class":264},"0.0",[35,554,274],{"class":90},[35,556,557],{"class":241},"index",[35,559,113],{"class":86},[35,561,420],{"class":264},[35,563,564],{"class":90},"(n_weeks), ",[35,566,393],{"class":241},[35,568,113],{"class":86},[35,570,571],{"class":90},"plan.index)\n",[35,573,574,577,580,583],{"class":37,"line":107},[35,575,576],{"class":86},"for",[35,578,579],{"class":90}," ws, row ",[35,581,582],{"class":86},"in",[35,584,585],{"class":90}," plan.iterrows():\n",[35,587,588,591,593,596,598,600,603,606,609,611,613,616,619,621,623],{"class":37,"line":131},[35,589,590],{"class":90},"    weeks_span ",[35,592,113],{"class":86},[35,594,595],{"class":264}," range",[35,597,423],{"class":90},[35,599,426],{"class":264},[35,601,602],{"class":90},"(row[",[35,604,605],{"class":45},"\"Start week\"",[35,607,608],{"class":90},"]), ",[35,610,426],{"class":264},[35,612,602],{"class":90},[35,614,615],{"class":45},"\"End week\"",[35,617,618],{"class":90},"]) ",[35,620,444],{"class":86},[35,622,447],{"class":264},[35,624,128],{"class":90},[35,626,627,630,633,636,638,641,644,646,649,652],{"class":37,"line":146},[35,628,629],{"class":90},"    planned.loc[",[35,631,632],{"class":264},"list",[35,634,635],{"class":90},"(weeks_span), ws] ",[35,637,113],{"class":86},[35,639,640],{"class":90}," row[",[35,642,643],{"class":45},"\"Budget\"",[35,645,206],{"class":90},[35,647,648],{"class":86},"\u002F",[35,650,651],{"class":264}," len",[35,653,654],{"class":90},"(weeks_span)\n",[35,656,657],{"class":37,"line":161},[35,658,104],{"emptyLinePlaceholder":103},[35,660,661,664,666,669,671,673,675],{"class":37,"line":177},[35,662,663],{"class":90},"cum_plan ",[35,665,113],{"class":86},[35,667,668],{"class":90}," planned.sum(",[35,670,348],{"class":241},[35,672,113],{"class":86},[35,674,353],{"class":264},[35,676,677],{"class":90},").cumsum()\n",[35,679,680,683,685,688,690,692,694],{"class":37,"line":192},[35,681,682],{"class":90},"cum_actual ",[35,684,113],{"class":86},[35,686,687],{"class":90}," actual.sum(",[35,689,348],{"class":241},[35,691,113],{"class":86},[35,693,353],{"class":264},[35,695,677],{"class":90},[35,697,698,701,703,706,708,710],{"class":37,"line":197},[35,699,700],{"class":90},"week_now ",[35,702,113],{"class":86},[35,704,705],{"class":90}," cum_actual.index[",[35,707,255],{"class":86},[35,709,353],{"class":264},[35,711,227],{"class":90},[35,713,714,716,718,721,724,728,731,734,737,739,742,745,747,750,752,755,757,759,762,764,767,770,773,775,777,780],{"class":37,"line":230},[35,715,464],{"class":264},[35,717,423],{"class":90},[35,719,720],{"class":86},"f",[35,722,723],{"class":45},"\"week ",[35,725,727],{"class":726},"sSjpA","{",[35,729,730],{"class":90},"week_now",[35,732,733],{"class":726},"}",[35,735,736],{"class":45},": planned to date ",[35,738,727],{"class":726},[35,740,741],{"class":90},"cum_plan[week_now]",[35,743,744],{"class":86},":,.0f",[35,746,733],{"class":726},[35,748,749],{"class":45},", actual ",[35,751,727],{"class":726},[35,753,754],{"class":90},"cum_actual[week_now]",[35,756,744],{"class":86},[35,758,733],{"class":726},[35,760,761],{"class":45},", budget ",[35,763,727],{"class":726},[35,765,766],{"class":90},"plan[",[35,768,769],{"class":45},"'Budget'",[35,771,772],{"class":90},"].sum()",[35,774,744],{"class":86},[35,776,733],{"class":726},[35,778,779],{"class":45},"\"",[35,781,128],{"class":90},[10,783,784],{},"An even spread is the simplest phasing and often good enough; if the plan has front-loaded or back-loaded spend, read the weekly planned figures from the planning workbook instead. By the end of week 11 the plan expected 123,636 of the 160,000 budget to be spent; actual spend is 88,600. On its own that looks like an underspend — but whether it is good or bad depends on how much work was done for it, which is the next step.",[786,787,795,796,795,800,795,804,795,810,795,819,795,826,795,831,795,835,795,841,795,846,795,850,795,854,795,857,795,861,795,864,795,868,795,871,795,877,795,881,795,885,795,889,795,893,795,899,795,905,795,908,795,911,795,914,795,917,795,920,795,923,795,926,795,929,795,932],"svg",{"viewBox":788,"role":789,"ariaLabelledBy":790,"xmlns":793,"style":794},"4 0 748 280","img",[791,792],"pb-burn-t","pb-burn-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[797,798,799],"title",{"id":791},"Cumulative spend against plan",[801,802,803],"desc",{"id":792},"Planned cumulative spend reaches 123,636 by week 11 on the way to the 160,000 budget. Actual cumulative spend tracks below it, reaching 88,600 by week 11, while the earned value of the work completed is 92,500.",[805,806],"rect",{"x":480,"y":359,"width":807,"height":808,"fill":809},"748","280","#ffffff",[805,811],{"x":812,"y":813,"width":814,"height":814,"rx":815,"fill":816,"stroke":817,"style":818},"64.0","18.0","14.0","3","#e7ebef","var(--line,#cdd5e6)","stroke-width:1.5px",[820,821,825],"text",{"x":822,"y":823,"style":824},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","Planned",[805,827],{"x":828,"y":813,"width":814,"height":814,"rx":815,"fill":829,"stroke":830,"style":818},"162.0","#f0f4ff","var(--brand,#5b5cf0)",[820,832,834],{"x":833,"y":823,"style":824},"182.0","Actual",[37,836],{"x1":837,"y1":838,"x2":839,"y2":838,"stroke":817,"style":840},"57.6","242.0","736.0","stroke-width:1px",[820,842,359],{"x":843,"y":844,"style":845},"32.8","246.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[37,847],{"x1":837,"y1":848,"x2":839,"y2":848,"stroke":817,"style":849},"178.7","stroke-width:1px;stroke-dasharray:5 4",[820,851,853],{"x":843,"y":852,"style":845},"182.7","50k",[37,855],{"x1":837,"y1":856,"x2":839,"y2":856,"stroke":817,"style":849},"115.3",[820,858,860],{"x":843,"y":859,"style":845},"119.3","100k",[37,862],{"x1":837,"y1":863,"x2":839,"y2":863,"stroke":817,"style":849},"52.0",[820,865,867],{"x":843,"y":866,"style":845},"56.0","150k",[37,869],{"x1":837,"y1":838,"x2":839,"y2":838,"stroke":870,"style":818},"var(--muted,#5b6780)",[820,872,876],{"x":873,"y":874,"style":875},"125.5","260.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","W0",[820,878,880],{"x":879,"y":874,"style":875},"261.1","W3",[820,882,884],{"x":883,"y":874,"style":875},"396.8","W6",[820,886,888],{"x":887,"y":874,"style":875},"532.5","W9",[820,890,892],{"x":891,"y":874,"style":875},"668.2","W11",[894,895],"path",{"d":896,"fill":897,"stroke":870,"style":898},"M125.5,233.6 L261.1,197.9 L396.8,149.9 L532.5,118.8 L668.2,85.4","none","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[900,901],"circle",{"cx":873,"cy":902,"r":903,"fill":809,"stroke":870,"style":904},"233.6","4.0","stroke-width:2px",[900,906],{"cx":879,"cy":907,"r":903,"fill":809,"stroke":870,"style":904},"197.9",[900,909],{"cx":883,"cy":910,"r":903,"fill":809,"stroke":870,"style":904},"149.9",[900,912],{"cx":887,"cy":913,"r":903,"fill":809,"stroke":870,"style":904},"118.8",[900,915],{"cx":891,"cy":916,"r":903,"fill":809,"stroke":870,"style":904},"85.4",[894,918],{"d":919,"fill":897,"stroke":830,"style":898},"M125.5,238.0 L261.1,214.1 L396.8,179.3 L532.5,153.3 L668.2,129.8",[900,921],{"cx":873,"cy":922,"r":903,"fill":809,"stroke":830,"style":904},"238.0",[900,924],{"cx":879,"cy":925,"r":903,"fill":809,"stroke":830,"style":904},"214.1",[900,927],{"cx":883,"cy":928,"r":903,"fill":809,"stroke":830,"style":904},"179.3",[900,930],{"cx":887,"cy":931,"r":903,"fill":809,"stroke":830,"style":904},"153.3",[900,933],{"cx":891,"cy":934,"r":903,"fill":809,"stroke":830,"style":904},"129.8",[20,936,938],{"id":937},"step-3-earned-value-is-the-spend-buying-progress","Step 3 — Earned value: is the spend buying progress?",[10,940,941],{},"Earned value (EV) is the budget of the work actually done: a workstream's budget times its percent complete. Comparing it with actual cost (AC) and planned value (PV) gives two indices:",[25,943,945],{"className":77,"code":944,"language":79,"meta":30,"style":30},"pv = planned.loc[:week_now].sum()\nac = actual.sum()\nev = plan[\"Budget\"] * plan[\"% complete\"] \u002F 100\n\nev_table = pd.DataFrame({\"Budget\": plan[\"Budget\"], \"PV\": pv, \"EV\": ev, \"AC\": ac})\nev_table[\"CPI\"] = (ev_table[\"EV\"] \u002F ev_table[\"AC\"]).round(2)        # cost performance\nev_table[\"SPI\"] = (ev_table[\"EV\"] \u002F ev_table[\"PV\"]).round(2)        # schedule performance\nev_table[\"EAC\"] = (ev_table[\"Budget\"] \u002F ev_table[\"CPI\"]).round(0)    # estimate at completion\nev_table.loc[\"Project\"] = ev_table[[\"Budget\", \"PV\", \"EV\", \"AC\"]].sum().tolist() + [None] * 3\ntot = ev_table.loc[\"Project\"]\nev_table.loc[\"Project\", [\"CPI\", \"SPI\"]] = [round(tot[\"EV\"] \u002F tot[\"AC\"], 2), round(tot[\"EV\"] \u002F tot[\"PV\"], 2)]\nev_table.loc[\"Project\", \"EAC\"] = round(tot[\"Budget\"] \u002F ev_table.loc[\"Project\", \"CPI\"])\nprint(ev_table.round({\"Budget\": 0, \"PV\": 0, \"EV\": 0, \"AC\": 0, \"EAC\": 0}).to_string())\n",[32,946,947,957,967,1000,1004,1042,1081,1113,1146,1193,1207,1272,1308],{"__ignoreMap":30},[35,948,949,952,954],{"class":37,"line":38},[35,950,951],{"class":90},"pv ",[35,953,113],{"class":86},[35,955,956],{"class":90}," planned.loc[:week_now].sum()\n",[35,958,959,962,964],{"class":37,"line":100},[35,960,961],{"class":90},"ac ",[35,963,113],{"class":86},[35,965,966],{"class":90}," actual.sum()\n",[35,968,969,972,974,977,979,981,983,985,987,990,993,995,997],{"class":37,"line":107},[35,970,971],{"class":90},"ev ",[35,973,113],{"class":86},[35,975,976],{"class":90}," plan[",[35,978,643],{"class":45},[35,980,206],{"class":90},[35,982,219],{"class":86},[35,984,976],{"class":90},[35,986,779],{"class":45},[35,988,989],{"class":726},"% c",[35,991,992],{"class":45},"omplete\"",[35,994,206],{"class":90},[35,996,648],{"class":86},[35,998,999],{"class":264}," 100\n",[35,1001,1002],{"class":37,"line":131},[35,1003,104],{"emptyLinePlaceholder":103},[35,1005,1006,1009,1011,1014,1016,1019,1021,1024,1027,1030,1033,1036,1039],{"class":37,"line":146},[35,1007,1008],{"class":90},"ev_table ",[35,1010,113],{"class":86},[35,1012,1013],{"class":90}," pd.DataFrame({",[35,1015,643],{"class":45},[35,1017,1018],{"class":90},": plan[",[35,1020,643],{"class":45},[35,1022,1023],{"class":90},"], ",[35,1025,1026],{"class":45},"\"PV\"",[35,1028,1029],{"class":90},": pv, ",[35,1031,1032],{"class":45},"\"EV\"",[35,1034,1035],{"class":90},": ev, ",[35,1037,1038],{"class":45},"\"AC\"",[35,1040,1041],{"class":90},": ac})\n",[35,1043,1044,1047,1050,1052,1054,1057,1059,1061,1063,1066,1068,1071,1074,1077],{"class":37,"line":161},[35,1045,1046],{"class":90},"ev_table[",[35,1048,1049],{"class":45},"\"CPI\"",[35,1051,206],{"class":90},[35,1053,113],{"class":86},[35,1055,1056],{"class":90}," (ev_table[",[35,1058,1032],{"class":45},[35,1060,206],{"class":90},[35,1062,648],{"class":86},[35,1064,1065],{"class":90}," ev_table[",[35,1067,1038],{"class":45},[35,1069,1070],{"class":90},"]).round(",[35,1072,1073],{"class":264},"2",[35,1075,1076],{"class":90},")        ",[35,1078,1080],{"class":1079},"s-wDw","# cost performance\n",[35,1082,1083,1085,1088,1090,1092,1094,1096,1098,1100,1102,1104,1106,1108,1110],{"class":37,"line":177},[35,1084,1046],{"class":90},[35,1086,1087],{"class":45},"\"SPI\"",[35,1089,206],{"class":90},[35,1091,113],{"class":86},[35,1093,1056],{"class":90},[35,1095,1032],{"class":45},[35,1097,206],{"class":90},[35,1099,648],{"class":86},[35,1101,1065],{"class":90},[35,1103,1026],{"class":45},[35,1105,1070],{"class":90},[35,1107,1073],{"class":264},[35,1109,1076],{"class":90},[35,1111,1112],{"class":1079},"# schedule performance\n",[35,1114,1115,1117,1120,1122,1124,1126,1128,1130,1132,1134,1136,1138,1140,1143],{"class":37,"line":192},[35,1116,1046],{"class":90},[35,1118,1119],{"class":45},"\"EAC\"",[35,1121,206],{"class":90},[35,1123,113],{"class":86},[35,1125,1056],{"class":90},[35,1127,643],{"class":45},[35,1129,206],{"class":90},[35,1131,648],{"class":86},[35,1133,1065],{"class":90},[35,1135,1049],{"class":45},[35,1137,1070],{"class":90},[35,1139,359],{"class":264},[35,1141,1142],{"class":90},")    ",[35,1144,1145],{"class":1079},"# estimate at completion\n",[35,1147,1148,1151,1154,1156,1158,1161,1163,1165,1167,1169,1171,1173,1175,1178,1180,1183,1186,1188,1190],{"class":37,"line":197},[35,1149,1150],{"class":90},"ev_table.loc[",[35,1152,1153],{"class":45},"\"Project\"",[35,1155,206],{"class":90},[35,1157,113],{"class":86},[35,1159,1160],{"class":90}," ev_table[[",[35,1162,643],{"class":45},[35,1164,274],{"class":90},[35,1166,1026],{"class":45},[35,1168,274],{"class":90},[35,1170,1032],{"class":45},[35,1172,274],{"class":90},[35,1174,1038],{"class":45},[35,1176,1177],{"class":90},"]].sum().tolist() ",[35,1179,444],{"class":86},[35,1181,1182],{"class":90}," [",[35,1184,1185],{"class":264},"None",[35,1187,206],{"class":90},[35,1189,219],{"class":86},[35,1191,1192],{"class":264}," 3\n",[35,1194,1195,1198,1200,1203,1205],{"class":37,"line":230},[35,1196,1197],{"class":90},"tot ",[35,1199,113],{"class":86},[35,1201,1202],{"class":90}," ev_table.loc[",[35,1204,1153],{"class":45},[35,1206,227],{"class":90},[35,1208,1209,1211,1213,1216,1218,1220,1222,1225,1227,1229,1231,1234,1236,1238,1240,1243,1245,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1269],{"class":37,"line":287},[35,1210,1150],{"class":90},[35,1212,1153],{"class":45},[35,1214,1215],{"class":90},", [",[35,1217,1049],{"class":45},[35,1219,274],{"class":90},[35,1221,1087],{"class":45},[35,1223,1224],{"class":90},"]] ",[35,1226,113],{"class":86},[35,1228,1182],{"class":90},[35,1230,498],{"class":264},[35,1232,1233],{"class":90},"(tot[",[35,1235,1032],{"class":45},[35,1237,206],{"class":90},[35,1239,648],{"class":86},[35,1241,1242],{"class":90}," tot[",[35,1244,1038],{"class":45},[35,1246,1023],{"class":90},[35,1248,1073],{"class":264},[35,1250,450],{"class":90},[35,1252,498],{"class":264},[35,1254,1233],{"class":90},[35,1256,1032],{"class":45},[35,1258,206],{"class":90},[35,1260,648],{"class":86},[35,1262,1242],{"class":90},[35,1264,1026],{"class":45},[35,1266,1023],{"class":90},[35,1268,1073],{"class":264},[35,1270,1271],{"class":90},")]\n",[35,1273,1274,1276,1278,1280,1282,1284,1286,1289,1291,1293,1295,1297,1299,1301,1303,1305],{"class":37,"line":332},[35,1275,1150],{"class":90},[35,1277,1153],{"class":45},[35,1279,274],{"class":90},[35,1281,1119],{"class":45},[35,1283,206],{"class":90},[35,1285,113],{"class":86},[35,1287,1288],{"class":264}," round",[35,1290,1233],{"class":90},[35,1292,643],{"class":45},[35,1294,206],{"class":90},[35,1296,648],{"class":86},[35,1298,1202],{"class":90},[35,1300,1153],{"class":45},[35,1302,274],{"class":90},[35,1304,1049],{"class":45},[35,1306,1307],{"class":90},"])\n",[35,1309,1310,1312,1315,1317,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338,1340,1342,1344,1346,1348,1350,1352,1354],{"class":37,"line":337},[35,1311,464],{"class":264},[35,1313,1314],{"class":90},"(ev_table.round({",[35,1316,643],{"class":45},[35,1318,1319],{"class":90},": ",[35,1321,359],{"class":264},[35,1323,274],{"class":90},[35,1325,1026],{"class":45},[35,1327,1319],{"class":90},[35,1329,359],{"class":264},[35,1331,274],{"class":90},[35,1333,1032],{"class":45},[35,1335,1319],{"class":90},[35,1337,359],{"class":264},[35,1339,274],{"class":90},[35,1341,1038],{"class":45},[35,1343,1319],{"class":90},[35,1345,359],{"class":264},[35,1347,274],{"class":90},[35,1349,1119],{"class":45},[35,1351,1319],{"class":90},[35,1353,359],{"class":264},[35,1355,1356],{"class":90},"}).to_string())\n",[10,1358,1359,1360,1363,1364,1367],{},"The cost performance index (CPI = EV \u002F AC) says how much work each unit of spend buys: below 1.0 means over cost. The schedule performance index (SPI = EV \u002F PV) compares work done with work planned: below 1.0 means behind schedule. Dividing the budget by the CPI gives a simple ",[521,1361,1362],{},"estimate at completion"," — what the project will cost if it keeps spending at the current efficiency. For the project as a whole, CPI is 1.04 — the 88,600 spent has bought 92,500 of planned work, so cost efficiency is fine — but SPI is 0.75: only three quarters of the work planned by now is done. The underspend from Step 2 is really a ",[521,1365,1366],{},"delay",". Workstream by workstream, Design is complete and came in well under budget (CPI 1.39); Build is roughly on cost (CPI 0.95) but behind (SPI 0.67); Test has spent 7,700 for an estimated 10% of its work. Test's forecast of 76,923 shows the limit of budget ÷ CPI: early in a workstream, a few weeks of set-up cost against little measurable progress produce a wildly pessimistic estimate, so treat EAC as meaningful only once a workstream is well under way. Percent complete is the weak link here, since it is an estimate — ask for it per deliverable rather than as a gut feeling for the whole workstream.",[20,1369,1371],{"id":1370},"step-4-flag-workstreams-at-risk","Step 4 — Flag workstreams at risk",[10,1373,1374],{},"Turn the indices into a short list the sponsor can act on:",[25,1376,1378],{"className":77,"code":1377,"language":79,"meta":30,"style":30},"def flag(row):\n    issues = []\n    if row[\"CPI\"] \u003C 0.95:\n        issues.append(f\"over cost (CPI {row['CPI']:.2f})\")\n    if row[\"SPI\"] \u003C 0.90:\n        issues.append(f\"behind plan (SPI {row['SPI']:.2f})\")\n    if row[\"EAC\"] > row[\"Budget\"] * 1.05:\n        issues.append(f\"forecast {row['EAC'] - row['Budget']:+,.0f} vs budget\")\n    return \"; \".join(issues) or \"on track\"\n\nev_table[\"Status\"] = ev_table.apply(flag, axis=1)\nprint(ev_table[[\"CPI\", \"SPI\", \"EAC\", \"Status\"]].to_string())\n",[32,1379,1380,1392,1402,1422,1453,1470,1496,1522,1558,1575,1579,1601],{"__ignoreMap":30},[35,1381,1382,1385,1389],{"class":37,"line":38},[35,1383,1384],{"class":86},"def",[35,1386,1388],{"class":1387},"s_Opv"," flag",[35,1390,1391],{"class":90},"(row):\n",[35,1393,1394,1397,1399],{"class":37,"line":100},[35,1395,1396],{"class":90},"    issues ",[35,1398,113],{"class":86},[35,1400,1401],{"class":90}," []\n",[35,1403,1404,1407,1409,1411,1413,1416,1419],{"class":37,"line":107},[35,1405,1406],{"class":86},"    if",[35,1408,640],{"class":90},[35,1410,1049],{"class":45},[35,1412,206],{"class":90},[35,1414,1415],{"class":86},"\u003C",[35,1417,1418],{"class":264}," 0.95",[35,1420,1421],{"class":90},":\n",[35,1423,1424,1427,1429,1432,1434,1437,1440,1443,1446,1448,1451],{"class":37,"line":131},[35,1425,1426],{"class":90},"        issues.append(",[35,1428,720],{"class":86},[35,1430,1431],{"class":45},"\"over cost (CPI ",[35,1433,727],{"class":726},[35,1435,1436],{"class":90},"row[",[35,1438,1439],{"class":45},"'CPI'",[35,1441,1442],{"class":90},"]",[35,1444,1445],{"class":86},":.2f",[35,1447,733],{"class":726},[35,1449,1450],{"class":45},")\"",[35,1452,128],{"class":90},[35,1454,1455,1457,1459,1461,1463,1465,1468],{"class":37,"line":146},[35,1456,1406],{"class":86},[35,1458,640],{"class":90},[35,1460,1087],{"class":45},[35,1462,206],{"class":90},[35,1464,1415],{"class":86},[35,1466,1467],{"class":264}," 0.90",[35,1469,1421],{"class":90},[35,1471,1472,1474,1476,1479,1481,1483,1486,1488,1490,1492,1494],{"class":37,"line":161},[35,1473,1426],{"class":90},[35,1475,720],{"class":86},[35,1477,1478],{"class":45},"\"behind plan (SPI ",[35,1480,727],{"class":726},[35,1482,1436],{"class":90},[35,1484,1485],{"class":45},"'SPI'",[35,1487,1442],{"class":90},[35,1489,1445],{"class":86},[35,1491,733],{"class":726},[35,1493,1450],{"class":45},[35,1495,128],{"class":90},[35,1497,1498,1500,1502,1504,1506,1509,1511,1513,1515,1517,1520],{"class":37,"line":177},[35,1499,1406],{"class":86},[35,1501,640],{"class":90},[35,1503,1119],{"class":45},[35,1505,206],{"class":90},[35,1507,1508],{"class":86},">",[35,1510,640],{"class":90},[35,1512,643],{"class":45},[35,1514,206],{"class":90},[35,1516,219],{"class":86},[35,1518,1519],{"class":264}," 1.05",[35,1521,1421],{"class":90},[35,1523,1524,1526,1528,1531,1533,1535,1538,1540,1542,1544,1546,1548,1551,1553,1556],{"class":37,"line":192},[35,1525,1426],{"class":90},[35,1527,720],{"class":86},[35,1529,1530],{"class":45},"\"forecast ",[35,1532,727],{"class":726},[35,1534,1436],{"class":90},[35,1536,1537],{"class":45},"'EAC'",[35,1539,206],{"class":90},[35,1541,255],{"class":86},[35,1543,640],{"class":90},[35,1545,769],{"class":45},[35,1547,1442],{"class":90},[35,1549,1550],{"class":86},":+,.0f",[35,1552,733],{"class":726},[35,1554,1555],{"class":45}," vs budget\"",[35,1557,128],{"class":90},[35,1559,1560,1563,1566,1569,1572],{"class":37,"line":197},[35,1561,1562],{"class":86},"    return",[35,1564,1565],{"class":45}," \"; \"",[35,1567,1568],{"class":90},".join(issues) ",[35,1570,1571],{"class":86},"or",[35,1573,1574],{"class":45}," \"on track\"\n",[35,1576,1577],{"class":37,"line":230},[35,1578,104],{"emptyLinePlaceholder":103},[35,1580,1581,1583,1586,1588,1590,1593,1595,1597,1599],{"class":37,"line":287},[35,1582,1046],{"class":90},[35,1584,1585],{"class":45},"\"Status\"",[35,1587,206],{"class":90},[35,1589,113],{"class":86},[35,1591,1592],{"class":90}," ev_table.apply(flag, ",[35,1594,348],{"class":241},[35,1596,113],{"class":86},[35,1598,353],{"class":264},[35,1600,128],{"class":90},[35,1602,1603,1605,1608,1610,1612,1614,1616,1618,1620,1622],{"class":37,"line":332},[35,1604,464],{"class":264},[35,1606,1607],{"class":90},"(ev_table[[",[35,1609,1049],{"class":45},[35,1611,274],{"class":90},[35,1613,1087],{"class":45},[35,1615,274],{"class":90},[35,1617,1119],{"class":45},[35,1619,274],{"class":90},[35,1621,1585],{"class":45},[35,1623,1624],{"class":90},"]].to_string())\n",[10,1626,1627],{},"The thresholds — 5% over cost, 10% behind schedule, a forecast more than 5% over budget — are typical starting points; projects with contingency may tolerate more. Build is flagged as behind plan with a forecast overrun of about 4,700 — its CPI of 0.95 sits exactly on the threshold, so it is not flagged on cost yet; Test is flagged on all three counts for the early-stage reasons above, and the project as a whole is behind plan but not over cost. A workstream far behind schedule but within cost is a different conversation from one that is over cost, which is why the reasons are listed rather than merged into one colour.",[786,1629,795,1635,795,1638,795,1641,795,1645,795,1651,795,1656,795,1662,795,1665,795,1669,795,1672,795,1676,795,1679,795,1685,795,1687,795,1691,795,1693,795,1696,795,1698,795,1701,795,1704,795,1708,795,1710,795,1714,795,1716,795,1719,795,1721,795,1724,795,1727,795,1731,795,1733,795,1737,795,1739,795,1742,795,1744,795,1747,795,1750,795,1754,795,1756,795,1760,795,1762,795,1765,795,1767],{"viewBox":1630,"role":789,"ariaLabelledBy":1631,"xmlns":793,"style":1634},"4 0 752 208",[1632,1633],"pb-ev-t","pb-ev-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[797,1636,1637],{"id":1632},"Earned value by workstream at week 11",[801,1639,1640],{"id":1633},"Design is complete with a cost index of 1.39, well under budget. Build has a cost index of 0.95 and a schedule index of 0.67, behind plan with a small forecast overrun. Test has low indices because it has just started. The project overall has a cost index of 1.04 and a schedule index of 0.75, behind plan but not over cost.",[805,1642],{"x":480,"y":359,"width":1643,"height":1644,"fill":809},"752","208",[805,1646],{"x":1647,"y":1648,"width":1649,"height":1650,"rx":359,"fill":809,"stroke":817,"style":840},"20.0","16.0","73.8","40.0",[805,1652],{"x":1653,"y":1648,"width":1654,"height":1650,"rx":359,"fill":829,"stroke":830,"style":1655},"93.8","215.4","stroke-width:1.2px",[820,1657,1661],{"x":1658,"y":1659,"style":1660},"201.5","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","CPI",[805,1663],{"x":1664,"y":1648,"width":1654,"height":1650,"rx":359,"fill":829,"stroke":830,"style":1655},"309.2",[820,1666,1668],{"x":1667,"y":1659,"style":1660},"416.9","SPI",[805,1670],{"x":1671,"y":1648,"width":1654,"height":1650,"rx":359,"fill":829,"stroke":830,"style":1655},"524.6",[820,1673,1675],{"x":1674,"y":1659,"style":1660},"632.3","status",[805,1677],{"x":1647,"y":866,"width":1649,"height":1678,"rx":359,"fill":816,"stroke":817,"style":840},"34.0",[820,1680,1684],{"x":1681,"y":1682,"style":1683},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Design",[805,1686],{"x":1653,"y":866,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1688,1690],{"x":1658,"y":1689,"style":875},"77.0","1.39",[805,1692],{"x":1664,"y":866,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1694,1695],{"x":1667,"y":1689,"style":875},"1.00",[805,1697],{"x":1671,"y":866,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1699,1700],{"x":1674,"y":1689,"style":875},"on track",[805,1702],{"x":1647,"y":1703,"width":1649,"height":1678,"rx":359,"fill":816,"stroke":817,"style":840},"90.0",[820,1705,1707],{"x":1681,"y":1706,"style":1683},"112.0","Build",[805,1709],{"x":1653,"y":1703,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1711,1713],{"x":1658,"y":1712,"style":875},"111.0","0.95",[805,1715],{"x":1664,"y":1703,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1717,1718],{"x":1667,"y":1712,"style":875},"0.67",[805,1720],{"x":1671,"y":1703,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1722,1723],{"x":1674,"y":1712,"style":875},"behind, +4,737",[805,1725],{"x":1647,"y":1726,"width":1649,"height":1678,"rx":359,"fill":816,"stroke":817,"style":840},"124.0",[820,1728,1730],{"x":1681,"y":1729,"style":1683},"146.0","Test",[805,1732],{"x":1653,"y":1726,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1734,1736],{"x":1658,"y":1735,"style":875},"145.0","0.39",[805,1738],{"x":1664,"y":1726,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1740,1741],{"x":1667,"y":1735,"style":875},"0.30",[805,1743],{"x":1671,"y":1726,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1745,1746],{"x":1674,"y":1735,"style":875},"early, all flags",[805,1748],{"x":1647,"y":1749,"width":1649,"height":1678,"rx":359,"fill":816,"stroke":817,"style":840},"158.0",[820,1751,1753],{"x":1681,"y":1752,"style":1683},"180.0","Project",[805,1755],{"x":1653,"y":1749,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1757,1759],{"x":1658,"y":1758,"style":875},"179.0","1.04",[805,1761],{"x":1664,"y":1749,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1763,1764],{"x":1667,"y":1758,"style":875},"0.75",[805,1766],{"x":1671,"y":1749,"width":1654,"height":1678,"rx":359,"fill":809,"stroke":817,"style":840},[820,1768,1769],{"x":1674,"y":1758,"style":875},"behind plan",[20,1771,1773],{"id":1772},"step-5-write-the-report-with-a-burn-chart","Step 5 — Write the report with a burn chart",[10,1775,1776],{},"The workbook has the earned-value table on top and a weekly sheet feeding a line chart of planned versus actual cumulative spend:",[25,1778,1780],{"className":77,"code":1779,"language":79,"meta":30,"style":30},"from openpyxl import Workbook\nfrom openpyxl.chart import LineChart, Reference\nfrom openpyxl.styles import Font\nfrom openpyxl.utils.dataframe import dataframe_to_rows\n\nwb = Workbook()\nws = wb.active\nws.title = \"Burn report\"\nws[\"A1\"] = f\"Project budget burn — week {week_now} ({as_of:%d %b %Y})\"\nws[\"A1\"].font = Font(bold=True, size=14)\nfor r in dataframe_to_rows(ev_table.reset_index().rename(columns={\"index\": \"Workstream\"}), index=False, header=True):\n    ws.append(r)\nfor c in ws[2]:\n    c.font = Font(bold=True)\nfor row in ws.iter_rows(min_row=3, max_row=ws.max_row, min_col=2, max_col=5):\n    for c in row:\n        c.number_format = \"#,##0\"\n\nweekly = wb.create_sheet(\"Weekly\")\nweekly.append([\"Week\", \"Planned (cum.)\", \"Actual (cum.)\"])\nfor w in range(n_weeks):\n    weekly.append([f\"W{w}\", round(cum_plan[w]), round(cum_actual[w]) if w \u003C= week_now else None])\n\nchart = LineChart()\nchart.title = \"Cumulative spend vs plan\"\nchart.y_axis.title = \"EUR\"\nchart.x_axis.title = \"Project week\"\nchart.add_data(Reference(weekly, min_col=2, max_col=3, min_row=1, max_row=n_weeks + 1), titles_from_data=True)\nchart.set_categories(Reference(weekly, min_col=1, min_row=2, max_row=n_weeks + 1))\nchart.x_axis.delete = False\nchart.y_axis.delete = False\nchart.width, chart.height = 22, 10\nws.add_chart(chart, \"A10\")\nfor col, w in zip(\"ABCDEFGHI\", (14, 11, 11, 11, 11, 7, 7, 11, 46)):\n    ws.column_dimensions[col].width = w\nwb.save(\"project_burn_week11.xlsx\")\nprint(\"written; actual series stops at week\", week_now)\n",[32,1781,1782,1795,1807,1819,1831,1835,1845,1855,1865,1918,1952,1999,2004,2021,2038,2086,2098,2108,2113,2129,2149,2164,2215,2220,2231,2242,2253,2264,2315,2349,2360,2370,2386,2397,2458,2469,2480],{"__ignoreMap":30},[35,1783,1784,1787,1790,1792],{"class":37,"line":38},[35,1785,1786],{"class":86},"from",[35,1788,1789],{"class":90}," openpyxl ",[35,1791,87],{"class":86},[35,1793,1794],{"class":90}," Workbook\n",[35,1796,1797,1799,1802,1804],{"class":37,"line":100},[35,1798,1786],{"class":86},[35,1800,1801],{"class":90}," openpyxl.chart ",[35,1803,87],{"class":86},[35,1805,1806],{"class":90}," LineChart, Reference\n",[35,1808,1809,1811,1814,1816],{"class":37,"line":107},[35,1810,1786],{"class":86},[35,1812,1813],{"class":90}," openpyxl.styles ",[35,1815,87],{"class":86},[35,1817,1818],{"class":90}," Font\n",[35,1820,1821,1823,1826,1828],{"class":37,"line":131},[35,1822,1786],{"class":86},[35,1824,1825],{"class":90}," openpyxl.utils.dataframe ",[35,1827,87],{"class":86},[35,1829,1830],{"class":90}," dataframe_to_rows\n",[35,1832,1833],{"class":37,"line":146},[35,1834,104],{"emptyLinePlaceholder":103},[35,1836,1837,1840,1842],{"class":37,"line":161},[35,1838,1839],{"class":90},"wb ",[35,1841,113],{"class":86},[35,1843,1844],{"class":90}," Workbook()\n",[35,1846,1847,1850,1852],{"class":37,"line":177},[35,1848,1849],{"class":90},"ws ",[35,1851,113],{"class":86},[35,1853,1854],{"class":90}," wb.active\n",[35,1856,1857,1860,1862],{"class":37,"line":192},[35,1858,1859],{"class":90},"ws.title ",[35,1861,113],{"class":86},[35,1863,1864],{"class":45}," \"Burn report\"\n",[35,1866,1867,1870,1873,1875,1877,1880,1883,1885,1887,1889,1892,1894,1897,1900,1903,1905,1908,1910,1913,1915],{"class":37,"line":197},[35,1868,1869],{"class":90},"ws[",[35,1871,1872],{"class":45},"\"A1\"",[35,1874,206],{"class":90},[35,1876,113],{"class":86},[35,1878,1879],{"class":86}," f",[35,1881,1882],{"class":45},"\"Project budget burn — week ",[35,1884,727],{"class":726},[35,1886,730],{"class":90},[35,1888,733],{"class":726},[35,1890,1891],{"class":45}," (",[35,1893,727],{"class":726},[35,1895,1896],{"class":90},"as_of:",[35,1898,1899],{"class":86},"%",[35,1901,1902],{"class":90},"d ",[35,1904,1899],{"class":86},[35,1906,1907],{"class":90},"b ",[35,1909,1899],{"class":86},[35,1911,1912],{"class":90},"Y",[35,1914,733],{"class":726},[35,1916,1917],{"class":45},")\"\n",[35,1919,1920,1922,1924,1927,1929,1932,1935,1937,1940,1942,1945,1947,1950],{"class":37,"line":230},[35,1921,1869],{"class":90},[35,1923,1872],{"class":45},[35,1925,1926],{"class":90},"].font ",[35,1928,113],{"class":86},[35,1930,1931],{"class":90}," Font(",[35,1933,1934],{"class":241},"bold",[35,1936,113],{"class":86},[35,1938,1939],{"class":264},"True",[35,1941,274],{"class":90},[35,1943,1944],{"class":241},"size",[35,1946,113],{"class":86},[35,1948,1949],{"class":264},"14",[35,1951,128],{"class":90},[35,1953,1954,1956,1959,1961,1964,1966,1968,1970,1973,1975,1977,1980,1982,1984,1987,1989,1992,1994,1996],{"class":37,"line":287},[35,1955,576],{"class":86},[35,1957,1958],{"class":90}," r ",[35,1960,582],{"class":86},[35,1962,1963],{"class":90}," dataframe_to_rows(ev_table.reset_index().rename(",[35,1965,393],{"class":241},[35,1967,113],{"class":86},[35,1969,727],{"class":90},[35,1971,1972],{"class":45},"\"index\"",[35,1974,1319],{"class":90},[35,1976,125],{"class":45},[35,1978,1979],{"class":90},"}), ",[35,1981,557],{"class":241},[35,1983,113],{"class":86},[35,1985,1986],{"class":264},"False",[35,1988,274],{"class":90},[35,1990,1991],{"class":241},"header",[35,1993,113],{"class":86},[35,1995,1939],{"class":264},[35,1997,1998],{"class":90},"):\n",[35,2000,2001],{"class":37,"line":332},[35,2002,2003],{"class":90},"    ws.append(r)\n",[35,2005,2006,2008,2011,2013,2016,2018],{"class":37,"line":337},[35,2007,576],{"class":86},[35,2009,2010],{"class":90}," c ",[35,2012,582],{"class":86},[35,2014,2015],{"class":90}," ws[",[35,2017,1073],{"class":264},[35,2019,2020],{"class":90},"]:\n",[35,2022,2023,2026,2028,2030,2032,2034,2036],{"class":37,"line":373},[35,2024,2025],{"class":90},"    c.font ",[35,2027,113],{"class":86},[35,2029,1931],{"class":90},[35,2031,1934],{"class":241},[35,2033,113],{"class":86},[35,2035,1939],{"class":264},[35,2037,128],{"class":90},[35,2039,2040,2042,2045,2047,2050,2053,2055,2057,2059,2062,2064,2067,2070,2072,2074,2076,2079,2081,2084],{"class":37,"line":410},[35,2041,576],{"class":86},[35,2043,2044],{"class":90}," row ",[35,2046,582],{"class":86},[35,2048,2049],{"class":90}," ws.iter_rows(",[35,2051,2052],{"class":241},"min_row",[35,2054,113],{"class":86},[35,2056,815],{"class":264},[35,2058,274],{"class":90},[35,2060,2061],{"class":241},"max_row",[35,2063,113],{"class":86},[35,2065,2066],{"class":90},"ws.max_row, ",[35,2068,2069],{"class":241},"min_col",[35,2071,113],{"class":86},[35,2073,1073],{"class":264},[35,2075,274],{"class":90},[35,2077,2078],{"class":241},"max_col",[35,2080,113],{"class":86},[35,2082,2083],{"class":264},"5",[35,2085,1998],{"class":90},[35,2087,2088,2091,2093,2095],{"class":37,"line":461},[35,2089,2090],{"class":86},"    for",[35,2092,2010],{"class":90},[35,2094,582],{"class":86},[35,2096,2097],{"class":90}," row:\n",[35,2099,2100,2103,2105],{"class":37,"line":486},[35,2101,2102],{"class":90},"        c.number_format ",[35,2104,113],{"class":86},[35,2106,2107],{"class":45}," \"#,##0\"\n",[35,2109,2111],{"class":37,"line":2110},18,[35,2112,104],{"emptyLinePlaceholder":103},[35,2114,2116,2119,2121,2124,2127],{"class":37,"line":2115},19,[35,2117,2118],{"class":90},"weekly ",[35,2120,113],{"class":86},[35,2122,2123],{"class":90}," wb.create_sheet(",[35,2125,2126],{"class":45},"\"Weekly\"",[35,2128,128],{"class":90},[35,2130,2132,2135,2137,2139,2142,2144,2147],{"class":37,"line":2131},20,[35,2133,2134],{"class":90},"weekly.append([",[35,2136,271],{"class":45},[35,2138,274],{"class":90},[35,2140,2141],{"class":45},"\"Planned (cum.)\"",[35,2143,274],{"class":90},[35,2145,2146],{"class":45},"\"Actual (cum.)\"",[35,2148,1307],{"class":90},[35,2150,2152,2154,2157,2159,2161],{"class":37,"line":2151},21,[35,2153,576],{"class":86},[35,2155,2156],{"class":90}," w ",[35,2158,582],{"class":86},[35,2160,595],{"class":264},[35,2162,2163],{"class":90},"(n_weeks):\n",[35,2165,2167,2170,2172,2175,2177,2180,2182,2184,2186,2188,2191,2193,2196,2199,2201,2204,2207,2210,2213],{"class":37,"line":2166},22,[35,2168,2169],{"class":90},"    weekly.append([",[35,2171,720],{"class":86},[35,2173,2174],{"class":45},"\"W",[35,2176,727],{"class":726},[35,2178,2179],{"class":90},"w",[35,2181,733],{"class":726},[35,2183,779],{"class":45},[35,2185,274],{"class":90},[35,2187,498],{"class":264},[35,2189,2190],{"class":90},"(cum_plan[w]), ",[35,2192,498],{"class":264},[35,2194,2195],{"class":90},"(cum_actual[w]) ",[35,2197,2198],{"class":86},"if",[35,2200,2156],{"class":90},[35,2202,2203],{"class":86},"\u003C=",[35,2205,2206],{"class":90}," week_now ",[35,2208,2209],{"class":86},"else",[35,2211,2212],{"class":264}," None",[35,2214,1307],{"class":90},[35,2216,2218],{"class":37,"line":2217},23,[35,2219,104],{"emptyLinePlaceholder":103},[35,2221,2223,2226,2228],{"class":37,"line":2222},24,[35,2224,2225],{"class":90},"chart ",[35,2227,113],{"class":86},[35,2229,2230],{"class":90}," LineChart()\n",[35,2232,2234,2237,2239],{"class":37,"line":2233},25,[35,2235,2236],{"class":90},"chart.title ",[35,2238,113],{"class":86},[35,2240,2241],{"class":45}," \"Cumulative spend vs plan\"\n",[35,2243,2245,2248,2250],{"class":37,"line":2244},26,[35,2246,2247],{"class":90},"chart.y_axis.title ",[35,2249,113],{"class":86},[35,2251,2252],{"class":45}," \"EUR\"\n",[35,2254,2256,2259,2261],{"class":37,"line":2255},27,[35,2257,2258],{"class":90},"chart.x_axis.title ",[35,2260,113],{"class":86},[35,2262,2263],{"class":45}," \"Project week\"\n",[35,2265,2267,2270,2272,2274,2276,2278,2280,2282,2284,2286,2288,2290,2292,2294,2296,2298,2300,2302,2304,2306,2309,2311,2313],{"class":37,"line":2266},28,[35,2268,2269],{"class":90},"chart.add_data(Reference(weekly, ",[35,2271,2069],{"class":241},[35,2273,113],{"class":86},[35,2275,1073],{"class":264},[35,2277,274],{"class":90},[35,2279,2078],{"class":241},[35,2281,113],{"class":86},[35,2283,815],{"class":264},[35,2285,274],{"class":90},[35,2287,2052],{"class":241},[35,2289,113],{"class":86},[35,2291,353],{"class":264},[35,2293,274],{"class":90},[35,2295,2061],{"class":241},[35,2297,113],{"class":86},[35,2299,534],{"class":90},[35,2301,444],{"class":86},[35,2303,447],{"class":264},[35,2305,450],{"class":90},[35,2307,2308],{"class":241},"titles_from_data",[35,2310,113],{"class":86},[35,2312,1939],{"class":264},[35,2314,128],{"class":90},[35,2316,2318,2321,2323,2325,2327,2329,2331,2333,2335,2337,2339,2341,2343,2345,2347],{"class":37,"line":2317},29,[35,2319,2320],{"class":90},"chart.set_categories(Reference(weekly, ",[35,2322,2069],{"class":241},[35,2324,113],{"class":86},[35,2326,353],{"class":264},[35,2328,274],{"class":90},[35,2330,2052],{"class":241},[35,2332,113],{"class":86},[35,2334,1073],{"class":264},[35,2336,274],{"class":90},[35,2338,2061],{"class":241},[35,2340,113],{"class":86},[35,2342,534],{"class":90},[35,2344,444],{"class":86},[35,2346,447],{"class":264},[35,2348,407],{"class":90},[35,2350,2352,2355,2357],{"class":37,"line":2351},30,[35,2353,2354],{"class":90},"chart.x_axis.delete ",[35,2356,113],{"class":86},[35,2358,2359],{"class":264}," False\n",[35,2361,2363,2366,2368],{"class":37,"line":2362},31,[35,2364,2365],{"class":90},"chart.y_axis.delete ",[35,2367,113],{"class":86},[35,2369,2359],{"class":264},[35,2371,2373,2376,2378,2381,2383],{"class":37,"line":2372},32,[35,2374,2375],{"class":90},"chart.width, chart.height ",[35,2377,113],{"class":86},[35,2379,2380],{"class":264}," 22",[35,2382,274],{"class":90},[35,2384,2385],{"class":264},"10\n",[35,2387,2389,2392,2395],{"class":37,"line":2388},33,[35,2390,2391],{"class":90},"ws.add_chart(chart, ",[35,2393,2394],{"class":45},"\"A10\"",[35,2396,128],{"class":90},[35,2398,2400,2402,2405,2407,2410,2412,2415,2418,2420,2422,2425,2427,2429,2431,2433,2435,2437,2439,2442,2444,2446,2448,2450,2452,2455],{"class":37,"line":2399},34,[35,2401,576],{"class":86},[35,2403,2404],{"class":90}," col, w ",[35,2406,582],{"class":86},[35,2408,2409],{"class":264}," zip",[35,2411,423],{"class":90},[35,2413,2414],{"class":45},"\"ABCDEFGHI\"",[35,2416,2417],{"class":90},", (",[35,2419,1949],{"class":264},[35,2421,274],{"class":90},[35,2423,2424],{"class":264},"11",[35,2426,274],{"class":90},[35,2428,2424],{"class":264},[35,2430,274],{"class":90},[35,2432,2424],{"class":264},[35,2434,274],{"class":90},[35,2436,2424],{"class":264},[35,2438,274],{"class":90},[35,2440,2441],{"class":264},"7",[35,2443,274],{"class":90},[35,2445,2441],{"class":264},[35,2447,274],{"class":90},[35,2449,2424],{"class":264},[35,2451,274],{"class":90},[35,2453,2454],{"class":264},"46",[35,2456,2457],{"class":90},")):\n",[35,2459,2461,2464,2466],{"class":37,"line":2460},35,[35,2462,2463],{"class":90},"    ws.column_dimensions[col].width ",[35,2465,113],{"class":86},[35,2467,2468],{"class":90}," w\n",[35,2470,2472,2475,2478],{"class":37,"line":2471},36,[35,2473,2474],{"class":90},"wb.save(",[35,2476,2477],{"class":45},"\"project_burn_week11.xlsx\"",[35,2479,128],{"class":90},[35,2481,2483,2485,2487,2490],{"class":37,"line":2482},37,[35,2484,464],{"class":264},[35,2486,423],{"class":90},[35,2488,2489],{"class":45},"\"written; actual series stops at week\"",[35,2491,2492],{"class":90},", week_now)\n",[10,2494,2495,2496,2498,2499,2502,2503,2506,2507,18],{},"Future weeks have no actual value (",[32,2497,1185],{},"), so the actual line stops at the current week while the planned line runs to the end — the gap between them is the picture sponsors remember. ",[32,2500,2501],{},"x_axis.delete = False"," and ",[32,2504,2505],{},"y_axis.delete = False"," make sure the axes are shown; openpyxl's defaults can leave them hidden in current Excel versions. For charts of this kind in general, see ",[14,2508,2510],{"href":2509},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002F","Add a Line Chart to an Excel Report with Python",[786,2512,795,2517,795,2520,795,2523,795,2526,795,2531,795,2540,795,2546,795,2550,795,2554,795,2559,795,2564,795,2567,795,2574,795,2579,795,2582,795,2585,795,2589,795,2592,795,2597,795,2604,795,2610,795,2614,795,2618,795,2621,795,2626,795,2633,795,2638],{"viewBox":2513,"role":789,"ariaLabelledBy":2514,"xmlns":793,"style":1634},"4 0 752 142",[2515,2516],"pb-flow-t","pb-flow-d",[797,2518,2519],{"id":2515},"From raw costs to a burn report",[801,2521,2522],{"id":2516},"Timesheet hours times rates and supplier invoices become weekly actual costs per workstream. The budget is spread over planned weeks. Percent complete gives earned value, which with planned value and actual cost produces CPI, SPI and a forecast, flagged and charted in Excel.",[805,2524],{"x":480,"y":359,"width":1643,"height":2525,"fill":809},"142",[805,2527],{"x":1647,"y":1648,"width":2528,"height":2529,"rx":2530,"fill":816,"stroke":817,"style":904},"153.0","110.5","12",[2532,2533,2535],"g",{"transform":2534},"translate(84.5,29.0) scale(1.000)",[894,2536],{"d":2537,"fill":897,"stroke":2538,"style":2539},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[820,2541,2545],{"x":2542,"y":2543,"style":2544},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Actual cost",[820,2547,2549],{"x":2542,"y":2548,"style":875},"95.0","hours × rate +",[820,2551,2553],{"x":2542,"y":2552,"style":875},"112.5","invoices",[37,2555],{"x1":2556,"y1":2557,"x2":2558,"y2":2557,"stroke":830,"style":904},"178.0","71.2","200.0",[2560,2561],"polygon",{"points":2562,"fill":2563},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[805,2565],{"x":2566,"y":1648,"width":2528,"height":2529,"rx":2530,"fill":829,"stroke":830,"style":904},"209.0",[2532,2568,2570],{"transform":2569},"translate(273.5,29.0) scale(1.000)",[894,2571],{"d":2572,"fill":897,"stroke":2573,"style":2539},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--brand-strong,#4338ca)",[820,2575,2578],{"x":2576,"y":2543,"style":2577},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Planned value",[820,2580,2581],{"x":2576,"y":2548,"style":875},"budget over planned",[820,2583,2584],{"x":2576,"y":2552,"style":875},"weeks",[37,2586],{"x1":2587,"y1":2557,"x2":2588,"y2":2557,"stroke":830,"style":904},"367.0","389.0",[2560,2590],{"points":2591,"fill":2563},"393.0,71.2 384.0,76.2 384.0,66.2",[805,2593],{"x":2594,"y":1648,"width":2528,"height":2529,"rx":2530,"fill":2595,"stroke":2596,"style":904},"398.0","#d9f4f1","var(--teal,#0f9488)",[2532,2598,2600],{"transform":2599},"translate(462.5,37.8) scale(1.000)",[894,2601],{"d":2602,"fill":897,"stroke":2603,"style":2539},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[820,2605,2609],{"x":2606,"y":2607,"style":2608},"474.5","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Earned value",[820,2611,2613],{"x":2606,"y":2612,"style":875},"103.8","budget × % complete",[37,2615],{"x1":2616,"y1":2557,"x2":2617,"y2":2557,"stroke":830,"style":904},"556.0","578.0",[2560,2619],{"points":2620,"fill":2563},"582.0,71.2 573.0,76.2 573.0,66.2",[805,2622],{"x":2623,"y":1648,"width":2528,"height":2529,"rx":2530,"fill":2624,"stroke":2625,"style":904},"587.0","#fdefd8","var(--gold,#b4740a)",[2532,2627,2629],{"transform":2628},"translate(651.5,37.8) scale(1.000)",[894,2630],{"d":2631,"fill":897,"stroke":2632,"style":2539},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7","var(--gold-ink,#7a4e06)",[820,2634,2637],{"x":2635,"y":2607,"style":2636},"663.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Report",[820,2639,2640],{"x":2635,"y":2612,"style":875},"CPI, SPI, EAC, chart",[20,2642,2644],{"id":2643},"common-pitfalls","Common pitfalls",[2646,2647,2648,2656,2662,2668,2674],"ul",{},[2649,2650,2651,2655],"li",{},[2652,2653,2654],"strong",{},"Comparing spend with the total budget only."," Compare with a time-phased plan and with earned value.",[2649,2657,2658,2661],{},[2652,2659,2660],{},"Gaps in the weekly series."," Reindex weeks so cumulative sums have no holes.",[2649,2663,2664,2667],{},[2652,2665,2666],{},"Percent complete as a guess."," Base it on deliverables, or the indices mean little.",[2649,2669,2670,2673],{},[2652,2671,2672],{},"Labour without supplier costs (or vice versa)."," Combine every source of actual cost.",[2649,2675,2676,2679,2680,2683],{},[2652,2677,2678],{},"Hidden chart axes."," Set ",[32,2681,2682],{},"delete = False"," on openpyxl chart axes.",[20,2685,2687],{"id":2686},"conclusion","Conclusion",[10,2689,2690],{},"A project burn report brings together actual cost from timesheets and invoices, a time-phased planned value from the budget, and earned value from percent complete. From these, CPI shows whether spend is buying the expected progress, SPI whether the work is on schedule, and budget divided by CPI forecasts the final cost. Flag workstreams with clear reasons, and chart cumulative spend against plan so the trend is visible at a glance.",[20,2692,2694],{"id":2693},"frequently-asked-questions","Frequently asked questions",[10,2696,2697,2700],{},[2652,2698,2699],{},"How do I calculate budget burn in Python?","\nSum actual costs per week from timesheets and invoices, take the cumulative sum, and compare it with a cumulative planned budget spread over the project's weeks.",[10,2702,2703,2706],{},[2652,2704,2705],{},"What are CPI and SPI?","\nCPI is earned value divided by actual cost and shows cost efficiency; SPI is earned value divided by planned value and shows schedule progress. Values below 1 mean over cost or behind schedule.",[10,2708,2709,2712],{},[2652,2710,2711],{},"How do I forecast a project's final cost?","\nA common estimate at completion is the budget divided by the cost performance index, which assumes the project continues at its current cost efficiency.",[10,2714,2715,2718],{},[2652,2716,2717],{},"How do I chart planned versus actual spend in Excel with Python?","\nWrite weekly cumulative planned and actual figures to a sheet, leave future actuals empty, and add an openpyxl LineChart with both series and the weeks as categories.",[20,2720,2722],{"id":2721},"related","Related",[2646,2724,2725,2731,2738,2745],{},[2649,2726,2727,2728,2730],{},"Up one level: ",[14,2729,17],{"href":16}," — finance and operations reports in Excel.",[2649,2732,2733,2737],{},[14,2734,2736],{"href":2735},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks\u002F","Consolidate Timesheets from Many Workbooks"," — where the hours come from.",[2649,2739,2740,2744],{},[14,2741,2743],{"href":2742},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-staff-utilisation-report-from-timesheets\u002F","Build a Staff Utilisation Report from Timesheets"," — the same hours from the people angle.",[2649,2746,2747,2751],{},[14,2748,2750],{"href":2749},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report"," — variance for operating budgets.",[2753,2754,2755],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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}",{"title":30,"searchDepth":100,"depth":100,"links":2757},[2758,2759,2760,2761,2762,2763,2764,2765,2766,2767],{"id":22,"depth":100,"text":23},{"id":70,"depth":100,"text":71},{"id":515,"depth":100,"text":516},{"id":937,"depth":100,"text":938},{"id":1370,"depth":100,"text":1371},{"id":1772,"depth":100,"text":1773},{"id":2643,"depth":100,"text":2644},{"id":2686,"depth":100,"text":2687},{"id":2693,"depth":100,"text":2694},{"id":2721,"depth":100,"text":2722},"2026-10-02","Track how fast a project is spending its budget with pandas and openpyxl: combine timesheet costs and supplier invoices into weekly actuals, compare cumulative spend with a time-phased budget, add earned value with CPI and SPI from percent complete, forecast the cost at completion, flag workstreams at risk, and chart burn against plan in Excel.","md",[2772,2774,2776,2778],{"q":2699,"a":2773},"Sum actual costs per week from timesheets and invoices, take the cumulative sum, and compare it with a cumulative planned budget spread over the project's weeks.",{"q":2705,"a":2775},"CPI is earned value divided by actual cost and shows cost efficiency; SPI is earned value divided by planned value and shows schedule progress. Values below 1 mean over cost or behind schedule.",{"q":2711,"a":2777},"A common estimate at completion is the budget divided by the cost performance index, which assumes the project continues at its current cost efficiency.",{"q":2717,"a":2779},"Write weekly cumulative planned and actual figures to a sheet, leave future actuals empty, and add an openpyxl LineChart with both series and the weeks as categories.","build a project budget burn report",null,{"breadcrumb":2783},[2784,2786,2789],{"name":2785,"item":648},"Home",{"name":2787,"item":2788},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2790,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report",{"title":2793,"description":2794},"Build a Project Budget Burn Report in Excel with Python","A project burn report from timesheets and invoices: weekly cumulative actuals vs planned budget, earned value, CPI and SPI, estimate at completion, workstream flags and an openpyxl burn chart.","build-a-project-budget-burn-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report\u002Findex",[2798,2799],"report-layouts","charts-and-images","how-to","Ow7cY0y1Uc2N3VLkXGDKUKtDl0iv-5WMHHPMfNXs4mg",[2803,2807],{"title":2804,"path":2805,"stem":2806,"children":-1},"Build a Profit and Loss Statement from a General Ledger","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger\u002Findex",{"title":2808,"path":2809,"stem":2810,"children":-1},"Build a Purchase Order Tracking Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-purchase-order-tracking-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-purchase-order-tracking-report\u002Findex",[2812,2814],{"path":2813,"title":2790},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2815,"title":2787},"\u002Fautomating-reporting-workflows",[],[2818,2822,2825,2828,2831,2834],{"path":2819,"title":2820,"kind":2821},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2823,"title":2824,"kind":2800},"\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":2826,"title":2827,"kind":2800},"\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":2829,"title":2830,"kind":2800},"\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":2832,"title":2833,"kind":2800},"\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":2835,"title":2836,"kind":2800},"\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",1790972126772]