[{"data":1,"prerenderedAt":2225},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker":2190,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker":2199,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker":2204,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker":2205},{"id":4,"title":5,"body":6,"dateModified":2155,"datePublished":2155,"description":2156,"extension":2157,"faq":2158,"keyword":2167,"kind":2168,"meta":2169,"navigation":128,"path":2179,"seo":2180,"slug":2183,"stem":2184,"topics":2185,"type":2188,"__hash__":2189},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker\u002Findex.md","Build a Month-End Close Checklist Tracker",{"type":7,"value":8,"toc":2144},"minimark",[9,28,33,65,76,80,462,477,701,705,1142,1161,1164,1241,1245,1520,1527,1628,1632,2012,2018,2024,2028,2065,2069,2076,2080,2089,2095,2101,2107,2111,2140],[10,11,12,13,17,18,21,22,27],"p",{},"A month-end close is a list of thirty or more tasks — post the payroll journal, reconcile the bank accounts, run depreciation, accrue expenses, review the trial balance, publish the management pack — each with an owner and a deadline expressed relative to the end of the month: ",[14,15,16],"em",{},"working day 2",", ",[14,19,20],{},"working day −1",". Most teams keep the list in a spreadsheet that someone updates by hand each morning, recalculating dates every month and colouring cells as tasks slip. Most of that is mechanical. This page builds a tracker that does it: deadlines stored as working-day offsets and converted to real dates for each month with a holiday calendar, status read from the sheet the team updates, overdue and blocked tasks flagged — a task is blocked when something it depends on is not done — progress compared with plan, and a colour-coded workbook written each morning. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl holidays\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56,59,62],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," pandas",[44,60,61],{"class":54}," openpyxl",[44,63,64],{"class":54}," holidays\n",[10,66,67,68,71,72,75],{},"Two inputs. The ",[14,69,70],{},"checklist"," is permanent: task, owner, deadline as a working-day offset from month end, and the tasks each one depends on. The ",[14,73,74],{},"status"," sheet is what the team updates during the close: status and completion date. The close being tracked is September 2026, and the tracker is run on the morning of Tuesday 6 October.",[29,77,79],{"id":78},"step-1-turn-working-day-offsets-into-dates","Step 1 — Turn working-day offsets into dates",[34,81,85],{"className":82,"code":83,"language":84,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import holidays\nimport numpy as np\nimport pandas as pd\n\ntasks = pd.read_excel(\"close_checklist.xlsx\", keep_default_na=False)\nperiod_end = pd.Timestamp(\"2026-09-30\")\nuk = [np.datetime64(d) for d in holidays.country_holidays(\"GB\", subdiv=\"ENG\", years=[2026])]\n\ndef working_day(offset):\n    \"\"\"WD+1 is the first working day after month end; WD-1 is the last working day of the month.\"\"\"\n    end = np.datetime64(period_end.date())\n    if offset > 0:\n        return pd.Timestamp(np.busday_offset(end, offset, roll=\"forward\", holidays=uk))\n    last_wd = np.busday_offset(end, 0, roll=\"backward\", holidays=uk)\n    return pd.Timestamp(np.busday_offset(last_wd, offset + 1, roll=\"backward\", holidays=uk))\n\ntasks[\"Due\"] = tasks[\"Working day\"].map(working_day)\nprint(tasks[[\"Task\", \"Working day\", \"Due\"]].assign(Due=tasks[\"Due\"].dt.strftime(\"%a %d %b\")).to_string(index=False))\n","python",[41,86,87,96,110,123,130,160,176,228,233,246,252,263,281,308,340,371,376,399],{"__ignoreMap":39},[44,88,89,93],{"class":46,"line":47},[44,90,92],{"class":91},"s-kum","import",[44,94,64],{"class":95},"skGVy",[44,97,99,101,104,107],{"class":46,"line":98},2,[44,100,92],{"class":91},[44,102,103],{"class":95}," numpy ",[44,105,106],{"class":91},"as",[44,108,109],{"class":95}," np\n",[44,111,113,115,118,120],{"class":46,"line":112},3,[44,114,92],{"class":91},[44,116,117],{"class":95}," pandas ",[44,119,106],{"class":91},[44,121,122],{"class":95}," pd\n",[44,124,126],{"class":46,"line":125},4,[44,127,129],{"emptyLinePlaceholder":128},true,"\n",[44,131,133,136,139,142,145,147,151,153,157],{"class":46,"line":132},5,[44,134,135],{"class":95},"tasks ",[44,137,138],{"class":91},"=",[44,140,141],{"class":95}," pd.read_excel(",[44,143,144],{"class":54},"\"close_checklist.xlsx\"",[44,146,17],{"class":95},[44,148,150],{"class":149},"sa561","keep_default_na",[44,152,138],{"class":91},[44,154,156],{"class":155},"sP0c6","False",[44,158,159],{"class":95},")\n",[44,161,163,166,168,171,174],{"class":46,"line":162},6,[44,164,165],{"class":95},"period_end ",[44,167,138],{"class":91},[44,169,170],{"class":95}," pd.Timestamp(",[44,172,173],{"class":54},"\"2026-09-30\"",[44,175,159],{"class":95},[44,177,179,182,184,187,190,193,196,199,202,204,207,209,212,214,217,219,222,225],{"class":46,"line":178},7,[44,180,181],{"class":95},"uk ",[44,183,138],{"class":91},[44,185,186],{"class":95}," [np.datetime64(d) ",[44,188,189],{"class":91},"for",[44,191,192],{"class":95}," d ",[44,194,195],{"class":91},"in",[44,197,198],{"class":95}," holidays.country_holidays(",[44,200,201],{"class":54},"\"GB\"",[44,203,17],{"class":95},[44,205,206],{"class":149},"subdiv",[44,208,138],{"class":91},[44,210,211],{"class":54},"\"ENG\"",[44,213,17],{"class":95},[44,215,216],{"class":149},"years",[44,218,138],{"class":91},[44,220,221],{"class":95},"[",[44,223,224],{"class":155},"2026",[44,226,227],{"class":95},"])]\n",[44,229,231],{"class":46,"line":230},8,[44,232,129],{"emptyLinePlaceholder":128},[44,234,236,239,243],{"class":46,"line":235},9,[44,237,238],{"class":91},"def",[44,240,242],{"class":241},"s_Opv"," working_day",[44,244,245],{"class":95},"(offset):\n",[44,247,249],{"class":46,"line":248},10,[44,250,251],{"class":54},"    \"\"\"WD+1 is the first working day after month end; WD-1 is the last working day of the month.\"\"\"\n",[44,253,255,258,260],{"class":46,"line":254},11,[44,256,257],{"class":95},"    end ",[44,259,138],{"class":91},[44,261,262],{"class":95}," np.datetime64(period_end.date())\n",[44,264,266,269,272,275,278],{"class":46,"line":265},12,[44,267,268],{"class":91},"    if",[44,270,271],{"class":95}," offset ",[44,273,274],{"class":91},">",[44,276,277],{"class":155}," 0",[44,279,280],{"class":95},":\n",[44,282,284,287,290,293,295,298,300,303,305],{"class":46,"line":283},13,[44,285,286],{"class":91},"        return",[44,288,289],{"class":95}," pd.Timestamp(np.busday_offset(end, offset, ",[44,291,292],{"class":149},"roll",[44,294,138],{"class":91},[44,296,297],{"class":54},"\"forward\"",[44,299,17],{"class":95},[44,301,302],{"class":149},"holidays",[44,304,138],{"class":91},[44,306,307],{"class":95},"uk))\n",[44,309,311,314,316,319,322,324,326,328,331,333,335,337],{"class":46,"line":310},14,[44,312,313],{"class":95},"    last_wd ",[44,315,138],{"class":91},[44,317,318],{"class":95}," np.busday_offset(end, ",[44,320,321],{"class":155},"0",[44,323,17],{"class":95},[44,325,292],{"class":149},[44,327,138],{"class":91},[44,329,330],{"class":54},"\"backward\"",[44,332,17],{"class":95},[44,334,302],{"class":149},[44,336,138],{"class":91},[44,338,339],{"class":95},"uk)\n",[44,341,343,346,349,352,355,357,359,361,363,365,367,369],{"class":46,"line":342},15,[44,344,345],{"class":91},"    return",[44,347,348],{"class":95}," pd.Timestamp(np.busday_offset(last_wd, offset ",[44,350,351],{"class":91},"+",[44,353,354],{"class":155}," 1",[44,356,17],{"class":95},[44,358,292],{"class":149},[44,360,138],{"class":91},[44,362,330],{"class":54},[44,364,17],{"class":95},[44,366,302],{"class":149},[44,368,138],{"class":91},[44,370,307],{"class":95},[44,372,374],{"class":46,"line":373},16,[44,375,129],{"emptyLinePlaceholder":128},[44,377,379,382,385,388,390,393,396],{"class":46,"line":378},17,[44,380,381],{"class":95},"tasks[",[44,383,384],{"class":54},"\"Due\"",[44,386,387],{"class":95},"] ",[44,389,138],{"class":91},[44,391,392],{"class":95}," tasks[",[44,394,395],{"class":54},"\"Working day\"",[44,397,398],{"class":95},"].map(working_day)\n",[44,400,402,405,408,411,413,415,417,419,422,425,427,429,431,434,437,441,444,447,449,452,455,457,459],{"class":46,"line":401},18,[44,403,404],{"class":155},"print",[44,406,407],{"class":95},"(tasks[[",[44,409,410],{"class":54},"\"Task\"",[44,412,17],{"class":95},[44,414,395],{"class":54},[44,416,17],{"class":95},[44,418,384],{"class":54},[44,420,421],{"class":95},"]].assign(",[44,423,424],{"class":149},"Due",[44,426,138],{"class":91},[44,428,381],{"class":95},[44,430,384],{"class":54},[44,432,433],{"class":95},"].dt.strftime(",[44,435,436],{"class":54},"\"",[44,438,440],{"class":439},"sSjpA","%a",[44,442,443],{"class":439}," %d",[44,445,446],{"class":439}," %b",[44,448,436],{"class":54},[44,450,451],{"class":95},")).to_string(",[44,453,454],{"class":149},"index",[44,456,138],{"class":91},[44,458,156],{"class":155},[44,460,461],{"class":95},"))\n",[10,463,464,465,468,469,472,473,476],{},"Close checklists measure time in working days relative to month end because calendar dates move every month while the rhythm of the close does not. ",[14,466,467],{},"WD+1"," is the first working day after the month ends, ",[14,470,471],{},"WD−1"," the last working day of the month itself. NumPy's ",[41,474,475],{},"busday_offset"," does the counting, skipping weekends and the public holidays passed to it, so the same checklist gives the right dates in a month with a bank holiday. September 2026 ends on a Wednesday, so WD−1 is Wednesday 30 September, WD+1 Thursday 1 October, and WD+5 — the management pack — Wednesday 7 October, after the weekend. Storing the offset rather than the date is the key design decision: the checklist never needs editing month to month.",[478,479,487,488,487,492,487,496,487,503,487,512,487,516,487,523,487,527,487,531,487,535,487,539,487,543,487,548,487,553,487,557,487,559,487,562,487,564,487,567,487,570,487,574,487,576,487,579,487,581,487,584,487,586,487,589,487,592,487,596,487,598,487,600,487,602,487,605,487,607,487,610,487,613,487,617,487,619,487,622,487,624,487,627,487,629,487,632,487,635,487,639,487,641,487,644,487,646,487,649,487,651,487,654,487,657,487,661,487,665,487,669,487,671,487,674,487,676,487,679,487,682,487,686,487,688,487,691,487,693,487,696,487,698],"svg",{"viewBox":480,"role":481,"ariaLabelledBy":482,"xmlns":485,"style":486},"132 0 495 252","img",[483,484],"close-days-t","close-days-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:495px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[489,490,491],"title",{"id":483},"Working days around the September 2026 month end",[493,494,495],"desc",{"id":484},"September ends on Wednesday the 30th, which is working day minus 1. Working days 1 and 2 are Thursday 1 and Friday 2 October. The weekend is skipped, so working days 3, 4 and 5 are Monday 5, Tuesday 6 and Wednesday 7 October.",[497,498],"rect",{"x":499,"y":321,"width":500,"height":501,"fill":502},"132","495","252","#ffffff",[497,504],{"x":505,"y":506,"width":507,"height":508,"rx":321,"fill":509,"stroke":510,"style":511},"148.7","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[497,513],{"x":514,"y":506,"width":515,"height":508,"rx":321,"fill":509,"stroke":510,"style":511},"180.7","102.3",[517,518,522],"text",{"x":519,"y":520,"style":521},"231.8","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[497,524],{"x":525,"y":506,"width":526,"height":508,"rx":321,"fill":509,"stroke":510,"style":511},"282.9","98.8",[517,528,530],{"x":529,"y":520,"style":521},"332.4","B",[497,532],{"x":533,"y":506,"width":534,"height":508,"rx":321,"fill":509,"stroke":510,"style":511},"381.8","229.6",[517,536,538],{"x":537,"y":520,"style":521},"496.6","C",[497,540],{"x":505,"y":541,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"40.0","28.0",[517,544,547],{"x":545,"y":546,"style":521},"164.7","59.0","1",[497,549],{"x":514,"y":541,"width":515,"height":542,"rx":321,"fill":550,"stroke":551,"style":552},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[517,554,556],{"x":519,"y":546,"style":555},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Working day",[497,558],{"x":525,"y":541,"width":526,"height":542,"rx":321,"fill":550,"stroke":551,"style":552},[517,560,561],{"x":529,"y":546,"style":555},"Date",[497,563],{"x":533,"y":541,"width":534,"height":542,"rx":321,"fill":550,"stroke":551,"style":552},[517,565,566],{"x":537,"y":546,"style":555},"Tasks due",[497,568],{"x":505,"y":569,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"68.0",[517,571,573],{"x":545,"y":572,"style":521},"87.0","2",[497,575],{"x":514,"y":569,"width":515,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,577,471],{"x":519,"y":572,"style":578},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[497,580],{"x":525,"y":569,"width":526,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,582,583],{"x":529,"y":572,"style":578},"Wed 30 Sep",[497,585],{"x":533,"y":569,"width":534,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,587,588],{"x":537,"y":572,"style":578},"sales cut-off",[497,590],{"x":505,"y":591,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"96.0",[517,593,595],{"x":545,"y":594,"style":521},"115.0","3",[497,597],{"x":514,"y":591,"width":515,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,599,467],{"x":519,"y":594,"style":578},[497,601],{"x":525,"y":591,"width":526,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,603,604],{"x":529,"y":594,"style":578},"Thu 01 Oct",[497,606],{"x":533,"y":591,"width":534,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,608,609],{"x":537,"y":594,"style":578},"payroll journal",[497,611],{"x":505,"y":612,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"124.0",[517,614,616],{"x":545,"y":615,"style":521},"143.0","4",[497,618],{"x":514,"y":612,"width":515,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,620,621],{"x":519,"y":615,"style":578},"WD+2",[497,623],{"x":525,"y":612,"width":526,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,625,626],{"x":529,"y":615,"style":578},"Fri 02 Oct",[497,628],{"x":533,"y":612,"width":534,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,630,631],{"x":537,"y":615,"style":578},"bank recs, accruals, depreciation",[497,633],{"x":505,"y":634,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"152.0",[517,636,638],{"x":545,"y":637,"style":521},"171.0","5",[497,640],{"x":514,"y":634,"width":515,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,642,643],{"x":519,"y":637,"style":578},"WD+3",[497,645],{"x":525,"y":634,"width":526,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,647,648],{"x":529,"y":637,"style":578},"Mon 05 Oct",[497,650],{"x":533,"y":634,"width":534,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,652,653],{"x":537,"y":637,"style":578},"intercompany, stock, revenue",[497,655],{"x":505,"y":656,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"180.0",[517,658,660],{"x":545,"y":659,"style":521},"199.0","6",[497,662],{"x":514,"y":656,"width":515,"height":542,"rx":321,"fill":663,"stroke":664,"style":552},"#fdefd8","var(--gold,#b4740a)",[517,666,668],{"x":519,"y":659,"style":667},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","WD+4",[497,670],{"x":525,"y":656,"width":526,"height":542,"rx":321,"fill":663,"stroke":664,"style":552},[517,672,673],{"x":529,"y":659,"style":667},"Tue 06 Oct",[497,675],{"x":533,"y":656,"width":534,"height":542,"rx":321,"fill":663,"stroke":664,"style":552},[517,677,678],{"x":537,"y":659,"style":667},"trial balance review",[497,680],{"x":505,"y":681,"width":507,"height":542,"rx":321,"fill":509,"stroke":510,"style":511},"208.0",[517,683,685],{"x":545,"y":684,"style":521},"227.0","7",[497,687],{"x":514,"y":681,"width":515,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,689,690],{"x":519,"y":684,"style":578},"WD+5",[497,692],{"x":525,"y":681,"width":526,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,694,695],{"x":529,"y":684,"style":578},"Wed 07 Oct",[497,697],{"x":533,"y":681,"width":534,"height":542,"rx":321,"fill":502,"stroke":510,"style":511},[517,699,700],{"x":537,"y":684,"style":578},"management pack",[29,702,704],{"id":703},"step-2-status-overdue-and-blocked-tasks","Step 2 — Status, overdue and blocked tasks",[34,706,708],{"className":82,"code":707,"language":84,"meta":39,"style":39},"today = pd.Timestamp(\"2026-10-06\")\nstatus = pd.read_excel(\"close_status.xlsx\", parse_dates=[\"Completed\"])\ntracker = tasks.merge(status, on=\"Task\", how=\"left\")\ntracker[\"Status\"] = tracker[\"Status\"].fillna(\"Not started\")\n\ndone = set(tracker.loc[tracker[\"Status\"] == \"Done\", \"Task\"])\ndeps = tracker[\"Depends on\"].str.split(\",\").map(lambda xs: [x.strip() for x in xs if x.strip()])\ntracker[\"Waiting for\"] = deps.map(lambda xs: \", \".join(x for x in xs if x not in done))\n\ndef flag(row):\n    if row[\"Status\"] == \"Done\":\n        return \"Done late\" if row[\"Completed\"] > row[\"Due\"] else \"Done\"\n    if row[\"Waiting for\"]:\n        return \"Blocked\"\n    return \"Overdue\" if row[\"Due\"] \u003C today else (\"Due today\" if row[\"Due\"] == today else \"On track\")\n\ntracker[\"Flag\"] = tracker.apply(flag, axis=1)\nprint(tracker[[\"Task\", \"Owner\", \"Due\", \"Status\", \"Waiting for\", \"Flag\"]].assign(Due=tracker[\"Due\"].dt.strftime(\"%d %b\")).to_string(index=False))\n",[41,709,710,724,751,780,805,809,838,881,927,931,941,958,988,999,1006,1054,1058,1081],{"__ignoreMap":39},[44,711,712,715,717,719,722],{"class":46,"line":47},[44,713,714],{"class":95},"today ",[44,716,138],{"class":91},[44,718,170],{"class":95},[44,720,721],{"class":54},"\"2026-10-06\"",[44,723,159],{"class":95},[44,725,726,729,731,733,736,738,741,743,745,748],{"class":46,"line":98},[44,727,728],{"class":95},"status ",[44,730,138],{"class":91},[44,732,141],{"class":95},[44,734,735],{"class":54},"\"close_status.xlsx\"",[44,737,17],{"class":95},[44,739,740],{"class":149},"parse_dates",[44,742,138],{"class":91},[44,744,221],{"class":95},[44,746,747],{"class":54},"\"Completed\"",[44,749,750],{"class":95},"])\n",[44,752,753,756,758,761,764,766,768,770,773,775,778],{"class":46,"line":112},[44,754,755],{"class":95},"tracker ",[44,757,138],{"class":91},[44,759,760],{"class":95}," tasks.merge(status, ",[44,762,763],{"class":149},"on",[44,765,138],{"class":91},[44,767,410],{"class":54},[44,769,17],{"class":95},[44,771,772],{"class":149},"how",[44,774,138],{"class":91},[44,776,777],{"class":54},"\"left\"",[44,779,159],{"class":95},[44,781,782,785,788,790,792,795,797,800,803],{"class":46,"line":125},[44,783,784],{"class":95},"tracker[",[44,786,787],{"class":54},"\"Status\"",[44,789,387],{"class":95},[44,791,138],{"class":91},[44,793,794],{"class":95}," tracker[",[44,796,787],{"class":54},[44,798,799],{"class":95},"].fillna(",[44,801,802],{"class":54},"\"Not started\"",[44,804,159],{"class":95},[44,806,807],{"class":46,"line":132},[44,808,129],{"emptyLinePlaceholder":128},[44,810,811,814,816,819,822,824,826,829,832,834,836],{"class":46,"line":162},[44,812,813],{"class":95},"done ",[44,815,138],{"class":91},[44,817,818],{"class":155}," set",[44,820,821],{"class":95},"(tracker.loc[tracker[",[44,823,787],{"class":54},[44,825,387],{"class":95},[44,827,828],{"class":91},"==",[44,830,831],{"class":54}," \"Done\"",[44,833,17],{"class":95},[44,835,410],{"class":54},[44,837,750],{"class":95},[44,839,840,843,845,847,850,853,856,859,862,865,867,870,872,875,878],{"class":46,"line":178},[44,841,842],{"class":95},"deps ",[44,844,138],{"class":91},[44,846,794],{"class":95},[44,848,849],{"class":54},"\"Depends on\"",[44,851,852],{"class":95},"].str.split(",[44,854,855],{"class":54},"\",\"",[44,857,858],{"class":95},").map(",[44,860,861],{"class":91},"lambda",[44,863,864],{"class":95}," xs: [x.strip() ",[44,866,189],{"class":91},[44,868,869],{"class":95}," x ",[44,871,195],{"class":91},[44,873,874],{"class":95}," xs ",[44,876,877],{"class":91},"if",[44,879,880],{"class":95}," x.strip()])\n",[44,882,883,885,888,890,892,895,897,900,903,906,908,910,912,914,916,918,921,924],{"class":46,"line":230},[44,884,784],{"class":95},[44,886,887],{"class":54},"\"Waiting for\"",[44,889,387],{"class":95},[44,891,138],{"class":91},[44,893,894],{"class":95}," deps.map(",[44,896,861],{"class":91},[44,898,899],{"class":95}," xs: ",[44,901,902],{"class":54},"\", \"",[44,904,905],{"class":95},".join(x ",[44,907,189],{"class":91},[44,909,869],{"class":95},[44,911,195],{"class":91},[44,913,874],{"class":95},[44,915,877],{"class":91},[44,917,869],{"class":95},[44,919,920],{"class":91},"not",[44,922,923],{"class":91}," in",[44,925,926],{"class":95}," done))\n",[44,928,929],{"class":46,"line":235},[44,930,129],{"emptyLinePlaceholder":128},[44,932,933,935,938],{"class":46,"line":248},[44,934,238],{"class":91},[44,936,937],{"class":241}," flag",[44,939,940],{"class":95},"(row):\n",[44,942,943,945,948,950,952,954,956],{"class":46,"line":254},[44,944,268],{"class":91},[44,946,947],{"class":95}," row[",[44,949,787],{"class":54},[44,951,387],{"class":95},[44,953,828],{"class":91},[44,955,831],{"class":54},[44,957,280],{"class":95},[44,959,960,962,965,968,970,972,974,976,978,980,982,985],{"class":46,"line":265},[44,961,286],{"class":91},[44,963,964],{"class":54}," \"Done late\"",[44,966,967],{"class":91}," if",[44,969,947],{"class":95},[44,971,747],{"class":54},[44,973,387],{"class":95},[44,975,274],{"class":91},[44,977,947],{"class":95},[44,979,384],{"class":54},[44,981,387],{"class":95},[44,983,984],{"class":91},"else",[44,986,987],{"class":54}," \"Done\"\n",[44,989,990,992,994,996],{"class":46,"line":283},[44,991,268],{"class":91},[44,993,947],{"class":95},[44,995,887],{"class":54},[44,997,998],{"class":95},"]:\n",[44,1000,1001,1003],{"class":46,"line":310},[44,1002,286],{"class":91},[44,1004,1005],{"class":54}," \"Blocked\"\n",[44,1007,1008,1010,1013,1015,1017,1019,1021,1024,1027,1029,1032,1035,1037,1039,1041,1043,1045,1047,1049,1052],{"class":46,"line":342},[44,1009,345],{"class":91},[44,1011,1012],{"class":54}," \"Overdue\"",[44,1014,967],{"class":91},[44,1016,947],{"class":95},[44,1018,384],{"class":54},[44,1020,387],{"class":95},[44,1022,1023],{"class":91},"\u003C",[44,1025,1026],{"class":95}," today ",[44,1028,984],{"class":91},[44,1030,1031],{"class":95}," (",[44,1033,1034],{"class":54},"\"Due today\"",[44,1036,967],{"class":91},[44,1038,947],{"class":95},[44,1040,384],{"class":54},[44,1042,387],{"class":95},[44,1044,828],{"class":91},[44,1046,1026],{"class":95},[44,1048,984],{"class":91},[44,1050,1051],{"class":54}," \"On track\"",[44,1053,159],{"class":95},[44,1055,1056],{"class":46,"line":373},[44,1057,129],{"emptyLinePlaceholder":128},[44,1059,1060,1062,1065,1067,1069,1072,1075,1077,1079],{"class":46,"line":378},[44,1061,784],{"class":95},[44,1063,1064],{"class":54},"\"Flag\"",[44,1066,387],{"class":95},[44,1068,138],{"class":91},[44,1070,1071],{"class":95}," tracker.apply(flag, ",[44,1073,1074],{"class":149},"axis",[44,1076,138],{"class":91},[44,1078,547],{"class":155},[44,1080,159],{"class":95},[44,1082,1083,1085,1088,1090,1092,1095,1097,1099,1101,1103,1105,1107,1109,1111,1113,1115,1117,1119,1121,1123,1125,1128,1130,1132,1134,1136,1138,1140],{"class":46,"line":401},[44,1084,404],{"class":155},[44,1086,1087],{"class":95},"(tracker[[",[44,1089,410],{"class":54},[44,1091,17],{"class":95},[44,1093,1094],{"class":54},"\"Owner\"",[44,1096,17],{"class":95},[44,1098,384],{"class":54},[44,1100,17],{"class":95},[44,1102,787],{"class":54},[44,1104,17],{"class":95},[44,1106,887],{"class":54},[44,1108,17],{"class":95},[44,1110,1064],{"class":54},[44,1112,421],{"class":95},[44,1114,424],{"class":149},[44,1116,138],{"class":91},[44,1118,784],{"class":95},[44,1120,384],{"class":54},[44,1122,433],{"class":95},[44,1124,436],{"class":54},[44,1126,1127],{"class":439},"%d",[44,1129,446],{"class":439},[44,1131,436],{"class":54},[44,1133,451],{"class":95},[44,1135,454],{"class":149},[44,1137,138],{"class":91},[44,1139,156],{"class":155},[44,1141,461],{"class":95},[10,1143,1144,1145,1148,1149,1152,1153,1156,1157,1160],{},"Joining the status sheet to the checklist gives each task its current state; tasks nobody has touched yet count as ",[14,1146,1147],{},"not started",". A task is ",[14,1150,1151],{},"blocked"," when any of the tasks it depends on is not done, and the tracker lists exactly which ones it is waiting for. That one column changes the morning conversation: the intercompany reconciliation is late, but its owner cannot finish it until treasury completes the bank reconciliations, and the trial balance review, due today, is waiting for three other tasks. Tasks that are not blocked and are past their due date are ",[14,1154,1155],{},"overdue",". Completed tasks are checked too — the revenue review was done on WD+3 as planned, but if it had been completed after its due date it would show as ",[14,1158,1159],{},"done late",", which is useful history when the close timetable is reviewed.",[10,1162,1163],{},"The distinction matters for what happens next. An overdue task needs its owner to finish it; a blocked task needs someone else to finish theirs first, and chasing its owner only adds noise. On this Tuesday morning two tasks are overdue — the bank reconciliations and the stock valuation, both in progress — and three are blocked behind them.",[478,1165,487,1171,487,1174,487,1177,487,1181,487,1190,487,1196,487,1201,487,1205,487,1209,487,1213,487,1217,487,1220,487,1225,487,1229,487,1232,487,1235,487,1238],{"viewBox":1166,"role":481,"ariaLabelledBy":1167,"xmlns":485,"style":1170},"4 0 752 199",[1168,1169],"close-flags-t","close-flags-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[489,1172,1173],{"id":1168},"Overdue or blocked",[493,1175,1176],{"id":1169},"An overdue task is past its due date with all its dependencies done, so its owner is the person to talk to. A blocked task has at least one dependency still open, so the people to talk to are the owners of those dependencies.",[497,1178],{"x":616,"y":321,"width":1179,"height":1180,"fill":502},"752","199",[497,1182],{"x":1183,"y":506,"width":1184,"height":1185,"rx":1186,"fill":1187,"stroke":1188,"style":1189},"20.0","351.0","167.0","14","#fce9e9","var(--danger,#dc2626)","stroke-width:2px",[517,1191,1195],{"x":1192,"y":1193,"style":1194},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Overdue",[46,1197],{"x1":1198,"y1":1199,"x2":1200,"y2":1199,"stroke":1188,"style":511},"36.0","57.0","355.0",[517,1202,1204],{"x":1192,"y":1203,"style":578},"77.0","past its due date",[517,1206,1208],{"x":1192,"y":1207,"style":578},"103.0","all dependencies done",[517,1210,1212],{"x":1192,"y":1211,"style":578},"129.0","chase the task's owner",[517,1214,1216],{"x":1192,"y":1215,"style":578},"155.0","e.g. T03 bank recs",[497,1218],{"x":1219,"y":506,"width":1184,"height":1185,"rx":1186,"fill":663,"stroke":664,"style":1189},"389.0",[517,1221,1224],{"x":1222,"y":1193,"style":1223},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Blocked",[46,1226],{"x1":1227,"y1":1199,"x2":1228,"y2":1199,"stroke":664,"style":511},"405.0","724.0",[517,1230,1231],{"x":1222,"y":1203,"style":578},"a dependency is not done",[517,1233,1234],{"x":1222,"y":1207,"style":578},"may not be late yet",[517,1236,1237],{"x":1222,"y":1211,"style":578},"chase the dependency's owner",[517,1239,1240],{"x":1222,"y":1215,"style":578},"e.g. T09 waits for T03, T06, T07",[29,1242,1244],{"id":1243},"step-3-progress-against-plan-and-the-critical-chain","Step 3 — Progress against plan, and the critical chain",[34,1246,1248],{"className":82,"code":1247,"language":84,"meta":39,"style":39},"by_day = tracker.groupby(\"Due\").size().cumsum().rename(\"Planned\")\nactual = tracker.dropna(subset=[\"Completed\"]).groupby(\"Completed\").size().cumsum().rename(\"Done\")\nprogress = pd.concat([by_day, actual], axis=1, sort=True).ffill().fillna(0).astype(int)\nprogress = progress[progress.index \u003C= today]\nprint(progress.assign(Gap=progress[\"Done\"] - progress[\"Planned\"]).rename(index=lambda d: d.strftime(\"%a %d %b\")).to_string())\n\npack = tracker.set_index(\"Task\")\ndef chain(task):\n    waiting = [t for t in pack.loc[task, \"Waiting for\"].split(\", \") if t]\n    return [task] + (chain(waiting[0]) if waiting else [])\nprint(\"what holds up the pack:\", \" ← \".join(chain(\"T10\")))\n",[41,1249,1250,1270,1301,1340,1355,1406,1410,1424,1434,1469,1496],{"__ignoreMap":39},[44,1251,1252,1255,1257,1260,1262,1265,1268],{"class":46,"line":47},[44,1253,1254],{"class":95},"by_day ",[44,1256,138],{"class":91},[44,1258,1259],{"class":95}," tracker.groupby(",[44,1261,384],{"class":54},[44,1263,1264],{"class":95},").size().cumsum().rename(",[44,1266,1267],{"class":54},"\"Planned\"",[44,1269,159],{"class":95},[44,1271,1272,1275,1277,1280,1283,1285,1287,1289,1292,1294,1296,1299],{"class":46,"line":98},[44,1273,1274],{"class":95},"actual ",[44,1276,138],{"class":91},[44,1278,1279],{"class":95}," tracker.dropna(",[44,1281,1282],{"class":149},"subset",[44,1284,138],{"class":91},[44,1286,221],{"class":95},[44,1288,747],{"class":54},[44,1290,1291],{"class":95},"]).groupby(",[44,1293,747],{"class":54},[44,1295,1264],{"class":95},[44,1297,1298],{"class":54},"\"Done\"",[44,1300,159],{"class":95},[44,1302,1303,1306,1308,1311,1313,1315,1317,1319,1322,1324,1327,1330,1332,1335,1338],{"class":46,"line":112},[44,1304,1305],{"class":95},"progress ",[44,1307,138],{"class":91},[44,1309,1310],{"class":95}," pd.concat([by_day, actual], ",[44,1312,1074],{"class":149},[44,1314,138],{"class":91},[44,1316,547],{"class":155},[44,1318,17],{"class":95},[44,1320,1321],{"class":149},"sort",[44,1323,138],{"class":91},[44,1325,1326],{"class":155},"True",[44,1328,1329],{"class":95},").ffill().fillna(",[44,1331,321],{"class":155},[44,1333,1334],{"class":95},").astype(",[44,1336,1337],{"class":155},"int",[44,1339,159],{"class":95},[44,1341,1342,1344,1346,1349,1352],{"class":46,"line":125},[44,1343,1305],{"class":95},[44,1345,138],{"class":91},[44,1347,1348],{"class":95}," progress[progress.index ",[44,1350,1351],{"class":91},"\u003C=",[44,1353,1354],{"class":95}," today]\n",[44,1356,1357,1359,1362,1365,1367,1370,1372,1374,1377,1380,1382,1385,1387,1390,1393,1395,1397,1399,1401,1403],{"class":46,"line":132},[44,1358,404],{"class":155},[44,1360,1361],{"class":95},"(progress.assign(",[44,1363,1364],{"class":149},"Gap",[44,1366,138],{"class":91},[44,1368,1369],{"class":95},"progress[",[44,1371,1298],{"class":54},[44,1373,387],{"class":95},[44,1375,1376],{"class":91},"-",[44,1378,1379],{"class":95}," progress[",[44,1381,1267],{"class":54},[44,1383,1384],{"class":95},"]).rename(",[44,1386,454],{"class":149},[44,1388,1389],{"class":91},"=lambda",[44,1391,1392],{"class":95}," d: d.strftime(",[44,1394,436],{"class":54},[44,1396,440],{"class":439},[44,1398,443],{"class":439},[44,1400,446],{"class":439},[44,1402,436],{"class":54},[44,1404,1405],{"class":95},")).to_string())\n",[44,1407,1408],{"class":46,"line":162},[44,1409,129],{"emptyLinePlaceholder":128},[44,1411,1412,1415,1417,1420,1422],{"class":46,"line":178},[44,1413,1414],{"class":95},"pack ",[44,1416,138],{"class":91},[44,1418,1419],{"class":95}," tracker.set_index(",[44,1421,410],{"class":54},[44,1423,159],{"class":95},[44,1425,1426,1428,1431],{"class":46,"line":230},[44,1427,238],{"class":91},[44,1429,1430],{"class":241}," chain",[44,1432,1433],{"class":95},"(task):\n",[44,1435,1436,1439,1441,1444,1446,1449,1451,1454,1456,1459,1461,1464,1466],{"class":46,"line":235},[44,1437,1438],{"class":95},"    waiting ",[44,1440,138],{"class":91},[44,1442,1443],{"class":95}," [t ",[44,1445,189],{"class":91},[44,1447,1448],{"class":95}," t ",[44,1450,195],{"class":91},[44,1452,1453],{"class":95}," pack.loc[task, ",[44,1455,887],{"class":54},[44,1457,1458],{"class":95},"].split(",[44,1460,902],{"class":54},[44,1462,1463],{"class":95},") ",[44,1465,877],{"class":91},[44,1467,1468],{"class":95}," t]\n",[44,1470,1471,1473,1476,1478,1481,1483,1486,1488,1491,1493],{"class":46,"line":248},[44,1472,345],{"class":91},[44,1474,1475],{"class":95}," [task] ",[44,1477,351],{"class":91},[44,1479,1480],{"class":95}," (chain(waiting[",[44,1482,321],{"class":155},[44,1484,1485],{"class":95},"]) ",[44,1487,877],{"class":91},[44,1489,1490],{"class":95}," waiting ",[44,1492,984],{"class":91},[44,1494,1495],{"class":95}," [])\n",[44,1497,1498,1500,1503,1506,1508,1511,1514,1517],{"class":46,"line":254},[44,1499,404],{"class":155},[44,1501,1502],{"class":95},"(",[44,1504,1505],{"class":54},"\"what holds up the pack:\"",[44,1507,17],{"class":95},[44,1509,1510],{"class":54},"\" ← \"",[44,1512,1513],{"class":95},".join(chain(",[44,1515,1516],{"class":54},"\"T10\"",[44,1518,1519],{"class":95},")))\n",[10,1521,1522,1523,1526],{},"A cumulative count of tasks planned by each date, next to the count actually completed, shows whether the close is keeping pace: by the end of Monday eight of the ten tasks should have been finished, but only five are, and with the trial balance review due today the gap widens to four. The gap tells management how far behind the close is; the dependency chain tells them why. Following the ",[14,1524,1525],{},"waiting for"," links back from the management pack shows the path that is holding everything up — the pack waits for the trial balance review, which waits first of all for the bank reconciliations — and that is where help should go. The bank reconciliations also block the intercompany reconciliation, so finishing them unblocks two tasks at once, while the stock valuation is the other open dependency of the review.",[478,1528,487,1533,487,1536,487,1539,487,1542,487,1547,487,1553,487,1557,487,1562,487,1567,487,1572,487,1575,487,1581,487,1584,487,1588,487,1591,487,1594,487,1597,487,1600,487,1605,487,1608,487,1612,487,1615,487,1618,487,1622,487,1626],{"viewBox":1529,"role":481,"ariaLabelledBy":1530,"xmlns":485,"style":1170},"4 0 752 126",[1531,1532],"close-chain-t","close-chain-d",[489,1534,1535],{"id":1531},"What holds up the management pack",[493,1537,1538],{"id":1532},"The management pack is waiting for the trial balance review. The review is waiting for the bank reconciliations, which are in progress and overdue, and also for the intercompany reconciliation, itself waiting for the bank reconciliations, and the stock valuation.",[497,1540],{"x":616,"y":321,"width":1179,"height":1541,"fill":502},"126",[497,1543],{"x":1183,"y":506,"width":1544,"height":1545,"rx":1546,"fill":509,"stroke":510,"style":1189},"153.0","94.0","12",[517,1548,1552],{"x":1549,"y":1550,"style":1551},"96.5","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","T10 Management",[517,1554,1556],{"x":1549,"y":1555,"style":1551},"67.8","pack",[517,1558,1561],{"x":1549,"y":1559,"style":1560},"87.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","due WD+5",[46,1563],{"x1":1564,"y1":1565,"x2":1566,"y2":1565,"stroke":551,"style":1189},"178.0","63.0","200.0",[1568,1569],"polygon",{"points":1570,"fill":1571},"204.0,63.0 195.0,68.0 195.0,58.0","#5b5cf0",[497,1573],{"x":1574,"y":506,"width":1544,"height":1545,"rx":1546,"fill":663,"stroke":664,"style":1189},"209.0",[517,1576,1580],{"x":1577,"y":1578,"style":1579},"285.5","41.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","T09 Trial balance",[517,1582,1583],{"x":1577,"y":546,"style":1579},"review",[517,1585,1587],{"x":1577,"y":1586,"style":1560},"78.5","blocked: T03, T06,",[517,1589,1590],{"x":1577,"y":591,"style":1560},"T07",[46,1592],{"x1":1593,"y1":1565,"x2":1219,"y2":1565,"stroke":551,"style":1189},"367.0",[1568,1595],{"points":1596,"fill":1571},"393.0,63.0 384.0,68.0 384.0,58.0",[497,1598],{"x":1599,"y":506,"width":1544,"height":1545,"rx":1546,"fill":1187,"stroke":1188,"style":1189},"398.0",[517,1601,1604],{"x":1602,"y":546,"style":1603},"474.5","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","T03 Bank recs",[517,1606,1607],{"x":1602,"y":1586,"style":1560},"in progress, overdue",[46,1609],{"x1":1610,"y1":1565,"x2":1611,"y2":1565,"stroke":551,"style":1189},"556.0","578.0",[1568,1613],{"points":1614,"fill":1571},"582.0,63.0 573.0,68.0 573.0,58.0",[497,1616],{"x":1617,"y":506,"width":1544,"height":1545,"rx":1546,"fill":663,"stroke":664,"style":1189},"587.0",[517,1619,1621],{"x":1620,"y":1550,"style":1579},"663.5","Also blocking T09",[517,1623,1625],{"x":1620,"y":1624,"style":1560},"69.8","T06 (waits for T03),",[517,1627,1590],{"x":1620,"y":1559,"style":1560},[29,1629,1631],{"id":1630},"step-4-the-tracker-workbook","Step 4 — The tracker workbook",[34,1633,1635],{"className":82,"code":1634,"language":84,"meta":39,"style":39},"from openpyxl import load_workbook\nfrom openpyxl.styles import PatternFill\n\nout = tracker[[\"Task\", \"Description\", \"Owner\", \"Working day\", \"Due\", \"Status\", \"Completed\", \"Waiting for\", \"Flag\"]]\nout = out.assign(Due=out[\"Due\"].dt.date, Completed=out[\"Completed\"].dt.date)\nwith pd.ExcelWriter(\"close_tracker_2026-09.xlsx\") as xw:\n    out.to_excel(xw, sheet_name=\"Tracker\", index=False)\n    progress.rename(index=lambda d: d.date()).to_excel(xw, sheet_name=\"Progress\")\nwb = load_workbook(\"close_tracker_2026-09.xlsx\")\nws = wb[\"Tracker\"]\ncolours = {\"Done\": \"C6EFCE\", \"Done late\": \"FFEB9C\", \"Blocked\": \"FCE4D6\", \"Overdue\": \"FFC7CE\", \"Due today\": \"FFEB9C\", \"On track\": \"FFFFFF\"}\nfor row in ws.iter_rows(min_row=2):\n    fill = PatternFill(\"solid\", fgColor=colours[row[8].value])\n    for cell in row:\n        cell.fill = fill\nws.freeze_panes = \"B2\"\nwb.save(\"close_tracker_2026-09.xlsx\")\nprint(out[\"Flag\"].value_counts().to_dict())\n",[41,1636,1637,1650,1662,1666,1714,1747,1765,1788,1809,1823,1838,1907,1929,1958,1971,1981,1991,2000],{"__ignoreMap":39},[44,1638,1639,1642,1645,1647],{"class":46,"line":47},[44,1640,1641],{"class":91},"from",[44,1643,1644],{"class":95}," openpyxl ",[44,1646,92],{"class":91},[44,1648,1649],{"class":95}," load_workbook\n",[44,1651,1652,1654,1657,1659],{"class":46,"line":98},[44,1653,1641],{"class":91},[44,1655,1656],{"class":95}," openpyxl.styles ",[44,1658,92],{"class":91},[44,1660,1661],{"class":95}," PatternFill\n",[44,1663,1664],{"class":46,"line":112},[44,1665,129],{"emptyLinePlaceholder":128},[44,1667,1668,1671,1673,1676,1678,1680,1683,1685,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705,1707,1709,1711],{"class":46,"line":125},[44,1669,1670],{"class":95},"out ",[44,1672,138],{"class":91},[44,1674,1675],{"class":95}," tracker[[",[44,1677,410],{"class":54},[44,1679,17],{"class":95},[44,1681,1682],{"class":54},"\"Description\"",[44,1684,17],{"class":95},[44,1686,1094],{"class":54},[44,1688,17],{"class":95},[44,1690,395],{"class":54},[44,1692,17],{"class":95},[44,1694,384],{"class":54},[44,1696,17],{"class":95},[44,1698,787],{"class":54},[44,1700,17],{"class":95},[44,1702,747],{"class":54},[44,1704,17],{"class":95},[44,1706,887],{"class":54},[44,1708,17],{"class":95},[44,1710,1064],{"class":54},[44,1712,1713],{"class":95},"]]\n",[44,1715,1716,1718,1720,1723,1725,1727,1730,1732,1735,1738,1740,1742,1744],{"class":46,"line":132},[44,1717,1670],{"class":95},[44,1719,138],{"class":91},[44,1721,1722],{"class":95}," out.assign(",[44,1724,424],{"class":149},[44,1726,138],{"class":91},[44,1728,1729],{"class":95},"out[",[44,1731,384],{"class":54},[44,1733,1734],{"class":95},"].dt.date, ",[44,1736,1737],{"class":149},"Completed",[44,1739,138],{"class":91},[44,1741,1729],{"class":95},[44,1743,747],{"class":54},[44,1745,1746],{"class":95},"].dt.date)\n",[44,1748,1749,1752,1755,1758,1760,1762],{"class":46,"line":162},[44,1750,1751],{"class":91},"with",[44,1753,1754],{"class":95}," pd.ExcelWriter(",[44,1756,1757],{"class":54},"\"close_tracker_2026-09.xlsx\"",[44,1759,1463],{"class":95},[44,1761,106],{"class":91},[44,1763,1764],{"class":95}," xw:\n",[44,1766,1767,1770,1773,1775,1778,1780,1782,1784,1786],{"class":46,"line":178},[44,1768,1769],{"class":95},"    out.to_excel(xw, ",[44,1771,1772],{"class":149},"sheet_name",[44,1774,138],{"class":91},[44,1776,1777],{"class":54},"\"Tracker\"",[44,1779,17],{"class":95},[44,1781,454],{"class":149},[44,1783,138],{"class":91},[44,1785,156],{"class":155},[44,1787,159],{"class":95},[44,1789,1790,1793,1795,1797,1800,1802,1804,1807],{"class":46,"line":230},[44,1791,1792],{"class":95},"    progress.rename(",[44,1794,454],{"class":149},[44,1796,1389],{"class":91},[44,1798,1799],{"class":95}," d: d.date()).to_excel(xw, ",[44,1801,1772],{"class":149},[44,1803,138],{"class":91},[44,1805,1806],{"class":54},"\"Progress\"",[44,1808,159],{"class":95},[44,1810,1811,1814,1816,1819,1821],{"class":46,"line":235},[44,1812,1813],{"class":95},"wb ",[44,1815,138],{"class":91},[44,1817,1818],{"class":95}," load_workbook(",[44,1820,1757],{"class":54},[44,1822,159],{"class":95},[44,1824,1825,1828,1830,1833,1835],{"class":46,"line":248},[44,1826,1827],{"class":95},"ws ",[44,1829,138],{"class":91},[44,1831,1832],{"class":95}," wb[",[44,1834,1777],{"class":54},[44,1836,1837],{"class":95},"]\n",[44,1839,1840,1843,1845,1848,1850,1853,1856,1858,1861,1863,1866,1868,1871,1873,1876,1878,1881,1883,1886,1888,1890,1892,1894,1896,1899,1901,1904],{"class":46,"line":254},[44,1841,1842],{"class":95},"colours ",[44,1844,138],{"class":91},[44,1846,1847],{"class":95}," {",[44,1849,1298],{"class":54},[44,1851,1852],{"class":95},": ",[44,1854,1855],{"class":54},"\"C6EFCE\"",[44,1857,17],{"class":95},[44,1859,1860],{"class":54},"\"Done late\"",[44,1862,1852],{"class":95},[44,1864,1865],{"class":54},"\"FFEB9C\"",[44,1867,17],{"class":95},[44,1869,1870],{"class":54},"\"Blocked\"",[44,1872,1852],{"class":95},[44,1874,1875],{"class":54},"\"FCE4D6\"",[44,1877,17],{"class":95},[44,1879,1880],{"class":54},"\"Overdue\"",[44,1882,1852],{"class":95},[44,1884,1885],{"class":54},"\"FFC7CE\"",[44,1887,17],{"class":95},[44,1889,1034],{"class":54},[44,1891,1852],{"class":95},[44,1893,1865],{"class":54},[44,1895,17],{"class":95},[44,1897,1898],{"class":54},"\"On track\"",[44,1900,1852],{"class":95},[44,1902,1903],{"class":54},"\"FFFFFF\"",[44,1905,1906],{"class":95},"}\n",[44,1908,1909,1911,1914,1916,1919,1922,1924,1926],{"class":46,"line":265},[44,1910,189],{"class":91},[44,1912,1913],{"class":95}," row ",[44,1915,195],{"class":91},[44,1917,1918],{"class":95}," ws.iter_rows(",[44,1920,1921],{"class":149},"min_row",[44,1923,138],{"class":91},[44,1925,573],{"class":155},[44,1927,1928],{"class":95},"):\n",[44,1930,1931,1934,1936,1939,1942,1944,1947,1949,1952,1955],{"class":46,"line":283},[44,1932,1933],{"class":95},"    fill ",[44,1935,138],{"class":91},[44,1937,1938],{"class":95}," PatternFill(",[44,1940,1941],{"class":54},"\"solid\"",[44,1943,17],{"class":95},[44,1945,1946],{"class":149},"fgColor",[44,1948,138],{"class":91},[44,1950,1951],{"class":95},"colours[row[",[44,1953,1954],{"class":155},"8",[44,1956,1957],{"class":95},"].value])\n",[44,1959,1960,1963,1966,1968],{"class":46,"line":310},[44,1961,1962],{"class":91},"    for",[44,1964,1965],{"class":95}," cell ",[44,1967,195],{"class":91},[44,1969,1970],{"class":95}," row:\n",[44,1972,1973,1976,1978],{"class":46,"line":342},[44,1974,1975],{"class":95},"        cell.fill ",[44,1977,138],{"class":91},[44,1979,1980],{"class":95}," fill\n",[44,1982,1983,1986,1988],{"class":46,"line":373},[44,1984,1985],{"class":95},"ws.freeze_panes ",[44,1987,138],{"class":91},[44,1989,1990],{"class":54}," \"B2\"\n",[44,1992,1993,1996,1998],{"class":46,"line":378},[44,1994,1995],{"class":95},"wb.save(",[44,1997,1757],{"class":54},[44,1999,159],{"class":95},[44,2001,2002,2004,2007,2009],{"class":46,"line":401},[44,2003,404],{"class":155},[44,2005,2006],{"class":95},"(out[",[44,2008,1064],{"class":54},[44,2010,2011],{"class":95},"].value_counts().to_dict())\n",[10,2013,2014,2015,27],{},"The workbook colours each row by its flag — green for done, red for overdue, orange for blocked, yellow for due today or done late — so the state of the close is visible at a glance, and a second sheet holds the progress table for a burn-up chart. Run each morning of the close from a scheduler and emailed to the team, it replaces the manual update entirely: owners only change their status, and everything else follows. Because the dates come from offsets, next month's tracker needs nothing more than a new ",[41,2016,2017],{},"period_end",[10,2019,2020,2021,2023],{},"Keeping each month's final tracker also builds a useful record. Over a year, the ",[14,2022,1159],{}," flags and the gaps in the progress table show which tasks routinely slip and which dependencies cause the waits, which is exactly the evidence needed to shorten the close: moving a task earlier, splitting a reconciliation between two people, or removing a dependency that turns out not to be real.",[29,2025,2027],{"id":2026},"common-pitfalls","Common pitfalls",[2029,2030,2031,2039,2047,2053,2059],"ul",{},[2032,2033,2034,2038],"li",{},[2035,2036,2037],"strong",{},"Storing dates instead of offsets."," Keep WD offsets in the checklist and compute dates each month.",[2032,2040,2041,2044,2045,27],{},[2035,2042,2043],{},"Ignoring holidays."," Pass the holiday calendar to ",[41,2046,475],{},[2032,2048,2049,2052],{},[2035,2050,2051],{},"Overdue without context."," Separate blocked tasks from genuinely late ones.",[2032,2054,2055,2058],{},[2035,2056,2057],{},"Unchecked dependencies."," Verify every dependency names a real task before the close starts.",[2032,2060,2061,2064],{},[2035,2062,2063],{},"Status edited in the tracker."," Keep status in its own sheet so the tracker can be rebuilt.",[29,2066,2068],{"id":2067},"conclusion","Conclusion",[10,2070,2071,2072,2075],{},"A month-end close tracker stores deadlines as working-day offsets and turns them into dates each month with ",[41,2073,2074],{},"numpy.busday_offset"," and a holiday calendar. Joining the team's status updates gives each task a flag — done, done late, blocked by named dependencies, overdue, due today or on track — and a cumulative count of planned and completed tasks shows whether the close is on pace, while the dependency chain shows what is holding it up.",[29,2077,2079],{"id":2078},"frequently-asked-questions","Frequently asked questions",[10,2081,2082,2085,2086,27],{},[2035,2083,2084],{},"How do I calculate working day 3 after month end in Python?","\nUse ",[41,2087,2088],{},"numpy.busday_offset(month_end, 3, roll=\"forward\", holidays=holiday_list)",[10,2090,2091,2094],{},[2035,2092,2093],{},"How do I find tasks blocked by unfinished dependencies?","\nStore each task's dependencies, collect the set of completed tasks, and list the dependencies of each task that are not in that set.",[10,2096,2097,2100],{},[2035,2098,2099],{},"Why store close deadlines as working-day offsets?","\nBecause the close follows the same rhythm every month while calendar dates and holidays change; the offsets never need editing.",[10,2102,2103,2106],{},[2035,2104,2105],{},"How do I show close progress against plan?","\nCount tasks due by each date cumulatively and compare with the cumulative count of tasks completed by each date.",[29,2108,2110],{"id":2109},"related","Related",[2029,2112,2113,2119,2126,2133],{},[2032,2114,2115,2116,2118],{},"Up one level: ",[23,2117,26],{"href":25}," — finance reports from Python.",[2032,2120,2121,2125],{},[23,2122,2124],{"href":2123},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-report-on-the-last-business-day-of-the-month\u002F","Run a Report on the Last Business Day of the Month"," — working-day scheduling.",[2032,2127,2128,2132],{},[23,2129,2131],{"href":2130},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-gantt-chart-in-excel-with-python\u002F","Build a Gantt Chart in Excel with Python"," — visualising the timetable.",[2032,2134,2135,2139],{},[23,2136,2138],{"href":2137},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-accruals-and-prepayments-schedule\u002F","Build an Accruals and Prepayments Schedule"," — one of the close tasks.",[2141,2142,2143],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":39,"searchDepth":98,"depth":98,"links":2145},[2146,2147,2148,2149,2150,2151,2152,2153,2154],{"id":31,"depth":98,"text":32},{"id":78,"depth":98,"text":79},{"id":703,"depth":98,"text":704},{"id":1243,"depth":98,"text":1244},{"id":1630,"depth":98,"text":1631},{"id":2026,"depth":98,"text":2027},{"id":2067,"depth":98,"text":2068},{"id":2078,"depth":98,"text":2079},{"id":2109,"depth":98,"text":2110},"2026-10-02","Turn a month-end close checklist into a tracker that updates itself: express each task's deadline as a working day relative to month end, convert those to real dates with a holiday calendar, read the team's status updates, flag overdue tasks and tasks blocked by unfinished dependencies, measure progress against plan day by day, and write a colour-coded Excel tracker.","md",[2159,2161,2163,2165],{"q":2084,"a":2160},"Use numpy.busday_offset(month_end, 3, roll=\"forward\", holidays=holiday_list).",{"q":2093,"a":2162},"Store each task's dependencies, collect the set of completed tasks, and list the dependencies of each task that are not in that set.",{"q":2099,"a":2164},"Because the close follows the same rhythm every month while calendar dates and holidays change; the offsets never need editing.",{"q":2105,"a":2166},"Count tasks due by each date cumulatively and compare with the cumulative count of tasks completed by each date.","month end close checklist python",null,{"breadcrumb":2170},[2171,2174,2177],{"name":2172,"item":2173},"Home","\u002F",{"name":2175,"item":2176},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2178,"item":25},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker",{"title":2181,"description":2182},"Build a Month-End Close Checklist Tracker with Python","Month-end close tracker with pandas: deadlines as working days (WD-2 to WD+5) turned into dates, status updates, overdue and blocked tasks from dependencies, progress against plan and a colour-coded Excel sheet.","build-a-month-end-close-checklist-tracker","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker\u002Findex",[2186,2187],"report-layouts","reliable-jobs","how-to","hYM4ZStvpiMrJBpTWCyETdVz-_vfn_QrwtvYdRTp9kM",[2191,2195],{"title":2192,"path":2193,"stem":2194,"children":-1},"Build a Maintenance Schedule in Excel","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-maintenance-schedule-in-excel","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-maintenance-schedule-in-excel\u002Findex",{"title":2196,"path":2197,"stem":2198,"children":-1},"Build a Monthly KPI Scorecard with Targets","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002Findex",[2200,2202],{"path":2201,"title":2178},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2203,"title":2175},"\u002Fautomating-reporting-workflows",[],[2206,2210,2213,2216,2219,2222],{"path":2207,"title":2208,"kind":2209},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2211,"title":2212,"kind":2188},"\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":2214,"title":2215,"kind":2188},"\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":2217,"title":2218,"kind":2188},"\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":2220,"title":2221,"kind":2188},"\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":2223,"title":2224,"kind":2188},"\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",1790972123964]