[{"data":1,"prerenderedAt":2609},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run":2574,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run":2583,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run":2588,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run":2589},{"id":4,"title":5,"body":6,"dateModified":2540,"datePublished":2540,"description":2541,"extension":2542,"faq":2543,"keyword":2552,"kind":2553,"meta":2554,"navigation":100,"path":2563,"seo":2564,"slug":2567,"stem":2568,"topics":2569,"type":2572,"__hash__":2573},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run\u002Findex.md","Build a Credit Control Dunning Letter Run",{"type":7,"value":8,"toc":2529},"minimark",[9,28,33,62,65,69,72,285,288,405,409,800,803,807,818,1235,1238,1383,1559,1563,2410,2418,2422,2457,2461,2464,2468,2474,2480,2486,2492,2496,2525],[10,11,12,13,17,18,21,22,27],"p",{},"Chasing overdue invoices works best when it is regular and consistent: a polite reminder a week after the due date, a firmer letter a fortnight later, a final notice, and only then a referral to collections or a stop on the account. That sequence — ",[14,15,16],"em",{},"dunning"," — is easy to describe and fiddly to run by hand from an aged debt listing. Each customer's level depends on their most overdue invoice ",[14,19,20],{},"and"," on which letter they last received; disputed invoices must not be chased; customers on an agreed payment plan should not get a final notice; a 12.50 balance is not worth a letter. This page builds the weekly run with pandas from an open receivables export and a letter history: it decides each customer's level, applies the exclusions and a minimum gap between letters, produces a workbook for credit control to review, generates a letter per customer listing the overdue invoices, and records the run so the next one escalates correctly. 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\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56,59],"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\n",[10,63,64],{},"Three inputs: the open receivables listing (one row per unpaid invoice with due date, outstanding amount and a status column where disputes are marked), the customer list with contact email and a payment-plan flag, and the history of letters already sent. The run date is Friday 2 October 2026.",[29,66,68],{"id":67},"step-1-the-policy-as-data","Step 1 — The policy, as data",[10,70,71],{},"Write the dunning policy down as a table: the days overdue at which each level applies, what it is called, and the rules around it:",[34,73,77],{"className":74,"code":75,"language":76,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nRUN_DATE = pd.Timestamp(\"2026-10-02\")\nLEVELS = pd.DataFrame({\"level\": [1, 2, 3, 4], \"min_days\": [7, 21, 35, 50],\n                       \"name\": [\"Friendly reminder\", \"Second reminder\", \"Final notice\", \"Referral to collections\"]})\nMIN_GAP_DAYS = 14          # at least two weeks between letters\nMIN_BALANCE = 50.00        # do not chase smaller overdue balances\nMAX_LEVEL_ON_PLAN = 1      # customers on a payment plan only get friendly reminders\nprint(LEVELS.to_string(index=False))\n","python",[41,78,79,95,102,121,186,215,230,244,258],{"__ignoreMap":39},[44,80,81,85,89,92],{"class":46,"line":47},[44,82,84],{"class":83},"s-kum","import",[44,86,88],{"class":87},"skGVy"," pandas ",[44,90,91],{"class":83},"as",[44,93,94],{"class":87}," pd\n",[44,96,98],{"class":46,"line":97},2,[44,99,101],{"emptyLinePlaceholder":100},true,"\n",[44,103,105,109,112,115,118],{"class":46,"line":104},3,[44,106,108],{"class":107},"sP0c6","RUN_DATE",[44,110,111],{"class":83}," =",[44,113,114],{"class":87}," pd.Timestamp(",[44,116,117],{"class":54},"\"2026-10-02\"",[44,119,120],{"class":87},")\n",[44,122,124,127,129,132,135,138,141,144,147,149,152,154,157,160,163,165,168,170,173,175,178,180,183],{"class":46,"line":123},4,[44,125,126],{"class":107},"LEVELS",[44,128,111],{"class":83},[44,130,131],{"class":87}," pd.DataFrame({",[44,133,134],{"class":54},"\"level\"",[44,136,137],{"class":87},": [",[44,139,140],{"class":107},"1",[44,142,143],{"class":87},", ",[44,145,146],{"class":107},"2",[44,148,143],{"class":87},[44,150,151],{"class":107},"3",[44,153,143],{"class":87},[44,155,156],{"class":107},"4",[44,158,159],{"class":87},"], ",[44,161,162],{"class":54},"\"min_days\"",[44,164,137],{"class":87},[44,166,167],{"class":107},"7",[44,169,143],{"class":87},[44,171,172],{"class":107},"21",[44,174,143],{"class":87},[44,176,177],{"class":107},"35",[44,179,143],{"class":87},[44,181,182],{"class":107},"50",[44,184,185],{"class":87},"],\n",[44,187,189,192,194,197,199,202,204,207,209,212],{"class":46,"line":188},5,[44,190,191],{"class":54},"                       \"name\"",[44,193,137],{"class":87},[44,195,196],{"class":54},"\"Friendly reminder\"",[44,198,143],{"class":87},[44,200,201],{"class":54},"\"Second reminder\"",[44,203,143],{"class":87},[44,205,206],{"class":54},"\"Final notice\"",[44,208,143],{"class":87},[44,210,211],{"class":54},"\"Referral to collections\"",[44,213,214],{"class":87},"]})\n",[44,216,218,221,223,226],{"class":46,"line":217},6,[44,219,220],{"class":107},"MIN_GAP_DAYS",[44,222,111],{"class":83},[44,224,225],{"class":107}," 14",[44,227,229],{"class":228},"s-wDw","          # at least two weeks between letters\n",[44,231,233,236,238,241],{"class":46,"line":232},7,[44,234,235],{"class":107},"MIN_BALANCE",[44,237,111],{"class":83},[44,239,240],{"class":107}," 50.00",[44,242,243],{"class":228},"        # do not chase smaller overdue balances\n",[44,245,247,250,252,255],{"class":46,"line":246},8,[44,248,249],{"class":107},"MAX_LEVEL_ON_PLAN",[44,251,111],{"class":83},[44,253,254],{"class":107}," 1",[44,256,257],{"class":228},"      # customers on a payment plan only get friendly reminders\n",[44,259,261,264,267,269,272,276,279,282],{"class":46,"line":260},9,[44,262,263],{"class":107},"print",[44,265,266],{"class":87},"(",[44,268,126],{"class":107},[44,270,271],{"class":87},".to_string(",[44,273,275],{"class":274},"sa561","index",[44,277,278],{"class":83},"=",[44,280,281],{"class":107},"False",[44,283,284],{"class":87},"))\n",[10,286,287],{},"Keeping the thresholds in one table makes the policy visible to the people who own it — credit control, not the developer — and lets it change without hunting through code. Level 4 is not a letter to the customer but an internal referral; it appears in the run so someone acts on it.",[289,290,298,299,298,303,298,307,298,314,298,321,298,331,298,338,298,343,298,347,298,351,298,357,298,363,298,367,298,369,298,371,298,376,298,381,298,384,298,386,298,388,298,393,298,398,298,401,298,403],"svg",{"viewBox":291,"role":292,"ariaLabelledBy":293,"xmlns":296,"style":297},"4 0 752 211","img",[294,295],"dun-levels-t","dun-levels-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  ",[300,301,302],"title",{"id":294},"The dunning sequence by days overdue",[304,305,306],"desc",{"id":295},"A friendly reminder at 7 days overdue, a second reminder at 21 days, a final notice at 35 days and a referral to collections at 50 days, with at least 14 days between letters to the same customer.",[308,309],"rect",{"x":156,"y":310,"width":311,"height":312,"fill":313},"0","752","211","#ffffff",[46,315],{"x1":316,"y1":317,"x2":318,"y2":317,"stroke":319,"style":320},"94.0","105.5","666.0","var(--muted,#5b6780)","stroke-width:2.5px",[308,322],{"x":323,"y":324,"width":325,"height":326,"rx":327,"fill":328,"stroke":329,"style":330},"20.0","16.0","172.0","57.5","12","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[332,333,337],"text",{"x":334,"y":335,"style":336},"106.0","40.5","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","7 days",[332,339,342],{"x":334,"y":340,"style":341},"59.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","friendly reminder",[46,344],{"x1":334,"y1":345,"x2":334,"y2":317,"stroke":319,"style":346},"73.5","stroke-width:1.5px",[348,349],"circle",{"cx":334,"cy":317,"r":350,"fill":329,"stroke":313,"style":330},"7.0",[308,352],{"x":353,"y":354,"width":325,"height":326,"rx":327,"fill":355,"stroke":356,"style":330},"202.7","137.5","#f0f4ff","var(--brand,#5b5cf0)",[332,358,362],{"x":359,"y":360,"style":361},"288.7","162.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","21 days",[332,364,366],{"x":359,"y":365,"style":341},"180.5","second reminder",[46,368],{"x1":359,"y1":317,"x2":359,"y2":354,"stroke":319,"style":346},[348,370],{"cx":359,"cy":317,"r":350,"fill":356,"stroke":313,"style":330},[308,372],{"x":373,"y":324,"width":325,"height":326,"rx":327,"fill":374,"stroke":375,"style":330},"385.3","#fdefd8","var(--gold,#b4740a)",[332,377,380],{"x":378,"y":335,"style":379},"471.3","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","35 days",[332,382,383],{"x":378,"y":340,"style":341},"final notice",[46,385],{"x1":378,"y1":345,"x2":378,"y2":317,"stroke":319,"style":346},[348,387],{"cx":378,"cy":317,"r":350,"fill":375,"stroke":313,"style":330},[308,389],{"x":390,"y":354,"width":325,"height":326,"rx":327,"fill":391,"stroke":392,"style":330},"568.0","#e7ebef","var(--line,#cdd5e6)",[332,394,397],{"x":395,"y":360,"style":396},"654.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","50 days",[332,399,400],{"x":395,"y":365,"style":341},"refer to collections",[46,402],{"x1":395,"y1":317,"x2":395,"y2":354,"stroke":319,"style":346},[348,404],{"cx":395,"cy":317,"r":350,"fill":319,"stroke":313,"style":330},[29,406,408],{"id":407},"step-2-overdue-position-per-customer","Step 2 — Overdue position per customer",[34,410,412],{"className":74,"code":411,"language":76,"meta":39,"style":39},"items = pd.read_excel(\"ar_open_items.xlsx\")\ncustomers = pd.read_excel(\"customers.xlsx\")\nhistory = pd.read_excel(\"dunning_history.xlsx\")\n\nitems[\"Days overdue\"] = (RUN_DATE - items[\"Due date\"]).dt.days\nchaseable = items[(items[\"Days overdue\"] > 0) & (items[\"Status\"].fillna(\"\") != \"disputed\")]\nposition = (chaseable.groupby([\"Customer\", \"Name\"], as_index=False)\n            .agg(Overdue=(\"Outstanding\", \"sum\"), Invoices=(\"Invoice\", \"count\"), Oldest_days=(\"Days overdue\", \"max\")))\nlast = history.sort_values(\"Sent\").groupby(\"Customer\").tail(1).set_index(\"Customer\")\nposition = (position.merge(customers, on=\"Customer\", how=\"left\")\n            .join(last.rename(columns={\"Sent\": \"Last sent\", \"Level\": \"Last level\"}), on=\"Customer\"))\nposition[\"Last level\"] = position[\"Last level\"].fillna(0).astype(int)\nprint(position[[\"Customer\", \"Name\", \"Overdue\", \"Oldest_days\", \"Last level\", \"Last sent\"]].to_string(index=False))\n",[41,413,414,429,443,457,461,491,540,569,626,656,685,728,757],{"__ignoreMap":39},[44,415,416,419,421,424,427],{"class":46,"line":47},[44,417,418],{"class":87},"items ",[44,420,278],{"class":83},[44,422,423],{"class":87}," pd.read_excel(",[44,425,426],{"class":54},"\"ar_open_items.xlsx\"",[44,428,120],{"class":87},[44,430,431,434,436,438,441],{"class":46,"line":97},[44,432,433],{"class":87},"customers ",[44,435,278],{"class":83},[44,437,423],{"class":87},[44,439,440],{"class":54},"\"customers.xlsx\"",[44,442,120],{"class":87},[44,444,445,448,450,452,455],{"class":46,"line":104},[44,446,447],{"class":87},"history ",[44,449,278],{"class":83},[44,451,423],{"class":87},[44,453,454],{"class":54},"\"dunning_history.xlsx\"",[44,456,120],{"class":87},[44,458,459],{"class":46,"line":123},[44,460,101],{"emptyLinePlaceholder":100},[44,462,463,466,469,472,474,477,479,482,485,488],{"class":46,"line":188},[44,464,465],{"class":87},"items[",[44,467,468],{"class":54},"\"Days overdue\"",[44,470,471],{"class":87},"] ",[44,473,278],{"class":83},[44,475,476],{"class":87}," (",[44,478,108],{"class":107},[44,480,481],{"class":83}," -",[44,483,484],{"class":87}," items[",[44,486,487],{"class":54},"\"Due date\"",[44,489,490],{"class":87},"]).dt.days\n",[44,492,493,496,498,501,503,505,508,511,514,517,520,523,526,529,531,534,537],{"class":46,"line":217},[44,494,495],{"class":87},"chaseable ",[44,497,278],{"class":83},[44,499,500],{"class":87}," items[(items[",[44,502,468],{"class":54},[44,504,471],{"class":87},[44,506,507],{"class":83},">",[44,509,510],{"class":107}," 0",[44,512,513],{"class":87},") ",[44,515,516],{"class":83},"&",[44,518,519],{"class":87}," (items[",[44,521,522],{"class":54},"\"Status\"",[44,524,525],{"class":87},"].fillna(",[44,527,528],{"class":54},"\"\"",[44,530,513],{"class":87},[44,532,533],{"class":83},"!=",[44,535,536],{"class":54}," \"disputed\"",[44,538,539],{"class":87},")]\n",[44,541,542,545,547,550,553,555,558,560,563,565,567],{"class":46,"line":232},[44,543,544],{"class":87},"position ",[44,546,278],{"class":83},[44,548,549],{"class":87}," (chaseable.groupby([",[44,551,552],{"class":54},"\"Customer\"",[44,554,143],{"class":87},[44,556,557],{"class":54},"\"Name\"",[44,559,159],{"class":87},[44,561,562],{"class":274},"as_index",[44,564,278],{"class":83},[44,566,281],{"class":107},[44,568,120],{"class":87},[44,570,571,574,577,579,581,584,586,589,592,595,597,599,602,604,607,609,612,614,616,618,620,623],{"class":46,"line":246},[44,572,573],{"class":87},"            .agg(",[44,575,576],{"class":274},"Overdue",[44,578,278],{"class":83},[44,580,266],{"class":87},[44,582,583],{"class":54},"\"Outstanding\"",[44,585,143],{"class":87},[44,587,588],{"class":54},"\"sum\"",[44,590,591],{"class":87},"), ",[44,593,594],{"class":274},"Invoices",[44,596,278],{"class":83},[44,598,266],{"class":87},[44,600,601],{"class":54},"\"Invoice\"",[44,603,143],{"class":87},[44,605,606],{"class":54},"\"count\"",[44,608,591],{"class":87},[44,610,611],{"class":274},"Oldest_days",[44,613,278],{"class":83},[44,615,266],{"class":87},[44,617,468],{"class":54},[44,619,143],{"class":87},[44,621,622],{"class":54},"\"max\"",[44,624,625],{"class":87},")))\n",[44,627,628,631,633,636,639,642,644,647,649,652,654],{"class":46,"line":260},[44,629,630],{"class":87},"last ",[44,632,278],{"class":83},[44,634,635],{"class":87}," history.sort_values(",[44,637,638],{"class":54},"\"Sent\"",[44,640,641],{"class":87},").groupby(",[44,643,552],{"class":54},[44,645,646],{"class":87},").tail(",[44,648,140],{"class":107},[44,650,651],{"class":87},").set_index(",[44,653,552],{"class":54},[44,655,120],{"class":87},[44,657,659,661,663,666,669,671,673,675,678,680,683],{"class":46,"line":658},10,[44,660,544],{"class":87},[44,662,278],{"class":83},[44,664,665],{"class":87}," (position.merge(customers, ",[44,667,668],{"class":274},"on",[44,670,278],{"class":83},[44,672,552],{"class":54},[44,674,143],{"class":87},[44,676,677],{"class":274},"how",[44,679,278],{"class":83},[44,681,682],{"class":54},"\"left\"",[44,684,120],{"class":87},[44,686,688,691,694,696,699,701,704,707,709,712,714,717,720,722,724,726],{"class":46,"line":687},11,[44,689,690],{"class":87},"            .join(last.rename(",[44,692,693],{"class":274},"columns",[44,695,278],{"class":83},[44,697,698],{"class":87},"{",[44,700,638],{"class":54},[44,702,703],{"class":87},": ",[44,705,706],{"class":54},"\"Last sent\"",[44,708,143],{"class":87},[44,710,711],{"class":54},"\"Level\"",[44,713,703],{"class":87},[44,715,716],{"class":54},"\"Last level\"",[44,718,719],{"class":87},"}), ",[44,721,668],{"class":274},[44,723,278],{"class":83},[44,725,552],{"class":54},[44,727,284],{"class":87},[44,729,731,734,736,738,740,743,745,747,749,752,755],{"class":46,"line":730},12,[44,732,733],{"class":87},"position[",[44,735,716],{"class":54},[44,737,471],{"class":87},[44,739,278],{"class":83},[44,741,742],{"class":87}," position[",[44,744,716],{"class":54},[44,746,525],{"class":87},[44,748,310],{"class":107},[44,750,751],{"class":87},").astype(",[44,753,754],{"class":107},"int",[44,756,120],{"class":87},[44,758,760,762,765,767,769,771,773,776,778,781,783,785,787,789,792,794,796,798],{"class":46,"line":759},13,[44,761,263],{"class":107},[44,763,764],{"class":87},"(position[[",[44,766,552],{"class":54},[44,768,143],{"class":87},[44,770,557],{"class":54},[44,772,143],{"class":87},[44,774,775],{"class":54},"\"Overdue\"",[44,777,143],{"class":87},[44,779,780],{"class":54},"\"Oldest_days\"",[44,782,143],{"class":87},[44,784,716],{"class":54},[44,786,143],{"class":87},[44,788,706],{"class":54},[44,790,791],{"class":87},"]].to_string(",[44,793,275],{"class":274},[44,795,278],{"class":83},[44,797,281],{"class":107},[44,799,284],{"class":87},[10,801,802],{},"Disputed invoices are removed before anything else — chasing an invoice the customer has queried damages the relationship and rarely gets it paid — and invoices not yet due are not overdue at all. Each customer's position is the total overdue, the number of overdue invoices and the age of the oldest, joined to the last letter they received. Zenith's 2,250 disputed invoice is not counted, although their other invoice is.",[29,804,806],{"id":805},"step-3-decide-each-customers-letter","Step 3 — Decide each customer's letter",[10,808,809,810,813,814,817],{},"The level a customer ",[14,811,812],{},"qualifies"," for comes from their oldest overdue invoice. What they actually ",[14,815,816],{},"get"," also depends on history and exclusions: at most one level above their last letter, not within two weeks of it, nothing for tiny balances, and only friendly reminders on a payment plan:",[34,819,821],{"className":74,"code":820,"language":76,"meta":39,"style":39},"def decide(r):\n    qualifies = int(LEVELS.loc[LEVELS[\"min_days\"] \u003C= r[\"Oldest_days\"], \"level\"].max()) if r[\"Oldest_days\"] >= LEVELS[\"min_days\"].min() else 0\n    if qualifies == 0:\n        return 0, \"not yet overdue enough\"\n    if r[\"Overdue\"] \u003C MIN_BALANCE:\n        return 0, f\"overdue balance below {MIN_BALANCE:.0f}\"\n    if pd.notna(r[\"Last sent\"]) and (RUN_DATE - r[\"Last sent\"]).days \u003C MIN_GAP_DAYS:\n        return 0, f\"last letter {(RUN_DATE - r['Last sent']).days} days ago\"\n    level = min(qualifies, r[\"Last level\"] + 1)\n    if r[\"Payment plan\"]:\n        level = min(level, MAX_LEVEL_ON_PLAN)\n        if level \u003C= r[\"Last level\"]:\n            return 0, \"on payment plan — no escalation\"\n    return level, LEVELS.set_index(\"level\").loc[level, \"name\"]\n\nposition[[\"Level\", \"Action\"]] = position.apply(decide, axis=1, result_type=\"expand\")\nprint(position[[\"Name\", \"Overdue\", \"Oldest_days\", \"Last level\", \"Level\", \"Action\"]].to_string(index=False))\n",[41,822,823,835,904,920,932,950,978,1012,1046,1070,1082,1098,1114,1126,1151,1156,1196],{"__ignoreMap":39},[44,824,825,828,832],{"class":46,"line":47},[44,826,827],{"class":83},"def",[44,829,831],{"class":830},"s_Opv"," decide",[44,833,834],{"class":87},"(r):\n",[44,836,837,840,842,845,847,849,852,854,857,859,861,864,867,869,871,873,876,879,881,883,885,888,891,893,895,898,901],{"class":46,"line":97},[44,838,839],{"class":87},"    qualifies ",[44,841,278],{"class":83},[44,843,844],{"class":107}," int",[44,846,266],{"class":87},[44,848,126],{"class":107},[44,850,851],{"class":87},".loc[",[44,853,126],{"class":107},[44,855,856],{"class":87},"[",[44,858,162],{"class":54},[44,860,471],{"class":87},[44,862,863],{"class":83},"\u003C=",[44,865,866],{"class":87}," r[",[44,868,780],{"class":54},[44,870,159],{"class":87},[44,872,134],{"class":54},[44,874,875],{"class":87},"].max()) ",[44,877,878],{"class":83},"if",[44,880,866],{"class":87},[44,882,780],{"class":54},[44,884,471],{"class":87},[44,886,887],{"class":83},">=",[44,889,890],{"class":107}," LEVELS",[44,892,856],{"class":87},[44,894,162],{"class":54},[44,896,897],{"class":87},"].min() ",[44,899,900],{"class":83},"else",[44,902,903],{"class":107}," 0\n",[44,905,906,909,912,915,917],{"class":46,"line":104},[44,907,908],{"class":83},"    if",[44,910,911],{"class":87}," qualifies ",[44,913,914],{"class":83},"==",[44,916,510],{"class":107},[44,918,919],{"class":87},":\n",[44,921,922,925,927,929],{"class":46,"line":123},[44,923,924],{"class":83},"        return",[44,926,510],{"class":107},[44,928,143],{"class":87},[44,930,931],{"class":54},"\"not yet overdue enough\"\n",[44,933,934,936,938,940,942,945,948],{"class":46,"line":188},[44,935,908],{"class":83},[44,937,866],{"class":87},[44,939,775],{"class":54},[44,941,471],{"class":87},[44,943,944],{"class":83},"\u003C",[44,946,947],{"class":107}," MIN_BALANCE",[44,949,919],{"class":87},[44,951,952,954,956,958,961,964,967,969,972,975],{"class":46,"line":217},[44,953,924],{"class":83},[44,955,510],{"class":107},[44,957,143],{"class":87},[44,959,960],{"class":83},"f",[44,962,963],{"class":54},"\"overdue balance below ",[44,965,698],{"class":966},"sSjpA",[44,968,235],{"class":107},[44,970,971],{"class":83},":.0f",[44,973,974],{"class":966},"}",[44,976,977],{"class":54},"\"\n",[44,979,980,982,985,987,990,992,994,996,998,1000,1002,1005,1007,1010],{"class":46,"line":232},[44,981,908],{"class":83},[44,983,984],{"class":87}," pd.notna(r[",[44,986,706],{"class":54},[44,988,989],{"class":87},"]) ",[44,991,20],{"class":83},[44,993,476],{"class":87},[44,995,108],{"class":107},[44,997,481],{"class":83},[44,999,866],{"class":87},[44,1001,706],{"class":54},[44,1003,1004],{"class":87},"]).days ",[44,1006,944],{"class":83},[44,1008,1009],{"class":107}," MIN_GAP_DAYS",[44,1011,919],{"class":87},[44,1013,1014,1016,1018,1020,1022,1025,1027,1029,1031,1033,1035,1038,1041,1043],{"class":46,"line":246},[44,1015,924],{"class":83},[44,1017,510],{"class":107},[44,1019,143],{"class":87},[44,1021,960],{"class":83},[44,1023,1024],{"class":54},"\"last letter ",[44,1026,698],{"class":966},[44,1028,266],{"class":87},[44,1030,108],{"class":107},[44,1032,481],{"class":83},[44,1034,866],{"class":87},[44,1036,1037],{"class":54},"'Last sent'",[44,1039,1040],{"class":87},"]).days",[44,1042,974],{"class":966},[44,1044,1045],{"class":54}," days ago\"\n",[44,1047,1048,1051,1053,1056,1059,1061,1063,1066,1068],{"class":46,"line":260},[44,1049,1050],{"class":87},"    level ",[44,1052,278],{"class":83},[44,1054,1055],{"class":107}," min",[44,1057,1058],{"class":87},"(qualifies, r[",[44,1060,716],{"class":54},[44,1062,471],{"class":87},[44,1064,1065],{"class":83},"+",[44,1067,254],{"class":107},[44,1069,120],{"class":87},[44,1071,1072,1074,1076,1079],{"class":46,"line":658},[44,1073,908],{"class":83},[44,1075,866],{"class":87},[44,1077,1078],{"class":54},"\"Payment plan\"",[44,1080,1081],{"class":87},"]:\n",[44,1083,1084,1087,1089,1091,1094,1096],{"class":46,"line":687},[44,1085,1086],{"class":87},"        level ",[44,1088,278],{"class":83},[44,1090,1055],{"class":107},[44,1092,1093],{"class":87},"(level, ",[44,1095,249],{"class":107},[44,1097,120],{"class":87},[44,1099,1100,1103,1106,1108,1110,1112],{"class":46,"line":730},[44,1101,1102],{"class":83},"        if",[44,1104,1105],{"class":87}," level ",[44,1107,863],{"class":83},[44,1109,866],{"class":87},[44,1111,716],{"class":54},[44,1113,1081],{"class":87},[44,1115,1116,1119,1121,1123],{"class":46,"line":759},[44,1117,1118],{"class":83},"            return",[44,1120,510],{"class":107},[44,1122,143],{"class":87},[44,1124,1125],{"class":54},"\"on payment plan — no escalation\"\n",[44,1127,1129,1132,1135,1137,1140,1142,1145,1148],{"class":46,"line":1128},14,[44,1130,1131],{"class":83},"    return",[44,1133,1134],{"class":87}," level, ",[44,1136,126],{"class":107},[44,1138,1139],{"class":87},".set_index(",[44,1141,134],{"class":54},[44,1143,1144],{"class":87},").loc[level, ",[44,1146,1147],{"class":54},"\"name\"",[44,1149,1150],{"class":87},"]\n",[44,1152,1154],{"class":46,"line":1153},15,[44,1155,101],{"emptyLinePlaceholder":100},[44,1157,1159,1162,1164,1166,1169,1172,1174,1177,1180,1182,1184,1186,1189,1191,1194],{"class":46,"line":1158},16,[44,1160,1161],{"class":87},"position[[",[44,1163,711],{"class":54},[44,1165,143],{"class":87},[44,1167,1168],{"class":54},"\"Action\"",[44,1170,1171],{"class":87},"]] ",[44,1173,278],{"class":83},[44,1175,1176],{"class":87}," position.apply(decide, ",[44,1178,1179],{"class":274},"axis",[44,1181,278],{"class":83},[44,1183,140],{"class":107},[44,1185,143],{"class":87},[44,1187,1188],{"class":274},"result_type",[44,1190,278],{"class":83},[44,1192,1193],{"class":54},"\"expand\"",[44,1195,120],{"class":87},[44,1197,1199,1201,1203,1205,1207,1209,1211,1213,1215,1217,1219,1221,1223,1225,1227,1229,1231,1233],{"class":46,"line":1198},17,[44,1200,263],{"class":107},[44,1202,764],{"class":87},[44,1204,557],{"class":54},[44,1206,143],{"class":87},[44,1208,775],{"class":54},[44,1210,143],{"class":87},[44,1212,780],{"class":54},[44,1214,143],{"class":87},[44,1216,716],{"class":54},[44,1218,143],{"class":87},[44,1220,711],{"class":54},[44,1222,143],{"class":87},[44,1224,1168],{"class":54},[44,1226,791],{"class":87},[44,1228,275],{"class":274},[44,1230,278],{"class":83},[44,1232,281],{"class":107},[44,1234,284],{"class":87},[10,1236,1237],{},"Three letters go out this week. Oakridge's oldest invoice is 49 days overdue, which qualifies for a final notice, but their last letter was a friendly reminder, so they get the second reminder; Café Señor gets a first reminder; Zenith, at 63 days with a second reminder already sent, gets the final notice. Brightside is 84 days overdue but on a payment plan, Granite had a letter a week ago, Harbour Print's 12.50 is below the threshold and Northwind is only three days late. Never jumping more than one level above the last letter matters most for customers who were never chased: an account that slipped through for 80 days still gets a friendly reminder first, not a referral out of nowhere — the sequence is what makes the final notice credible. The two-week gap stops a customer who received a second reminder last Friday from getting a final notice today. Each decision carries its reason, including the ones that result in no letter, so a reviewer can see why a large overdue balance is not being chased this week.",[289,1239,298,1244,298,1247,298,1250,298,1253,298,1258,298,1269,298,1274,298,1278,298,1282,298,1285,298,1290,298,1298,298,1303,298,1308,298,1315,298,1320,298,1325,298,1327,298,1330,298,1334,298,1337,298,1340,298,1347,298,1351,298,1354,298,1356,298,1359,298,1363,298,1366,298,1369,298,1376,298,1380],{"viewBox":1240,"role":292,"ariaLabelledBy":1241,"xmlns":296,"style":297},"4 0 752 272",[1242,1243],"dun-decide-t","dun-decide-d",[300,1245,1246],{"id":1242},"Which letter a customer gets this week",[304,1248,1249],{"id":1243},"Customers whose overdue balance is too small, or who received a letter in the last 14 days, get none. Otherwise the level is the one their oldest overdue invoice qualifies for, but at most one above their last letter, and only a friendly reminder if they are on a payment plan.",[308,1251],{"x":156,"y":310,"width":311,"height":1252,"fill":313},"272",[308,1254],{"x":1255,"y":324,"width":1256,"height":1257,"rx":327,"fill":355,"stroke":356,"style":330},"237.8","284.3","75.5",[1259,1260,1262],"g",{"transform":1261},"translate(368.0,29.0) scale(1.000)",[1263,1264],"path",{"d":1265,"fill":1266,"stroke":1267,"style":1268},"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21c0-4 3.6-7 8-7s8 3 8 7","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[332,1270,1273],{"x":1271,"y":1257,"style":1272},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Customer with overdue invoices",[46,1275],{"x1":1271,"y1":1276,"x2":1271,"y2":1277,"stroke":356,"style":330},"91.5","111.5",[46,1279],{"x1":1280,"y1":1277,"x2":1281,"y2":1277,"stroke":356,"style":330},"134.7","625.3",[46,1283],{"x1":1280,"y1":1277,"x2":1280,"y2":1284,"stroke":356,"style":330},"157.5",[1286,1287],"polygon",{"points":1288,"fill":1289},"134.7,161.5 129.7,152.5 139.7,152.5","#5b5cf0",[308,1291],{"x":1292,"y":1293,"width":1294,"height":1295,"rx":1296,"fill":313,"stroke":392,"style":1297},"48.1","121.5","173.1","21.0","10","stroke-width:1.2px",[332,1299,1302],{"x":1280,"y":1300,"style":1301},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","tiny balance \u002F recent letter",[308,1304],{"x":323,"y":1305,"width":1306,"height":1307,"rx":327,"fill":391,"stroke":392,"style":330},"163.5","229.3","93.0",[1259,1309,1311],{"transform":1310},"translate(122.7,176.5) scale(1.000)",[1263,1312],{"d":1313,"fill":1266,"stroke":1314,"style":1268},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--text,#172033)",[332,1316,1319],{"x":1280,"y":1317,"style":1318},"223.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","no letter",[332,1321,1324],{"x":1280,"y":1322,"style":1323},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","reason recorded",[46,1326],{"x1":1271,"y1":1277,"x2":1271,"y2":1284,"stroke":356,"style":330},[1286,1328],{"points":1329,"fill":1289},"380.0,161.5 375.0,152.5 385.0,152.5",[308,1331],{"x":1332,"y":1293,"width":1333,"height":1295,"rx":1296,"fill":313,"stroke":392,"style":1297},"330.8","98.3",[332,1335,1336],{"x":1271,"y":1300,"style":1301},"payment plan",[308,1338],{"x":1339,"y":1305,"width":1306,"height":1307,"rx":327,"fill":328,"stroke":329,"style":330},"265.3",[1259,1341,1343],{"transform":1342},"translate(368.0,176.5) scale(1.000)",[1263,1344],{"d":1345,"fill":1266,"stroke":1346,"style":1268},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--teal-ink,#0b6157)",[332,1348,1350],{"x":1271,"y":1317,"style":1349},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","friendly reminder only",[332,1352,1353],{"x":1271,"y":1322,"style":1323},"no escalation",[46,1355],{"x1":1281,"y1":1277,"x2":1281,"y2":1284,"stroke":356,"style":330},[1286,1357],{"points":1358,"fill":1289},"625.3,161.5 620.3,152.5 630.3,152.5",[308,1360],{"x":1361,"y":1293,"width":1362,"height":1295,"rx":1296,"fill":313,"stroke":392,"style":1297},"586.9","77.0",[332,1364,1365],{"x":1281,"y":1300,"style":1301},"otherwise",[308,1367],{"x":1368,"y":1305,"width":1306,"height":1307,"rx":327,"fill":374,"stroke":375,"style":330},"510.7",[1259,1370,1372],{"transform":1371},"translate(613.3,176.5) scale(1.000)",[1263,1373],{"d":1374,"fill":1266,"stroke":1375,"style":1268},"M3 6h18v12H3z M3 7l9 6 9-6","var(--gold-ink,#7a4e06)",[332,1377,1379],{"x":1281,"y":1317,"style":1378},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","min(qualifies, last + 1)",[332,1381,1382],{"x":1281,"y":1322,"style":1323},"one step at a time",[289,1384,298,1389,298,1392,298,1395,298,1398,298,1403,298,1407,298,1413,298,1416,298,1420,298,1423,298,1427,298,1431,298,1437,298,1439,298,1442,298,1444,298,1447,298,1449,298,1451,298,1454,298,1458,298,1460,298,1463,298,1465,298,1467,298,1469,298,1471,298,1474,298,1478,298,1480,298,1484,298,1486,298,1489,298,1491,298,1493,298,1496,298,1500,298,1502,298,1506,298,1508,298,1510,298,1512,298,1515,298,1518,298,1522,298,1524,298,1528,298,1530,298,1532,298,1534,298,1537,298,1540,298,1544,298,1546,298,1550,298,1552,298,1554,298,1556],{"viewBox":1385,"role":292,"ariaLabelledBy":1386,"xmlns":296,"style":297},"4 0 752 276",[1387,1388],"dun-run-t","dun-run-d",[300,1390,1391],{"id":1387},"This week's decisions",[304,1393,1394],{"id":1388},"Oakridge Farms qualifies for a final notice but gets a second reminder. Café Señor gets a friendly reminder and Zenith Logistics a final notice. Brightside is on a payment plan, Granite Tools had a letter seven days ago, Harbour Print's balance is too small and Northwind is only three days overdue, so they get no letter.",[308,1396],{"x":156,"y":310,"width":311,"height":1397,"fill":313},"276",[308,1399],{"x":323,"y":324,"width":1400,"height":1401,"rx":310,"fill":313,"stroke":392,"style":1402},"130.5","40.0","stroke-width:1px",[308,1404],{"x":1405,"y":324,"width":1406,"height":1401,"rx":310,"fill":355,"stroke":356,"style":1297},"150.5","196.5",[332,1408,1412],{"x":1409,"y":1410,"style":1411},"248.7","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","days overdue",[308,1414],{"x":1415,"y":324,"width":1406,"height":1401,"rx":310,"fill":355,"stroke":356,"style":1297},"347.0",[332,1417,1419],{"x":1418,"y":1410,"style":1411},"445.2","last letter",[308,1421],{"x":1422,"y":324,"width":1406,"height":1401,"rx":310,"fill":355,"stroke":356,"style":1297},"543.5",[332,1424,1426],{"x":1425,"y":1410,"style":1411},"641.7","this week",[308,1428],{"x":323,"y":1429,"width":1400,"height":1430,"rx":310,"fill":391,"stroke":392,"style":1402},"56.0","34.0",[332,1432,1436],{"x":1433,"y":1434,"style":1435},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Oakridge Farms",[308,1438],{"x":1405,"y":1429,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1440,1441],{"x":1409,"y":1362,"style":1323},"49",[308,1443],{"x":1415,"y":1429,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1445,1446],{"x":1418,"y":1362,"style":1323},"level 1",[308,1448],{"x":1422,"y":1429,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1450,366],{"x":1425,"y":1362,"style":1323},[308,1452],{"x":323,"y":1453,"width":1400,"height":1430,"rx":310,"fill":391,"stroke":392,"style":1402},"90.0",[332,1455,1457],{"x":1433,"y":1456,"style":1435},"112.0","Café Señor",[308,1459],{"x":1405,"y":1453,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1461,327],{"x":1409,"y":1462,"style":1323},"111.0",[308,1464],{"x":1415,"y":1453,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1466,1266],{"x":1418,"y":1462,"style":1323},[308,1468],{"x":1422,"y":1453,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1470,342],{"x":1425,"y":1462,"style":1323},[308,1472],{"x":323,"y":1473,"width":1400,"height":1430,"rx":310,"fill":391,"stroke":392,"style":1402},"124.0",[332,1475,1477],{"x":1433,"y":1476,"style":1435},"146.0","Zenith Logistics",[308,1479],{"x":1405,"y":1473,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1481,1483],{"x":1409,"y":1482,"style":1323},"145.0","63",[308,1485],{"x":1415,"y":1473,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1487,1488],{"x":1418,"y":1482,"style":1323},"level 2",[308,1490],{"x":1422,"y":1473,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1492,383],{"x":1425,"y":1482,"style":1323},[308,1494],{"x":323,"y":1495,"width":1400,"height":1430,"rx":310,"fill":391,"stroke":392,"style":1402},"158.0",[332,1497,1499],{"x":1433,"y":1498,"style":1435},"180.0","Brightside & Co",[308,1501],{"x":1405,"y":1495,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1503,1505],{"x":1409,"y":1504,"style":1323},"179.0","84",[308,1507],{"x":1415,"y":1495,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1509,1488],{"x":1418,"y":1504,"style":1323},[308,1511],{"x":1422,"y":1495,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1513,1514],{"x":1425,"y":1504,"style":1323},"none — payment plan",[308,1516],{"x":323,"y":1517,"width":1400,"height":1430,"rx":310,"fill":391,"stroke":392,"style":1402},"192.0",[332,1519,1521],{"x":1433,"y":1520,"style":1435},"214.0","Granite Tools",[308,1523],{"x":1405,"y":1517,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1525,1527],{"x":1409,"y":1526,"style":1323},"213.0","77",[308,1529],{"x":1415,"y":1517,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1531,1488],{"x":1418,"y":1526,"style":1323},[308,1533],{"x":1422,"y":1517,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1535,1536],{"x":1425,"y":1526,"style":1323},"none — 7 days ago",[308,1538],{"x":323,"y":1539,"width":1400,"height":1430,"rx":310,"fill":391,"stroke":392,"style":1402},"226.0",[332,1541,1543],{"x":1433,"y":1542,"style":1435},"248.0","Harbour Print",[308,1545],{"x":1405,"y":1539,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1547,1549],{"x":1409,"y":1548,"style":1323},"247.0","42",[308,1551],{"x":1415,"y":1539,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1553,1266],{"x":1418,"y":1548,"style":1323},[308,1555],{"x":1422,"y":1539,"width":1406,"height":1430,"rx":310,"fill":313,"stroke":392,"style":1402},[332,1557,1558],{"x":1425,"y":1548,"style":1323},"none — 12.50",[29,1560,1562],{"id":1561},"step-4-review-workbook-letters-and-the-log","Step 4 — Review workbook, letters and the log",[34,1564,1566],{"className":74,"code":1565,"language":76,"meta":39,"style":39},"from pathlib import Path\nfrom string import Template\n\nrun = position[position[\"Level\"] > 0].copy()\nLETTER = Template(\"\"\"Dear $name,\n\n$opening\n\nInvoice      Due date      Days overdue      Amount\n$lines\nTotal overdue: $total\n\n$closing\n\nCredit Control\n\"\"\")\nTEXT = {1: (\"Our records show the following invoices are now overdue. If you have already paid, please ignore this reminder.\",\n            \"We would be grateful for payment at your earliest convenience.\"),\n        2: (\"We wrote to you recently about the invoices below, which remain unpaid.\", \"Please arrange payment within 7 days.\"),\n        3: (\"Despite previous reminders, the invoices below remain unpaid.\",\n            \"Unless payment is received within 7 days we will refer the debt for collection and may suspend your account.\")}\nout = Path(\"letters\"); out.mkdir(exist_ok=True)\nfor r in run.itertuples(index=False):\n    if r.Level == 4:\n        continue                                   # internal referral, no customer letter\n    inv = chaseable[chaseable[\"Customer\"] == r.Customer].sort_values(\"Due date\")\n    lines = \"\\n\".join(f\"{inv_no:\u003C12} {due:%d %b %Y}   {days:>12}   {amount:>10,.2f}\"\n                      for inv_no, due, days, amount in inv[[\"Invoice\", \"Due date\", \"Days overdue\", \"Outstanding\"]].itertuples(index=False))\n    opening, closing = TEXT[r.Level]\n    (out \u002F f\"{RUN_DATE:%Y-%m-%d}_{r.Customer}_L{r.Level}.txt\").write_text(\n        LETTER.substitute(name=r.Name, opening=opening, lines=lines, total=f\"{r.Overdue:,.2f}\", closing=closing))\n\nwith pd.ExcelWriter(f\"dunning_run_{RUN_DATE:%Y-%m-%d}.xlsx\", engine=\"openpyxl\", date_format=\"dd mmm yyyy\", datetime_format=\"dd mmm yyyy\") as xw:\n    run.to_excel(xw, sheet_name=\"Letters\", index=False)\n    position[position[\"Level\"] == 0].to_excel(xw, sheet_name=\"No letter\", index=False)\n    chaseable.to_excel(xw, sheet_name=\"Overdue items\", index=False)\n\nlog = pd.concat([history, run[[\"Customer\"]].assign(Sent=RUN_DATE, Level=run[\"Level\"].values)], ignore_index=True)\nlog.to_excel(\"dunning_history.xlsx\", index=False)\nprint(sorted(p.name for p in out.iterdir()))\nprint((out \u002F f\"{RUN_DATE:%Y-%m-%d}_C003_L3.txt\").read_text())\n",[41,1567,1568,1581,1593,1597,1618,1631,1635,1640,1644,1649,1654,1659,1663,1668,1672,1677,1684,1705,1714,1732,1745,1754,1781,1805,1820,1829,1853,1936,1975,1989,2052,2116,2121,2196,2220,2254,2277,2282,2329,2347,2370],{"__ignoreMap":39},[44,1569,1570,1573,1576,1578],{"class":46,"line":47},[44,1571,1572],{"class":83},"from",[44,1574,1575],{"class":87}," pathlib ",[44,1577,84],{"class":83},[44,1579,1580],{"class":87}," Path\n",[44,1582,1583,1585,1588,1590],{"class":46,"line":97},[44,1584,1572],{"class":83},[44,1586,1587],{"class":87}," string ",[44,1589,84],{"class":83},[44,1591,1592],{"class":87}," Template\n",[44,1594,1595],{"class":46,"line":104},[44,1596,101],{"emptyLinePlaceholder":100},[44,1598,1599,1602,1604,1607,1609,1611,1613,1615],{"class":46,"line":123},[44,1600,1601],{"class":87},"run ",[44,1603,278],{"class":83},[44,1605,1606],{"class":87}," position[position[",[44,1608,711],{"class":54},[44,1610,471],{"class":87},[44,1612,507],{"class":83},[44,1614,510],{"class":107},[44,1616,1617],{"class":87},"].copy()\n",[44,1619,1620,1623,1625,1628],{"class":46,"line":188},[44,1621,1622],{"class":107},"LETTER",[44,1624,111],{"class":83},[44,1626,1627],{"class":87}," Template(",[44,1629,1630],{"class":54},"\"\"\"Dear $name,\n",[44,1632,1633],{"class":46,"line":217},[44,1634,101],{"emptyLinePlaceholder":100},[44,1636,1637],{"class":46,"line":232},[44,1638,1639],{"class":54},"$opening\n",[44,1641,1642],{"class":46,"line":246},[44,1643,101],{"emptyLinePlaceholder":100},[44,1645,1646],{"class":46,"line":260},[44,1647,1648],{"class":54},"Invoice      Due date      Days overdue      Amount\n",[44,1650,1651],{"class":46,"line":658},[44,1652,1653],{"class":54},"$lines\n",[44,1655,1656],{"class":46,"line":687},[44,1657,1658],{"class":54},"Total overdue: $total\n",[44,1660,1661],{"class":46,"line":730},[44,1662,101],{"emptyLinePlaceholder":100},[44,1664,1665],{"class":46,"line":759},[44,1666,1667],{"class":54},"$closing\n",[44,1669,1670],{"class":46,"line":1128},[44,1671,101],{"emptyLinePlaceholder":100},[44,1673,1674],{"class":46,"line":1153},[44,1675,1676],{"class":54},"Credit Control\n",[44,1678,1679,1682],{"class":46,"line":1158},[44,1680,1681],{"class":54},"\"\"\"",[44,1683,120],{"class":87},[44,1685,1686,1689,1691,1694,1696,1699,1702],{"class":46,"line":1198},[44,1687,1688],{"class":107},"TEXT",[44,1690,111],{"class":83},[44,1692,1693],{"class":87}," {",[44,1695,140],{"class":107},[44,1697,1698],{"class":87},": (",[44,1700,1701],{"class":54},"\"Our records show the following invoices are now overdue. If you have already paid, please ignore this reminder.\"",[44,1703,1704],{"class":87},",\n",[44,1706,1708,1711],{"class":46,"line":1707},18,[44,1709,1710],{"class":54},"            \"We would be grateful for payment at your earliest convenience.\"",[44,1712,1713],{"class":87},"),\n",[44,1715,1717,1720,1722,1725,1727,1730],{"class":46,"line":1716},19,[44,1718,1719],{"class":107},"        2",[44,1721,1698],{"class":87},[44,1723,1724],{"class":54},"\"We wrote to you recently about the invoices below, which remain unpaid.\"",[44,1726,143],{"class":87},[44,1728,1729],{"class":54},"\"Please arrange payment within 7 days.\"",[44,1731,1713],{"class":87},[44,1733,1735,1738,1740,1743],{"class":46,"line":1734},20,[44,1736,1737],{"class":107},"        3",[44,1739,1698],{"class":87},[44,1741,1742],{"class":54},"\"Despite previous reminders, the invoices below remain unpaid.\"",[44,1744,1704],{"class":87},[44,1746,1748,1751],{"class":46,"line":1747},21,[44,1749,1750],{"class":54},"            \"Unless payment is received within 7 days we will refer the debt for collection and may suspend your account.\"",[44,1752,1753],{"class":87},")}\n",[44,1755,1757,1760,1762,1765,1768,1771,1774,1776,1779],{"class":46,"line":1756},22,[44,1758,1759],{"class":87},"out ",[44,1761,278],{"class":83},[44,1763,1764],{"class":87}," Path(",[44,1766,1767],{"class":54},"\"letters\"",[44,1769,1770],{"class":87},"); out.mkdir(",[44,1772,1773],{"class":274},"exist_ok",[44,1775,278],{"class":83},[44,1777,1778],{"class":107},"True",[44,1780,120],{"class":87},[44,1782,1784,1787,1790,1793,1796,1798,1800,1802],{"class":46,"line":1783},23,[44,1785,1786],{"class":83},"for",[44,1788,1789],{"class":87}," r ",[44,1791,1792],{"class":83},"in",[44,1794,1795],{"class":87}," run.itertuples(",[44,1797,275],{"class":274},[44,1799,278],{"class":83},[44,1801,281],{"class":107},[44,1803,1804],{"class":87},"):\n",[44,1806,1808,1810,1813,1815,1818],{"class":46,"line":1807},24,[44,1809,908],{"class":83},[44,1811,1812],{"class":87}," r.Level ",[44,1814,914],{"class":83},[44,1816,1817],{"class":107}," 4",[44,1819,919],{"class":87},[44,1821,1823,1826],{"class":46,"line":1822},25,[44,1824,1825],{"class":83},"        continue",[44,1827,1828],{"class":228},"                                   # internal referral, no customer letter\n",[44,1830,1832,1835,1837,1840,1842,1844,1846,1849,1851],{"class":46,"line":1831},26,[44,1833,1834],{"class":87},"    inv ",[44,1836,278],{"class":83},[44,1838,1839],{"class":87}," chaseable[chaseable[",[44,1841,552],{"class":54},[44,1843,471],{"class":87},[44,1845,914],{"class":83},[44,1847,1848],{"class":87}," r.Customer].sort_values(",[44,1850,487],{"class":54},[44,1852,120],{"class":87},[44,1854,1856,1859,1861,1864,1867,1870,1873,1875,1877,1879,1882,1885,1887,1889,1892,1895,1898,1901,1903,1906,1908,1911,1913,1916,1919,1922,1924,1926,1929,1932,1934],{"class":46,"line":1855},27,[44,1857,1858],{"class":87},"    lines ",[44,1860,278],{"class":83},[44,1862,1863],{"class":54}," \"",[44,1865,1866],{"class":966},"\\n",[44,1868,1869],{"class":54},"\"",[44,1871,1872],{"class":87},".join(",[44,1874,960],{"class":83},[44,1876,1869],{"class":54},[44,1878,698],{"class":966},[44,1880,1881],{"class":87},"inv_no",[44,1883,1884],{"class":83},":\u003C12",[44,1886,974],{"class":966},[44,1888,1693],{"class":966},[44,1890,1891],{"class":87},"due",[44,1893,1894],{"class":83},":",[44,1896,1897],{"class":87},"%",[44,1899,1900],{"class":83},"d ",[44,1902,1897],{"class":87},[44,1904,1905],{"class":83},"b ",[44,1907,1897],{"class":87},[44,1909,1910],{"class":83},"Y",[44,1912,974],{"class":966},[44,1914,1915],{"class":966},"   {",[44,1917,1918],{"class":87},"days",[44,1920,1921],{"class":83},":>12",[44,1923,974],{"class":966},[44,1925,1915],{"class":966},[44,1927,1928],{"class":87},"amount",[44,1930,1931],{"class":83},":>10,.2f",[44,1933,974],{"class":966},[44,1935,977],{"class":54},[44,1937,1939,1942,1945,1947,1950,1952,1954,1956,1958,1960,1962,1964,1967,1969,1971,1973],{"class":46,"line":1938},28,[44,1940,1941],{"class":83},"                      for",[44,1943,1944],{"class":87}," inv_no, due, days, amount ",[44,1946,1792],{"class":83},[44,1948,1949],{"class":87}," inv[[",[44,1951,601],{"class":54},[44,1953,143],{"class":87},[44,1955,487],{"class":54},[44,1957,143],{"class":87},[44,1959,468],{"class":54},[44,1961,143],{"class":87},[44,1963,583],{"class":54},[44,1965,1966],{"class":87},"]].itertuples(",[44,1968,275],{"class":274},[44,1970,278],{"class":83},[44,1972,281],{"class":107},[44,1974,284],{"class":87},[44,1976,1978,1981,1983,1986],{"class":46,"line":1977},29,[44,1979,1980],{"class":87},"    opening, closing ",[44,1982,278],{"class":83},[44,1984,1985],{"class":107}," TEXT",[44,1987,1988],{"class":87},"[r.Level]\n",[44,1990,1992,1995,1998,2001,2003,2005,2007,2009,2011,2014,2016,2019,2021,2024,2026,2029,2031,2034,2036,2039,2041,2044,2046,2049],{"class":46,"line":1991},30,[44,1993,1994],{"class":87},"    (out ",[44,1996,1997],{"class":83},"\u002F",[44,1999,2000],{"class":83}," f",[44,2002,1869],{"class":54},[44,2004,698],{"class":966},[44,2006,108],{"class":107},[44,2008,1894],{"class":83},[44,2010,1897],{"class":87},[44,2012,2013],{"class":83},"Y-",[44,2015,1897],{"class":87},[44,2017,2018],{"class":83},"m-",[44,2020,1897],{"class":87},[44,2022,2023],{"class":83},"d",[44,2025,974],{"class":966},[44,2027,2028],{"class":54},"_",[44,2030,698],{"class":966},[44,2032,2033],{"class":87},"r.Customer",[44,2035,974],{"class":966},[44,2037,2038],{"class":54},"_L",[44,2040,698],{"class":966},[44,2042,2043],{"class":87},"r.Level",[44,2045,974],{"class":966},[44,2047,2048],{"class":54},".txt\"",[44,2050,2051],{"class":87},").write_text(\n",[44,2053,2055,2058,2061,2064,2066,2069,2072,2074,2077,2080,2082,2085,2088,2090,2092,2094,2096,2099,2102,2104,2106,2108,2111,2113],{"class":46,"line":2054},31,[44,2056,2057],{"class":107},"        LETTER",[44,2059,2060],{"class":87},".substitute(",[44,2062,2063],{"class":274},"name",[44,2065,278],{"class":83},[44,2067,2068],{"class":87},"r.Name, ",[44,2070,2071],{"class":274},"opening",[44,2073,278],{"class":83},[44,2075,2076],{"class":87},"opening, ",[44,2078,2079],{"class":274},"lines",[44,2081,278],{"class":83},[44,2083,2084],{"class":87},"lines, ",[44,2086,2087],{"class":274},"total",[44,2089,278],{"class":83},[44,2091,960],{"class":83},[44,2093,1869],{"class":54},[44,2095,698],{"class":966},[44,2097,2098],{"class":87},"r.Overdue",[44,2100,2101],{"class":83},":,.2f",[44,2103,974],{"class":966},[44,2105,1869],{"class":54},[44,2107,143],{"class":87},[44,2109,2110],{"class":274},"closing",[44,2112,278],{"class":83},[44,2114,2115],{"class":87},"closing))\n",[44,2117,2119],{"class":46,"line":2118},32,[44,2120,101],{"emptyLinePlaceholder":100},[44,2122,2124,2127,2130,2132,2135,2137,2139,2141,2143,2145,2148,2151,2153,2155,2157,2160,2162,2165,2167,2170,2172,2175,2177,2180,2182,2185,2187,2189,2191,2193],{"class":46,"line":2123},33,[44,2125,2126],{"class":83},"with",[44,2128,2129],{"class":87}," pd.ExcelWriter(",[44,2131,960],{"class":83},[44,2133,2134],{"class":54},"\"dunning_run_",[44,2136,698],{"class":966},[44,2138,108],{"class":107},[44,2140,1894],{"class":87},[44,2142,1897],{"class":83},[44,2144,1910],{"class":87},[44,2146,2147],{"class":83},"-%",[44,2149,2150],{"class":87},"m",[44,2152,2147],{"class":83},[44,2154,2023],{"class":87},[44,2156,974],{"class":966},[44,2158,2159],{"class":54},".xlsx\"",[44,2161,143],{"class":87},[44,2163,2164],{"class":274},"engine",[44,2166,278],{"class":83},[44,2168,2169],{"class":54},"\"openpyxl\"",[44,2171,143],{"class":87},[44,2173,2174],{"class":274},"date_format",[44,2176,278],{"class":83},[44,2178,2179],{"class":54},"\"dd mmm yyyy\"",[44,2181,143],{"class":87},[44,2183,2184],{"class":274},"datetime_format",[44,2186,278],{"class":83},[44,2188,2179],{"class":54},[44,2190,513],{"class":87},[44,2192,91],{"class":83},[44,2194,2195],{"class":87}," xw:\n",[44,2197,2199,2202,2205,2207,2210,2212,2214,2216,2218],{"class":46,"line":2198},34,[44,2200,2201],{"class":87},"    run.to_excel(xw, ",[44,2203,2204],{"class":274},"sheet_name",[44,2206,278],{"class":83},[44,2208,2209],{"class":54},"\"Letters\"",[44,2211,143],{"class":87},[44,2213,275],{"class":274},[44,2215,278],{"class":83},[44,2217,281],{"class":107},[44,2219,120],{"class":87},[44,2221,2223,2226,2228,2230,2232,2234,2237,2239,2241,2244,2246,2248,2250,2252],{"class":46,"line":2222},35,[44,2224,2225],{"class":87},"    position[position[",[44,2227,711],{"class":54},[44,2229,471],{"class":87},[44,2231,914],{"class":83},[44,2233,510],{"class":107},[44,2235,2236],{"class":87},"].to_excel(xw, ",[44,2238,2204],{"class":274},[44,2240,278],{"class":83},[44,2242,2243],{"class":54},"\"No letter\"",[44,2245,143],{"class":87},[44,2247,275],{"class":274},[44,2249,278],{"class":83},[44,2251,281],{"class":107},[44,2253,120],{"class":87},[44,2255,2257,2260,2262,2264,2267,2269,2271,2273,2275],{"class":46,"line":2256},36,[44,2258,2259],{"class":87},"    chaseable.to_excel(xw, ",[44,2261,2204],{"class":274},[44,2263,278],{"class":83},[44,2265,2266],{"class":54},"\"Overdue items\"",[44,2268,143],{"class":87},[44,2270,275],{"class":274},[44,2272,278],{"class":83},[44,2274,281],{"class":107},[44,2276,120],{"class":87},[44,2278,2280],{"class":46,"line":2279},37,[44,2281,101],{"emptyLinePlaceholder":100},[44,2283,2285,2288,2290,2293,2295,2298,2301,2303,2305,2307,2310,2312,2315,2317,2320,2323,2325,2327],{"class":46,"line":2284},38,[44,2286,2287],{"class":87},"log ",[44,2289,278],{"class":83},[44,2291,2292],{"class":87}," pd.concat([history, run[[",[44,2294,552],{"class":54},[44,2296,2297],{"class":87},"]].assign(",[44,2299,2300],{"class":274},"Sent",[44,2302,278],{"class":83},[44,2304,108],{"class":107},[44,2306,143],{"class":87},[44,2308,2309],{"class":274},"Level",[44,2311,278],{"class":83},[44,2313,2314],{"class":87},"run[",[44,2316,711],{"class":54},[44,2318,2319],{"class":87},"].values)], ",[44,2321,2322],{"class":274},"ignore_index",[44,2324,278],{"class":83},[44,2326,1778],{"class":107},[44,2328,120],{"class":87},[44,2330,2332,2335,2337,2339,2341,2343,2345],{"class":46,"line":2331},39,[44,2333,2334],{"class":87},"log.to_excel(",[44,2336,454],{"class":54},[44,2338,143],{"class":87},[44,2340,275],{"class":274},[44,2342,278],{"class":83},[44,2344,281],{"class":107},[44,2346,120],{"class":87},[44,2348,2350,2352,2354,2357,2360,2362,2365,2367],{"class":46,"line":2349},40,[44,2351,263],{"class":107},[44,2353,266],{"class":87},[44,2355,2356],{"class":107},"sorted",[44,2358,2359],{"class":87},"(p.name ",[44,2361,1786],{"class":83},[44,2363,2364],{"class":87}," p ",[44,2366,1792],{"class":83},[44,2368,2369],{"class":87}," out.iterdir()))\n",[44,2371,2373,2375,2378,2380,2382,2384,2386,2388,2390,2392,2394,2396,2398,2400,2402,2404,2407],{"class":46,"line":2372},41,[44,2374,263],{"class":107},[44,2376,2377],{"class":87},"((out ",[44,2379,1997],{"class":83},[44,2381,2000],{"class":83},[44,2383,1869],{"class":54},[44,2385,698],{"class":966},[44,2387,108],{"class":107},[44,2389,1894],{"class":87},[44,2391,1897],{"class":83},[44,2393,1910],{"class":87},[44,2395,2147],{"class":83},[44,2397,2150],{"class":87},[44,2399,2147],{"class":83},[44,2401,2023],{"class":87},[44,2403,974],{"class":966},[44,2405,2406],{"class":54},"_C003_L3.txt\"",[44,2408,2409],{"class":87},").read_text())\n",[10,2411,2412,2413,2417],{},"The review workbook separates customers getting a letter — with level, amount and reason — from those who are not, so credit control can check both lists before anything is sent. Letters are generated from a template per level with the customer's overdue invoices in a table; Zenith's final notice lists only the undisputed invoice. Plain text keeps the example short; the same data fills a Word letter in ",[23,2414,2416],{"href":2415},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows\u002F","Generate Word Documents from Excel Rows with Python"," or an HTML email. Finally, the run is appended to the letter history — only after review and sending in a real process — which is what makes next week's run escalate correctly rather than starting everyone again at level 1.",[29,2419,2421],{"id":2420},"common-pitfalls","Common pitfalls",[2423,2424,2425,2433,2439,2445,2451],"ul",{},[2426,2427,2428,2432],"li",{},[2429,2430,2431],"strong",{},"Chasing disputed invoices."," Remove them before working out levels.",[2426,2434,2435,2438],{},[2429,2436,2437],{},"Skipping levels."," Escalate at most one level above the last letter sent.",[2426,2440,2441,2444],{},[2429,2442,2443],{},"Letters every run."," Enforce a minimum gap since the last letter.",[2426,2446,2447,2450],{},[2429,2448,2449],{},"Final notices to customers on a payment plan."," Cap their level.",[2426,2452,2453,2456],{},[2429,2454,2455],{},"No history."," Record what was sent, or every run starts from the beginning.",[29,2458,2460],{"id":2459},"conclusion","Conclusion",[10,2462,2463],{},"A dunning run is a small set of rules applied consistently: compute each customer's overdue position from undisputed invoices, find the level their oldest invoice qualifies for, cap it at one above their last letter, respect a minimum gap, a minimum balance and payment plans, and record every decision with its reason. Write a review workbook, generate a letter per customer from a template per level, and append the run to the letter history so the next run continues the sequence.",[29,2465,2467],{"id":2466},"frequently-asked-questions","Frequently asked questions",[10,2469,2470,2473],{},[2429,2471,2472],{},"How do I decide which dunning letter to send?","\nBase the level on the days overdue of the customer's oldest undisputed invoice, but never more than one level above the last letter sent to them.",[10,2475,2476,2479],{},[2429,2477,2478],{},"How do I stop customers getting reminders too often?","\nKeep a history of letters sent and skip customers who received one within a minimum gap, such as 14 days.",[10,2481,2482,2485],{},[2429,2483,2484],{},"Should disputed invoices be included in reminder letters?","\nNo. Remove invoices marked as disputed before calculating the overdue balance and level.",[10,2487,2488,2491],{},[2429,2489,2490],{},"How do I generate a letter for each customer from Excel data?","\nGroup the overdue invoices by customer and fill a text, Word or HTML template per dunning level with the customer's name, invoice table and total.",[29,2493,2495],{"id":2494},"related","Related",[2423,2497,2498,2504,2511,2518],{},[2426,2499,2500,2501,2503],{},"Up one level: ",[23,2502,26],{"href":25}," — finance reports from Python.",[2426,2505,2506,2510],{},[23,2507,2509],{"href":2508},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-accounts-receivable-aging-report\u002F","Build an Accounts Receivable Aging Report"," — the aged debt picture behind the run.",[2426,2512,2513,2517],{},[23,2514,2516],{"href":2515},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report"," — emailing each customer.",[2426,2519,2520,2524],{},[23,2521,2523],{"href":2522},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-supplier-payment-run-proposal\u002F","Build a Supplier Payment Run Proposal"," — the payables counterpart.",[2526,2527,2528],"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":39,"searchDepth":97,"depth":97,"links":2530},[2531,2532,2533,2534,2535,2536,2537,2538,2539],{"id":31,"depth":97,"text":32},{"id":67,"depth":97,"text":68},{"id":407,"depth":97,"text":408},{"id":805,"depth":97,"text":806},{"id":1561,"depth":97,"text":1562},{"id":2420,"depth":97,"text":2421},{"id":2459,"depth":97,"text":2460},{"id":2466,"depth":97,"text":2467},{"id":2494,"depth":97,"text":2495},"2026-10-02","Run credit control reminders from an open receivables export with pandas: decide each customer's reminder level from their most overdue invoice and their letter history, leave out disputed invoices, payment plans and small balances, never escalate faster than the policy allows, produce a run workbook for review and a letter for each customer listing what they owe, and record what was sent.","md",[2544,2546,2548,2550],{"q":2472,"a":2545},"Base the level on the days overdue of the customer's oldest undisputed invoice, but never more than one level above the last letter sent to them.",{"q":2478,"a":2547},"Keep a history of letters sent and skip customers who received one within a minimum gap, such as 14 days.",{"q":2484,"a":2549},"No. Remove invoices marked as disputed before calculating the overdue balance and level.",{"q":2490,"a":2551},"Group the overdue invoices by customer and fill a text, Word or HTML template per dunning level with the customer's name, invoice table and total.","credit control dunning letter run",null,{"breadcrumb":2555},[2556,2558,2561],{"name":2557,"item":1997},"Home",{"name":2559,"item":2560},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2562,"item":25},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run",{"title":2565,"description":2566},"Build a Credit Control Dunning Letter Run with Python","Dunning letters from Excel receivables data: reminder levels by days overdue, letter history and minimum gaps, disputes, payment plans and thresholds, a review workbook, generated letters and a sent log.","build-a-credit-control-dunning-letter-run","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run\u002Findex",[2570,2571],"report-layouts","email-delivery","how-to","ZnbipFlnTqBXNAeABFqNeMr4KVs8uAILofvOeEb2hFQ",[2575,2579],{"title":2576,"path":2577,"stem":2578,"children":-1},"Build a Cash Position Report from Bank Statements","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements\u002Findex",{"title":2580,"path":2581,"stem":2582,"children":-1},"Build a Customer Churn Report from Subscription Data","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data\u002Findex",[2584,2586],{"path":2585,"title":2562},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2587,"title":2559},"\u002Fautomating-reporting-workflows",[],[2590,2594,2597,2600,2603,2606],{"path":2591,"title":2592,"kind":2593},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2595,"title":2596,"kind":2572},"\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":2598,"title":2599,"kind":2572},"\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":2601,"title":2602,"kind":2572},"\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":2604,"title":2605,"kind":2572},"\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":2607,"title":2608,"kind":2572},"\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",1790972121283]