[{"data":1,"prerenderedAt":3497},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns":3462,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns":3471,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns":3476,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns":3477},{"id":4,"title":5,"body":6,"dateModified":3427,"datePublished":3427,"description":3428,"extension":3429,"faq":3430,"keyword":3439,"kind":3440,"meta":3441,"navigation":152,"path":3451,"seo":3452,"slug":3455,"stem":3456,"topics":3457,"type":3460,"__hash__":3461},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns\u002Findex.md","Send Out Budget Templates and Consolidate the Returns",{"type":7,"value":8,"toc":3416},"minimark",[9,19,24,53,56,60,63,1242,1258,1437,1641,1645,1648,1986,1989,1993,1996,2933,2952,3081,3085,3298,3305,3309,3344,3348,3351,3355,3361,3367,3373,3379,3383,3412],[10,11,12,13,18],"p",{},"The budget round is a classic Excel workflow: finance sends a workbook to every budget holder, each fills in next year's figures, and finance stitches the returns together. Most of the pain is in the stitching. Files come back renamed, with rows inserted, formulas overwritten, the wrong cost centre's template filled in, or \"TBC\" typed into a number cell — and a spreadsheet of who has replied is kept by hand alongside. Python can run both halves: generate a template for each cost centre that is hard to break and easy to identify, then read every return, check it, consolidate the figures and report who is still missing. This page does that with openpyxl and pandas, using a small set of cost centres and simulated returns that include the usual mistakes. 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 openpyxl pandas\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}," openpyxl",[35,51,52],{"class":45}," pandas\n",[10,54,55],{},"The inputs are a list of cost centres with their budget holders, and last year's actuals per cost centre and account, which are pre-filled in each template as a reference. The budget is for 2027, collected in October 2026.",[20,57,59],{"id":58},"step-1-generate-one-template-per-cost-centre","Step 1 — Generate one template per cost centre",[10,61,62],{},"Each workbook carries the cost centre's own accounts and last year's actuals, input cells for the 2027 budget, a formula for the change, and a hidden sheet that records which cost centre and template version it is:",[25,64,68],{"className":65,"code":66,"language":67,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import hashlib\nfrom pathlib import Path\nimport pandas as pd\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill, Protection\nfrom openpyxl.worksheet.datavalidation import DataValidation\n\nVERSION = \"B2027-v1\"\ncentres = pd.DataFrame({\"cc\": [\"CC100\", \"CC200\", \"CC300\", \"CC400\"],\n                        \"name\": [\"Finance\", \"Operations\", \"Sales\", \"IT\"],\n                        \"holder\": [\"amira@example.com\", \"dev@example.com\", \"farid@example.com\", \"lena@example.com\"]})\naccounts = [\"6100 Salaries\", \"6200 Travel\", \"6300 Training\", \"6400 Software\", \"6500 Consumables\"]\nactuals = pd.DataFrame([(cc, acc, round(20000 + 9000 * i + 4000 * j, -2)) for i, cc in enumerate(centres[\"cc\"])\n                        for j, acc in enumerate(accounts)], columns=[\"cc\", \"account\", \"actual_2026\"])\nOUT = Path(\"outbox\"); OUT.mkdir(exist_ok=True)\nINPUT = PatternFill(\"solid\", fgColor=\"FFF4CE\")\n\ndef make_template(cc, name):\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Budget\"\n    ws[\"A1\"], ws[\"A1\"].font = f\"Budget 2027 — {cc} {name}\", Font(size=14, bold=True)\n    ws.append([])\n    ws.append([\"Account\", \"Actual 2026\", \"Budget 2027\", \"Change\", \"Comment\"])\n    rows = actuals[actuals[\"cc\"] == cc]\n    for r, (acc, act) in enumerate(zip(rows[\"account\"], rows[\"actual_2026\"]), start=4):\n        ws.append([acc, act, None, f'=IF(C{r}=\"\",\"\",C{r}-B{r})', None])\n        for col in \"CE\":\n            ws[f\"{col}{r}\"].protection = Protection(locked=False)\n            ws[f\"{col}{r}\"].fill = INPUT\n    last = 3 + len(rows)\n    ws.append([\"Total\", f\"=SUM(B4:B{last})\", f\"=SUM(C4:C{last})\", f\"=C{last + 1}-B{last + 1}\"])\n    dv = DataValidation(type=\"decimal\", operator=\"greaterThanOrEqual\", formula1=\"0\", allow_blank=True,\n                        showErrorMessage=True, errorTitle=\"Budget\", error=\"Enter an amount of 0 or more.\")\n    ws.add_data_validation(dv)\n    dv.add(f\"C4:C{last}\")\n    ws.protection.sheet = True\n    ws.protection.password = \"budget\"\n    meta = wb.create_sheet(\"_id\")\n    meta.append([\"cc\", \"version\", \"rows\", \"fingerprint\"])\n    meta.append([cc, VERSION, len(rows), hashlib.sha256(\"|\".join(rows[\"account\"]).encode()).hexdigest()[:16]])\n    meta.sheet_state = \"veryHidden\"\n    path = OUT \u002F f\"Budget_2027_{cc}.xlsx\"\n    wb.save(path)\n    return path\n\nsent = [make_template(cc, name) for cc, name in zip(centres[\"cc\"], centres[\"name\"])]\nprint([p.name for p in sent])\n","python",[32,69,70,80,94,108,121,134,147,154,167,207,235,264,301,373,412,445,471,476,489,500,511,522,590,596,627,649,691,742,759,801,830,849,924,975,1007,1013,1034,1045,1056,1072,1097,1130,1141,1169,1175,1184,1189,1223],{"__ignoreMap":30},[35,71,72,76],{"class":37,"line":38},[35,73,75],{"class":74},"s-kum","import",[35,77,79],{"class":78},"skGVy"," hashlib\n",[35,81,83,86,89,91],{"class":37,"line":82},2,[35,84,85],{"class":74},"from",[35,87,88],{"class":78}," pathlib ",[35,90,75],{"class":74},[35,92,93],{"class":78}," Path\n",[35,95,97,99,102,105],{"class":37,"line":96},3,[35,98,75],{"class":74},[35,100,101],{"class":78}," pandas ",[35,103,104],{"class":74},"as",[35,106,107],{"class":78}," pd\n",[35,109,111,113,116,118],{"class":37,"line":110},4,[35,112,85],{"class":74},[35,114,115],{"class":78}," openpyxl ",[35,117,75],{"class":74},[35,119,120],{"class":78}," Workbook\n",[35,122,124,126,129,131],{"class":37,"line":123},5,[35,125,85],{"class":74},[35,127,128],{"class":78}," openpyxl.styles ",[35,130,75],{"class":74},[35,132,133],{"class":78}," Font, PatternFill, Protection\n",[35,135,137,139,142,144],{"class":37,"line":136},6,[35,138,85],{"class":74},[35,140,141],{"class":78}," openpyxl.worksheet.datavalidation ",[35,143,75],{"class":74},[35,145,146],{"class":78}," DataValidation\n",[35,148,150],{"class":37,"line":149},7,[35,151,153],{"emptyLinePlaceholder":152},true,"\n",[35,155,157,161,164],{"class":37,"line":156},8,[35,158,160],{"class":159},"sP0c6","VERSION",[35,162,163],{"class":74}," =",[35,165,166],{"class":45}," \"B2027-v1\"\n",[35,168,170,173,176,179,182,185,188,191,194,196,199,201,204],{"class":37,"line":169},9,[35,171,172],{"class":78},"centres ",[35,174,175],{"class":74},"=",[35,177,178],{"class":78}," pd.DataFrame({",[35,180,181],{"class":45},"\"cc\"",[35,183,184],{"class":78},": [",[35,186,187],{"class":45},"\"CC100\"",[35,189,190],{"class":78},", ",[35,192,193],{"class":45},"\"CC200\"",[35,195,190],{"class":78},[35,197,198],{"class":45},"\"CC300\"",[35,200,190],{"class":78},[35,202,203],{"class":45},"\"CC400\"",[35,205,206],{"class":78},"],\n",[35,208,210,213,215,218,220,223,225,228,230,233],{"class":37,"line":209},10,[35,211,212],{"class":45},"                        \"name\"",[35,214,184],{"class":78},[35,216,217],{"class":45},"\"Finance\"",[35,219,190],{"class":78},[35,221,222],{"class":45},"\"Operations\"",[35,224,190],{"class":78},[35,226,227],{"class":45},"\"Sales\"",[35,229,190],{"class":78},[35,231,232],{"class":45},"\"IT\"",[35,234,206],{"class":78},[35,236,238,241,243,246,248,251,253,256,258,261],{"class":37,"line":237},11,[35,239,240],{"class":45},"                        \"holder\"",[35,242,184],{"class":78},[35,244,245],{"class":45},"\"amira@example.com\"",[35,247,190],{"class":78},[35,249,250],{"class":45},"\"dev@example.com\"",[35,252,190],{"class":78},[35,254,255],{"class":45},"\"farid@example.com\"",[35,257,190],{"class":78},[35,259,260],{"class":45},"\"lena@example.com\"",[35,262,263],{"class":78},"]})\n",[35,265,267,270,272,275,278,280,283,285,288,290,293,295,298],{"class":37,"line":266},12,[35,268,269],{"class":78},"accounts ",[35,271,175],{"class":74},[35,273,274],{"class":78}," [",[35,276,277],{"class":45},"\"6100 Salaries\"",[35,279,190],{"class":78},[35,281,282],{"class":45},"\"6200 Travel\"",[35,284,190],{"class":78},[35,286,287],{"class":45},"\"6300 Training\"",[35,289,190],{"class":78},[35,291,292],{"class":45},"\"6400 Software\"",[35,294,190],{"class":78},[35,296,297],{"class":45},"\"6500 Consumables\"",[35,299,300],{"class":78},"]\n",[35,302,304,307,309,312,315,318,321,324,327,330,333,336,339,341,344,347,350,353,356,359,362,365,368,370],{"class":37,"line":303},13,[35,305,306],{"class":78},"actuals ",[35,308,175],{"class":74},[35,310,311],{"class":78}," pd.DataFrame([(cc, acc, ",[35,313,314],{"class":159},"round",[35,316,317],{"class":78},"(",[35,319,320],{"class":159},"20000",[35,322,323],{"class":74}," +",[35,325,326],{"class":159}," 9000",[35,328,329],{"class":74}," *",[35,331,332],{"class":78}," i ",[35,334,335],{"class":74},"+",[35,337,338],{"class":159}," 4000",[35,340,329],{"class":74},[35,342,343],{"class":78}," j, ",[35,345,346],{"class":74},"-",[35,348,349],{"class":159},"2",[35,351,352],{"class":78},")) ",[35,354,355],{"class":74},"for",[35,357,358],{"class":78}," i, cc ",[35,360,361],{"class":74},"in",[35,363,364],{"class":159}," enumerate",[35,366,367],{"class":78},"(centres[",[35,369,181],{"class":45},[35,371,372],{"class":78},"])\n",[35,374,376,379,382,384,386,389,393,395,398,400,402,405,407,410],{"class":37,"line":375},14,[35,377,378],{"class":74},"                        for",[35,380,381],{"class":78}," j, acc ",[35,383,361],{"class":74},[35,385,364],{"class":159},[35,387,388],{"class":78},"(accounts)], ",[35,390,392],{"class":391},"sa561","columns",[35,394,175],{"class":74},[35,396,397],{"class":78},"[",[35,399,181],{"class":45},[35,401,190],{"class":78},[35,403,404],{"class":45},"\"account\"",[35,406,190],{"class":78},[35,408,409],{"class":45},"\"actual_2026\"",[35,411,372],{"class":78},[35,413,415,418,420,423,426,429,431,434,437,439,442],{"class":37,"line":414},15,[35,416,417],{"class":159},"OUT",[35,419,163],{"class":74},[35,421,422],{"class":78}," Path(",[35,424,425],{"class":45},"\"outbox\"",[35,427,428],{"class":78},"); ",[35,430,417],{"class":159},[35,432,433],{"class":78},".mkdir(",[35,435,436],{"class":391},"exist_ok",[35,438,175],{"class":74},[35,440,441],{"class":159},"True",[35,443,444],{"class":78},")\n",[35,446,448,451,453,456,459,461,464,466,469],{"class":37,"line":447},16,[35,449,450],{"class":159},"INPUT",[35,452,163],{"class":74},[35,454,455],{"class":78}," PatternFill(",[35,457,458],{"class":45},"\"solid\"",[35,460,190],{"class":78},[35,462,463],{"class":391},"fgColor",[35,465,175],{"class":74},[35,467,468],{"class":45},"\"FFF4CE\"",[35,470,444],{"class":78},[35,472,474],{"class":37,"line":473},17,[35,475,153],{"emptyLinePlaceholder":152},[35,477,479,482,486],{"class":37,"line":478},18,[35,480,481],{"class":74},"def",[35,483,485],{"class":484},"s_Opv"," make_template",[35,487,488],{"class":78},"(cc, name):\n",[35,490,492,495,497],{"class":37,"line":491},19,[35,493,494],{"class":78},"    wb ",[35,496,175],{"class":74},[35,498,499],{"class":78}," Workbook()\n",[35,501,503,506,508],{"class":37,"line":502},20,[35,504,505],{"class":78},"    ws ",[35,507,175],{"class":74},[35,509,510],{"class":78}," wb.active\n",[35,512,514,517,519],{"class":37,"line":513},21,[35,515,516],{"class":78},"    ws.title ",[35,518,175],{"class":74},[35,520,521],{"class":45}," \"Budget\"\n",[35,523,525,528,531,534,536,539,541,544,547,551,554,557,560,563,565,568,571,574,576,579,581,584,586,588],{"class":37,"line":524},22,[35,526,527],{"class":78},"    ws[",[35,529,530],{"class":45},"\"A1\"",[35,532,533],{"class":78},"], ws[",[35,535,530],{"class":45},[35,537,538],{"class":78},"].font ",[35,540,175],{"class":74},[35,542,543],{"class":74}," f",[35,545,546],{"class":45},"\"Budget 2027 — ",[35,548,550],{"class":549},"sSjpA","{",[35,552,553],{"class":78},"cc",[35,555,556],{"class":549},"}",[35,558,559],{"class":549}," {",[35,561,562],{"class":78},"name",[35,564,556],{"class":549},[35,566,567],{"class":45},"\"",[35,569,570],{"class":78},", Font(",[35,572,573],{"class":391},"size",[35,575,175],{"class":74},[35,577,578],{"class":159},"14",[35,580,190],{"class":78},[35,582,583],{"class":391},"bold",[35,585,175],{"class":74},[35,587,441],{"class":159},[35,589,444],{"class":78},[35,591,593],{"class":37,"line":592},23,[35,594,595],{"class":78},"    ws.append([])\n",[35,597,599,602,605,607,610,612,615,617,620,622,625],{"class":37,"line":598},24,[35,600,601],{"class":78},"    ws.append([",[35,603,604],{"class":45},"\"Account\"",[35,606,190],{"class":78},[35,608,609],{"class":45},"\"Actual 2026\"",[35,611,190],{"class":78},[35,613,614],{"class":45},"\"Budget 2027\"",[35,616,190],{"class":78},[35,618,619],{"class":45},"\"Change\"",[35,621,190],{"class":78},[35,623,624],{"class":45},"\"Comment\"",[35,626,372],{"class":78},[35,628,630,633,635,638,640,643,646],{"class":37,"line":629},25,[35,631,632],{"class":78},"    rows ",[35,634,175],{"class":74},[35,636,637],{"class":78}," actuals[actuals[",[35,639,181],{"class":45},[35,641,642],{"class":78},"] ",[35,644,645],{"class":74},"==",[35,647,648],{"class":78}," cc]\n",[35,650,652,655,658,660,662,664,667,670,672,675,677,680,683,685,688],{"class":37,"line":651},26,[35,653,654],{"class":74},"    for",[35,656,657],{"class":78}," r, (acc, act) ",[35,659,361],{"class":74},[35,661,364],{"class":159},[35,663,317],{"class":78},[35,665,666],{"class":159},"zip",[35,668,669],{"class":78},"(rows[",[35,671,404],{"class":45},[35,673,674],{"class":78},"], rows[",[35,676,409],{"class":45},[35,678,679],{"class":78},"]), ",[35,681,682],{"class":391},"start",[35,684,175],{"class":74},[35,686,687],{"class":159},"4",[35,689,690],{"class":78},"):\n",[35,692,694,697,700,702,705,708,710,713,715,718,720,722,724,727,729,731,733,736,738,740],{"class":37,"line":693},27,[35,695,696],{"class":78},"        ws.append([acc, act, ",[35,698,699],{"class":159},"None",[35,701,190],{"class":78},[35,703,704],{"class":74},"f",[35,706,707],{"class":45},"'=IF(C",[35,709,550],{"class":549},[35,711,712],{"class":78},"r",[35,714,556],{"class":549},[35,716,717],{"class":45},"=\"\",\"\",C",[35,719,550],{"class":549},[35,721,712],{"class":78},[35,723,556],{"class":549},[35,725,726],{"class":45},"-B",[35,728,550],{"class":549},[35,730,712],{"class":78},[35,732,556],{"class":549},[35,734,735],{"class":45},")'",[35,737,190],{"class":78},[35,739,699],{"class":159},[35,741,372],{"class":78},[35,743,745,748,751,753,756],{"class":37,"line":744},28,[35,746,747],{"class":74},"        for",[35,749,750],{"class":78}," col ",[35,752,361],{"class":74},[35,754,755],{"class":45}," \"CE\"",[35,757,758],{"class":78},":\n",[35,760,762,765,767,769,771,774,777,779,781,783,786,788,791,794,796,799],{"class":37,"line":761},29,[35,763,764],{"class":78},"            ws[",[35,766,704],{"class":74},[35,768,567],{"class":45},[35,770,550],{"class":549},[35,772,773],{"class":78},"col",[35,775,776],{"class":549},"}{",[35,778,712],{"class":78},[35,780,556],{"class":549},[35,782,567],{"class":45},[35,784,785],{"class":78},"].protection ",[35,787,175],{"class":74},[35,789,790],{"class":78}," Protection(",[35,792,793],{"class":391},"locked",[35,795,175],{"class":74},[35,797,798],{"class":159},"False",[35,800,444],{"class":78},[35,802,804,806,808,810,812,814,816,818,820,822,825,827],{"class":37,"line":803},30,[35,805,764],{"class":78},[35,807,704],{"class":74},[35,809,567],{"class":45},[35,811,550],{"class":549},[35,813,773],{"class":78},[35,815,776],{"class":549},[35,817,712],{"class":78},[35,819,556],{"class":549},[35,821,567],{"class":45},[35,823,824],{"class":78},"].fill ",[35,826,175],{"class":74},[35,828,829],{"class":159}," INPUT\n",[35,831,833,836,838,841,843,846],{"class":37,"line":832},31,[35,834,835],{"class":78},"    last ",[35,837,175],{"class":74},[35,839,840],{"class":159}," 3",[35,842,323],{"class":74},[35,844,845],{"class":159}," len",[35,847,848],{"class":78},"(rows)\n",[35,850,852,854,857,859,861,864,866,869,871,874,876,878,881,883,885,887,889,891,893,896,898,901,903,906,908,910,912,914,916,918,920,922],{"class":37,"line":851},32,[35,853,601],{"class":78},[35,855,856],{"class":45},"\"Total\"",[35,858,190],{"class":78},[35,860,704],{"class":74},[35,862,863],{"class":45},"\"=SUM(B4:B",[35,865,550],{"class":549},[35,867,868],{"class":78},"last",[35,870,556],{"class":549},[35,872,873],{"class":45},")\"",[35,875,190],{"class":78},[35,877,704],{"class":74},[35,879,880],{"class":45},"\"=SUM(C4:C",[35,882,550],{"class":549},[35,884,868],{"class":78},[35,886,556],{"class":549},[35,888,873],{"class":45},[35,890,190],{"class":78},[35,892,704],{"class":74},[35,894,895],{"class":45},"\"=C",[35,897,550],{"class":549},[35,899,900],{"class":78},"last ",[35,902,335],{"class":74},[35,904,905],{"class":159}," 1",[35,907,556],{"class":549},[35,909,726],{"class":45},[35,911,550],{"class":549},[35,913,900],{"class":78},[35,915,335],{"class":74},[35,917,905],{"class":159},[35,919,556],{"class":549},[35,921,567],{"class":45},[35,923,372],{"class":78},[35,925,927,930,932,935,938,940,943,945,948,950,953,955,958,960,963,965,968,970,972],{"class":37,"line":926},33,[35,928,929],{"class":78},"    dv ",[35,931,175],{"class":74},[35,933,934],{"class":78}," DataValidation(",[35,936,937],{"class":391},"type",[35,939,175],{"class":74},[35,941,942],{"class":45},"\"decimal\"",[35,944,190],{"class":78},[35,946,947],{"class":391},"operator",[35,949,175],{"class":74},[35,951,952],{"class":45},"\"greaterThanOrEqual\"",[35,954,190],{"class":78},[35,956,957],{"class":391},"formula1",[35,959,175],{"class":74},[35,961,962],{"class":45},"\"0\"",[35,964,190],{"class":78},[35,966,967],{"class":391},"allow_blank",[35,969,175],{"class":74},[35,971,441],{"class":159},[35,973,974],{"class":78},",\n",[35,976,978,981,983,985,987,990,992,995,997,1000,1002,1005],{"class":37,"line":977},34,[35,979,980],{"class":391},"                        showErrorMessage",[35,982,175],{"class":74},[35,984,441],{"class":159},[35,986,190],{"class":78},[35,988,989],{"class":391},"errorTitle",[35,991,175],{"class":74},[35,993,994],{"class":45},"\"Budget\"",[35,996,190],{"class":78},[35,998,999],{"class":391},"error",[35,1001,175],{"class":74},[35,1003,1004],{"class":45},"\"Enter an amount of 0 or more.\"",[35,1006,444],{"class":78},[35,1008,1010],{"class":37,"line":1009},35,[35,1011,1012],{"class":78},"    ws.add_data_validation(dv)\n",[35,1014,1016,1019,1021,1024,1026,1028,1030,1032],{"class":37,"line":1015},36,[35,1017,1018],{"class":78},"    dv.add(",[35,1020,704],{"class":74},[35,1022,1023],{"class":45},"\"C4:C",[35,1025,550],{"class":549},[35,1027,868],{"class":78},[35,1029,556],{"class":549},[35,1031,567],{"class":45},[35,1033,444],{"class":78},[35,1035,1037,1040,1042],{"class":37,"line":1036},37,[35,1038,1039],{"class":78},"    ws.protection.sheet ",[35,1041,175],{"class":74},[35,1043,1044],{"class":159}," True\n",[35,1046,1048,1051,1053],{"class":37,"line":1047},38,[35,1049,1050],{"class":78},"    ws.protection.password ",[35,1052,175],{"class":74},[35,1054,1055],{"class":45}," \"budget\"\n",[35,1057,1059,1062,1064,1067,1070],{"class":37,"line":1058},39,[35,1060,1061],{"class":78},"    meta ",[35,1063,175],{"class":74},[35,1065,1066],{"class":78}," wb.create_sheet(",[35,1068,1069],{"class":45},"\"_id\"",[35,1071,444],{"class":78},[35,1073,1075,1078,1080,1082,1085,1087,1090,1092,1095],{"class":37,"line":1074},40,[35,1076,1077],{"class":78},"    meta.append([",[35,1079,181],{"class":45},[35,1081,190],{"class":78},[35,1083,1084],{"class":45},"\"version\"",[35,1086,190],{"class":78},[35,1088,1089],{"class":45},"\"rows\"",[35,1091,190],{"class":78},[35,1093,1094],{"class":45},"\"fingerprint\"",[35,1096,372],{"class":78},[35,1098,1100,1103,1105,1107,1110,1113,1116,1119,1121,1124,1127],{"class":37,"line":1099},41,[35,1101,1102],{"class":78},"    meta.append([cc, ",[35,1104,160],{"class":159},[35,1106,190],{"class":78},[35,1108,1109],{"class":159},"len",[35,1111,1112],{"class":78},"(rows), hashlib.sha256(",[35,1114,1115],{"class":45},"\"|\"",[35,1117,1118],{"class":78},".join(rows[",[35,1120,404],{"class":45},[35,1122,1123],{"class":78},"]).encode()).hexdigest()[:",[35,1125,1126],{"class":159},"16",[35,1128,1129],{"class":78},"]])\n",[35,1131,1133,1136,1138],{"class":37,"line":1132},42,[35,1134,1135],{"class":78},"    meta.sheet_state ",[35,1137,175],{"class":74},[35,1139,1140],{"class":45}," \"veryHidden\"\n",[35,1142,1144,1147,1149,1152,1155,1157,1160,1162,1164,1166],{"class":37,"line":1143},43,[35,1145,1146],{"class":78},"    path ",[35,1148,175],{"class":74},[35,1150,1151],{"class":159}," OUT",[35,1153,1154],{"class":74}," \u002F",[35,1156,543],{"class":74},[35,1158,1159],{"class":45},"\"Budget_2027_",[35,1161,550],{"class":549},[35,1163,553],{"class":78},[35,1165,556],{"class":549},[35,1167,1168],{"class":45},".xlsx\"\n",[35,1170,1172],{"class":37,"line":1171},44,[35,1173,1174],{"class":78},"    wb.save(path)\n",[35,1176,1178,1181],{"class":37,"line":1177},45,[35,1179,1180],{"class":74},"    return",[35,1182,1183],{"class":78}," path\n",[35,1185,1187],{"class":37,"line":1186},46,[35,1188,153],{"emptyLinePlaceholder":152},[35,1190,1192,1195,1197,1200,1202,1205,1207,1210,1212,1214,1217,1220],{"class":37,"line":1191},47,[35,1193,1194],{"class":78},"sent ",[35,1196,175],{"class":74},[35,1198,1199],{"class":78}," [make_template(cc, name) ",[35,1201,355],{"class":74},[35,1203,1204],{"class":78}," cc, name ",[35,1206,361],{"class":74},[35,1208,1209],{"class":159}," zip",[35,1211,367],{"class":78},[35,1213,181],{"class":45},[35,1215,1216],{"class":78},"], centres[",[35,1218,1219],{"class":45},"\"name\"",[35,1221,1222],{"class":78},"])]\n",[35,1224,1226,1229,1232,1234,1237,1239],{"class":37,"line":1225},48,[35,1227,1228],{"class":159},"print",[35,1230,1231],{"class":78},"([p.name ",[35,1233,355],{"class":74},[35,1235,1236],{"class":78}," p ",[35,1238,361],{"class":74},[35,1240,1241],{"class":78}," sent])\n",[10,1243,1244,1245,1248,1249,1253,1254,18],{},"Only the budget and comment cells are unlocked and shaded; the sheet is protected, so the account list, the actuals and the formulas cannot be changed by accident — the password stops accidents, not determined people, which is all it is for. Data validation refuses negative or non-numeric budget entries as they are typed. The hidden ",[32,1246,1247],{},"_id"," sheet — set to ",[1250,1251,1252],"em",{},"very hidden",", so it does not appear in Excel's Unhide list — records the cost centre, template version, number of rows and a fingerprint of the account list, which is how the return is identified later however the file has been renamed. Sending each file to its holder is covered in ",[14,1255,1257],{"href":1256},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report",[1259,1260,1268,1269,1268,1273,1268,1277,1268,1284,1268,1294,1268,1305,1268,1312,1268,1317,1268,1321,1268,1326,1268,1331,1268,1336,1268,1343,1268,1349,1268,1353,1268,1357,1268,1360,1268,1365,1268,1372,1268,1377,1268,1381,1268,1384,1268,1388,1268,1391,1268,1396,1268,1403,1268,1408,1268,1411,1268,1414,1268,1418,1268,1421,1268,1424,1268,1430,1268,1434],"svg",{"viewBox":1261,"role":1262,"ariaLabelledBy":1263,"xmlns":1266,"style":1267},"4 0 752 142","img",[1264,1265],"bud-round-t","bud-round-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  ",[1270,1271,1272],"title",{"id":1264},"The budget round",[1274,1275,1276],"desc",{"id":1265},"Finance generates one protected, pre-filled template per cost centre with a hidden identity sheet and sends it to the budget holder. Returned files are identified by their hidden sheet, validated, consolidated into one table, and a tracker shows which cost centres are returned, invalid or missing.",[1278,1279],"rect",{"x":687,"y":1280,"width":1281,"height":1282,"fill":1283},"0","752","142","#ffffff",[1278,1285],{"x":1286,"y":1287,"width":1288,"height":1289,"rx":1290,"fill":1291,"stroke":1292,"style":1293},"20.0","16.0","115.2","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[1295,1296,1298],"g",{"transform":1297},"translate(65.6,29.0) scale(1.000)",[1299,1300],"path",{"d":1301,"fill":1302,"stroke":1303,"style":1304},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[1306,1307,1311],"text",{"x":1308,"y":1309,"style":1310},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Generate",[1306,1313,1316],{"x":1308,"y":1314,"style":1315},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","one per cost",[1306,1318,1320],{"x":1308,"y":1319,"style":1315},"112.5","centre",[37,1322],{"x1":1323,"y1":1324,"x2":1325,"y2":1324,"stroke":1292,"style":1293},"140.2","71.2","162.2",[1327,1328],"polygon",{"points":1329,"fill":1330},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[1278,1332],{"x":1333,"y":1287,"width":1288,"height":1289,"rx":1290,"fill":1334,"stroke":1335,"style":1293},"171.2","#e7ebef","var(--line,#cdd5e6)",[1295,1337,1339],{"transform":1338},"translate(216.8,37.8) scale(1.000)",[1299,1340],{"d":1341,"fill":1302,"stroke":1342,"style":1304},"M3 11l18-8-8 18-2-8z","var(--text,#172033)",[1306,1344,1348],{"x":1345,"y":1346,"style":1347},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Send",[1306,1350,1352],{"x":1345,"y":1351,"style":1315},"103.8","to each holder",[37,1354],{"x1":1355,"y1":1324,"x2":1356,"y2":1324,"stroke":1292,"style":1293},"291.4","313.4",[1327,1358],{"points":1359,"fill":1330},"317.4,71.2 308.4,76.2 308.4,66.2",[1278,1361],{"x":1362,"y":1287,"width":1288,"height":1289,"rx":1290,"fill":1363,"stroke":1364,"style":1293},"322.4","#fdefd8","var(--gold,#b4740a)",[1295,1366,1368],{"transform":1367},"translate(368.0,29.0) scale(1.000)",[1299,1369],{"d":1370,"fill":1302,"stroke":1371,"style":1304},"M8 11a4 4 0 1 0 .01 0z M11 13l9-9 M17 7l2 2 M15 9l2 2","var(--gold-ink,#7a4e06)",[1306,1373,1376],{"x":1374,"y":1309,"style":1375},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Identify",[1306,1378,1380],{"x":1374,"y":1379,"style":1375},"93.0","returns",[1306,1382,1383],{"x":1374,"y":1319,"style":1315},"hidden _id sheet",[37,1385],{"x1":1386,"y1":1324,"x2":1387,"y2":1324,"stroke":1292,"style":1293},"442.6","464.6",[1327,1389],{"points":1390,"fill":1330},"468.6,71.2 459.6,76.2 459.6,66.2",[1278,1392],{"x":1393,"y":1287,"width":1288,"height":1289,"rx":1290,"fill":1394,"stroke":1395,"style":1293},"473.6","#d9f4f1","var(--teal,#0f9488)",[1295,1397,1399],{"transform":1398},"translate(519.2,29.0) scale(1.000)",[1299,1400],{"d":1401,"fill":1302,"stroke":1402,"style":1304},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[1306,1404,1407],{"x":1405,"y":1309,"style":1406},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Validate",[1306,1409,1410],{"x":1405,"y":1314,"style":1315},"version, rows,",[1306,1412,1413],{"x":1405,"y":1319,"style":1315},"numbers",[37,1415],{"x1":1416,"y1":1324,"x2":1417,"y2":1324,"stroke":1292,"style":1293},"593.8","615.8",[1327,1419],{"points":1420,"fill":1330},"619.8,71.2 610.8,76.2 610.8,66.2",[1278,1422],{"x":1423,"y":1287,"width":1288,"height":1289,"rx":1290,"fill":1291,"stroke":1292,"style":1293},"624.8",[1295,1425,1427],{"transform":1426},"translate(670.4,37.8) scale(1.000)",[1299,1428],{"d":1429,"fill":1302,"stroke":1303,"style":1304},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14",[1306,1431,1433],{"x":1432,"y":1346,"style":1310},"682.4","Consolidate",[1306,1435,1436],{"x":1432,"y":1351,"style":1315},"plus a tracker",[1259,1438,1268,1444,1268,1447,1268,1450,1268,1455,1268,1461,1268,1465,1268,1471,1268,1475,1268,1478,1268,1482,1268,1486,1268,1490,1268,1494,1268,1498,1268,1502,1268,1506,1268,1511,1268,1514,1268,1518,1268,1520,1268,1523,1268,1525,1268,1528,1268,1530,1268,1533,1268,1535,1268,1538,1268,1541,1268,1544,1268,1546,1268,1550,1268,1552,1268,1555,1268,1557,1268,1559,1268,1562,1268,1564,1268,1567,1268,1571,1268,1573,1268,1576,1268,1578,1268,1581,1268,1583,1268,1585,1268,1587,1268,1589,1268,1592,1268,1595,1268,1597,1268,1600,1268,1602,1268,1605,1268,1607,1268,1609,1268,1611,1268,1613,1268,1616,1268,1620,1268,1622,1268,1626,1268,1628,1268,1631,1268,1633,1268,1635,1268,1637,1268,1639],{"viewBox":1439,"role":1262,"ariaLabelledBy":1440,"xmlns":1266,"style":1443},"113 0 534 196",[1441,1442],"bud-template-t","bud-template-d","width:100%;max-width:534px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1270,1445,1446],{"id":1441},"A generated template as the budget holder sees it",[1274,1448,1449],{"id":1442},"The title names the cost centre. Account names and 2026 actuals are locked. The 2027 budget and comment cells are unlocked and shaded, the change column and the total row are formulas, and the sheet is protected.",[1278,1451],{"x":1452,"y":1280,"width":1453,"height":1454,"fill":1283},"113","534","196",[1278,1456],{"x":1457,"y":1287,"width":1458,"height":1459,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"129.0","32.0","24.0","stroke-width:1px",[1278,1462],{"x":1463,"y":1287,"width":1464,"height":1459,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"161.0","111.6",[1306,1466,1470],{"x":1467,"y":1468,"style":1469},"216.9","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[1278,1472],{"x":1473,"y":1287,"width":1474,"height":1459,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"272.7","99.5",[1306,1476,1477],{"x":1362,"y":1468,"style":1469},"B",[1278,1479],{"x":1480,"y":1287,"width":1481,"height":1459,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"372.2","102.9",[1306,1483,1485],{"x":1484,"y":1468,"style":1469},"423.7","C",[1278,1487],{"x":1488,"y":1287,"width":1489,"height":1459,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"475.1","71.8",[1306,1491,1493],{"x":1492,"y":1468,"style":1469},"511.0","D",[1278,1495],{"x":1496,"y":1287,"width":1497,"height":1459,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"546.9","84.0",[1306,1499,1501],{"x":1500,"y":1468,"style":1469},"589.0","E",[1278,1503],{"x":1457,"y":1504,"width":1458,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"40.0","28.0",[1306,1507,1510],{"x":1508,"y":1509,"style":1469},"145.0","59.0","1",[1278,1512],{"x":1463,"y":1504,"width":1464,"height":1505,"rx":1280,"fill":1291,"stroke":1292,"style":1513},"stroke-width:1.5px",[1306,1515,1517],{"x":1467,"y":1509,"style":1516},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Account",[1278,1519],{"x":1473,"y":1504,"width":1474,"height":1505,"rx":1280,"fill":1291,"stroke":1292,"style":1513},[1306,1521,1522],{"x":1362,"y":1509,"style":1516},"Actual 2026",[1278,1524],{"x":1480,"y":1504,"width":1481,"height":1505,"rx":1280,"fill":1291,"stroke":1292,"style":1513},[1306,1526,1527],{"x":1484,"y":1509,"style":1516},"Budget 2027",[1278,1529],{"x":1488,"y":1504,"width":1489,"height":1505,"rx":1280,"fill":1291,"stroke":1292,"style":1513},[1306,1531,1532],{"x":1492,"y":1509,"style":1516},"Change",[1278,1534],{"x":1496,"y":1504,"width":1497,"height":1505,"rx":1280,"fill":1291,"stroke":1292,"style":1513},[1306,1536,1537],{"x":1500,"y":1509,"style":1516},"Comment",[1278,1539],{"x":1457,"y":1540,"width":1458,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"68.0",[1306,1542,349],{"x":1508,"y":1543,"style":1469},"87.0",[1278,1545],{"x":1463,"y":1540,"width":1464,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1547,1549],{"x":1467,"y":1543,"style":1548},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","6100 Salaries",[1278,1551],{"x":1473,"y":1540,"width":1474,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1553,1554],{"x":1362,"y":1543,"style":1548},"20,000",[1278,1556],{"x":1480,"y":1540,"width":1481,"height":1505,"rx":1280,"fill":1363,"stroke":1364,"style":1513},[1278,1558],{"x":1488,"y":1540,"width":1489,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1560,1561],{"x":1492,"y":1543,"style":1548},"=C−B",[1278,1563],{"x":1496,"y":1540,"width":1497,"height":1505,"rx":1280,"fill":1363,"stroke":1364,"style":1513},[1278,1565],{"x":1457,"y":1566,"width":1458,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"96.0",[1306,1568,1570],{"x":1508,"y":1569,"style":1469},"115.0","3",[1278,1572],{"x":1463,"y":1566,"width":1464,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1574,1575],{"x":1467,"y":1569,"style":1548},"6200 Travel",[1278,1577],{"x":1473,"y":1566,"width":1474,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1579,1580],{"x":1362,"y":1569,"style":1548},"24,000",[1278,1582],{"x":1480,"y":1566,"width":1481,"height":1505,"rx":1280,"fill":1363,"stroke":1364,"style":1513},[1278,1584],{"x":1488,"y":1566,"width":1489,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1586,1561],{"x":1492,"y":1569,"style":1548},[1278,1588],{"x":1496,"y":1566,"width":1497,"height":1505,"rx":1280,"fill":1363,"stroke":1364,"style":1513},[1278,1590],{"x":1457,"y":1591,"width":1458,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"124.0",[1306,1593,687],{"x":1508,"y":1594,"style":1469},"143.0",[1278,1596],{"x":1463,"y":1591,"width":1464,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1598,1599],{"x":1467,"y":1594,"style":1548},"6300 Training",[1278,1601],{"x":1473,"y":1591,"width":1474,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1603,1604],{"x":1362,"y":1594,"style":1548},"28,000",[1278,1606],{"x":1480,"y":1591,"width":1481,"height":1505,"rx":1280,"fill":1363,"stroke":1364,"style":1513},[1278,1608],{"x":1488,"y":1591,"width":1489,"height":1505,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,1610,1561],{"x":1492,"y":1594,"style":1548},[1278,1612],{"x":1496,"y":1591,"width":1497,"height":1505,"rx":1280,"fill":1363,"stroke":1364,"style":1513},[1278,1614],{"x":1457,"y":1615,"width":1458,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"152.0",[1306,1617,1619],{"x":1508,"y":1618,"style":1469},"171.0","5",[1278,1621],{"x":1463,"y":1615,"width":1464,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1513},[1306,1623,1625],{"x":1467,"y":1618,"style":1624},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Total",[1278,1627],{"x":1473,"y":1615,"width":1474,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1513},[1306,1629,1630],{"x":1362,"y":1618,"style":1624},"=SUM",[1278,1632],{"x":1480,"y":1615,"width":1481,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1513},[1306,1634,1630],{"x":1484,"y":1618,"style":1624},[1278,1636],{"x":1488,"y":1615,"width":1489,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1513},[1306,1638,1561],{"x":1492,"y":1618,"style":1624},[1278,1640],{"x":1496,"y":1615,"width":1497,"height":1505,"rx":1280,"fill":1334,"stroke":1335,"style":1513},[20,1642,1644],{"id":1643},"step-2-the-returns-come-back","Step 2 — The returns come back",[10,1646,1647],{},"Simulate four budget holders: one fills in everything correctly, one renames the file and leaves a cell blank, one returns another cost centre's template, and one has not replied yet:",[25,1649,1651],{"className":65,"code":1650,"language":67,"meta":30,"style":30},"from openpyxl import load_workbook\nimport shutil\n\nINBOX = Path(\"returns\"); INBOX.mkdir(exist_ok=True)\n\ndef fill(src, dest, values, comment=None):\n    wb = load_workbook(src)\n    ws = wb[\"Budget\"]\n    for r, v in enumerate(values, start=4):\n        ws[f\"C{r}\"] = v\n    if comment:\n        ws[\"E5\"] = comment\n    wb.save(dest)\n\nfill(OUT \u002F \"Budget_2027_CC100.xlsx\", INBOX \u002F \"Budget_2027_CC100.xlsx\", [21000, 5800, 9000, 12500, 4100])\nfill(OUT \u002F \"Budget_2027_CC200.xlsx\", INBOX \u002F \"ops budget FINAL v3.xlsx\", [30500, 33000, None, 47000, 52000], \"travel up: new site\")\nfill(OUT \u002F \"Budget_2027_CC100.xlsx\", INBOX \u002F \"Budget_2027_CC300.xlsx\", [1, 2, 3, 4, 5])\nprint(sorted(p.name for p in INBOX.iterdir()))\n",[32,1652,1653,1664,1671,1675,1701,1705,1721,1730,1743,1765,1790,1798,1812,1817,1821,1869,1921,1962],{"__ignoreMap":30},[35,1654,1655,1657,1659,1661],{"class":37,"line":38},[35,1656,85],{"class":74},[35,1658,115],{"class":78},[35,1660,75],{"class":74},[35,1662,1663],{"class":78}," load_workbook\n",[35,1665,1666,1668],{"class":37,"line":82},[35,1667,75],{"class":74},[35,1669,1670],{"class":78}," shutil\n",[35,1672,1673],{"class":37,"line":96},[35,1674,153],{"emptyLinePlaceholder":152},[35,1676,1677,1680,1682,1684,1687,1689,1691,1693,1695,1697,1699],{"class":37,"line":110},[35,1678,1679],{"class":159},"INBOX",[35,1681,163],{"class":74},[35,1683,422],{"class":78},[35,1685,1686],{"class":45},"\"returns\"",[35,1688,428],{"class":78},[35,1690,1679],{"class":159},[35,1692,433],{"class":78},[35,1694,436],{"class":391},[35,1696,175],{"class":74},[35,1698,441],{"class":159},[35,1700,444],{"class":78},[35,1702,1703],{"class":37,"line":123},[35,1704,153],{"emptyLinePlaceholder":152},[35,1706,1707,1709,1712,1715,1717,1719],{"class":37,"line":136},[35,1708,481],{"class":74},[35,1710,1711],{"class":484}," fill",[35,1713,1714],{"class":78},"(src, dest, values, comment",[35,1716,175],{"class":74},[35,1718,699],{"class":159},[35,1720,690],{"class":78},[35,1722,1723,1725,1727],{"class":37,"line":149},[35,1724,494],{"class":78},[35,1726,175],{"class":74},[35,1728,1729],{"class":78}," load_workbook(src)\n",[35,1731,1732,1734,1736,1739,1741],{"class":37,"line":156},[35,1733,505],{"class":78},[35,1735,175],{"class":74},[35,1737,1738],{"class":78}," wb[",[35,1740,994],{"class":45},[35,1742,300],{"class":78},[35,1744,1745,1747,1750,1752,1754,1757,1759,1761,1763],{"class":37,"line":169},[35,1746,654],{"class":74},[35,1748,1749],{"class":78}," r, v ",[35,1751,361],{"class":74},[35,1753,364],{"class":159},[35,1755,1756],{"class":78},"(values, ",[35,1758,682],{"class":391},[35,1760,175],{"class":74},[35,1762,687],{"class":159},[35,1764,690],{"class":78},[35,1766,1767,1770,1772,1775,1777,1779,1781,1783,1785,1787],{"class":37,"line":209},[35,1768,1769],{"class":78},"        ws[",[35,1771,704],{"class":74},[35,1773,1774],{"class":45},"\"C",[35,1776,550],{"class":549},[35,1778,712],{"class":78},[35,1780,556],{"class":549},[35,1782,567],{"class":45},[35,1784,642],{"class":78},[35,1786,175],{"class":74},[35,1788,1789],{"class":78}," v\n",[35,1791,1792,1795],{"class":37,"line":237},[35,1793,1794],{"class":74},"    if",[35,1796,1797],{"class":78}," comment:\n",[35,1799,1800,1802,1805,1807,1809],{"class":37,"line":266},[35,1801,1769],{"class":78},[35,1803,1804],{"class":45},"\"E5\"",[35,1806,642],{"class":78},[35,1808,175],{"class":74},[35,1810,1811],{"class":78}," comment\n",[35,1813,1814],{"class":37,"line":303},[35,1815,1816],{"class":78},"    wb.save(dest)\n",[35,1818,1819],{"class":37,"line":375},[35,1820,153],{"emptyLinePlaceholder":152},[35,1822,1823,1826,1828,1830,1833,1835,1837,1839,1841,1844,1847,1849,1852,1854,1857,1859,1862,1864,1867],{"class":37,"line":414},[35,1824,1825],{"class":78},"fill(",[35,1827,417],{"class":159},[35,1829,1154],{"class":74},[35,1831,1832],{"class":45}," \"Budget_2027_CC100.xlsx\"",[35,1834,190],{"class":78},[35,1836,1679],{"class":159},[35,1838,1154],{"class":74},[35,1840,1832],{"class":45},[35,1842,1843],{"class":78},", [",[35,1845,1846],{"class":159},"21000",[35,1848,190],{"class":78},[35,1850,1851],{"class":159},"5800",[35,1853,190],{"class":78},[35,1855,1856],{"class":159},"9000",[35,1858,190],{"class":78},[35,1860,1861],{"class":159},"12500",[35,1863,190],{"class":78},[35,1865,1866],{"class":159},"4100",[35,1868,372],{"class":78},[35,1870,1871,1873,1875,1877,1880,1882,1884,1886,1889,1891,1894,1896,1899,1901,1903,1905,1908,1910,1913,1916,1919],{"class":37,"line":447},[35,1872,1825],{"class":78},[35,1874,417],{"class":159},[35,1876,1154],{"class":74},[35,1878,1879],{"class":45}," \"Budget_2027_CC200.xlsx\"",[35,1881,190],{"class":78},[35,1883,1679],{"class":159},[35,1885,1154],{"class":74},[35,1887,1888],{"class":45}," \"ops budget FINAL v3.xlsx\"",[35,1890,1843],{"class":78},[35,1892,1893],{"class":159},"30500",[35,1895,190],{"class":78},[35,1897,1898],{"class":159},"33000",[35,1900,190],{"class":78},[35,1902,699],{"class":159},[35,1904,190],{"class":78},[35,1906,1907],{"class":159},"47000",[35,1909,190],{"class":78},[35,1911,1912],{"class":159},"52000",[35,1914,1915],{"class":78},"], ",[35,1917,1918],{"class":45},"\"travel up: new site\"",[35,1920,444],{"class":78},[35,1922,1923,1925,1927,1929,1931,1933,1935,1937,1940,1942,1944,1946,1948,1950,1952,1954,1956,1958,1960],{"class":37,"line":473},[35,1924,1825],{"class":78},[35,1926,417],{"class":159},[35,1928,1154],{"class":74},[35,1930,1832],{"class":45},[35,1932,190],{"class":78},[35,1934,1679],{"class":159},[35,1936,1154],{"class":74},[35,1938,1939],{"class":45}," \"Budget_2027_CC300.xlsx\"",[35,1941,1843],{"class":78},[35,1943,1510],{"class":159},[35,1945,190],{"class":78},[35,1947,349],{"class":159},[35,1949,190],{"class":78},[35,1951,1570],{"class":159},[35,1953,190],{"class":78},[35,1955,687],{"class":159},[35,1957,190],{"class":78},[35,1959,1619],{"class":159},[35,1961,372],{"class":78},[35,1963,1964,1966,1968,1971,1974,1976,1978,1980,1983],{"class":37,"line":478},[35,1965,1228],{"class":159},[35,1967,317],{"class":78},[35,1969,1970],{"class":159},"sorted",[35,1972,1973],{"class":78},"(p.name ",[35,1975,355],{"class":74},[35,1977,1236],{"class":78},[35,1979,361],{"class":74},[35,1981,1982],{"class":159}," INBOX",[35,1984,1985],{"class":78},".iterdir()))\n",[10,1987,1988],{},"The Operations holder saved the file as \"ops budget FINAL v3.xlsx\" — normal behaviour that would break any process relying on file names. The Sales holder filled in a copy of Finance's template and named it after their own cost centre, which is surprisingly common when people forward templates to each other.",[20,1990,1992],{"id":1991},"step-3-identify-validate-and-consolidate","Step 3 — Identify, validate and consolidate",[10,1994,1995],{},"Read every file in the inbox, identify it from its hidden sheet rather than its name, and check it before taking any numbers:",[25,1997,1999],{"className":65,"code":1998,"language":67,"meta":30,"style":30},"def read_return(path):\n    wb = load_workbook(path, data_only=False)\n    if \"_id\" not in wb.sheetnames:\n        return None, [\"not a budget template\"]\n    cc, version, nrows, fingerprint = [c.value for c in wb[\"_id\"][2]]\n    problems = []\n    if version != VERSION:\n        problems.append(f\"template version {version}, expected {VERSION}\")\n    ws = wb[\"Budget\"]\n    rows = [[c.value for c in row] for row in ws.iter_rows(min_row=4, max_row=3 + nrows, max_col=5)]\n    accounts_now = [r[0] for r in rows]\n    if hashlib.sha256(\"|\".join(map(str, accounts_now)).encode()).hexdigest()[:16] != fingerprint:\n        problems.append(\"account rows changed\")\n    blanks = [r[0] for r in rows if r[2] is None]\n    if blanks:\n        problems.append(f\"blank budget for {', '.join(blanks)}\")\n    bad = [r[0] for r in rows if r[2] is not None and not isinstance(r[2], (int, float))]\n    if bad:\n        problems.append(f\"non-numeric budget for {', '.join(bad)}\")\n    data = pd.DataFrame(rows, columns=[\"account\", \"actual_2026\", \"budget_2027\", \"change\", \"comment\"]).assign(cc=cc, file=path.name)\n    return data, problems\n\ntracker, frames = [], []\nfor path in sorted(INBOX.glob(\"*.xlsx\")):\n    data, problems = read_return(path)\n    cc = data[\"cc\"].iloc[0] if data is not None else None\n    expected = path.stem.split(\"_\")[-1]\n    if cc and expected.startswith(\"CC\") and expected != cc:\n        problems.append(f\"file named {expected} but contains {cc}'s template\")\n    tracker.append({\"file\": path.name, \"cc\": cc, \"for\": expected if expected.startswith(\"CC\") else cc,\n                    \"status\": \"valid\" if not problems else \"invalid\", \"problems\": \"; \".join(problems)})\n    if data is not None and not problems:\n        frames.append(data)\n\ntracker = pd.DataFrame(tracker)\nreturned = set(tracker.loc[tracker[\"status\"] == \"valid\", \"cc\"])\nstatus = centres.assign(status=centres[\"cc\"].map(lambda c: \"valid\" if c in returned else\n                                                  (\"invalid\" if c in set(tracker[\"for\"]) else \"missing\")))\nprint(tracker.to_string(index=False))\nprint(status[[\"cc\", \"name\", \"holder\", \"status\"]].to_string(index=False))\n",[32,2000,2001,2011,2029,2045,2060,2089,2099,2114,2144,2156,2215,2239,2271,2280,2319,2326,2349,2411,2418,2440,2495,2502,2506,2516,2541,2551,2587,2609,2638,2668,2704,2741,2760,2765,2769,2779,2808,2851,2883,2900],{"__ignoreMap":30},[35,2002,2003,2005,2008],{"class":37,"line":38},[35,2004,481],{"class":74},[35,2006,2007],{"class":484}," read_return",[35,2009,2010],{"class":78},"(path):\n",[35,2012,2013,2015,2017,2020,2023,2025,2027],{"class":37,"line":82},[35,2014,494],{"class":78},[35,2016,175],{"class":74},[35,2018,2019],{"class":78}," load_workbook(path, ",[35,2021,2022],{"class":391},"data_only",[35,2024,175],{"class":74},[35,2026,798],{"class":159},[35,2028,444],{"class":78},[35,2030,2031,2033,2036,2039,2042],{"class":37,"line":96},[35,2032,1794],{"class":74},[35,2034,2035],{"class":45}," \"_id\"",[35,2037,2038],{"class":74}," not",[35,2040,2041],{"class":74}," in",[35,2043,2044],{"class":78}," wb.sheetnames:\n",[35,2046,2047,2050,2053,2055,2058],{"class":37,"line":110},[35,2048,2049],{"class":74},"        return",[35,2051,2052],{"class":159}," None",[35,2054,1843],{"class":78},[35,2056,2057],{"class":45},"\"not a budget template\"",[35,2059,300],{"class":78},[35,2061,2062,2065,2067,2070,2072,2075,2077,2079,2081,2084,2086],{"class":37,"line":123},[35,2063,2064],{"class":78},"    cc, version, nrows, fingerprint ",[35,2066,175],{"class":74},[35,2068,2069],{"class":78}," [c.value ",[35,2071,355],{"class":74},[35,2073,2074],{"class":78}," c ",[35,2076,361],{"class":74},[35,2078,1738],{"class":78},[35,2080,1069],{"class":45},[35,2082,2083],{"class":78},"][",[35,2085,349],{"class":159},[35,2087,2088],{"class":78},"]]\n",[35,2090,2091,2094,2096],{"class":37,"line":136},[35,2092,2093],{"class":78},"    problems ",[35,2095,175],{"class":74},[35,2097,2098],{"class":78}," []\n",[35,2100,2101,2103,2106,2109,2112],{"class":37,"line":149},[35,2102,1794],{"class":74},[35,2104,2105],{"class":78}," version ",[35,2107,2108],{"class":74},"!=",[35,2110,2111],{"class":159}," VERSION",[35,2113,758],{"class":78},[35,2115,2116,2119,2121,2124,2126,2129,2131,2134,2136,2138,2140,2142],{"class":37,"line":156},[35,2117,2118],{"class":78},"        problems.append(",[35,2120,704],{"class":74},[35,2122,2123],{"class":45},"\"template version ",[35,2125,550],{"class":549},[35,2127,2128],{"class":78},"version",[35,2130,556],{"class":549},[35,2132,2133],{"class":45},", expected ",[35,2135,550],{"class":549},[35,2137,160],{"class":159},[35,2139,556],{"class":549},[35,2141,567],{"class":45},[35,2143,444],{"class":78},[35,2145,2146,2148,2150,2152,2154],{"class":37,"line":169},[35,2147,505],{"class":78},[35,2149,175],{"class":74},[35,2151,1738],{"class":78},[35,2153,994],{"class":45},[35,2155,300],{"class":78},[35,2157,2158,2160,2162,2165,2167,2169,2171,2174,2176,2179,2181,2184,2187,2189,2191,2193,2196,2198,2200,2202,2205,2208,2210,2212],{"class":37,"line":209},[35,2159,632],{"class":78},[35,2161,175],{"class":74},[35,2163,2164],{"class":78}," [[c.value ",[35,2166,355],{"class":74},[35,2168,2074],{"class":78},[35,2170,361],{"class":74},[35,2172,2173],{"class":78}," row] ",[35,2175,355],{"class":74},[35,2177,2178],{"class":78}," row ",[35,2180,361],{"class":74},[35,2182,2183],{"class":78}," ws.iter_rows(",[35,2185,2186],{"class":391},"min_row",[35,2188,175],{"class":74},[35,2190,687],{"class":159},[35,2192,190],{"class":78},[35,2194,2195],{"class":391},"max_row",[35,2197,175],{"class":74},[35,2199,1570],{"class":159},[35,2201,323],{"class":74},[35,2203,2204],{"class":78}," nrows, ",[35,2206,2207],{"class":391},"max_col",[35,2209,175],{"class":74},[35,2211,1619],{"class":159},[35,2213,2214],{"class":78},")]\n",[35,2216,2217,2220,2222,2225,2227,2229,2231,2234,2236],{"class":37,"line":237},[35,2218,2219],{"class":78},"    accounts_now ",[35,2221,175],{"class":74},[35,2223,2224],{"class":78}," [r[",[35,2226,1280],{"class":159},[35,2228,642],{"class":78},[35,2230,355],{"class":74},[35,2232,2233],{"class":78}," r ",[35,2235,361],{"class":74},[35,2237,2238],{"class":78}," rows]\n",[35,2240,2241,2243,2246,2248,2251,2254,2256,2259,2262,2264,2266,2268],{"class":37,"line":266},[35,2242,1794],{"class":74},[35,2244,2245],{"class":78}," hashlib.sha256(",[35,2247,1115],{"class":45},[35,2249,2250],{"class":78},".join(",[35,2252,2253],{"class":159},"map",[35,2255,317],{"class":78},[35,2257,2258],{"class":159},"str",[35,2260,2261],{"class":78},", accounts_now)).encode()).hexdigest()[:",[35,2263,1126],{"class":159},[35,2265,642],{"class":78},[35,2267,2108],{"class":74},[35,2269,2270],{"class":78}," fingerprint:\n",[35,2272,2273,2275,2278],{"class":37,"line":303},[35,2274,2118],{"class":78},[35,2276,2277],{"class":45},"\"account rows changed\"",[35,2279,444],{"class":78},[35,2281,2282,2285,2287,2289,2291,2293,2295,2297,2299,2302,2305,2308,2310,2312,2315,2317],{"class":37,"line":375},[35,2283,2284],{"class":78},"    blanks ",[35,2286,175],{"class":74},[35,2288,2224],{"class":78},[35,2290,1280],{"class":159},[35,2292,642],{"class":78},[35,2294,355],{"class":74},[35,2296,2233],{"class":78},[35,2298,361],{"class":74},[35,2300,2301],{"class":78}," rows ",[35,2303,2304],{"class":74},"if",[35,2306,2307],{"class":78}," r[",[35,2309,349],{"class":159},[35,2311,642],{"class":78},[35,2313,2314],{"class":74},"is",[35,2316,2052],{"class":159},[35,2318,300],{"class":78},[35,2320,2321,2323],{"class":37,"line":414},[35,2322,1794],{"class":74},[35,2324,2325],{"class":78}," blanks:\n",[35,2327,2328,2330,2332,2335,2337,2340,2343,2345,2347],{"class":37,"line":447},[35,2329,2118],{"class":78},[35,2331,704],{"class":74},[35,2333,2334],{"class":45},"\"blank budget for ",[35,2336,550],{"class":549},[35,2338,2339],{"class":45},"', '",[35,2341,2342],{"class":78},".join(blanks)",[35,2344,556],{"class":549},[35,2346,567],{"class":45},[35,2348,444],{"class":78},[35,2350,2351,2354,2356,2358,2360,2362,2364,2366,2368,2370,2372,2374,2376,2378,2380,2382,2384,2387,2389,2392,2395,2397,2400,2403,2405,2408],{"class":37,"line":473},[35,2352,2353],{"class":78},"    bad ",[35,2355,175],{"class":74},[35,2357,2224],{"class":78},[35,2359,1280],{"class":159},[35,2361,642],{"class":78},[35,2363,355],{"class":74},[35,2365,2233],{"class":78},[35,2367,361],{"class":74},[35,2369,2301],{"class":78},[35,2371,2304],{"class":74},[35,2373,2307],{"class":78},[35,2375,349],{"class":159},[35,2377,642],{"class":78},[35,2379,2314],{"class":74},[35,2381,2038],{"class":74},[35,2383,2052],{"class":159},[35,2385,2386],{"class":74}," and",[35,2388,2038],{"class":74},[35,2390,2391],{"class":159}," isinstance",[35,2393,2394],{"class":78},"(r[",[35,2396,349],{"class":159},[35,2398,2399],{"class":78},"], (",[35,2401,2402],{"class":159},"int",[35,2404,190],{"class":78},[35,2406,2407],{"class":159},"float",[35,2409,2410],{"class":78},"))]\n",[35,2412,2413,2415],{"class":37,"line":478},[35,2414,1794],{"class":74},[35,2416,2417],{"class":78}," bad:\n",[35,2419,2420,2422,2424,2427,2429,2431,2434,2436,2438],{"class":37,"line":491},[35,2421,2118],{"class":78},[35,2423,704],{"class":74},[35,2425,2426],{"class":45},"\"non-numeric budget for ",[35,2428,550],{"class":549},[35,2430,2339],{"class":45},[35,2432,2433],{"class":78},".join(bad)",[35,2435,556],{"class":549},[35,2437,567],{"class":45},[35,2439,444],{"class":78},[35,2441,2442,2445,2447,2450,2452,2454,2456,2458,2460,2462,2464,2467,2469,2472,2474,2477,2480,2482,2484,2487,2490,2492],{"class":37,"line":502},[35,2443,2444],{"class":78},"    data ",[35,2446,175],{"class":74},[35,2448,2449],{"class":78}," pd.DataFrame(rows, ",[35,2451,392],{"class":391},[35,2453,175],{"class":74},[35,2455,397],{"class":78},[35,2457,404],{"class":45},[35,2459,190],{"class":78},[35,2461,409],{"class":45},[35,2463,190],{"class":78},[35,2465,2466],{"class":45},"\"budget_2027\"",[35,2468,190],{"class":78},[35,2470,2471],{"class":45},"\"change\"",[35,2473,190],{"class":78},[35,2475,2476],{"class":45},"\"comment\"",[35,2478,2479],{"class":78},"]).assign(",[35,2481,553],{"class":391},[35,2483,175],{"class":74},[35,2485,2486],{"class":78},"cc, ",[35,2488,2489],{"class":391},"file",[35,2491,175],{"class":74},[35,2493,2494],{"class":78},"path.name)\n",[35,2496,2497,2499],{"class":37,"line":513},[35,2498,1180],{"class":74},[35,2500,2501],{"class":78}," data, problems\n",[35,2503,2504],{"class":37,"line":524},[35,2505,153],{"emptyLinePlaceholder":152},[35,2507,2508,2511,2513],{"class":37,"line":592},[35,2509,2510],{"class":78},"tracker, frames ",[35,2512,175],{"class":74},[35,2514,2515],{"class":78}," [], []\n",[35,2517,2518,2520,2523,2525,2528,2530,2532,2535,2538],{"class":37,"line":598},[35,2519,355],{"class":74},[35,2521,2522],{"class":78}," path ",[35,2524,361],{"class":74},[35,2526,2527],{"class":159}," sorted",[35,2529,317],{"class":78},[35,2531,1679],{"class":159},[35,2533,2534],{"class":78},".glob(",[35,2536,2537],{"class":45},"\"*.xlsx\"",[35,2539,2540],{"class":78},")):\n",[35,2542,2543,2546,2548],{"class":37,"line":629},[35,2544,2545],{"class":78},"    data, problems ",[35,2547,175],{"class":74},[35,2549,2550],{"class":78}," read_return(path)\n",[35,2552,2553,2556,2558,2561,2563,2566,2568,2570,2572,2575,2577,2579,2581,2584],{"class":37,"line":651},[35,2554,2555],{"class":78},"    cc ",[35,2557,175],{"class":74},[35,2559,2560],{"class":78}," data[",[35,2562,181],{"class":45},[35,2564,2565],{"class":78},"].iloc[",[35,2567,1280],{"class":159},[35,2569,642],{"class":78},[35,2571,2304],{"class":74},[35,2573,2574],{"class":78}," data ",[35,2576,2314],{"class":74},[35,2578,2038],{"class":74},[35,2580,2052],{"class":159},[35,2582,2583],{"class":74}," else",[35,2585,2586],{"class":159}," None\n",[35,2588,2589,2592,2594,2597,2600,2603,2605,2607],{"class":37,"line":693},[35,2590,2591],{"class":78},"    expected ",[35,2593,175],{"class":74},[35,2595,2596],{"class":78}," path.stem.split(",[35,2598,2599],{"class":45},"\"_\"",[35,2601,2602],{"class":78},")[",[35,2604,346],{"class":74},[35,2606,1510],{"class":159},[35,2608,300],{"class":78},[35,2610,2611,2613,2616,2619,2622,2625,2628,2630,2633,2635],{"class":37,"line":744},[35,2612,1794],{"class":74},[35,2614,2615],{"class":78}," cc ",[35,2617,2618],{"class":74},"and",[35,2620,2621],{"class":78}," expected.startswith(",[35,2623,2624],{"class":45},"\"CC\"",[35,2626,2627],{"class":78},") ",[35,2629,2618],{"class":74},[35,2631,2632],{"class":78}," expected ",[35,2634,2108],{"class":74},[35,2636,2637],{"class":78}," cc:\n",[35,2639,2640,2642,2644,2647,2649,2652,2654,2657,2659,2661,2663,2666],{"class":37,"line":761},[35,2641,2118],{"class":78},[35,2643,704],{"class":74},[35,2645,2646],{"class":45},"\"file named ",[35,2648,550],{"class":549},[35,2650,2651],{"class":78},"expected",[35,2653,556],{"class":549},[35,2655,2656],{"class":45}," but contains ",[35,2658,550],{"class":549},[35,2660,553],{"class":78},[35,2662,556],{"class":549},[35,2664,2665],{"class":45},"'s template\"",[35,2667,444],{"class":78},[35,2669,2670,2673,2676,2679,2681,2684,2687,2690,2692,2694,2696,2698,2701],{"class":37,"line":803},[35,2671,2672],{"class":78},"    tracker.append({",[35,2674,2675],{"class":45},"\"file\"",[35,2677,2678],{"class":78},": path.name, ",[35,2680,181],{"class":45},[35,2682,2683],{"class":78},": cc, ",[35,2685,2686],{"class":45},"\"for\"",[35,2688,2689],{"class":78},": expected ",[35,2691,2304],{"class":74},[35,2693,2621],{"class":78},[35,2695,2624],{"class":45},[35,2697,2627],{"class":78},[35,2699,2700],{"class":74},"else",[35,2702,2703],{"class":78}," cc,\n",[35,2705,2706,2709,2712,2715,2718,2720,2723,2725,2728,2730,2733,2735,2738],{"class":37,"line":832},[35,2707,2708],{"class":45},"                    \"status\"",[35,2710,2711],{"class":78},": ",[35,2713,2714],{"class":45},"\"valid\"",[35,2716,2717],{"class":74}," if",[35,2719,2038],{"class":74},[35,2721,2722],{"class":78}," problems ",[35,2724,2700],{"class":74},[35,2726,2727],{"class":45}," \"invalid\"",[35,2729,190],{"class":78},[35,2731,2732],{"class":45},"\"problems\"",[35,2734,2711],{"class":78},[35,2736,2737],{"class":45},"\"; \"",[35,2739,2740],{"class":78},".join(problems)})\n",[35,2742,2743,2745,2747,2749,2751,2753,2755,2757],{"class":37,"line":851},[35,2744,1794],{"class":74},[35,2746,2574],{"class":78},[35,2748,2314],{"class":74},[35,2750,2038],{"class":74},[35,2752,2052],{"class":159},[35,2754,2386],{"class":74},[35,2756,2038],{"class":74},[35,2758,2759],{"class":78}," problems:\n",[35,2761,2762],{"class":37,"line":926},[35,2763,2764],{"class":78},"        frames.append(data)\n",[35,2766,2767],{"class":37,"line":977},[35,2768,153],{"emptyLinePlaceholder":152},[35,2770,2771,2774,2776],{"class":37,"line":1009},[35,2772,2773],{"class":78},"tracker ",[35,2775,175],{"class":74},[35,2777,2778],{"class":78}," pd.DataFrame(tracker)\n",[35,2780,2781,2784,2786,2789,2792,2795,2797,2799,2802,2804,2806],{"class":37,"line":1015},[35,2782,2783],{"class":78},"returned ",[35,2785,175],{"class":74},[35,2787,2788],{"class":159}," set",[35,2790,2791],{"class":78},"(tracker.loc[tracker[",[35,2793,2794],{"class":45},"\"status\"",[35,2796,642],{"class":78},[35,2798,645],{"class":74},[35,2800,2801],{"class":45}," \"valid\"",[35,2803,190],{"class":78},[35,2805,181],{"class":45},[35,2807,372],{"class":78},[35,2809,2810,2813,2815,2818,2821,2823,2826,2828,2831,2834,2837,2839,2841,2843,2845,2848],{"class":37,"line":1036},[35,2811,2812],{"class":78},"status ",[35,2814,175],{"class":74},[35,2816,2817],{"class":78}," centres.assign(",[35,2819,2820],{"class":391},"status",[35,2822,175],{"class":74},[35,2824,2825],{"class":78},"centres[",[35,2827,181],{"class":45},[35,2829,2830],{"class":78},"].map(",[35,2832,2833],{"class":74},"lambda",[35,2835,2836],{"class":78}," c: ",[35,2838,2714],{"class":45},[35,2840,2717],{"class":74},[35,2842,2074],{"class":78},[35,2844,361],{"class":74},[35,2846,2847],{"class":78}," returned ",[35,2849,2850],{"class":74},"else\n",[35,2852,2853,2856,2859,2861,2863,2865,2867,2870,2872,2875,2877,2880],{"class":37,"line":1047},[35,2854,2855],{"class":78},"                                                  (",[35,2857,2858],{"class":45},"\"invalid\"",[35,2860,2717],{"class":74},[35,2862,2074],{"class":78},[35,2864,361],{"class":74},[35,2866,2788],{"class":159},[35,2868,2869],{"class":78},"(tracker[",[35,2871,2686],{"class":45},[35,2873,2874],{"class":78},"]) ",[35,2876,2700],{"class":74},[35,2878,2879],{"class":45}," \"missing\"",[35,2881,2882],{"class":78},")))\n",[35,2884,2885,2887,2890,2893,2895,2897],{"class":37,"line":1058},[35,2886,1228],{"class":159},[35,2888,2889],{"class":78},"(tracker.to_string(",[35,2891,2892],{"class":391},"index",[35,2894,175],{"class":74},[35,2896,798],{"class":159},[35,2898,2899],{"class":78},"))\n",[35,2901,2902,2904,2907,2909,2911,2913,2915,2918,2920,2922,2925,2927,2929,2931],{"class":37,"line":1074},[35,2903,1228],{"class":159},[35,2905,2906],{"class":78},"(status[[",[35,2908,181],{"class":45},[35,2910,190],{"class":78},[35,2912,1219],{"class":45},[35,2914,190],{"class":78},[35,2916,2917],{"class":45},"\"holder\"",[35,2919,190],{"class":78},[35,2921,2794],{"class":45},[35,2923,2924],{"class":78},"]].to_string(",[35,2926,2892],{"class":391},[35,2928,175],{"class":74},[35,2930,798],{"class":159},[35,2932,2899],{"class":78},[10,2934,2935,2936,2939,2940,2943,2944,2947,2948,2951],{},"The hidden sheet identifies the renamed Operations file correctly as CC200, and its blank Training budget is reported by account name rather than as a cell address. The file named after Sales turns out to contain Finance's template, and — since Finance's own return already arrived — it is rejected rather than counted twice. The tracker records which cost centre each file was ",[1250,2937,2938],{},"meant"," for — from a file name that follows the pattern, otherwise from the hidden sheet — so Sales shows as invalid rather than missing: they did reply, just with the wrong workbook. The status table combines the tracker with the list of cost centres, so the missing IT return is visible even though there is no file for it. In a real round, the ",[1250,2941,2942],{},"invalid"," and ",[1250,2945,2946],{},"missing"," lists become reminder emails; only ",[1250,2949,2950],{},"valid"," returns are consolidated.",[1259,2953,1268,2958,1268,2961,1268,2964,1268,2967,1268,2970,1268,2975,1268,2980,1268,2983,1268,2987,1268,2990,1268,2993,1268,2997,1268,3002,1268,3004,1268,3008,1268,3010,1268,3013,1268,3015,1268,3017,1268,3020,1268,3024,1268,3026,1268,3030,1268,3032,1268,3035,1268,3037,1268,3040,1268,3042,1268,3046,1268,3048,1268,3051,1268,3053,1268,3055,1268,3057,1268,3060,1268,3063,1268,3067,1268,3069,1268,3073,1268,3075,1268,3077,1268,3079],{"viewBox":2954,"role":1262,"ariaLabelledBy":2955,"xmlns":1266,"style":1267},"4 0 752 208",[2956,2957],"bud-status-t","bud-status-d",[1270,2959,2960],{"id":2956},"What the checks found in each return",[1274,2962,2963],{"id":2957},"CC100 Finance is valid. CC200 Operations was renamed but identified from its hidden sheet, and is invalid because the Training budget is blank. The file named for CC300 Sales contains Finance's template and is rejected. CC400 IT has not returned anything.",[1278,2965],{"x":687,"y":1280,"width":1281,"height":2966,"fill":1283},"208",[1278,2968],{"x":1286,"y":1287,"width":2969,"height":1504,"rx":1280,"fill":1283,"stroke":1335,"style":1460},"99.1",[1278,2971],{"x":2972,"y":1287,"width":2973,"height":1504,"rx":1280,"fill":1291,"stroke":1292,"style":2974},"119.1","207.0","stroke-width:1.2px",[1306,2976,2489],{"x":2977,"y":2978,"style":2979},"222.6","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[1278,2981],{"x":2982,"y":1287,"width":2973,"height":1504,"rx":1280,"fill":1291,"stroke":1292,"style":2974},"326.1",[1306,2984,2986],{"x":2985,"y":2978,"style":2979},"429.6","identified as",[1278,2988],{"x":2989,"y":1287,"width":2973,"height":1504,"rx":1280,"fill":1291,"stroke":1292,"style":2974},"533.0",[1306,2991,2820],{"x":2992,"y":2978,"style":2979},"636.5",[1278,2994],{"x":1286,"y":2995,"width":2969,"height":2996,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"56.0","34.0",[1306,2998,3001],{"x":1458,"y":2999,"style":3000},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Finance",[1278,3003],{"x":2972,"y":2995,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3005,3007],{"x":2977,"y":3006,"style":1315},"77.0","Budget_2027_CC100.xlsx",[1278,3009],{"x":2982,"y":2995,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3011,3012],{"x":2985,"y":3006,"style":1315},"CC100",[1278,3014],{"x":2989,"y":2995,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3016,2950],{"x":2992,"y":3006,"style":1315},[1278,3018],{"x":1286,"y":3019,"width":2969,"height":2996,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"90.0",[1306,3021,3023],{"x":1458,"y":3022,"style":3000},"112.0","Operations",[1278,3025],{"x":2972,"y":3019,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3027,3029],{"x":2977,"y":3028,"style":1315},"111.0","ops budget FINAL v3.xlsx",[1278,3031],{"x":2982,"y":3019,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3033,3034],{"x":2985,"y":3028,"style":1315},"CC200",[1278,3036],{"x":2989,"y":3019,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3038,3039],{"x":2992,"y":3028,"style":1315},"invalid — blank Training",[1278,3041],{"x":1286,"y":1591,"width":2969,"height":2996,"rx":1280,"fill":1334,"stroke":1335,"style":1460},[1306,3043,3045],{"x":1458,"y":3044,"style":3000},"146.0","Sales",[1278,3047],{"x":2972,"y":1591,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3049,3050],{"x":2977,"y":1508,"style":1315},"Budget_2027_CC300.xlsx",[1278,3052],{"x":2982,"y":1591,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3054,3012],{"x":2985,"y":1508,"style":1315},[1278,3056],{"x":2989,"y":1591,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3058,3059],{"x":2992,"y":1508,"style":1315},"invalid — wrong template",[1278,3061],{"x":1286,"y":3062,"width":2969,"height":2996,"rx":1280,"fill":1334,"stroke":1335,"style":1460},"158.0",[1306,3064,3066],{"x":1458,"y":3065,"style":3000},"180.0","IT",[1278,3068],{"x":2972,"y":3062,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3070,3072],{"x":2977,"y":3071,"style":1315},"179.0","—",[1278,3074],{"x":2982,"y":3062,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3076,3072],{"x":2985,"y":3071,"style":1315},[1278,3078],{"x":2989,"y":3062,"width":2973,"height":2996,"rx":1280,"fill":1283,"stroke":1335,"style":1460},[1306,3080,2946],{"x":2992,"y":3071,"style":1315},[20,3082,3084],{"id":3083},"step-4-the-consolidated-budget-and-the-tracker","Step 4 — The consolidated budget and the tracker",[25,3086,3088],{"className":65,"code":3087,"language":67,"meta":30,"style":30},"budget = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()\nby_account = budget.pivot_table(index=\"account\", columns=\"cc\", values=\"budget_2027\", aggfunc=\"sum\", margins=True, margins_name=\"Total\")\nwith pd.ExcelWriter(\"budget_2027_consolidated.xlsx\", engine=\"openpyxl\") as xw:\n    status.to_excel(xw, sheet_name=\"Tracker\", index=False)\n    tracker.to_excel(xw, sheet_name=\"Files\", index=False)\n    budget.to_excel(xw, sheet_name=\"Lines\", index=False)\n    by_account.to_excel(xw, sheet_name=\"By account\")\nprint(by_account.to_string())\n",[32,3089,3090,3119,3182,3210,3233,3255,3277,3291],{"__ignoreMap":30},[35,3091,3092,3095,3097,3100,3103,3105,3107,3109,3111,3114,3116],{"class":37,"line":38},[35,3093,3094],{"class":78},"budget ",[35,3096,175],{"class":74},[35,3098,3099],{"class":78}," pd.concat(frames, ",[35,3101,3102],{"class":391},"ignore_index",[35,3104,175],{"class":74},[35,3106,441],{"class":159},[35,3108,2627],{"class":78},[35,3110,2304],{"class":74},[35,3112,3113],{"class":78}," frames ",[35,3115,2700],{"class":74},[35,3117,3118],{"class":78}," pd.DataFrame()\n",[35,3120,3121,3124,3126,3129,3131,3133,3135,3137,3139,3141,3143,3145,3148,3150,3152,3154,3157,3159,3162,3164,3167,3169,3171,3173,3176,3178,3180],{"class":37,"line":82},[35,3122,3123],{"class":78},"by_account ",[35,3125,175],{"class":74},[35,3127,3128],{"class":78}," budget.pivot_table(",[35,3130,2892],{"class":391},[35,3132,175],{"class":74},[35,3134,404],{"class":45},[35,3136,190],{"class":78},[35,3138,392],{"class":391},[35,3140,175],{"class":74},[35,3142,181],{"class":45},[35,3144,190],{"class":78},[35,3146,3147],{"class":391},"values",[35,3149,175],{"class":74},[35,3151,2466],{"class":45},[35,3153,190],{"class":78},[35,3155,3156],{"class":391},"aggfunc",[35,3158,175],{"class":74},[35,3160,3161],{"class":45},"\"sum\"",[35,3163,190],{"class":78},[35,3165,3166],{"class":391},"margins",[35,3168,175],{"class":74},[35,3170,441],{"class":159},[35,3172,190],{"class":78},[35,3174,3175],{"class":391},"margins_name",[35,3177,175],{"class":74},[35,3179,856],{"class":45},[35,3181,444],{"class":78},[35,3183,3184,3187,3190,3193,3195,3198,3200,3203,3205,3207],{"class":37,"line":96},[35,3185,3186],{"class":74},"with",[35,3188,3189],{"class":78}," pd.ExcelWriter(",[35,3191,3192],{"class":45},"\"budget_2027_consolidated.xlsx\"",[35,3194,190],{"class":78},[35,3196,3197],{"class":391},"engine",[35,3199,175],{"class":74},[35,3201,3202],{"class":45},"\"openpyxl\"",[35,3204,2627],{"class":78},[35,3206,104],{"class":74},[35,3208,3209],{"class":78}," xw:\n",[35,3211,3212,3215,3218,3220,3223,3225,3227,3229,3231],{"class":37,"line":110},[35,3213,3214],{"class":78},"    status.to_excel(xw, ",[35,3216,3217],{"class":391},"sheet_name",[35,3219,175],{"class":74},[35,3221,3222],{"class":45},"\"Tracker\"",[35,3224,190],{"class":78},[35,3226,2892],{"class":391},[35,3228,175],{"class":74},[35,3230,798],{"class":159},[35,3232,444],{"class":78},[35,3234,3235,3238,3240,3242,3245,3247,3249,3251,3253],{"class":37,"line":123},[35,3236,3237],{"class":78},"    tracker.to_excel(xw, ",[35,3239,3217],{"class":391},[35,3241,175],{"class":74},[35,3243,3244],{"class":45},"\"Files\"",[35,3246,190],{"class":78},[35,3248,2892],{"class":391},[35,3250,175],{"class":74},[35,3252,798],{"class":159},[35,3254,444],{"class":78},[35,3256,3257,3260,3262,3264,3267,3269,3271,3273,3275],{"class":37,"line":136},[35,3258,3259],{"class":78},"    budget.to_excel(xw, ",[35,3261,3217],{"class":391},[35,3263,175],{"class":74},[35,3265,3266],{"class":45},"\"Lines\"",[35,3268,190],{"class":78},[35,3270,2892],{"class":391},[35,3272,175],{"class":74},[35,3274,798],{"class":159},[35,3276,444],{"class":78},[35,3278,3279,3282,3284,3286,3289],{"class":37,"line":149},[35,3280,3281],{"class":78},"    by_account.to_excel(xw, ",[35,3283,3217],{"class":391},[35,3285,175],{"class":74},[35,3287,3288],{"class":45},"\"By account\"",[35,3290,444],{"class":78},[35,3292,3293,3295],{"class":37,"line":156},[35,3294,1228],{"class":159},[35,3296,3297],{"class":78},"(by_account.to_string())\n",[10,3299,3300,3301,18],{},"The consolidated workbook has the tracker first — who is valid, invalid and missing — then the file-level problems, every budget line with the file it came from, and a pivot by account and cost centre. Rerunning the script after more returns arrive rebuilds everything from the inbox, so there is no hand-maintained list to drift out of date. Comparing the consolidated budget against actuals as the year goes on is covered in ",[14,3302,3304],{"href":3303},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report",[20,3306,3308],{"id":3307},"common-pitfalls","Common pitfalls",[3310,3311,3312,3320,3326,3332,3338],"ul",{},[3313,3314,3315,3319],"li",{},[3316,3317,3318],"strong",{},"Identifying returns by file name."," People rename files; use a hidden identity sheet.",[3313,3321,3322,3325],{},[3316,3323,3324],{},"Unprotected formulas."," Lock everything but the input cells and protect the sheet.",[3313,3327,3328,3331],{},[3316,3329,3330],{},"Trusting typed values."," Validate as entered and again on return.",[3313,3333,3334,3337],{},[3316,3335,3336],{},"Counting a forwarded template twice."," Check the cost centre inside the file.",[3313,3339,3340,3343],{},[3316,3341,3342],{},"A tracker kept by hand."," Rebuild it from the files on every run.",[20,3345,3347],{"id":3346},"conclusion","Conclusion",[10,3349,3350],{},"Generate each budget holder's workbook from Python with their accounts and prior-year actuals, unlocked and validated input cells, protected formulas and a very hidden sheet recording cost centre, version and a fingerprint of the rows. When the returns arrive, identify each from that sheet, check version, rows and values, consolidate only valid returns, and rebuild a tracker of valid, invalid and missing cost centres every time the script runs.",[20,3352,3354],{"id":3353},"frequently-asked-questions","Frequently asked questions",[10,3356,3357,3360],{},[3316,3358,3359],{},"How do I send a different Excel template to each budget holder?","\nLoop over the cost centres and build each workbook with openpyxl from the same function, pre-filled with that cost centre's accounts and figures, then email each file to its holder.",[10,3362,3363,3366],{},[3316,3364,3365],{},"How can I tell which cost centre a returned file belongs to?","\nStore the cost centre and template version on a very hidden sheet when generating the template and read it back, rather than relying on the file name.",[10,3368,3369,3372],{},[3316,3370,3371],{},"How do I stop people breaking formulas in a budget template?","\nUnlock only the input cells, protect the sheet, and add data validation to the input cells so only valid numbers can be entered.",[10,3374,3375,3378],{},[3316,3376,3377],{},"How do I consolidate many returned budget workbooks with pandas?","\nRead each file, validate it, turn its rows into a DataFrame tagged with the cost centre, concatenate the valid ones and pivot by account and cost centre.",[20,3380,3382],{"id":3381},"related","Related",[3310,3384,3385,3391,3398,3405],{},[3313,3386,3387,3388,3390],{},"Up one level: ",[14,3389,17],{"href":16}," — finance reports from Python.",[3313,3392,3393,3397],{},[14,3394,3396],{"href":3395},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks\u002F","Consolidate Timesheets from Many Workbooks"," — consolidating other returns.",[3313,3399,3400,3404],{},[14,3401,3403],{"href":3402},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002F","Lock Cells and Protect a Sheet with openpyxl"," — the protection used here.",[3313,3406,3407,3411],{},[14,3408,3410],{"href":3409},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job\u002F","Quarantine Bad Excel Files in a Batch Job"," — handling files that fail checks.",[3413,3414,3415],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":30,"searchDepth":82,"depth":82,"links":3417},[3418,3419,3420,3421,3422,3423,3424,3425,3426],{"id":22,"depth":82,"text":23},{"id":58,"depth":82,"text":59},{"id":1643,"depth":82,"text":1644},{"id":1991,"depth":82,"text":1992},{"id":3083,"depth":82,"text":3084},{"id":3307,"depth":82,"text":3308},{"id":3346,"depth":82,"text":3347},{"id":3353,"depth":82,"text":3354},{"id":3381,"depth":82,"text":3382},"2026-10-02","Run the annual budget round from Python: generate one pre-filled budget workbook per cost centre with locked formulas, validated input cells and a hidden identity sheet, then read the returned files back, check that each is the right template for the right cost centre with sensible numbers, consolidate them into one table and keep a tracker of who has returned what.","md",[3431,3433,3435,3437],{"q":3359,"a":3432},"Loop over the cost centres and build each workbook with openpyxl from the same function, pre-filled with that cost centre's accounts and figures, then email each file to its holder.",{"q":3365,"a":3434},"Store the cost centre and template version on a very hidden sheet when generating the template and read it back, rather than relying on the file name.",{"q":3371,"a":3436},"Unlock only the input cells, protect the sheet, and add data validation to the input cells so only valid numbers can be entered.",{"q":3377,"a":3438},"Read each file, validate it, turn its rows into a DataFrame tagged with the cost centre, concatenate the valid ones and pivot by account and cost centre.","send out budget templates and consolidate the returns",null,{"breadcrumb":3442},[3443,3446,3449],{"name":3444,"item":3445},"Home","\u002F",{"name":3447,"item":3448},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":3450,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns",{"title":3453,"description":3454},"Send Out Budget Templates and Consolidate Returns in Python","Budget collection with openpyxl and pandas: per-cost-centre templates with prior-year actuals, protected formulas, data validation and a hidden ID sheet; validating returns, consolidating them and tracking who is missing.","send-out-budget-templates-and-consolidate-the-returns","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns\u002Findex",[3458,3459],"report-layouts","data-validation","how-to","Zj2szoUknA4WJp8vwN1XHurvzdPbzjOz9nDEjEs7uDs",[3463,3467],{"title":3464,"path":3465,"stem":3466,"children":-1},"Reconcile Payroll to the General Ledger","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Freconcile-payroll-to-the-general-ledger","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Freconcile-payroll-to-the-general-ledger\u002Findex",{"title":3468,"path":3469,"stem":3470,"children":-1},"Update a Price List with Effective Dates","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates\u002Findex",[3472,3474],{"path":3473,"title":3450},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":3475,"title":3447},"\u002Fautomating-reporting-workflows",[],[3478,3482,3485,3488,3491,3494],{"path":3479,"title":3480,"kind":3481},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":3483,"title":3484,"kind":3460},"\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":3486,"title":3487,"kind":3460},"\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":3489,"title":3490,"kind":3460},"\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":3492,"title":3493,"kind":3460},"\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":3495,"title":3496,"kind":3460},"\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",1790972128116]