[{"data":1,"prerenderedAt":2538},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report":2503,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report":2512,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report":2517,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report":2518},{"id":4,"title":5,"body":6,"dateModified":2469,"datePublished":2469,"description":2470,"extension":2471,"faq":2472,"keyword":2481,"kind":2482,"meta":2483,"navigation":115,"path":2492,"seo":2493,"slug":2496,"stem":2497,"topics":2498,"type":2501,"__hash__":2502},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report\u002Findex.md","Build a Support Ticket SLA Report",{"type":7,"value":8,"toc":2457},"minimark",[9,24,29,61,67,71,74,372,383,387,390,831,838,971,975,978,1337,1346,1350,1353,1665,1668,1753,1757,1760,2209,2216,2344,2348,2383,2387,2394,2398,2404,2410,2416,2422,2426,2453],[10,11,12,13,17,18,23],"p",{},"A service desk promises its customers that an urgent ticket gets a first response within an hour and a fix within a working day, and a low-priority one within two days. Whether it keeps that promise is a question the helpdesk tool's built-in dashboards often answer badly: they count calendar hours, so a ticket logged at 16:55 on Friday \"breaches\" over the weekend; they cannot combine data from two tools; and they do not produce the monthly Excel pack the customer contract asks for. Calculating SLA attainment from the raw ticket export solves all three. The central piece is measuring elapsed time in ",[14,15,16],"em",{},"business hours","; the rest is grouping and presentation. This page builds the calculation, flags breaches and tickets about to breach, summarises attainment, and writes the report. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas numpy openpyxl\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," numpy",[40,59,60],{"class":50}," openpyxl\n",[10,62,63,66],{},[37,64,65],{},"tickets.xlsx"," is a September 2026 export with one row per ticket: ID, created time, priority (P1 most urgent), the team that handled it, the time of the first response, and the resolved time (blank for tickets still open). Service hours are 09:00–17:00 Monday to Friday, UK bank holidays excluded, and the report is run at 17:00 on 30 September.",[25,68,70],{"id":69},"step-1-define-the-targets-and-the-service-calendar","Step 1 — Define the targets and the service calendar",[10,72,73],{},"SLA targets are stated in business hours per priority. Keep them, the working day and the holidays as data:",[30,75,79],{"className":76,"code":77,"language":78,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nimport numpy as np\n\nSLA = pd.DataFrame({\"Priority\": [\"P1\", \"P2\", \"P3\"],\n                    \"Response h\": [1, 4, 8],\n                    \"Resolve h\": [8, 24, 40]}).set_index(\"Priority\")\nDAY_START, DAY_END = 9, 17                         # service hours\nHOLIDAYS = pd.to_datetime([\"2026-08-31\", \"2026-12-25\", \"2026-12-28\"])\nAS_OF = pd.Timestamp(\"2026-09-30 17:00\")\n\ntickets = pd.read_excel(\"tickets.xlsx\")\ntickets = tickets.join(SLA, on=\"Priority\", validate=\"many_to_one\")\nprint(SLA.to_string())\nprint(len(tickets), \"tickets |\", tickets[\"Resolved\"].isna().sum(), \"still open\")\n","python",[37,80,81,97,110,117,153,176,204,229,256,272,277,294,328,342],{"__ignoreMap":35},[40,82,83,87,91,94],{"class":42,"line":43},[40,84,86],{"class":85},"s-kum","import",[40,88,90],{"class":89},"skGVy"," pandas ",[40,92,93],{"class":85},"as",[40,95,96],{"class":89}," pd\n",[40,98,100,102,105,107],{"class":42,"line":99},2,[40,101,86],{"class":85},[40,103,104],{"class":89}," numpy ",[40,106,93],{"class":85},[40,108,109],{"class":89}," np\n",[40,111,113],{"class":42,"line":112},3,[40,114,116],{"emptyLinePlaceholder":115},true,"\n",[40,118,120,124,127,130,133,136,139,142,145,147,150],{"class":42,"line":119},4,[40,121,123],{"class":122},"sP0c6","SLA",[40,125,126],{"class":85}," =",[40,128,129],{"class":89}," pd.DataFrame({",[40,131,132],{"class":50},"\"Priority\"",[40,134,135],{"class":89},": [",[40,137,138],{"class":50},"\"P1\"",[40,140,141],{"class":89},", ",[40,143,144],{"class":50},"\"P2\"",[40,146,141],{"class":89},[40,148,149],{"class":50},"\"P3\"",[40,151,152],{"class":89},"],\n",[40,154,156,159,161,164,166,169,171,174],{"class":42,"line":155},5,[40,157,158],{"class":50},"                    \"Response h\"",[40,160,135],{"class":89},[40,162,163],{"class":122},"1",[40,165,141],{"class":89},[40,167,168],{"class":122},"4",[40,170,141],{"class":89},[40,172,173],{"class":122},"8",[40,175,152],{"class":89},[40,177,179,182,184,186,188,191,193,196,199,201],{"class":42,"line":178},6,[40,180,181],{"class":50},"                    \"Resolve h\"",[40,183,135],{"class":89},[40,185,173],{"class":122},[40,187,141],{"class":89},[40,189,190],{"class":122},"24",[40,192,141],{"class":89},[40,194,195],{"class":122},"40",[40,197,198],{"class":89},"]}).set_index(",[40,200,132],{"class":50},[40,202,203],{"class":89},")\n",[40,205,207,210,212,215,217,220,222,225],{"class":42,"line":206},7,[40,208,209],{"class":122},"DAY_START",[40,211,141],{"class":89},[40,213,214],{"class":122},"DAY_END",[40,216,126],{"class":85},[40,218,219],{"class":122}," 9",[40,221,141],{"class":89},[40,223,224],{"class":122},"17",[40,226,228],{"class":227},"s-wDw","                         # service hours\n",[40,230,232,235,237,240,243,245,248,250,253],{"class":42,"line":231},8,[40,233,234],{"class":122},"HOLIDAYS",[40,236,126],{"class":85},[40,238,239],{"class":89}," pd.to_datetime([",[40,241,242],{"class":50},"\"2026-08-31\"",[40,244,141],{"class":89},[40,246,247],{"class":50},"\"2026-12-25\"",[40,249,141],{"class":89},[40,251,252],{"class":50},"\"2026-12-28\"",[40,254,255],{"class":89},"])\n",[40,257,259,262,264,267,270],{"class":42,"line":258},9,[40,260,261],{"class":122},"AS_OF",[40,263,126],{"class":85},[40,265,266],{"class":89}," pd.Timestamp(",[40,268,269],{"class":50},"\"2026-09-30 17:00\"",[40,271,203],{"class":89},[40,273,275],{"class":42,"line":274},10,[40,276,116],{"emptyLinePlaceholder":115},[40,278,280,283,286,289,292],{"class":42,"line":279},11,[40,281,282],{"class":89},"tickets ",[40,284,285],{"class":85},"=",[40,287,288],{"class":89}," pd.read_excel(",[40,290,291],{"class":50},"\"tickets.xlsx\"",[40,293,203],{"class":89},[40,295,297,299,301,304,306,308,312,314,316,318,321,323,326],{"class":42,"line":296},12,[40,298,282],{"class":89},[40,300,285],{"class":85},[40,302,303],{"class":89}," tickets.join(",[40,305,123],{"class":122},[40,307,141],{"class":89},[40,309,311],{"class":310},"sa561","on",[40,313,285],{"class":85},[40,315,132],{"class":50},[40,317,141],{"class":89},[40,319,320],{"class":310},"validate",[40,322,285],{"class":85},[40,324,325],{"class":50},"\"many_to_one\"",[40,327,203],{"class":89},[40,329,331,334,337,339],{"class":42,"line":330},13,[40,332,333],{"class":122},"print",[40,335,336],{"class":89},"(",[40,338,123],{"class":122},[40,340,341],{"class":89},".to_string())\n",[40,343,345,347,349,352,355,358,361,364,367,370],{"class":42,"line":344},14,[40,346,333],{"class":122},[40,348,336],{"class":89},[40,350,351],{"class":122},"len",[40,353,354],{"class":89},"(tickets), ",[40,356,357],{"class":50},"\"tickets |\"",[40,359,360],{"class":89},", tickets[",[40,362,363],{"class":50},"\"Resolved\"",[40,365,366],{"class":89},"].isna().sum(), ",[40,368,369],{"class":50},"\"still open\"",[40,371,203],{"class":89},[10,373,374,375,378,379,382],{},"A P2's resolution target of 24 business hours means three working days, not one calendar day. Joining the targets onto each ticket with ",[37,376,377],{},"validate=\"many_to_one\""," guards against a priority appearing twice in the targets table; a ticket with an unknown priority would get ",[37,380,381],{},"NaN"," targets, which is worth asserting against in a real job.",[25,384,386],{"id":385},"step-2-elapsed-time-in-business-hours","Step 2 — Elapsed time in business hours",[10,388,389],{},"The number of business hours between two timestamps counts only the parts of each working day inside service hours. Whole days in between are counted with NumPy's business-day functions, which also skip holidays; the first and last day are clipped to the service window:",[30,391,393],{"className":76,"code":392,"language":78,"meta":35,"style":35},"HOURS_PER_DAY = DAY_END - DAY_START\nBUSDAY = np.busdaycalendar(weekmask=\"1111100\", holidays=HOLIDAYS.values.astype(\"datetime64[D]\"))\n\ndef clip_to_day(ts):\n    \"\"\"Hours of ts's own day that fall inside service hours, up to ts.\"\"\"\n    if not np.is_busday(np.datetime64(ts.date()), busdaycal=BUSDAY):\n        return 0.0\n    t = ts.hour + ts.minute \u002F 60 + ts.second \u002F 3600\n    return min(max(t, DAY_START), DAY_END) - DAY_START\n\ndef business_hours(start, end):\n    if pd.isna(start) or pd.isna(end) or end \u003C= start:\n        return 0.0 if not (pd.isna(start) or pd.isna(end)) else np.nan\n    d0, d1 = np.datetime64(start.date()), np.datetime64(end.date())\n    full_days = np.busday_count(d0, d1, busdaycal=BUSDAY)      # business days from d0 up to (not incl.) d1\n    hours = full_days * HOURS_PER_DAY - clip_to_day(start) + clip_to_day(end)\n    return round(hours, 4)\n\nchecks = [(\"2026-09-25 16:50\", \"2026-09-28 09:20\"),     # Friday afternoon to Monday morning\n          (\"2026-09-30 08:00\", \"2026-09-30 12:30\"),     # starts before opening\n          (\"2026-08-28 16:00\", \"2026-09-01 10:00\")]     # across a bank holiday weekend\nfor a, b in checks:\n    print(f\"{a} → {b}: {business_hours(pd.Timestamp(a), pd.Timestamp(b))} business hours\")\n",[37,394,395,411,447,451,463,468,489,497,530,561,565,575,599,625,635,658,685,700,705,730,749,768,783],{"__ignoreMap":35},[40,396,397,400,402,405,408],{"class":42,"line":43},[40,398,399],{"class":122},"HOURS_PER_DAY",[40,401,126],{"class":85},[40,403,404],{"class":122}," DAY_END",[40,406,407],{"class":85}," -",[40,409,410],{"class":122}," DAY_START\n",[40,412,413,416,418,421,424,426,429,431,434,436,438,441,444],{"class":42,"line":99},[40,414,415],{"class":122},"BUSDAY",[40,417,126],{"class":85},[40,419,420],{"class":89}," np.busdaycalendar(",[40,422,423],{"class":310},"weekmask",[40,425,285],{"class":85},[40,427,428],{"class":50},"\"1111100\"",[40,430,141],{"class":89},[40,432,433],{"class":310},"holidays",[40,435,285],{"class":85},[40,437,234],{"class":122},[40,439,440],{"class":89},".values.astype(",[40,442,443],{"class":50},"\"datetime64[D]\"",[40,445,446],{"class":89},"))\n",[40,448,449],{"class":42,"line":112},[40,450,116],{"emptyLinePlaceholder":115},[40,452,453,456,460],{"class":42,"line":119},[40,454,455],{"class":85},"def",[40,457,459],{"class":458},"s_Opv"," clip_to_day",[40,461,462],{"class":89},"(ts):\n",[40,464,465],{"class":42,"line":155},[40,466,467],{"class":50},"    \"\"\"Hours of ts's own day that fall inside service hours, up to ts.\"\"\"\n",[40,469,470,473,476,479,482,484,486],{"class":42,"line":178},[40,471,472],{"class":85},"    if",[40,474,475],{"class":85}," not",[40,477,478],{"class":89}," np.is_busday(np.datetime64(ts.date()), ",[40,480,481],{"class":310},"busdaycal",[40,483,285],{"class":85},[40,485,415],{"class":122},[40,487,488],{"class":89},"):\n",[40,490,491,494],{"class":42,"line":206},[40,492,493],{"class":85},"        return",[40,495,496],{"class":122}," 0.0\n",[40,498,499,502,504,507,510,513,516,519,522,525,527],{"class":42,"line":231},[40,500,501],{"class":89},"    t ",[40,503,285],{"class":85},[40,505,506],{"class":89}," ts.hour ",[40,508,509],{"class":85},"+",[40,511,512],{"class":89}," ts.minute ",[40,514,515],{"class":85},"\u002F",[40,517,518],{"class":122}," 60",[40,520,521],{"class":85}," +",[40,523,524],{"class":89}," ts.second ",[40,526,515],{"class":85},[40,528,529],{"class":122}," 3600\n",[40,531,532,535,538,540,543,546,548,551,553,556,559],{"class":42,"line":258},[40,533,534],{"class":85},"    return",[40,536,537],{"class":122}," min",[40,539,336],{"class":89},[40,541,542],{"class":122},"max",[40,544,545],{"class":89},"(t, ",[40,547,209],{"class":122},[40,549,550],{"class":89},"), ",[40,552,214],{"class":122},[40,554,555],{"class":89},") ",[40,557,558],{"class":85},"-",[40,560,410],{"class":122},[40,562,563],{"class":42,"line":274},[40,564,116],{"emptyLinePlaceholder":115},[40,566,567,569,572],{"class":42,"line":279},[40,568,455],{"class":85},[40,570,571],{"class":458}," business_hours",[40,573,574],{"class":89},"(start, end):\n",[40,576,577,579,582,585,588,590,593,596],{"class":42,"line":296},[40,578,472],{"class":85},[40,580,581],{"class":89}," pd.isna(start) ",[40,583,584],{"class":85},"or",[40,586,587],{"class":89}," pd.isna(end) ",[40,589,584],{"class":85},[40,591,592],{"class":89}," end ",[40,594,595],{"class":85},"\u003C=",[40,597,598],{"class":89}," start:\n",[40,600,601,603,606,609,611,614,616,619,622],{"class":42,"line":330},[40,602,493],{"class":85},[40,604,605],{"class":122}," 0.0",[40,607,608],{"class":85}," if",[40,610,475],{"class":85},[40,612,613],{"class":89}," (pd.isna(start) ",[40,615,584],{"class":85},[40,617,618],{"class":89}," pd.isna(end)) ",[40,620,621],{"class":85},"else",[40,623,624],{"class":89}," np.nan\n",[40,626,627,630,632],{"class":42,"line":344},[40,628,629],{"class":89},"    d0, d1 ",[40,631,285],{"class":85},[40,633,634],{"class":89}," np.datetime64(start.date()), np.datetime64(end.date())\n",[40,636,638,641,643,646,648,650,652,655],{"class":42,"line":637},15,[40,639,640],{"class":89},"    full_days ",[40,642,285],{"class":85},[40,644,645],{"class":89}," np.busday_count(d0, d1, ",[40,647,481],{"class":310},[40,649,285],{"class":85},[40,651,415],{"class":122},[40,653,654],{"class":89},")      ",[40,656,657],{"class":227},"# business days from d0 up to (not incl.) d1\n",[40,659,661,664,666,669,672,675,677,680,682],{"class":42,"line":660},16,[40,662,663],{"class":89},"    hours ",[40,665,285],{"class":85},[40,667,668],{"class":89}," full_days ",[40,670,671],{"class":85},"*",[40,673,674],{"class":122}," HOURS_PER_DAY",[40,676,407],{"class":85},[40,678,679],{"class":89}," clip_to_day(start) ",[40,681,509],{"class":85},[40,683,684],{"class":89}," clip_to_day(end)\n",[40,686,688,690,693,696,698],{"class":42,"line":687},17,[40,689,534],{"class":85},[40,691,692],{"class":122}," round",[40,694,695],{"class":89},"(hours, ",[40,697,168],{"class":122},[40,699,203],{"class":89},[40,701,703],{"class":42,"line":702},18,[40,704,116],{"emptyLinePlaceholder":115},[40,706,708,711,713,716,719,721,724,727],{"class":42,"line":707},19,[40,709,710],{"class":89},"checks ",[40,712,285],{"class":85},[40,714,715],{"class":89}," [(",[40,717,718],{"class":50},"\"2026-09-25 16:50\"",[40,720,141],{"class":89},[40,722,723],{"class":50},"\"2026-09-28 09:20\"",[40,725,726],{"class":89},"),     ",[40,728,729],{"class":227},"# Friday afternoon to Monday morning\n",[40,731,733,736,739,741,744,746],{"class":42,"line":732},20,[40,734,735],{"class":89},"          (",[40,737,738],{"class":50},"\"2026-09-30 08:00\"",[40,740,141],{"class":89},[40,742,743],{"class":50},"\"2026-09-30 12:30\"",[40,745,726],{"class":89},[40,747,748],{"class":227},"# starts before opening\n",[40,750,752,754,757,759,762,765],{"class":42,"line":751},21,[40,753,735],{"class":89},[40,755,756],{"class":50},"\"2026-08-28 16:00\"",[40,758,141],{"class":89},[40,760,761],{"class":50},"\"2026-09-01 10:00\"",[40,763,764],{"class":89},")]     ",[40,766,767],{"class":227},"# across a bank holiday weekend\n",[40,769,771,774,777,780],{"class":42,"line":770},22,[40,772,773],{"class":85},"for",[40,775,776],{"class":89}," a, b ",[40,778,779],{"class":85},"in",[40,781,782],{"class":89}," checks:\n",[40,784,786,789,791,794,797,801,803,806,809,811,814,816,819,821,824,826,829],{"class":42,"line":785},23,[40,787,788],{"class":122},"    print",[40,790,336],{"class":89},[40,792,793],{"class":85},"f",[40,795,796],{"class":50},"\"",[40,798,800],{"class":799},"sSjpA","{",[40,802,19],{"class":89},[40,804,805],{"class":799},"}",[40,807,808],{"class":50}," → ",[40,810,800],{"class":799},[40,812,813],{"class":89},"b",[40,815,805],{"class":799},[40,817,818],{"class":50},": ",[40,820,800],{"class":799},[40,822,823],{"class":89},"business_hours(pd.Timestamp(a), pd.Timestamp(b))",[40,825,805],{"class":799},[40,827,828],{"class":50}," business hours\"",[40,830,203],{"class":89},[10,832,833,834,837],{},"The trick is in ",[37,835,836],{},"full_days * HOURS_PER_DAY - clip_to_day(start) + clip_to_day(end)",": count every business day from the start date up to the end date as a full day, take off the part of the first day before the ticket arrived, and add the part of the last day up to the end. Ticket INC-5181, logged at 16:50 on a Friday and answered at 09:20 on Monday, took 30 business minutes (0.5 hours) — not 64 calendar hours. A ticket logged at 08:00 before opening starts its clock at 09:00, and the August bank holiday Monday is skipped entirely. These three cases are the ones to keep as unit tests, because they are where hand-rolled business-time calculations usually go wrong.",[839,840,848,849,848,853,848,857,848,864,848,871,848,881,848,888,848,893,848,897,848,901,848,907,848,913,848,917,848,919,848,921,848,924,848,928,848,931,848,933,848,935,848,938,848,942,848,945,848,947,848,949,848,954,848,960,848,964,848,967,848,969],"svg",{"viewBox":841,"role":842,"ariaLabelledBy":843,"xmlns":846,"style":847},"4 0 752 245","img",[844,845],"sla-clock-t","sla-clock-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  ",[850,851,852],"title",{"id":844},"Business hours for a Friday-evening ticket",[854,855,856],"desc",{"id":845},"Ticket INC-5181 is created at 16:50 on Friday 25 September. Ten business minutes pass before 17:00. Saturday and Sunday do not count. On Monday the clock resumes at 09:00 and the first response at 09:20 adds twenty minutes, so the response took half a business hour.",[858,859],"rect",{"x":168,"y":860,"width":861,"height":862,"fill":863},"0","752","245","#ffffff",[42,865],{"x1":866,"y1":867,"x2":868,"y2":867,"stroke":869,"style":870},"76.0","122.5","684.0","var(--muted,#5b6780)","stroke-width:2.5px",[858,872],{"x":873,"y":874,"width":875,"height":876,"rx":877,"fill":878,"stroke":879,"style":880},"20.0","16.0","136.0","74.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[882,883,887],"text",{"x":884,"y":885,"style":886},"88.0","49.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Fri 16:50",[882,889,892],{"x":884,"y":890,"style":891},"67.5","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","created",[42,894],{"x1":884,"y1":895,"x2":884,"y2":867,"stroke":869,"style":896},"90.5","stroke-width:1.5px",[898,899],"circle",{"cx":884,"cy":867,"r":900,"fill":879,"stroke":863,"style":880},"7.0",[858,902],{"x":903,"y":904,"width":875,"height":876,"rx":877,"fill":905,"stroke":906,"style":880},"166.0","154.5","#e7ebef","var(--line,#cdd5e6)",[882,908,912],{"x":909,"y":910,"style":911},"234.0","187.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Fri 17:00",[882,914,916],{"x":909,"y":915,"style":891},"206.0","+0.17 h, clock stops",[42,918],{"x1":909,"y1":867,"x2":909,"y2":904,"stroke":869,"style":896},[898,920],{"cx":909,"cy":867,"r":900,"fill":869,"stroke":863,"style":880},[858,922],{"x":923,"y":874,"width":875,"height":876,"rx":877,"fill":863,"stroke":906,"style":880},"312.0",[882,925,927],{"x":926,"y":885,"style":911},"380.0","Sat–Sun",[882,929,930],{"x":926,"y":890,"style":891},"not counted",[42,932],{"x1":926,"y1":895,"x2":926,"y2":867,"stroke":869,"style":896},[898,934],{"cx":926,"cy":867,"r":900,"fill":879,"stroke":863,"style":880},[858,936],{"x":937,"y":904,"width":875,"height":876,"rx":877,"fill":905,"stroke":906,"style":880},"458.0",[882,939,941],{"x":940,"y":910,"style":911},"526.0","Mon 09:00",[882,943,944],{"x":940,"y":915,"style":891},"clock resumes",[42,946],{"x1":940,"y1":867,"x2":940,"y2":904,"stroke":869,"style":896},[898,948],{"cx":940,"cy":867,"r":900,"fill":869,"stroke":863,"style":880},[858,950],{"x":951,"y":874,"width":875,"height":876,"rx":877,"fill":952,"stroke":953,"style":880},"604.0","#d9f4f1","var(--teal,#0f9488)",[882,955,959],{"x":956,"y":957,"style":958},"672.0","40.5","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Mon 09:20",[882,961,963],{"x":956,"y":962,"style":891},"59.0","response · 0.5 h",[882,965,966],{"x":956,"y":866,"style":891},"total",[42,968],{"x1":956,"y1":895,"x2":956,"y2":867,"stroke":869,"style":896},[898,970],{"cx":956,"cy":867,"r":900,"fill":953,"stroke":863,"style":880},[25,972,974],{"id":973},"step-3-breaches-and-tickets-at-risk","Step 3 — Breaches and tickets at risk",[10,976,977],{},"Apply the calculation to every ticket. Resolved tickets are measured to their resolution; open tickets are measured to the report time, which shows how much of their allowance is used up:",[30,979,981],{"className":76,"code":980,"language":78,"meta":35,"style":35},"tickets[\"Response bh\"] = [business_hours(a, b) for a, b in zip(tickets[\"Created\"], tickets[\"First response\"])]\ntickets[\"Elapsed bh\"] = [business_hours(a, b if pd.notna(b) else AS_OF)\n                         for a, b in zip(tickets[\"Created\"], tickets[\"Resolved\"])]\ntickets[\"Response met\"] = tickets[\"Response bh\"] \u003C= tickets[\"Response h\"]\ntickets[\"Resolve met\"] = np.where(tickets[\"Resolved\"].notna(), tickets[\"Elapsed bh\"] \u003C= tickets[\"Resolve h\"], np.nan)\ntickets[\"Used %\"] = (tickets[\"Elapsed bh\"] \u002F tickets[\"Resolve h\"] * 100).round(0)\n\nopen_ = tickets[tickets[\"Resolved\"].isna()]\nat_risk = open_[open_[\"Used %\"] >= 75].sort_values(\"Used %\", ascending=False)\nprint(\"open:\", len(open_), \"| at risk (≥75% of allowance used):\", len(at_risk),\n      \"| already breached:\", int((open_[\"Used %\"] > 100).sum()))\nprint(at_risk[[\"Ticket\", \"Priority\", \"Team\", \"Elapsed bh\", \"Resolve h\", \"Used %\"]].round(1).head(5).to_string(index=False))\n",[37,982,983,1023,1050,1071,1099,1132,1170,1174,1189,1226,1258,1283],{"__ignoreMap":35},[40,984,985,988,991,994,996,999,1001,1003,1005,1008,1011,1014,1017,1020],{"class":42,"line":43},[40,986,987],{"class":89},"tickets[",[40,989,990],{"class":50},"\"Response bh\"",[40,992,993],{"class":89},"] ",[40,995,285],{"class":85},[40,997,998],{"class":89}," [business_hours(a, b) ",[40,1000,773],{"class":85},[40,1002,776],{"class":89},[40,1004,779],{"class":85},[40,1006,1007],{"class":122}," zip",[40,1009,1010],{"class":89},"(tickets[",[40,1012,1013],{"class":50},"\"Created\"",[40,1015,1016],{"class":89},"], tickets[",[40,1018,1019],{"class":50},"\"First response\"",[40,1021,1022],{"class":89},"])]\n",[40,1024,1025,1027,1030,1032,1034,1037,1040,1043,1045,1048],{"class":42,"line":99},[40,1026,987],{"class":89},[40,1028,1029],{"class":50},"\"Elapsed bh\"",[40,1031,993],{"class":89},[40,1033,285],{"class":85},[40,1035,1036],{"class":89}," [business_hours(a, b ",[40,1038,1039],{"class":85},"if",[40,1041,1042],{"class":89}," pd.notna(b) ",[40,1044,621],{"class":85},[40,1046,1047],{"class":122}," AS_OF",[40,1049,203],{"class":89},[40,1051,1052,1055,1057,1059,1061,1063,1065,1067,1069],{"class":42,"line":112},[40,1053,1054],{"class":85},"                         for",[40,1056,776],{"class":89},[40,1058,779],{"class":85},[40,1060,1007],{"class":122},[40,1062,1010],{"class":89},[40,1064,1013],{"class":50},[40,1066,1016],{"class":89},[40,1068,363],{"class":50},[40,1070,1022],{"class":89},[40,1072,1073,1075,1078,1080,1082,1085,1087,1089,1091,1093,1096],{"class":42,"line":119},[40,1074,987],{"class":89},[40,1076,1077],{"class":50},"\"Response met\"",[40,1079,993],{"class":89},[40,1081,285],{"class":85},[40,1083,1084],{"class":89}," tickets[",[40,1086,990],{"class":50},[40,1088,993],{"class":89},[40,1090,595],{"class":85},[40,1092,1084],{"class":89},[40,1094,1095],{"class":50},"\"Response h\"",[40,1097,1098],{"class":89},"]\n",[40,1100,1101,1103,1106,1108,1110,1113,1115,1118,1120,1122,1124,1126,1129],{"class":42,"line":155},[40,1102,987],{"class":89},[40,1104,1105],{"class":50},"\"Resolve met\"",[40,1107,993],{"class":89},[40,1109,285],{"class":85},[40,1111,1112],{"class":89}," np.where(tickets[",[40,1114,363],{"class":50},[40,1116,1117],{"class":89},"].notna(), tickets[",[40,1119,1029],{"class":50},[40,1121,993],{"class":89},[40,1123,595],{"class":85},[40,1125,1084],{"class":89},[40,1127,1128],{"class":50},"\"Resolve h\"",[40,1130,1131],{"class":89},"], np.nan)\n",[40,1133,1134,1136,1139,1141,1143,1146,1148,1150,1152,1154,1156,1158,1160,1163,1166,1168],{"class":42,"line":178},[40,1135,987],{"class":89},[40,1137,1138],{"class":50},"\"Used %\"",[40,1140,993],{"class":89},[40,1142,285],{"class":85},[40,1144,1145],{"class":89}," (tickets[",[40,1147,1029],{"class":50},[40,1149,993],{"class":89},[40,1151,515],{"class":85},[40,1153,1084],{"class":89},[40,1155,1128],{"class":50},[40,1157,993],{"class":89},[40,1159,671],{"class":85},[40,1161,1162],{"class":122}," 100",[40,1164,1165],{"class":89},").round(",[40,1167,860],{"class":122},[40,1169,203],{"class":89},[40,1171,1172],{"class":42,"line":206},[40,1173,116],{"emptyLinePlaceholder":115},[40,1175,1176,1179,1181,1184,1186],{"class":42,"line":231},[40,1177,1178],{"class":89},"open_ ",[40,1180,285],{"class":85},[40,1182,1183],{"class":89}," tickets[tickets[",[40,1185,363],{"class":50},[40,1187,1188],{"class":89},"].isna()]\n",[40,1190,1191,1194,1196,1199,1201,1203,1206,1209,1212,1214,1216,1219,1221,1224],{"class":42,"line":258},[40,1192,1193],{"class":89},"at_risk ",[40,1195,285],{"class":85},[40,1197,1198],{"class":89}," open_[open_[",[40,1200,1138],{"class":50},[40,1202,993],{"class":89},[40,1204,1205],{"class":85},">=",[40,1207,1208],{"class":122}," 75",[40,1210,1211],{"class":89},"].sort_values(",[40,1213,1138],{"class":50},[40,1215,141],{"class":89},[40,1217,1218],{"class":310},"ascending",[40,1220,285],{"class":85},[40,1222,1223],{"class":122},"False",[40,1225,203],{"class":89},[40,1227,1228,1230,1232,1235,1237,1239,1242,1245,1248,1251,1253,1255],{"class":42,"line":274},[40,1229,333],{"class":122},[40,1231,336],{"class":89},[40,1233,1234],{"class":50},"\"open:\"",[40,1236,141],{"class":89},[40,1238,351],{"class":122},[40,1240,1241],{"class":89},"(open_), ",[40,1243,1244],{"class":50},"\"| at risk (≥75",[40,1246,1247],{"class":799},"% o",[40,1249,1250],{"class":50},"f allowance used):\"",[40,1252,141],{"class":89},[40,1254,351],{"class":122},[40,1256,1257],{"class":89},"(at_risk),\n",[40,1259,1260,1263,1265,1268,1271,1273,1275,1278,1280],{"class":42,"line":279},[40,1261,1262],{"class":50},"      \"| already breached:\"",[40,1264,141],{"class":89},[40,1266,1267],{"class":122},"int",[40,1269,1270],{"class":89},"((open_[",[40,1272,1138],{"class":50},[40,1274,993],{"class":89},[40,1276,1277],{"class":85},">",[40,1279,1162],{"class":122},[40,1281,1282],{"class":89},").sum()))\n",[40,1284,1285,1287,1290,1293,1295,1297,1299,1302,1304,1306,1308,1310,1312,1314,1317,1319,1322,1325,1328,1331,1333,1335],{"class":42,"line":296},[40,1286,333],{"class":122},[40,1288,1289],{"class":89},"(at_risk[[",[40,1291,1292],{"class":50},"\"Ticket\"",[40,1294,141],{"class":89},[40,1296,132],{"class":50},[40,1298,141],{"class":89},[40,1300,1301],{"class":50},"\"Team\"",[40,1303,141],{"class":89},[40,1305,1029],{"class":50},[40,1307,141],{"class":89},[40,1309,1128],{"class":50},[40,1311,141],{"class":89},[40,1313,1138],{"class":50},[40,1315,1316],{"class":89},"]].round(",[40,1318,163],{"class":122},[40,1320,1321],{"class":89},").head(",[40,1323,1324],{"class":122},"5",[40,1326,1327],{"class":89},").to_string(",[40,1329,1330],{"class":310},"index",[40,1332,285],{"class":85},[40,1334,1223],{"class":122},[40,1336,446],{"class":89},[10,1338,1339,1342,1343,1345],{},[37,1340,1341],{},"Resolve met"," is left empty (",[37,1344,381],{},") for open tickets: they have neither met nor missed their target yet, and counting them as met would flatter the figures. Instead, open tickets get a \"used\" percentage, and those at 75% or more of their allowance go on an at-risk list — the most useful part of the report for the team lead, because those tickets can still be saved. Tickets already past 100% are breaches in waiting and are listed first. In this export, 16 tickets are open and four are at risk: INC-5186, a P2 open since 22 September, has used 208% of its 24-hour allowance and has already breached; INC-5184, a P3, has used 39 of its 40 hours; and two P2s are at 94–95%.",[25,1347,1349],{"id":1348},"step-4-attainment-by-priority-team-and-week","Step 4 — Attainment by priority, team and week",[10,1351,1352],{},"SLA attainment is the share of tickets that met their target. Compute it from resolved tickets only, for resolution, and from all responded tickets for first response:",[30,1354,1356],{"className":76,"code":1355,"language":78,"meta":35,"style":35},"resolved = tickets[tickets[\"Resolved\"].notna()].copy()\nresolved[\"Resolve met\"] = resolved[\"Resolve met\"].astype(bool)\n\nby_priority = pd.DataFrame({\n    \"Tickets\": tickets.groupby(\"Priority\").size(),\n    \"Response met %\": (tickets.groupby(\"Priority\")[\"Response met\"].mean() * 100).round(1),\n    \"Resolved\": resolved.groupby(\"Priority\").size(),\n    \"Resolve met %\": (resolved.groupby(\"Priority\")[\"Resolve met\"].mean() * 100).round(1),\n    \"Median resolve bh\": resolved.groupby(\"Priority\")[\"Elapsed bh\"].median().round(1),\n})\nby_team = (resolved.pivot_table(index=\"Team\", columns=\"Priority\", values=\"Resolve met\", aggfunc=\"mean\") * 100).round(1)\nweekly = (resolved.set_index(\"Resolved\").groupby(pd.Grouper(freq=\"W-SUN\"))[\"Resolve met\"].mean() * 100).round(1)\nprint(by_priority.to_string())\nprint(by_team.to_string())\nprint(weekly.rename(lambda d: f\"w\u002Fe {d:%d %b}\").to_string())\n",[37,1357,1358,1372,1396,1400,1410,1423,1452,1464,1490,1510,1515,1571,1611,1618,1625],{"__ignoreMap":35},[40,1359,1360,1363,1365,1367,1369],{"class":42,"line":43},[40,1361,1362],{"class":89},"resolved ",[40,1364,285],{"class":85},[40,1366,1183],{"class":89},[40,1368,363],{"class":50},[40,1370,1371],{"class":89},"].notna()].copy()\n",[40,1373,1374,1377,1379,1381,1383,1386,1388,1391,1394],{"class":42,"line":99},[40,1375,1376],{"class":89},"resolved[",[40,1378,1105],{"class":50},[40,1380,993],{"class":89},[40,1382,285],{"class":85},[40,1384,1385],{"class":89}," resolved[",[40,1387,1105],{"class":50},[40,1389,1390],{"class":89},"].astype(",[40,1392,1393],{"class":122},"bool",[40,1395,203],{"class":89},[40,1397,1398],{"class":42,"line":112},[40,1399,116],{"emptyLinePlaceholder":115},[40,1401,1402,1405,1407],{"class":42,"line":119},[40,1403,1404],{"class":89},"by_priority ",[40,1406,285],{"class":85},[40,1408,1409],{"class":89}," pd.DataFrame({\n",[40,1411,1412,1415,1418,1420],{"class":42,"line":155},[40,1413,1414],{"class":50},"    \"Tickets\"",[40,1416,1417],{"class":89},": tickets.groupby(",[40,1419,132],{"class":50},[40,1421,1422],{"class":89},").size(),\n",[40,1424,1425,1428,1431,1433,1436,1438,1441,1443,1445,1447,1449],{"class":42,"line":178},[40,1426,1427],{"class":50},"    \"Response met %\"",[40,1429,1430],{"class":89},": (tickets.groupby(",[40,1432,132],{"class":50},[40,1434,1435],{"class":89},")[",[40,1437,1077],{"class":50},[40,1439,1440],{"class":89},"].mean() ",[40,1442,671],{"class":85},[40,1444,1162],{"class":122},[40,1446,1165],{"class":89},[40,1448,163],{"class":122},[40,1450,1451],{"class":89},"),\n",[40,1453,1454,1457,1460,1462],{"class":42,"line":206},[40,1455,1456],{"class":50},"    \"Resolved\"",[40,1458,1459],{"class":89},": resolved.groupby(",[40,1461,132],{"class":50},[40,1463,1422],{"class":89},[40,1465,1466,1469,1472,1474,1476,1478,1480,1482,1484,1486,1488],{"class":42,"line":231},[40,1467,1468],{"class":50},"    \"Resolve met %\"",[40,1470,1471],{"class":89},": (resolved.groupby(",[40,1473,132],{"class":50},[40,1475,1435],{"class":89},[40,1477,1105],{"class":50},[40,1479,1440],{"class":89},[40,1481,671],{"class":85},[40,1483,1162],{"class":122},[40,1485,1165],{"class":89},[40,1487,163],{"class":122},[40,1489,1451],{"class":89},[40,1491,1492,1495,1497,1499,1501,1503,1506,1508],{"class":42,"line":258},[40,1493,1494],{"class":50},"    \"Median resolve bh\"",[40,1496,1459],{"class":89},[40,1498,132],{"class":50},[40,1500,1435],{"class":89},[40,1502,1029],{"class":50},[40,1504,1505],{"class":89},"].median().round(",[40,1507,163],{"class":122},[40,1509,1451],{"class":89},[40,1511,1512],{"class":42,"line":274},[40,1513,1514],{"class":89},"})\n",[40,1516,1517,1520,1522,1525,1527,1529,1531,1533,1536,1538,1540,1542,1545,1547,1549,1551,1554,1556,1559,1561,1563,1565,1567,1569],{"class":42,"line":279},[40,1518,1519],{"class":89},"by_team ",[40,1521,285],{"class":85},[40,1523,1524],{"class":89}," (resolved.pivot_table(",[40,1526,1330],{"class":310},[40,1528,285],{"class":85},[40,1530,1301],{"class":50},[40,1532,141],{"class":89},[40,1534,1535],{"class":310},"columns",[40,1537,285],{"class":85},[40,1539,132],{"class":50},[40,1541,141],{"class":89},[40,1543,1544],{"class":310},"values",[40,1546,285],{"class":85},[40,1548,1105],{"class":50},[40,1550,141],{"class":89},[40,1552,1553],{"class":310},"aggfunc",[40,1555,285],{"class":85},[40,1557,1558],{"class":50},"\"mean\"",[40,1560,555],{"class":89},[40,1562,671],{"class":85},[40,1564,1162],{"class":122},[40,1566,1165],{"class":89},[40,1568,163],{"class":122},[40,1570,203],{"class":89},[40,1572,1573,1576,1578,1581,1583,1586,1589,1591,1594,1597,1599,1601,1603,1605,1607,1609],{"class":42,"line":296},[40,1574,1575],{"class":89},"weekly ",[40,1577,285],{"class":85},[40,1579,1580],{"class":89}," (resolved.set_index(",[40,1582,363],{"class":50},[40,1584,1585],{"class":89},").groupby(pd.Grouper(",[40,1587,1588],{"class":310},"freq",[40,1590,285],{"class":85},[40,1592,1593],{"class":50},"\"W-SUN\"",[40,1595,1596],{"class":89},"))[",[40,1598,1105],{"class":50},[40,1600,1440],{"class":89},[40,1602,671],{"class":85},[40,1604,1162],{"class":122},[40,1606,1165],{"class":89},[40,1608,163],{"class":122},[40,1610,203],{"class":89},[40,1612,1613,1615],{"class":42,"line":330},[40,1614,333],{"class":122},[40,1616,1617],{"class":89},"(by_priority.to_string())\n",[40,1619,1620,1622],{"class":42,"line":344},[40,1621,333],{"class":122},[40,1623,1624],{"class":89},"(by_team.to_string())\n",[40,1626,1627,1629,1632,1635,1638,1640,1643,1645,1648,1651,1654,1656,1658,1660,1662],{"class":42,"line":637},[40,1628,333],{"class":122},[40,1630,1631],{"class":89},"(weekly.rename(",[40,1633,1634],{"class":85},"lambda",[40,1636,1637],{"class":89}," d: ",[40,1639,793],{"class":85},[40,1641,1642],{"class":50},"\"w\u002Fe ",[40,1644,800],{"class":799},[40,1646,1647],{"class":89},"d:",[40,1649,1650],{"class":85},"%",[40,1652,1653],{"class":89},"d ",[40,1655,1650],{"class":85},[40,1657,813],{"class":89},[40,1659,805],{"class":799},[40,1661,796],{"class":50},[40,1663,1664],{"class":89},").to_string())\n",[10,1666,1667],{},"In September, 97–99% of resolved tickets met their resolution target in every priority, with median resolution times of 3.7, 9.9 and 17.0 business hours for P1, P2 and P3. The misses cluster in the Network team's P1 and P2 tickets. The last weekly bucket, ending 4 October, holds only the three days to the report date, so treat its lower figure with care. The table per priority is the headline: how many tickets, the share answered and resolved within target, and the median business hours to resolve. The team-by-priority pivot shows where misses concentrate, and the weekly series shows whether things are getting better. Medians are used instead of means because ticket durations are heavily skewed — one ticket open for a week would drag a mean far away from the typical experience.",[839,1669,848,1674,848,1677,848,1680,848,1683,848,1690,848,1701,848,1707,848,1713,848,1718,848,1722,848,1726,848,1729,848,1736,848,1740,848,1744,848,1747,848,1750],{"viewBox":1670,"role":842,"ariaLabelledBy":1671,"xmlns":846,"style":847},"4 0 752 207",[1672,1673],"sla-measure-t","sla-measure-d",[850,1675,1676],{"id":1672},"Calendar hours versus business hours",[854,1678,1679],{"id":1673},"Measured in calendar hours, tickets logged late on Friday appear to breach over the weekend and attainment is understated. Measured in business hours within the service window, excluding weekends and holidays, the clock only runs when the desk is open, matching how the contract is written.",[858,1681],{"x":168,"y":860,"width":861,"height":1682,"fill":863},"207",[858,1684],{"x":873,"y":874,"width":1685,"height":1686,"rx":1687,"fill":1688,"stroke":1689,"style":880},"351.0","175.0","14","#fce9e9","var(--danger,#dc2626)",[1691,1692,1694],"g",{"transform":1693},"translate(182.5,29.0) scale(1.083)",[1695,1696],"path",{"d":1697,"fill":1698,"stroke":1699,"style":1700},"M6 6l12 12 M18 6L6 18","none","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[882,1702,1706],{"x":1703,"y":1704,"style":1705},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","calendar hours",[42,1708],{"x1":1709,"y1":1710,"x2":1711,"y2":1710,"stroke":1689,"style":1712},"36.0","91.0","355.0","stroke-width:1px",[882,1714,1717],{"x":1703,"y":1715,"style":1716},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Friday 16:50 → Monday 09:20 = 64.5 h",[882,1719,1721],{"x":1703,"y":1720,"style":1716},"137.0","weekend counts against the desk",[882,1723,1725],{"x":1703,"y":1724,"style":1716},"163.0","attainment understated",[858,1727],{"x":1728,"y":874,"width":1685,"height":1686,"rx":1687,"fill":952,"stroke":953,"style":880},"389.0",[1691,1730,1732],{"transform":1731},"translate(551.5,29.0) scale(1.083)",[1695,1733],{"d":1734,"fill":1698,"stroke":1735,"style":1700},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[882,1737,16],{"x":1738,"y":1704,"style":1739},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[42,1741],{"x1":1742,"y1":1710,"x2":1743,"y2":1710,"stroke":953,"style":1712},"405.0","724.0",[882,1745,1746],{"x":1738,"y":1715,"style":1716},"same ticket = 0.5 h",[882,1748,1749],{"x":1738,"y":1720,"style":1716},"09:00–17:00, Mon–Fri, no holidays",[882,1751,1752],{"x":1738,"y":1724,"style":1716},"matches the SLA contract",[25,1754,1756],{"id":1755},"step-5-write-the-report","Step 5 — Write the report",[10,1758,1759],{},"The workbook puts what needs action first — the at-risk list — then the attainment tables and the full ticket list:",[30,1761,1763],{"className":76,"code":1762,"language":78,"meta":35,"style":35},"from openpyxl.styles import Font, PatternFill\n\nwith pd.ExcelWriter(\"sla_report_2026-09.xlsx\", engine=\"openpyxl\") as xw:\n    at_risk[[\"Ticket\", \"Priority\", \"Team\", \"Created\", \"Elapsed bh\", \"Resolve h\", \"Used %\"]].to_excel(xw, sheet_name=\"At risk\", index=False)\n    by_priority.to_excel(xw, sheet_name=\"By priority\")\n    by_team.to_excel(xw, sheet_name=\"By team\")\n    weekly.rename(\"Resolve met %\").to_frame().to_excel(xw, sheet_name=\"Weekly\")\n    tickets.to_excel(xw, sheet_name=\"Tickets\", index=False)\n    red, amber = PatternFill(\"solid\", fgColor=\"FFC7CE\"), PatternFill(\"solid\", fgColor=\"FFEB9C\")\n    ws = xw.sheets[\"At risk\"]\n    for row in ws.iter_rows(min_row=2):\n        row[-1].fill = red if (row[-1].value or 0) > 100 else amber\n    for sheet in xw.book.worksheets:\n        for c in sheet[1]:\n            c.font = Font(bold=True)\n        sheet.freeze_panes = \"A2\"\n        for col in sheet.columns:\n            sheet.column_dimensions[col[0].column_letter].width = 16\nprint(\"sheets:\", [\"At risk\", \"By priority\", \"By team\", \"Weekly\", \"Tickets\"])\nprint(\"overall resolve met:\", round(resolved[\"Resolve met\"].mean() * 100, 1), \"%\")\n",[37,1764,1765,1778,1782,1810,1862,1876,1890,1910,1932,1971,1985,2008,2054,2066,2084,2104,2114,2126,2141,2173],{"__ignoreMap":35},[40,1766,1767,1770,1773,1775],{"class":42,"line":43},[40,1768,1769],{"class":85},"from",[40,1771,1772],{"class":89}," openpyxl.styles ",[40,1774,86],{"class":85},[40,1776,1777],{"class":89}," Font, PatternFill\n",[40,1779,1780],{"class":42,"line":99},[40,1781,116],{"emptyLinePlaceholder":115},[40,1783,1784,1787,1790,1793,1795,1798,1800,1803,1805,1807],{"class":42,"line":112},[40,1785,1786],{"class":85},"with",[40,1788,1789],{"class":89}," pd.ExcelWriter(",[40,1791,1792],{"class":50},"\"sla_report_2026-09.xlsx\"",[40,1794,141],{"class":89},[40,1796,1797],{"class":310},"engine",[40,1799,285],{"class":85},[40,1801,1802],{"class":50},"\"openpyxl\"",[40,1804,555],{"class":89},[40,1806,93],{"class":85},[40,1808,1809],{"class":89}," xw:\n",[40,1811,1812,1815,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835,1837,1839,1841,1844,1847,1849,1852,1854,1856,1858,1860],{"class":42,"line":119},[40,1813,1814],{"class":89},"    at_risk[[",[40,1816,1292],{"class":50},[40,1818,141],{"class":89},[40,1820,132],{"class":50},[40,1822,141],{"class":89},[40,1824,1301],{"class":50},[40,1826,141],{"class":89},[40,1828,1013],{"class":50},[40,1830,141],{"class":89},[40,1832,1029],{"class":50},[40,1834,141],{"class":89},[40,1836,1128],{"class":50},[40,1838,141],{"class":89},[40,1840,1138],{"class":50},[40,1842,1843],{"class":89},"]].to_excel(xw, ",[40,1845,1846],{"class":310},"sheet_name",[40,1848,285],{"class":85},[40,1850,1851],{"class":50},"\"At risk\"",[40,1853,141],{"class":89},[40,1855,1330],{"class":310},[40,1857,285],{"class":85},[40,1859,1223],{"class":122},[40,1861,203],{"class":89},[40,1863,1864,1867,1869,1871,1874],{"class":42,"line":155},[40,1865,1866],{"class":89},"    by_priority.to_excel(xw, ",[40,1868,1846],{"class":310},[40,1870,285],{"class":85},[40,1872,1873],{"class":50},"\"By priority\"",[40,1875,203],{"class":89},[40,1877,1878,1881,1883,1885,1888],{"class":42,"line":178},[40,1879,1880],{"class":89},"    by_team.to_excel(xw, ",[40,1882,1846],{"class":310},[40,1884,285],{"class":85},[40,1886,1887],{"class":50},"\"By team\"",[40,1889,203],{"class":89},[40,1891,1892,1895,1898,1901,1903,1905,1908],{"class":42,"line":206},[40,1893,1894],{"class":89},"    weekly.rename(",[40,1896,1897],{"class":50},"\"Resolve met %\"",[40,1899,1900],{"class":89},").to_frame().to_excel(xw, ",[40,1902,1846],{"class":310},[40,1904,285],{"class":85},[40,1906,1907],{"class":50},"\"Weekly\"",[40,1909,203],{"class":89},[40,1911,1912,1915,1917,1919,1922,1924,1926,1928,1930],{"class":42,"line":231},[40,1913,1914],{"class":89},"    tickets.to_excel(xw, ",[40,1916,1846],{"class":310},[40,1918,285],{"class":85},[40,1920,1921],{"class":50},"\"Tickets\"",[40,1923,141],{"class":89},[40,1925,1330],{"class":310},[40,1927,285],{"class":85},[40,1929,1223],{"class":122},[40,1931,203],{"class":89},[40,1933,1934,1937,1939,1942,1945,1947,1950,1952,1955,1958,1960,1962,1964,1966,1969],{"class":42,"line":258},[40,1935,1936],{"class":89},"    red, amber ",[40,1938,285],{"class":85},[40,1940,1941],{"class":89}," PatternFill(",[40,1943,1944],{"class":50},"\"solid\"",[40,1946,141],{"class":89},[40,1948,1949],{"class":310},"fgColor",[40,1951,285],{"class":85},[40,1953,1954],{"class":50},"\"FFC7CE\"",[40,1956,1957],{"class":89},"), PatternFill(",[40,1959,1944],{"class":50},[40,1961,141],{"class":89},[40,1963,1949],{"class":310},[40,1965,285],{"class":85},[40,1967,1968],{"class":50},"\"FFEB9C\"",[40,1970,203],{"class":89},[40,1972,1973,1976,1978,1981,1983],{"class":42,"line":274},[40,1974,1975],{"class":89},"    ws ",[40,1977,285],{"class":85},[40,1979,1980],{"class":89}," xw.sheets[",[40,1982,1851],{"class":50},[40,1984,1098],{"class":89},[40,1986,1987,1990,1993,1995,1998,2001,2003,2006],{"class":42,"line":279},[40,1988,1989],{"class":85},"    for",[40,1991,1992],{"class":89}," row ",[40,1994,779],{"class":85},[40,1996,1997],{"class":89}," ws.iter_rows(",[40,1999,2000],{"class":310},"min_row",[40,2002,285],{"class":85},[40,2004,2005],{"class":122},"2",[40,2007,488],{"class":89},[40,2009,2010,2013,2015,2017,2020,2022,2025,2027,2030,2032,2034,2037,2039,2042,2044,2046,2048,2051],{"class":42,"line":296},[40,2011,2012],{"class":89},"        row[",[40,2014,558],{"class":85},[40,2016,163],{"class":122},[40,2018,2019],{"class":89},"].fill ",[40,2021,285],{"class":85},[40,2023,2024],{"class":89}," red ",[40,2026,1039],{"class":85},[40,2028,2029],{"class":89}," (row[",[40,2031,558],{"class":85},[40,2033,163],{"class":122},[40,2035,2036],{"class":89},"].value ",[40,2038,584],{"class":85},[40,2040,2041],{"class":122}," 0",[40,2043,555],{"class":89},[40,2045,1277],{"class":85},[40,2047,1162],{"class":122},[40,2049,2050],{"class":85}," else",[40,2052,2053],{"class":89}," amber\n",[40,2055,2056,2058,2061,2063],{"class":42,"line":330},[40,2057,1989],{"class":85},[40,2059,2060],{"class":89}," sheet ",[40,2062,779],{"class":85},[40,2064,2065],{"class":89}," xw.book.worksheets:\n",[40,2067,2068,2071,2074,2076,2079,2081],{"class":42,"line":344},[40,2069,2070],{"class":85},"        for",[40,2072,2073],{"class":89}," c ",[40,2075,779],{"class":85},[40,2077,2078],{"class":89}," sheet[",[40,2080,163],{"class":122},[40,2082,2083],{"class":89},"]:\n",[40,2085,2086,2089,2091,2094,2097,2099,2102],{"class":42,"line":637},[40,2087,2088],{"class":89},"            c.font ",[40,2090,285],{"class":85},[40,2092,2093],{"class":89}," Font(",[40,2095,2096],{"class":310},"bold",[40,2098,285],{"class":85},[40,2100,2101],{"class":122},"True",[40,2103,203],{"class":89},[40,2105,2106,2109,2111],{"class":42,"line":660},[40,2107,2108],{"class":89},"        sheet.freeze_panes ",[40,2110,285],{"class":85},[40,2112,2113],{"class":50}," \"A2\"\n",[40,2115,2116,2118,2121,2123],{"class":42,"line":687},[40,2117,2070],{"class":85},[40,2119,2120],{"class":89}," col ",[40,2122,779],{"class":85},[40,2124,2125],{"class":89}," sheet.columns:\n",[40,2127,2128,2131,2133,2136,2138],{"class":42,"line":702},[40,2129,2130],{"class":89},"            sheet.column_dimensions[col[",[40,2132,860],{"class":122},[40,2134,2135],{"class":89},"].column_letter].width ",[40,2137,285],{"class":85},[40,2139,2140],{"class":122}," 16\n",[40,2142,2143,2145,2147,2150,2153,2155,2157,2159,2161,2163,2165,2167,2169,2171],{"class":42,"line":707},[40,2144,333],{"class":122},[40,2146,336],{"class":89},[40,2148,2149],{"class":50},"\"sheets:\"",[40,2151,2152],{"class":89},", [",[40,2154,1851],{"class":50},[40,2156,141],{"class":89},[40,2158,1873],{"class":50},[40,2160,141],{"class":89},[40,2162,1887],{"class":50},[40,2164,141],{"class":89},[40,2166,1907],{"class":50},[40,2168,141],{"class":89},[40,2170,1921],{"class":50},[40,2172,255],{"class":89},[40,2174,2175,2177,2179,2182,2184,2187,2190,2192,2194,2196,2198,2200,2202,2204,2207],{"class":42,"line":732},[40,2176,333],{"class":122},[40,2178,336],{"class":89},[40,2180,2181],{"class":50},"\"overall resolve met:\"",[40,2183,141],{"class":89},[40,2185,2186],{"class":122},"round",[40,2188,2189],{"class":89},"(resolved[",[40,2191,1105],{"class":50},[40,2193,1440],{"class":89},[40,2195,671],{"class":85},[40,2197,1162],{"class":122},[40,2199,141],{"class":89},[40,2201,163],{"class":122},[40,2203,550],{"class":89},[40,2205,2206],{"class":50},"\"%\"",[40,2208,203],{"class":89},[10,2210,2211,2212,23],{},"Breached open tickets are shaded red and those approaching breach amber. Datetimes from pandas are naive here, so openpyxl writes them as ordinary Excel dates; if your export has time zones, convert to local time first. The full ticket list with every computed column — response and resolution business hours, met flags, percentage used — lets anyone audit a figure in the summary by filtering. For the monthly management pack, the headline attainment figures feed straight into ",[19,2213,2215],{"href":2214},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002F","Build a Monthly KPI Scorecard with Targets",[839,2217,848,2222,848,2225,848,2228,848,2231,848,2235,848,2243,848,2249,848,2254,848,2258,848,2263,848,2268,848,2271,848,2278,848,2283,848,2286,848,2289,848,2292,848,2295,848,2300,848,2307,848,2313,848,2317,848,2321,848,2324,848,2327,848,2333,848,2338,848,2341],{"viewBox":2218,"role":842,"ariaLabelledBy":2219,"xmlns":846,"style":847},"4 0 752 142",[2220,2221],"sla-flow-t","sla-flow-d",[850,2223,2224],{"id":2220},"From ticket export to SLA report",[854,2226,2227],{"id":2221},"Targets per priority are joined onto each ticket, business hours are measured for response and resolution, open tickets are measured to the report time and flagged when close to breach, and attainment is summarised by priority, team and week.",[858,2229],{"x":168,"y":860,"width":861,"height":2230,"fill":863},"142",[858,2232],{"x":873,"y":874,"width":2233,"height":2234,"rx":877,"fill":905,"stroke":906,"style":880},"153.0","110.5",[1691,2236,2238],{"transform":2237},"translate(84.5,29.0) scale(1.000)",[1695,2239],{"d":2240,"fill":1698,"stroke":2241,"style":2242},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[882,2244,2248],{"x":2245,"y":2246,"style":2247},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Targets",[882,2250,2253],{"x":2245,"y":2251,"style":2252},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","response, resolve per",[882,2255,2257],{"x":2245,"y":2256,"style":2252},"112.5","priority",[42,2259],{"x1":2260,"y1":2261,"x2":2262,"y2":2261,"stroke":879,"style":880},"178.0","71.2","200.0",[2264,2265],"polygon",{"points":2266,"fill":2267},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[858,2269],{"x":2270,"y":874,"width":2233,"height":2234,"rx":877,"fill":878,"stroke":879,"style":880},"209.0",[1691,2272,2274],{"transform":2273},"translate(273.5,29.0) scale(1.000)",[1695,2275],{"d":2276,"fill":1698,"stroke":2277,"style":2242},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--brand-strong,#4338ca)",[882,2279,2282],{"x":2280,"y":2246,"style":2281},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Business hours",[882,2284,2285],{"x":2280,"y":2251,"style":2252},"busday_count +",[882,2287,2288],{"x":2280,"y":2256,"style":2252},"clipped ends",[42,2290],{"x1":2291,"y1":2261,"x2":1728,"y2":2261,"stroke":879,"style":880},"367.0",[2264,2293],{"points":2294,"fill":2267},"393.0,71.2 384.0,76.2 384.0,66.2",[858,2296],{"x":2297,"y":874,"width":2233,"height":2234,"rx":877,"fill":2298,"stroke":2299,"style":880},"398.0","#fdefd8","var(--gold,#b4740a)",[1691,2301,2303],{"transform":2302},"translate(462.5,37.8) scale(1.000)",[1695,2304],{"d":2305,"fill":1698,"stroke":2306,"style":2242},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[882,2308,2312],{"x":2309,"y":2310,"style":2311},"474.5","84.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Breaches & risk",[882,2314,2316],{"x":2309,"y":2315,"style":2252},"103.8","met flags, % used",[42,2318],{"x1":2319,"y1":2261,"x2":2320,"y2":2261,"stroke":879,"style":880},"556.0","578.0",[2264,2322],{"points":2323,"fill":2267},"582.0,71.2 573.0,76.2 573.0,66.2",[858,2325],{"x":2326,"y":874,"width":2233,"height":2234,"rx":877,"fill":952,"stroke":953,"style":880},"587.0",[1691,2328,2330],{"transform":2329},"translate(651.5,29.0) scale(1.000)",[1695,2331],{"d":2332,"fill":1698,"stroke":1735,"style":2242},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6",[882,2334,2337],{"x":2335,"y":2246,"style":2336},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Report",[882,2339,2340],{"x":2335,"y":2251,"style":2252},"at risk first, then",[882,2342,2343],{"x":2335,"y":2256,"style":2252},"attainment",[25,2345,2347],{"id":2346},"common-pitfalls","Common pitfalls",[2349,2350,2351,2359,2365,2371,2377],"ul",{},[2352,2353,2354,2358],"li",{},[2355,2356,2357],"strong",{},"Calendar hours."," Weekends and nights make the desk look worse than it is; measure business hours.",[2352,2360,2361,2364],{},[2355,2362,2363],{},"Counting open tickets as met."," Leave them out of resolution attainment and show them as at risk.",[2352,2366,2367,2370],{},[2355,2368,2369],{},"Forgetting holidays."," Pass them to the business-day calendar.",[2352,2372,2373,2376],{},[2355,2374,2375],{},"Means for durations."," A few long tickets distort the mean; report medians.",[2352,2378,2379,2382],{},[2355,2380,2381],{},"Unknown priorities."," A ticket without a target silently drops out; assert every priority is known.",[25,2384,2386],{"id":2385},"conclusion","Conclusion",[10,2388,2389,2390,2393],{},"An SLA report from a ticket export rests on one well-tested function: business hours between two timestamps, counting whole business days with ",[37,2391,2392],{},"numpy.busday_count"," and clipping the first and last day to the service window. With it, compare each ticket's response and resolution times with the targets for its priority, measure open tickets to the report time to find those at risk, summarise attainment by priority, team and week from resolved tickets, and write a workbook that puts the at-risk list first.",[25,2395,2397],{"id":2396},"frequently-asked-questions","Frequently asked questions",[10,2399,2400,2403],{},[2355,2401,2402],{},"How do I calculate business hours between two dates in Python?","\nCount the business days between the two dates with numpy.busday_count using a calendar that excludes weekends and holidays, multiply by the hours per day, subtract the hours of the first day before the start, and add the hours of the last day up to the end.",[10,2405,2406,2409],{},[2355,2407,2408],{},"How do I calculate SLA attainment in pandas?","\nJoin the target hours per priority onto each ticket, compute elapsed business hours, mark each resolved ticket as met if the elapsed time is within target, and take the mean of that flag by priority, team or week.",[10,2411,2412,2415],{},[2355,2413,2414],{},"How should open tickets be handled in an SLA report?","\nExclude them from resolution attainment, measure their elapsed time to the report time, and list those that have used most of their allowance as at risk.",[10,2417,2418,2421],{},[2355,2419,2420],{},"Why use the median resolution time instead of the mean?","\nTicket durations are skewed by a few very long tickets, which pull the mean up. The median reflects the typical ticket.",[25,2423,2425],{"id":2424},"related","Related",[2349,2427,2428,2434,2441,2448],{},[2352,2429,2430,2431,2433],{},"Up one level: ",[19,2432,22],{"href":21}," — finance and operations reports in Excel.",[2352,2435,2436,2440],{},[19,2437,2439],{"href":2438},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report\u002F","Build an On-Time Delivery Report"," — another promised-versus-actual report.",[2352,2442,2443,2447],{},[19,2444,2446],{"href":2445},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-working-hours-between-two-timestamps\u002F","Calculate Working Hours Between Two Timestamps"," — the business-hours calculation in more depth.",[2352,2449,2450,2452],{},[19,2451,2215],{"href":2214}," — where attainment figures end up.",[2454,2455,2456],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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":35,"searchDepth":99,"depth":99,"links":2458},[2459,2460,2461,2462,2463,2464,2465,2466,2467,2468],{"id":27,"depth":99,"text":28},{"id":69,"depth":99,"text":70},{"id":385,"depth":99,"text":386},{"id":973,"depth":99,"text":974},{"id":1348,"depth":99,"text":1349},{"id":1755,"depth":99,"text":1756},{"id":2346,"depth":99,"text":2347},{"id":2385,"depth":99,"text":2386},{"id":2396,"depth":99,"text":2397},{"id":2424,"depth":99,"text":2425},"2026-10-02","Measure a helpdesk against its service levels from a ticket export with pandas: SLA targets per priority, elapsed time in business hours that skips nights, weekends and holidays, first-response and resolution breaches, open tickets close to breaching, attainment by priority, team and week, and an Excel report with the at-risk list on top.","md",[2473,2475,2477,2479],{"q":2402,"a":2474},"Count the business days between the two dates with numpy.busday_count using a calendar that excludes weekends and holidays, multiply by the hours per day, subtract the hours of the first day before the start, and add the hours of the last day up to the end.",{"q":2408,"a":2476},"Join the target hours per priority onto each ticket, compute elapsed business hours, mark each resolved ticket as met if the elapsed time is within target, and take the mean of that flag by priority, team or week.",{"q":2414,"a":2478},"Exclude them from resolution attainment, measure their elapsed time to the report time, and list those that have used most of their allowance as at risk.",{"q":2420,"a":2480},"Ticket durations are skewed by a few very long tickets, which pull the mean up. The median reflects the typical ticket.","build a support ticket sla report",null,{"breadcrumb":2484},[2485,2487,2490],{"name":2486,"item":515},"Home",{"name":2488,"item":2489},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2491,"item":21},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report",{"title":2494,"description":2495},"Build a Support Ticket SLA Report in Excel with Python","An SLA report from a helpdesk export: business-hours elapsed time, response and resolution targets by priority, breach flags, at-risk open tickets, attainment by priority and week, and a formatted Excel workbook.","build-a-support-ticket-sla-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report\u002Findex",[2499,2500],"report-layouts","dates-and-times","how-to","V6kXwSKPu2vfsSsF5CXsVXes1grO3glcdRYkdUqjNwQ",[2504,2508],{"title":2505,"path":2506,"stem":2507,"children":-1},"Build a Supplier Scorecard","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-supplier-scorecard","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-supplier-scorecard\u002Findex",{"title":2509,"path":2510,"stem":2511,"children":-1},"Build a Training Compliance Matrix","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-training-compliance-matrix","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-training-compliance-matrix\u002Findex",[2513,2515],{"path":2514,"title":2491},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2516,"title":2488},"\u002Fautomating-reporting-workflows",[],[2519,2523,2526,2529,2532,2535],{"path":2520,"title":2521,"kind":2522},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2524,"title":2525,"kind":2501},"\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":2527,"title":2528,"kind":2501},"\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":2530,"title":2531,"kind":2501},"\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":2533,"title":2534,"kind":2501},"\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":2536,"title":2537,"kind":2501},"\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",1790972127354]