[{"data":1,"prerenderedAt":3065},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger":3,"surround:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger":3030,"ancestors:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger":3038,"children:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger":3043,"related:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger":3044},{"id":4,"title":5,"body":6,"dateModified":2996,"datePublished":2996,"description":2997,"extension":2998,"faq":2999,"keyword":3008,"kind":3009,"meta":3010,"navigation":114,"path":3019,"seo":3020,"slug":3023,"stem":3024,"topics":3025,"type":3028,"__hash__":3029},"docs\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger\u002Findex.md","Generate Customer Account Statements from a Ledger",{"type":7,"value":8,"toc":2984},"minimark",[9,19,24,56,74,78,86,338,345,349,352,531,541,814,818,825,1323,1326,1330,1333,2149,2162,2398,2402,2405,2745,2753,2873,2877,2912,2916,2919,2923,2929,2935,2941,2947,2951,2980],[10,11,12,13,18],"p",{},"At month end, accounts receivable sends each customer a statement: what they owed at the start of the month, every invoice, payment and credit note since, the running balance, what they owe now, and how much of it is overdue. The data is all in the sales ledger, usually exported to Excel, but turning it into dozens of individual statements by hand is slow and error-prone. With pandas, the calculations are a few group operations: the balance brought forward is a sum of everything before the period, the running balance is a cumulative sum, and the ageing comes from allocating payments to the oldest invoices first. XlsxWriter then lays out one statement workbook per customer. This page builds that job and finishes with checks that tie every statement back to the ledger. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002F","Generating Excel Reports from Templates",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl",[35,54,55],{"class":45}," xlsxwriter\n",[10,57,58,61,62,65,66,69,70,73],{},[32,59,60],{},"ledger.xlsx"," is a receivables ledger with one row per transaction: customer code and name, date, type (",[32,63,64],{},"INV"," invoice, ",[32,67,68],{},"PAY"," payment, ",[32,71,72],{},"CRN"," credit note), reference and a signed amount — positive for what the customer owes, negative for what they paid or were credited. The statement period is September 2026, and invoices are due 30 days after their date.",[20,75,77],{"id":76},"step-1-split-the-ledger-at-the-period-start","Step 1 — Split the ledger at the period start",[10,79,80,81,85],{},"Everything before the period collapses into one ",[82,83,84],"em",{},"balance brought forward"," line per customer; transactions inside the period are listed individually:",[25,87,91],{"className":88,"code":89,"language":90,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nledger = pd.read_excel(\"ledger.xlsx\", sheet_name=\"Ledger\").sort_values([\"Customer\", \"Date\", \"Reference\"])\nstart, end = pd.Timestamp(\"2026-09-01\"), pd.Timestamp(\"2026-09-30\")\n\nbefore = ledger[ledger[\"Date\"] \u003C start]\nperiod = ledger[(ledger[\"Date\"] >= start) & (ledger[\"Date\"] \u003C= end)].copy()\n\nopening = before.groupby(\"Customer\")[\"Amount\"].sum()\ncustomers = ledger.drop_duplicates(\"Customer\").set_index(\"Customer\")[\"Name\"]\nopening = opening.reindex(customers.index, fill_value=0.0)\nprint(opening.round(2).to_dict())\n","python",[32,92,93,109,116,162,185,190,212,249,254,276,302,323],{"__ignoreMap":30},[35,94,95,99,103,106],{"class":37,"line":38},[35,96,98],{"class":97},"s-kum","import",[35,100,102],{"class":101},"skGVy"," pandas ",[35,104,105],{"class":97},"as",[35,107,108],{"class":101}," pd\n",[35,110,112],{"class":37,"line":111},2,[35,113,115],{"emptyLinePlaceholder":114},true,"\n",[35,117,119,122,125,128,131,134,138,140,143,146,149,151,154,156,159],{"class":37,"line":118},3,[35,120,121],{"class":101},"ledger ",[35,123,124],{"class":97},"=",[35,126,127],{"class":101}," pd.read_excel(",[35,129,130],{"class":45},"\"ledger.xlsx\"",[35,132,133],{"class":101},", ",[35,135,137],{"class":136},"sa561","sheet_name",[35,139,124],{"class":97},[35,141,142],{"class":45},"\"Ledger\"",[35,144,145],{"class":101},").sort_values([",[35,147,148],{"class":45},"\"Customer\"",[35,150,133],{"class":101},[35,152,153],{"class":45},"\"Date\"",[35,155,133],{"class":101},[35,157,158],{"class":45},"\"Reference\"",[35,160,161],{"class":101},"])\n",[35,163,165,168,170,173,176,179,182],{"class":37,"line":164},4,[35,166,167],{"class":101},"start, end ",[35,169,124],{"class":97},[35,171,172],{"class":101}," pd.Timestamp(",[35,174,175],{"class":45},"\"2026-09-01\"",[35,177,178],{"class":101},"), pd.Timestamp(",[35,180,181],{"class":45},"\"2026-09-30\"",[35,183,184],{"class":101},")\n",[35,186,188],{"class":37,"line":187},5,[35,189,115],{"emptyLinePlaceholder":114},[35,191,193,196,198,201,203,206,209],{"class":37,"line":192},6,[35,194,195],{"class":101},"before ",[35,197,124],{"class":97},[35,199,200],{"class":101}," ledger[ledger[",[35,202,153],{"class":45},[35,204,205],{"class":101},"] ",[35,207,208],{"class":97},"\u003C",[35,210,211],{"class":101}," start]\n",[35,213,215,218,220,223,225,227,230,233,236,239,241,243,246],{"class":37,"line":214},7,[35,216,217],{"class":101},"period ",[35,219,124],{"class":97},[35,221,222],{"class":101}," ledger[(ledger[",[35,224,153],{"class":45},[35,226,205],{"class":101},[35,228,229],{"class":97},">=",[35,231,232],{"class":101}," start) ",[35,234,235],{"class":97},"&",[35,237,238],{"class":101}," (ledger[",[35,240,153],{"class":45},[35,242,205],{"class":101},[35,244,245],{"class":97},"\u003C=",[35,247,248],{"class":101}," end)].copy()\n",[35,250,252],{"class":37,"line":251},8,[35,253,115],{"emptyLinePlaceholder":114},[35,255,257,260,262,265,267,270,273],{"class":37,"line":256},9,[35,258,259],{"class":101},"opening ",[35,261,124],{"class":97},[35,263,264],{"class":101}," before.groupby(",[35,266,148],{"class":45},[35,268,269],{"class":101},")[",[35,271,272],{"class":45},"\"Amount\"",[35,274,275],{"class":101},"].sum()\n",[35,277,279,282,284,287,289,292,294,296,299],{"class":37,"line":278},10,[35,280,281],{"class":101},"customers ",[35,283,124],{"class":97},[35,285,286],{"class":101}," ledger.drop_duplicates(",[35,288,148],{"class":45},[35,290,291],{"class":101},").set_index(",[35,293,148],{"class":45},[35,295,269],{"class":101},[35,297,298],{"class":45},"\"Name\"",[35,300,301],{"class":101},"]\n",[35,303,305,307,309,312,315,317,321],{"class":37,"line":304},11,[35,306,259],{"class":101},[35,308,124],{"class":97},[35,310,311],{"class":101}," opening.reindex(customers.index, ",[35,313,314],{"class":136},"fill_value",[35,316,124],{"class":97},[35,318,320],{"class":319},"sP0c6","0.0",[35,322,184],{"class":101},[35,324,326,329,332,335],{"class":37,"line":325},12,[35,327,328],{"class":319},"print",[35,330,331],{"class":101},"(opening.round(",[35,333,334],{"class":319},"2",[35,336,337],{"class":101},").to_dict())\n",[10,339,340,341,344],{},"Oakridge Farms brings forward 860.00 — the July invoice was paid in August, leaving the August invoice open. Café Señor brings forward 1,170.00 from two unpaid invoices. Zenith Logistics had no activity before September, and ",[32,342,343],{},"reindex(..., fill_value=0.0)"," gives it an explicit zero rather than leaving it out — every customer with activity gets a statement, even one that starts from nothing.",[20,346,348],{"id":347},"step-2-running-balance-through-the-period","Step 2 — Running balance through the period",[10,350,351],{},"The running balance on each line is the opening balance plus the cumulative sum of the period's amounts:",[25,353,355],{"className":88,"code":354,"language":90,"meta":30,"style":30},"period[\"Balance\"] = period.groupby(\"Customer\")[\"Amount\"].cumsum() + period[\"Customer\"].map(opening)\nclosing = opening.add(period.groupby(\"Customer\")[\"Amount\"].sum(), fill_value=0.0)\n\noak = period[period[\"Customer\"] == \"C001\"][[\"Date\", \"Type\", \"Reference\", \"Amount\", \"Balance\"]]\nprint(oak.assign(Date=oak[\"Date\"].dt.strftime(\"%d %b\")).to_string(index=False))\nprint(\"closing:\", closing.round(2).to_dict())\n",[32,356,357,392,419,423,468,514],{"__ignoreMap":30},[35,358,359,362,365,367,369,372,374,376,378,381,384,387,389],{"class":37,"line":38},[35,360,361],{"class":101},"period[",[35,363,364],{"class":45},"\"Balance\"",[35,366,205],{"class":101},[35,368,124],{"class":97},[35,370,371],{"class":101}," period.groupby(",[35,373,148],{"class":45},[35,375,269],{"class":101},[35,377,272],{"class":45},[35,379,380],{"class":101},"].cumsum() ",[35,382,383],{"class":97},"+",[35,385,386],{"class":101}," period[",[35,388,148],{"class":45},[35,390,391],{"class":101},"].map(opening)\n",[35,393,394,397,399,402,404,406,408,411,413,415,417],{"class":37,"line":111},[35,395,396],{"class":101},"closing ",[35,398,124],{"class":97},[35,400,401],{"class":101}," opening.add(period.groupby(",[35,403,148],{"class":45},[35,405,269],{"class":101},[35,407,272],{"class":45},[35,409,410],{"class":101},"].sum(), ",[35,412,314],{"class":136},[35,414,124],{"class":97},[35,416,320],{"class":319},[35,418,184],{"class":101},[35,420,421],{"class":37,"line":118},[35,422,115],{"emptyLinePlaceholder":114},[35,424,425,428,430,433,435,437,440,443,446,448,450,453,455,457,459,461,463,465],{"class":37,"line":164},[35,426,427],{"class":101},"oak ",[35,429,124],{"class":97},[35,431,432],{"class":101}," period[period[",[35,434,148],{"class":45},[35,436,205],{"class":101},[35,438,439],{"class":97},"==",[35,441,442],{"class":45}," \"C001\"",[35,444,445],{"class":101},"][[",[35,447,153],{"class":45},[35,449,133],{"class":101},[35,451,452],{"class":45},"\"Type\"",[35,454,133],{"class":101},[35,456,158],{"class":45},[35,458,133],{"class":101},[35,460,272],{"class":45},[35,462,133],{"class":101},[35,464,364],{"class":45},[35,466,467],{"class":101},"]]\n",[35,469,470,472,475,478,480,483,485,488,491,495,498,500,503,506,508,511],{"class":37,"line":187},[35,471,328],{"class":319},[35,473,474],{"class":101},"(oak.assign(",[35,476,477],{"class":136},"Date",[35,479,124],{"class":97},[35,481,482],{"class":101},"oak[",[35,484,153],{"class":45},[35,486,487],{"class":101},"].dt.strftime(",[35,489,490],{"class":45},"\"",[35,492,494],{"class":493},"sSjpA","%d",[35,496,497],{"class":493}," %b",[35,499,490],{"class":45},[35,501,502],{"class":101},")).to_string(",[35,504,505],{"class":136},"index",[35,507,124],{"class":97},[35,509,510],{"class":319},"False",[35,512,513],{"class":101},"))\n",[35,515,516,518,521,524,527,529],{"class":37,"line":192},[35,517,328],{"class":319},[35,519,520],{"class":101},"(",[35,522,523],{"class":45},"\"closing:\"",[35,525,526],{"class":101},", closing.round(",[35,528,334],{"class":319},[35,530,337],{"class":101},[10,532,533,536,537,18],{},[32,534,535],{},"groupby(...).cumsum()"," restarts for each customer, and mapping the opening balance onto every row shifts the whole series. The last line of each customer's running balance equals their closing balance — Oakridge owes 1,760.00, Café Señor 1,250.00, and Zenith Logistics has paid in full. Sorting by date and then reference keeps same-day transactions in a stable order, so a statement regenerated next week shows the lines in the same sequence. Grouped cumulative sums are covered further in ",[14,538,540],{"href":539},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-month-to-date-and-year-to-date-totals-with-pandas\u002F","Calculate Month-to-Date and Year-to-Date Totals with pandas",[542,543,551,552,551,556,551,560,551,568,551,576,551,580,551,587,551,591,551,595,551,599,551,603,551,606,551,611,551,613,551,616,551,618,551,621,551,623,551,626,551,629,551,632,551,634,551,638,551,640,551,643,551,645,551,648,551,651,551,655,551,657,551,660,551,662,551,665,551,667,551,670,551,673,551,677,551,679,551,682,551,684,551,687,551,689,551,692,551,695,551,699,551,701,551,704,551,706,551,709,551,711,551,714,551,718,551,723,551,727,551,731,551,734,551,738,551,740,551,742,551,745,551,748,551,750,551,753,551,756,551,759,551,761,551,764,551,767,551,770,551,775,551,779,551,785,551,789,551,796,551,801,551,809],"svg",{"viewBox":544,"role":545,"ariaLabelledBy":546,"xmlns":549,"style":550},"16 0 728 220","img",[547,548],"st-running-t","st-running-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:728px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[553,554,555],"title",{"id":547},"Oakridge Farms' September lines with a running balance",[557,558,559],"desc",{"id":548},"The balance brought forward of 860 starts the running balance. The invoice of 1,540 takes it to 2,400, the payment of 500 to 1,900 and the credit note of 140 to 1,760, which is the closing balance.",[561,562],"rect",{"x":563,"y":564,"width":565,"height":566,"fill":567},"16","0","728","220","#ffffff",[561,569],{"x":570,"y":571,"width":570,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"32.0","40.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[561,577],{"x":578,"y":571,"width":579,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"64.0","90.4",[581,582,586],"text",{"x":583,"y":584,"style":585},"109.2","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[561,588],{"x":589,"y":571,"width":590,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"154.4","84.0",[581,592,594],{"x":593,"y":584,"style":585},"196.5","B",[561,596],{"x":597,"y":571,"width":598,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"238.5","72.7",[581,600,602],{"x":601,"y":584,"style":585},"274.8","C",[561,604],{"x":570,"y":578,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"28.0",[581,607,610],{"x":608,"y":609,"style":585},"48.0","83.0","1",[561,612],{"x":578,"y":578,"width":579,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,614,477],{"x":583,"y":609,"style":615},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[561,617],{"x":589,"y":578,"width":590,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,619,620],{"x":593,"y":609,"style":615},"Ref",[561,622],{"x":597,"y":578,"width":598,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,624,625],{"x":601,"y":609,"style":615},"Amount",[561,627],{"x":570,"y":628,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"92.0",[581,630,334],{"x":608,"y":631,"style":585},"111.0",[561,633],{"x":578,"y":628,"width":579,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,635,637],{"x":583,"y":631,"style":636},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","before 1 Sep",[561,639],{"x":589,"y":628,"width":590,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,641,642],{"x":593,"y":631,"style":636},"(4 rows)",[561,644],{"x":597,"y":628,"width":598,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,646,647],{"x":601,"y":631,"style":636},"860.00",[561,649],{"x":570,"y":650,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"120.0",[581,652,654],{"x":608,"y":653,"style":585},"139.0","3",[561,656],{"x":578,"y":650,"width":579,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,658,659],{"x":583,"y":653,"style":636},"05 Sep",[561,661],{"x":589,"y":650,"width":590,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,663,664],{"x":593,"y":653,"style":636},"INV-1102",[561,666],{"x":597,"y":650,"width":598,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,668,669],{"x":601,"y":653,"style":636},"1,540.00",[561,671],{"x":570,"y":672,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"148.0",[581,674,676],{"x":608,"y":675,"style":585},"167.0","4",[561,678],{"x":578,"y":672,"width":579,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,680,681],{"x":583,"y":675,"style":636},"18 Sep",[561,683],{"x":589,"y":672,"width":590,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,685,686],{"x":593,"y":675,"style":636},"BACS 0918",[561,688],{"x":597,"y":672,"width":598,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,690,691],{"x":601,"y":675,"style":636},"-500.00",[561,693],{"x":570,"y":694,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"176.0",[581,696,698],{"x":608,"y":697,"style":585},"195.0","5",[561,700],{"x":578,"y":694,"width":579,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,702,703],{"x":583,"y":697,"style":636},"25 Sep",[561,705],{"x":589,"y":694,"width":590,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,707,708],{"x":593,"y":697,"style":636},"CRN-0007",[561,710],{"x":597,"y":694,"width":598,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,712,713],{"x":601,"y":697,"style":636},"-140.00",[561,715],{"x":716,"y":571,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},"539.8","112.7",[581,719,722],{"x":720,"y":721,"style":615},"596.1","59.0","Line",[561,724],{"x":725,"y":571,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},"652.5","75.5",[581,728,730],{"x":729,"y":721,"style":615},"690.2","Balance",[561,732],{"x":716,"y":733,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},"68.0",[581,735,737],{"x":720,"y":736,"style":636},"87.0","Brought forward",[561,739],{"x":725,"y":733,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,741,647],{"x":729,"y":736,"style":636},[561,743],{"x":716,"y":744,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},"96.0",[581,746,664],{"x":720,"y":747,"style":636},"115.0",[561,749],{"x":725,"y":744,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,751,752],{"x":729,"y":747,"style":636},"2,400.00",[561,754],{"x":716,"y":755,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},"124.0",[581,757,686],{"x":720,"y":758,"style":636},"143.0",[561,760],{"x":725,"y":755,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,762,763],{"x":729,"y":758,"style":636},"1,900.00",[561,765],{"x":716,"y":766,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},"152.0",[581,768,708],{"x":720,"y":769,"style":636},"171.0",[561,771],{"x":725,"y":766,"width":726,"height":605,"rx":564,"fill":772,"stroke":773,"style":774},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[581,776,778],{"x":729,"y":769,"style":777},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","1,760.00",[581,780,784],{"x":781,"y":782,"style":783},"171.6","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","ledger rows",[581,786,788],{"x":787,"y":782,"style":783},"633.9","statement lines",[37,790],{"x1":791,"y1":792,"x2":793,"y2":792,"stroke":794,"style":795},"321.2","110.0","525.8","var(--brand,#5b5cf0)","stroke-width:2px",[797,798],"polygon",{"points":799,"fill":800},"529.8,110.0 520.8,115.0 520.8,105.0","#5b5cf0",[561,802],{"x":803,"y":804,"width":805,"height":806,"rx":807,"fill":567,"stroke":574,"style":808},"350.1","77.0","150.8","22.0","11","stroke-width:1.2px",[581,810,813],{"x":811,"y":628,"style":812},"425.5","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","opening + cumsum()",[20,815,817],{"id":816},"step-3-age-the-balance-by-allocating-payments","Step 3 — Age the balance by allocating payments",[10,819,820,821,824],{},"The ageing summary says how much of the balance is current and how much is 1–30, 31–60 or more than 60 days overdue. That depends on ",[82,822,823],{},"which"," invoices are still open, and the usual convention — when payments are not matched to specific invoices — is first in, first out: payments and credits settle the oldest invoices first:",[25,826,828],{"className":88,"code":827,"language":90,"meta":30,"style":30},"def open_invoices(txns):\n    \"\"\"Allocate payments and credits to the oldest invoices; return what remains open.\"\"\"\n    invoices = txns[txns[\"Amount\"] > 0][[\"Date\", \"Reference\", \"Amount\"]].to_dict(\"records\")\n    credit = -txns.loc[txns[\"Amount\"] \u003C 0, \"Amount\"].sum()\n    for inv in invoices:                      # already sorted by date\n        used = min(credit, inv[\"Amount\"])\n        inv[\"Open\"] = round(inv[\"Amount\"] - used, 2)\n        credit -= used\n    return [i for i in invoices if i[\"Open\"] > 0], round(credit, 2)\n\ndef age(txns, as_of, terms=30):\n    buckets = {\"Current\": 0.0, \"1-30\": 0.0, \"31-60\": 0.0, \"Over 60\": 0.0}\n    still_open, unapplied = open_invoices(txns)\n    for inv in still_open:\n        overdue = (as_of - (inv[\"Date\"] + pd.Timedelta(days=terms))).days\n        key = \"Current\" if overdue \u003C= 0 else \"1-30\" if overdue \u003C= 30 else \"31-60\" if overdue \u003C= 60 else \"Over 60\"\n        buckets[key] += inv[\"Open\"]\n    buckets[\"Unapplied credit\"] = -unapplied\n    return buckets\n\nupto_end = ledger[ledger[\"Date\"] \u003C= end]\nageing = pd.DataFrame({c: age(g, end) for c, g in upto_end.groupby(\"Customer\")}).T.round(2)\nprint(ageing)\n",[32,829,830,842,847,887,914,932,949,981,992,1038,1042,1060,1108,1119,1131,1164,1219,1235,1253,1261,1266,1285,1315],{"__ignoreMap":30},[35,831,832,835,839],{"class":37,"line":38},[35,833,834],{"class":97},"def",[35,836,838],{"class":837},"s_Opv"," open_invoices",[35,840,841],{"class":101},"(txns):\n",[35,843,844],{"class":37,"line":111},[35,845,846],{"class":45},"    \"\"\"Allocate payments and credits to the oldest invoices; return what remains open.\"\"\"\n",[35,848,849,852,854,857,859,861,864,867,869,871,873,875,877,879,882,885],{"class":37,"line":118},[35,850,851],{"class":101},"    invoices ",[35,853,124],{"class":97},[35,855,856],{"class":101}," txns[txns[",[35,858,272],{"class":45},[35,860,205],{"class":101},[35,862,863],{"class":97},">",[35,865,866],{"class":319}," 0",[35,868,445],{"class":101},[35,870,153],{"class":45},[35,872,133],{"class":101},[35,874,158],{"class":45},[35,876,133],{"class":101},[35,878,272],{"class":45},[35,880,881],{"class":101},"]].to_dict(",[35,883,884],{"class":45},"\"records\"",[35,886,184],{"class":101},[35,888,889,892,894,897,900,902,904,906,908,910,912],{"class":37,"line":164},[35,890,891],{"class":101},"    credit ",[35,893,124],{"class":97},[35,895,896],{"class":97}," -",[35,898,899],{"class":101},"txns.loc[txns[",[35,901,272],{"class":45},[35,903,205],{"class":101},[35,905,208],{"class":97},[35,907,866],{"class":319},[35,909,133],{"class":101},[35,911,272],{"class":45},[35,913,275],{"class":101},[35,915,916,919,922,925,928],{"class":37,"line":187},[35,917,918],{"class":97},"    for",[35,920,921],{"class":101}," inv ",[35,923,924],{"class":97},"in",[35,926,927],{"class":101}," invoices:                      ",[35,929,931],{"class":930},"s-wDw","# already sorted by date\n",[35,933,934,937,939,942,945,947],{"class":37,"line":192},[35,935,936],{"class":101},"        used ",[35,938,124],{"class":97},[35,940,941],{"class":319}," min",[35,943,944],{"class":101},"(credit, inv[",[35,946,272],{"class":45},[35,948,161],{"class":101},[35,950,951,954,957,959,961,964,967,969,971,974,977,979],{"class":37,"line":214},[35,952,953],{"class":101},"        inv[",[35,955,956],{"class":45},"\"Open\"",[35,958,205],{"class":101},[35,960,124],{"class":97},[35,962,963],{"class":319}," round",[35,965,966],{"class":101},"(inv[",[35,968,272],{"class":45},[35,970,205],{"class":101},[35,972,973],{"class":97},"-",[35,975,976],{"class":101}," used, ",[35,978,334],{"class":319},[35,980,184],{"class":101},[35,982,983,986,989],{"class":37,"line":251},[35,984,985],{"class":101},"        credit ",[35,987,988],{"class":97},"-=",[35,990,991],{"class":101}," used\n",[35,993,994,997,1000,1003,1006,1008,1011,1014,1017,1019,1021,1023,1025,1028,1031,1034,1036],{"class":37,"line":256},[35,995,996],{"class":97},"    return",[35,998,999],{"class":101}," [i ",[35,1001,1002],{"class":97},"for",[35,1004,1005],{"class":101}," i ",[35,1007,924],{"class":97},[35,1009,1010],{"class":101}," invoices ",[35,1012,1013],{"class":97},"if",[35,1015,1016],{"class":101}," i[",[35,1018,956],{"class":45},[35,1020,205],{"class":101},[35,1022,863],{"class":97},[35,1024,866],{"class":319},[35,1026,1027],{"class":101},"], ",[35,1029,1030],{"class":319},"round",[35,1032,1033],{"class":101},"(credit, ",[35,1035,334],{"class":319},[35,1037,184],{"class":101},[35,1039,1040],{"class":37,"line":278},[35,1041,115],{"emptyLinePlaceholder":114},[35,1043,1044,1046,1049,1052,1054,1057],{"class":37,"line":304},[35,1045,834],{"class":97},[35,1047,1048],{"class":837}," age",[35,1050,1051],{"class":101},"(txns, as_of, terms",[35,1053,124],{"class":97},[35,1055,1056],{"class":319},"30",[35,1058,1059],{"class":101},"):\n",[35,1061,1062,1065,1067,1070,1073,1076,1078,1080,1083,1085,1087,1089,1092,1094,1096,1098,1101,1103,1105],{"class":37,"line":325},[35,1063,1064],{"class":101},"    buckets ",[35,1066,124],{"class":97},[35,1068,1069],{"class":101}," {",[35,1071,1072],{"class":45},"\"Current\"",[35,1074,1075],{"class":101},": ",[35,1077,320],{"class":319},[35,1079,133],{"class":101},[35,1081,1082],{"class":45},"\"1-30\"",[35,1084,1075],{"class":101},[35,1086,320],{"class":319},[35,1088,133],{"class":101},[35,1090,1091],{"class":45},"\"31-60\"",[35,1093,1075],{"class":101},[35,1095,320],{"class":319},[35,1097,133],{"class":101},[35,1099,1100],{"class":45},"\"Over 60\"",[35,1102,1075],{"class":101},[35,1104,320],{"class":319},[35,1106,1107],{"class":101},"}\n",[35,1109,1111,1114,1116],{"class":37,"line":1110},13,[35,1112,1113],{"class":101},"    still_open, unapplied ",[35,1115,124],{"class":97},[35,1117,1118],{"class":101}," open_invoices(txns)\n",[35,1120,1122,1124,1126,1128],{"class":37,"line":1121},14,[35,1123,918],{"class":97},[35,1125,921],{"class":101},[35,1127,924],{"class":97},[35,1129,1130],{"class":101}," still_open:\n",[35,1132,1134,1137,1139,1142,1144,1147,1149,1151,1153,1156,1159,1161],{"class":37,"line":1133},15,[35,1135,1136],{"class":101},"        overdue ",[35,1138,124],{"class":97},[35,1140,1141],{"class":101}," (as_of ",[35,1143,973],{"class":97},[35,1145,1146],{"class":101}," (inv[",[35,1148,153],{"class":45},[35,1150,205],{"class":101},[35,1152,383],{"class":97},[35,1154,1155],{"class":101}," pd.Timedelta(",[35,1157,1158],{"class":136},"days",[35,1160,124],{"class":97},[35,1162,1163],{"class":101},"terms))).days\n",[35,1165,1167,1170,1172,1175,1178,1181,1183,1185,1188,1191,1193,1195,1197,1200,1202,1205,1207,1209,1211,1214,1216],{"class":37,"line":1166},16,[35,1168,1169],{"class":101},"        key ",[35,1171,124],{"class":97},[35,1173,1174],{"class":45}," \"Current\"",[35,1176,1177],{"class":97}," if",[35,1179,1180],{"class":101}," overdue ",[35,1182,245],{"class":97},[35,1184,866],{"class":319},[35,1186,1187],{"class":97}," else",[35,1189,1190],{"class":45}," \"1-30\"",[35,1192,1177],{"class":97},[35,1194,1180],{"class":101},[35,1196,245],{"class":97},[35,1198,1199],{"class":319}," 30",[35,1201,1187],{"class":97},[35,1203,1204],{"class":45}," \"31-60\"",[35,1206,1177],{"class":97},[35,1208,1180],{"class":101},[35,1210,245],{"class":97},[35,1212,1213],{"class":319}," 60",[35,1215,1187],{"class":97},[35,1217,1218],{"class":45}," \"Over 60\"\n",[35,1220,1222,1225,1228,1231,1233],{"class":37,"line":1221},17,[35,1223,1224],{"class":101},"        buckets[key] ",[35,1226,1227],{"class":97},"+=",[35,1229,1230],{"class":101}," inv[",[35,1232,956],{"class":45},[35,1234,301],{"class":101},[35,1236,1238,1241,1244,1246,1248,1250],{"class":37,"line":1237},18,[35,1239,1240],{"class":101},"    buckets[",[35,1242,1243],{"class":45},"\"Unapplied credit\"",[35,1245,205],{"class":101},[35,1247,124],{"class":97},[35,1249,896],{"class":97},[35,1251,1252],{"class":101},"unapplied\n",[35,1254,1256,1258],{"class":37,"line":1255},19,[35,1257,996],{"class":97},[35,1259,1260],{"class":101}," buckets\n",[35,1262,1264],{"class":37,"line":1263},20,[35,1265,115],{"emptyLinePlaceholder":114},[35,1267,1269,1272,1274,1276,1278,1280,1282],{"class":37,"line":1268},21,[35,1270,1271],{"class":101},"upto_end ",[35,1273,124],{"class":97},[35,1275,200],{"class":101},[35,1277,153],{"class":45},[35,1279,205],{"class":101},[35,1281,245],{"class":97},[35,1283,1284],{"class":101}," end]\n",[35,1286,1288,1291,1293,1296,1298,1301,1303,1306,1308,1311,1313],{"class":37,"line":1287},22,[35,1289,1290],{"class":101},"ageing ",[35,1292,124],{"class":97},[35,1294,1295],{"class":101}," pd.DataFrame({c: age(g, end) ",[35,1297,1002],{"class":97},[35,1299,1300],{"class":101}," c, g ",[35,1302,924],{"class":97},[35,1304,1305],{"class":101}," upto_end.groupby(",[35,1307,148],{"class":45},[35,1309,1310],{"class":101},")}).T.round(",[35,1312,334],{"class":319},[35,1314,184],{"class":101},[35,1316,1318,1320],{"class":37,"line":1317},23,[35,1319,328],{"class":319},[35,1321,1322],{"class":101},"(ageing)\n",[10,1324,1325],{},"For Oakridge Farms, the July invoice was fully paid in August; September's payment and credit note (640.00 in total) then settle the August invoice down to 220.00, which is 10 days overdue on 30 September, and the September invoice of 1,540.00 is current. Café Señor's June invoice is reduced by the September card payment to 150.00 and is more than 60 days overdue — exactly the line a credit controller wants to see. A payment larger than all open invoices appears as unapplied credit, a negative amount, so the ageing buckets still add up to the balance. If your ledger records which invoice each payment settles, allocate by that link instead of FIFO.",[20,1327,1329],{"id":1328},"step-4-lay-out-one-statement-per-customer","Step 4 — Lay out one statement per customer",[10,1331,1332],{},"Each customer gets a workbook with a header, the statement lines and the ageing summary. XlsxWriter writes it with formats for money and dates:",[25,1334,1336],{"className":88,"code":1335,"language":90,"meta":30,"style":30},"import xlsxwriter\nfrom pathlib import Path\n\nPath(\"statements\").mkdir(exist_ok=True)\n\ndef write_statement(code):\n    name = customers[code]\n    lines = period[period[\"Customer\"] == code]\n    book = xlsxwriter.Workbook(f\"statements\u002F{code}_2026-09.xlsx\")\n    ws = book.add_worksheet(\"Statement\")\n    title = book.add_format({\"bold\": True, \"font_size\": 16, \"font_color\": \"#4338CA\"})\n    head = book.add_format({\"bold\": True, \"bottom\": 1, \"bg_color\": \"#EEF2FF\"})\n    money = book.add_format({\"num_format\": \"#,##0.00;[Red]-#,##0.00\"})\n    bold_money = book.add_format({\"num_format\": \"#,##0.00\", \"bold\": True, \"top\": 1})\n    day = book.add_format({\"num_format\": \"dd mmm yyyy\", \"align\": \"left\"})\n    ws.set_column(\"A:A\", 14); ws.set_column(\"B:C\", 16); ws.set_column(\"D:E\", 14)\n\n    ws.write(\"A1\", \"Statement of account\", title)\n    ws.write(\"A3\", name); ws.write(\"A4\", f\"Account {code}\")\n    ws.write(\"D3\", \"Period\"); ws.write(\"E3\", f\"{start:%d %b} – {end:%d %b %Y}\")\n    ws.write_row(\"A6\", [\"Date\", \"Type\", \"Reference\", \"Amount\", \"Balance\"], head)\n    ws.write_datetime(\"A7\", start.to_pydatetime(), day)\n    ws.write(\"C7\", \"Balance brought forward\")\n    ws.write_number(\"E7\", float(opening[code]), money)\n    r = 7\n    for row in lines.itertuples():\n        ws.write_datetime(r, 0, row.Date.to_pydatetime(), day)\n        ws.write_row(r, 1, [row.Type, row.Reference])\n        ws.write_number(r, 3, row.Amount, money)\n        ws.write_number(r, 4, row.Balance, money)\n        r += 1\n    ws.write(r, 3, \"Amount due\", book.add_format({\"bold\": True, \"top\": 1}))\n    ws.write_number(r, 4, float(closing[code]), bold_money)\n\n    r += 2\n    ws.write(r, 0, \"Ageing\", head)\n    buckets = ageing.loc[code]\n    ws.write_row(r + 1, 0, list(buckets.index), head)\n    ws.write_row(r + 2, 0, [float(v) for v in buckets], money)\n    ws.set_landscape(); ws.fit_to_pages(1, 0)\n    book.close()\n    return f\"statements\u002F{code}_2026-09.xlsx\"\n\npaths = [write_statement(c) for c in customers.index]\nprint(paths)\n",[32,1337,1338,1344,1357,1361,1382,1386,1396,1406,1424,1453,1468,1507,1543,1562,1597,1625,1659,1663,1679,1709,1782,1814,1825,1839,1856,1867,1880,1891,1902,1913,1923,1934,1967,1982,1987,1997,2012,2022,2045,2076,2090,2096,2115,2120,2141],{"__ignoreMap":30},[35,1339,1340,1342],{"class":37,"line":38},[35,1341,98],{"class":97},[35,1343,55],{"class":101},[35,1345,1346,1349,1352,1354],{"class":37,"line":111},[35,1347,1348],{"class":97},"from",[35,1350,1351],{"class":101}," pathlib ",[35,1353,98],{"class":97},[35,1355,1356],{"class":101}," Path\n",[35,1358,1359],{"class":37,"line":118},[35,1360,115],{"emptyLinePlaceholder":114},[35,1362,1363,1366,1369,1372,1375,1377,1380],{"class":37,"line":164},[35,1364,1365],{"class":101},"Path(",[35,1367,1368],{"class":45},"\"statements\"",[35,1370,1371],{"class":101},").mkdir(",[35,1373,1374],{"class":136},"exist_ok",[35,1376,124],{"class":97},[35,1378,1379],{"class":319},"True",[35,1381,184],{"class":101},[35,1383,1384],{"class":37,"line":187},[35,1385,115],{"emptyLinePlaceholder":114},[35,1387,1388,1390,1393],{"class":37,"line":192},[35,1389,834],{"class":97},[35,1391,1392],{"class":837}," write_statement",[35,1394,1395],{"class":101},"(code):\n",[35,1397,1398,1401,1403],{"class":37,"line":214},[35,1399,1400],{"class":101},"    name ",[35,1402,124],{"class":97},[35,1404,1405],{"class":101}," customers[code]\n",[35,1407,1408,1411,1413,1415,1417,1419,1421],{"class":37,"line":251},[35,1409,1410],{"class":101},"    lines ",[35,1412,124],{"class":97},[35,1414,432],{"class":101},[35,1416,148],{"class":45},[35,1418,205],{"class":101},[35,1420,439],{"class":97},[35,1422,1423],{"class":101}," code]\n",[35,1425,1426,1429,1431,1434,1437,1440,1443,1445,1448,1451],{"class":37,"line":256},[35,1427,1428],{"class":101},"    book ",[35,1430,124],{"class":97},[35,1432,1433],{"class":101}," xlsxwriter.Workbook(",[35,1435,1436],{"class":97},"f",[35,1438,1439],{"class":45},"\"statements\u002F",[35,1441,1442],{"class":493},"{",[35,1444,32],{"class":101},[35,1446,1447],{"class":493},"}",[35,1449,1450],{"class":45},"_2026-09.xlsx\"",[35,1452,184],{"class":101},[35,1454,1455,1458,1460,1463,1466],{"class":37,"line":278},[35,1456,1457],{"class":101},"    ws ",[35,1459,124],{"class":97},[35,1461,1462],{"class":101}," book.add_worksheet(",[35,1464,1465],{"class":45},"\"Statement\"",[35,1467,184],{"class":101},[35,1469,1470,1473,1475,1478,1481,1483,1485,1487,1490,1492,1494,1496,1499,1501,1504],{"class":37,"line":304},[35,1471,1472],{"class":101},"    title ",[35,1474,124],{"class":97},[35,1476,1477],{"class":101}," book.add_format({",[35,1479,1480],{"class":45},"\"bold\"",[35,1482,1075],{"class":101},[35,1484,1379],{"class":319},[35,1486,133],{"class":101},[35,1488,1489],{"class":45},"\"font_size\"",[35,1491,1075],{"class":101},[35,1493,563],{"class":319},[35,1495,133],{"class":101},[35,1497,1498],{"class":45},"\"font_color\"",[35,1500,1075],{"class":101},[35,1502,1503],{"class":45},"\"#4338CA\"",[35,1505,1506],{"class":101},"})\n",[35,1508,1509,1512,1514,1516,1518,1520,1522,1524,1527,1529,1531,1533,1536,1538,1541],{"class":37,"line":325},[35,1510,1511],{"class":101},"    head ",[35,1513,124],{"class":97},[35,1515,1477],{"class":101},[35,1517,1480],{"class":45},[35,1519,1075],{"class":101},[35,1521,1379],{"class":319},[35,1523,133],{"class":101},[35,1525,1526],{"class":45},"\"bottom\"",[35,1528,1075],{"class":101},[35,1530,610],{"class":319},[35,1532,133],{"class":101},[35,1534,1535],{"class":45},"\"bg_color\"",[35,1537,1075],{"class":101},[35,1539,1540],{"class":45},"\"#EEF2FF\"",[35,1542,1506],{"class":101},[35,1544,1545,1548,1550,1552,1555,1557,1560],{"class":37,"line":1110},[35,1546,1547],{"class":101},"    money ",[35,1549,124],{"class":97},[35,1551,1477],{"class":101},[35,1553,1554],{"class":45},"\"num_format\"",[35,1556,1075],{"class":101},[35,1558,1559],{"class":45},"\"#,##0.00;[Red]-#,##0.00\"",[35,1561,1506],{"class":101},[35,1563,1564,1567,1569,1571,1573,1575,1578,1580,1582,1584,1586,1588,1591,1593,1595],{"class":37,"line":1121},[35,1565,1566],{"class":101},"    bold_money ",[35,1568,124],{"class":97},[35,1570,1477],{"class":101},[35,1572,1554],{"class":45},[35,1574,1075],{"class":101},[35,1576,1577],{"class":45},"\"#,##0.00\"",[35,1579,133],{"class":101},[35,1581,1480],{"class":45},[35,1583,1075],{"class":101},[35,1585,1379],{"class":319},[35,1587,133],{"class":101},[35,1589,1590],{"class":45},"\"top\"",[35,1592,1075],{"class":101},[35,1594,610],{"class":319},[35,1596,1506],{"class":101},[35,1598,1599,1602,1604,1606,1608,1610,1613,1615,1618,1620,1623],{"class":37,"line":1133},[35,1600,1601],{"class":101},"    day ",[35,1603,124],{"class":97},[35,1605,1477],{"class":101},[35,1607,1554],{"class":45},[35,1609,1075],{"class":101},[35,1611,1612],{"class":45},"\"dd mmm yyyy\"",[35,1614,133],{"class":101},[35,1616,1617],{"class":45},"\"align\"",[35,1619,1075],{"class":101},[35,1621,1622],{"class":45},"\"left\"",[35,1624,1506],{"class":101},[35,1626,1627,1630,1633,1635,1638,1641,1644,1646,1648,1650,1653,1655,1657],{"class":37,"line":1166},[35,1628,1629],{"class":101},"    ws.set_column(",[35,1631,1632],{"class":45},"\"A:A\"",[35,1634,133],{"class":101},[35,1636,1637],{"class":319},"14",[35,1639,1640],{"class":101},"); ws.set_column(",[35,1642,1643],{"class":45},"\"B:C\"",[35,1645,133],{"class":101},[35,1647,563],{"class":319},[35,1649,1640],{"class":101},[35,1651,1652],{"class":45},"\"D:E\"",[35,1654,133],{"class":101},[35,1656,1637],{"class":319},[35,1658,184],{"class":101},[35,1660,1661],{"class":37,"line":1221},[35,1662,115],{"emptyLinePlaceholder":114},[35,1664,1665,1668,1671,1673,1676],{"class":37,"line":1237},[35,1666,1667],{"class":101},"    ws.write(",[35,1669,1670],{"class":45},"\"A1\"",[35,1672,133],{"class":101},[35,1674,1675],{"class":45},"\"Statement of account\"",[35,1677,1678],{"class":101},", title)\n",[35,1680,1681,1683,1686,1689,1692,1694,1696,1699,1701,1703,1705,1707],{"class":37,"line":1255},[35,1682,1667],{"class":101},[35,1684,1685],{"class":45},"\"A3\"",[35,1687,1688],{"class":101},", name); ws.write(",[35,1690,1691],{"class":45},"\"A4\"",[35,1693,133],{"class":101},[35,1695,1436],{"class":97},[35,1697,1698],{"class":45},"\"Account ",[35,1700,1442],{"class":493},[35,1702,32],{"class":101},[35,1704,1447],{"class":493},[35,1706,490],{"class":45},[35,1708,184],{"class":101},[35,1710,1711,1713,1716,1718,1721,1724,1727,1729,1731,1733,1735,1738,1741,1744,1747,1749,1752,1754,1757,1759,1762,1764,1766,1768,1771,1773,1776,1778,1780],{"class":37,"line":1263},[35,1712,1667],{"class":101},[35,1714,1715],{"class":45},"\"D3\"",[35,1717,133],{"class":101},[35,1719,1720],{"class":45},"\"Period\"",[35,1722,1723],{"class":101},"); ws.write(",[35,1725,1726],{"class":45},"\"E3\"",[35,1728,133],{"class":101},[35,1730,1436],{"class":97},[35,1732,490],{"class":45},[35,1734,1442],{"class":493},[35,1736,1737],{"class":101},"start",[35,1739,1740],{"class":97},":",[35,1742,1743],{"class":101},"%",[35,1745,1746],{"class":97},"d ",[35,1748,1743],{"class":101},[35,1750,1751],{"class":97},"b",[35,1753,1447],{"class":493},[35,1755,1756],{"class":45}," – ",[35,1758,1442],{"class":493},[35,1760,1761],{"class":101},"end:",[35,1763,1743],{"class":97},[35,1765,1746],{"class":101},[35,1767,1743],{"class":97},[35,1769,1770],{"class":101},"b ",[35,1772,1743],{"class":97},[35,1774,1775],{"class":101},"Y",[35,1777,1447],{"class":493},[35,1779,490],{"class":45},[35,1781,184],{"class":101},[35,1783,1784,1787,1790,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811],{"class":37,"line":1268},[35,1785,1786],{"class":101},"    ws.write_row(",[35,1788,1789],{"class":45},"\"A6\"",[35,1791,1792],{"class":101},", [",[35,1794,153],{"class":45},[35,1796,133],{"class":101},[35,1798,452],{"class":45},[35,1800,133],{"class":101},[35,1802,158],{"class":45},[35,1804,133],{"class":101},[35,1806,272],{"class":45},[35,1808,133],{"class":101},[35,1810,364],{"class":45},[35,1812,1813],{"class":101},"], head)\n",[35,1815,1816,1819,1822],{"class":37,"line":1287},[35,1817,1818],{"class":101},"    ws.write_datetime(",[35,1820,1821],{"class":45},"\"A7\"",[35,1823,1824],{"class":101},", start.to_pydatetime(), day)\n",[35,1826,1827,1829,1832,1834,1837],{"class":37,"line":1317},[35,1828,1667],{"class":101},[35,1830,1831],{"class":45},"\"C7\"",[35,1833,133],{"class":101},[35,1835,1836],{"class":45},"\"Balance brought forward\"",[35,1838,184],{"class":101},[35,1840,1842,1845,1848,1850,1853],{"class":37,"line":1841},24,[35,1843,1844],{"class":101},"    ws.write_number(",[35,1846,1847],{"class":45},"\"E7\"",[35,1849,133],{"class":101},[35,1851,1852],{"class":319},"float",[35,1854,1855],{"class":101},"(opening[code]), money)\n",[35,1857,1859,1862,1864],{"class":37,"line":1858},25,[35,1860,1861],{"class":101},"    r ",[35,1863,124],{"class":97},[35,1865,1866],{"class":319}," 7\n",[35,1868,1870,1872,1875,1877],{"class":37,"line":1869},26,[35,1871,918],{"class":97},[35,1873,1874],{"class":101}," row ",[35,1876,924],{"class":97},[35,1878,1879],{"class":101}," lines.itertuples():\n",[35,1881,1883,1886,1888],{"class":37,"line":1882},27,[35,1884,1885],{"class":101},"        ws.write_datetime(r, ",[35,1887,564],{"class":319},[35,1889,1890],{"class":101},", row.Date.to_pydatetime(), day)\n",[35,1892,1894,1897,1899],{"class":37,"line":1893},28,[35,1895,1896],{"class":101},"        ws.write_row(r, ",[35,1898,610],{"class":319},[35,1900,1901],{"class":101},", [row.Type, row.Reference])\n",[35,1903,1905,1908,1910],{"class":37,"line":1904},29,[35,1906,1907],{"class":101},"        ws.write_number(r, ",[35,1909,654],{"class":319},[35,1911,1912],{"class":101},", row.Amount, money)\n",[35,1914,1916,1918,1920],{"class":37,"line":1915},30,[35,1917,1907],{"class":101},[35,1919,676],{"class":319},[35,1921,1922],{"class":101},", row.Balance, money)\n",[35,1924,1926,1929,1931],{"class":37,"line":1925},31,[35,1927,1928],{"class":101},"        r ",[35,1930,1227],{"class":97},[35,1932,1933],{"class":319}," 1\n",[35,1935,1937,1940,1942,1944,1947,1950,1952,1954,1956,1958,1960,1962,1964],{"class":37,"line":1936},32,[35,1938,1939],{"class":101},"    ws.write(r, ",[35,1941,654],{"class":319},[35,1943,133],{"class":101},[35,1945,1946],{"class":45},"\"Amount due\"",[35,1948,1949],{"class":101},", book.add_format({",[35,1951,1480],{"class":45},[35,1953,1075],{"class":101},[35,1955,1379],{"class":319},[35,1957,133],{"class":101},[35,1959,1590],{"class":45},[35,1961,1075],{"class":101},[35,1963,610],{"class":319},[35,1965,1966],{"class":101},"}))\n",[35,1968,1970,1973,1975,1977,1979],{"class":37,"line":1969},33,[35,1971,1972],{"class":101},"    ws.write_number(r, ",[35,1974,676],{"class":319},[35,1976,133],{"class":101},[35,1978,1852],{"class":319},[35,1980,1981],{"class":101},"(closing[code]), bold_money)\n",[35,1983,1985],{"class":37,"line":1984},34,[35,1986,115],{"emptyLinePlaceholder":114},[35,1988,1990,1992,1994],{"class":37,"line":1989},35,[35,1991,1861],{"class":101},[35,1993,1227],{"class":97},[35,1995,1996],{"class":319}," 2\n",[35,1998,2000,2002,2004,2006,2009],{"class":37,"line":1999},36,[35,2001,1939],{"class":101},[35,2003,564],{"class":319},[35,2005,133],{"class":101},[35,2007,2008],{"class":45},"\"Ageing\"",[35,2010,2011],{"class":101},", head)\n",[35,2013,2015,2017,2019],{"class":37,"line":2014},37,[35,2016,1064],{"class":101},[35,2018,124],{"class":97},[35,2020,2021],{"class":101}," ageing.loc[code]\n",[35,2023,2025,2028,2030,2033,2035,2037,2039,2042],{"class":37,"line":2024},38,[35,2026,2027],{"class":101},"    ws.write_row(r ",[35,2029,383],{"class":97},[35,2031,2032],{"class":319}," 1",[35,2034,133],{"class":101},[35,2036,564],{"class":319},[35,2038,133],{"class":101},[35,2040,2041],{"class":319},"list",[35,2043,2044],{"class":101},"(buckets.index), head)\n",[35,2046,2048,2050,2052,2055,2057,2059,2061,2063,2066,2068,2071,2073],{"class":37,"line":2047},39,[35,2049,2027],{"class":101},[35,2051,383],{"class":97},[35,2053,2054],{"class":319}," 2",[35,2056,133],{"class":101},[35,2058,564],{"class":319},[35,2060,1792],{"class":101},[35,2062,1852],{"class":319},[35,2064,2065],{"class":101},"(v) ",[35,2067,1002],{"class":97},[35,2069,2070],{"class":101}," v ",[35,2072,924],{"class":97},[35,2074,2075],{"class":101}," buckets], money)\n",[35,2077,2079,2082,2084,2086,2088],{"class":37,"line":2078},40,[35,2080,2081],{"class":101},"    ws.set_landscape(); ws.fit_to_pages(",[35,2083,610],{"class":319},[35,2085,133],{"class":101},[35,2087,564],{"class":319},[35,2089,184],{"class":101},[35,2091,2093],{"class":37,"line":2092},41,[35,2094,2095],{"class":101},"    book.close()\n",[35,2097,2099,2101,2104,2106,2108,2110,2112],{"class":37,"line":2098},42,[35,2100,996],{"class":97},[35,2102,2103],{"class":97}," f",[35,2105,1439],{"class":45},[35,2107,1442],{"class":493},[35,2109,32],{"class":101},[35,2111,1447],{"class":493},[35,2113,2114],{"class":45},"_2026-09.xlsx\"\n",[35,2116,2118],{"class":37,"line":2117},43,[35,2119,115],{"emptyLinePlaceholder":114},[35,2121,2123,2126,2128,2131,2133,2136,2138],{"class":37,"line":2122},44,[35,2124,2125],{"class":101},"paths ",[35,2127,124],{"class":97},[35,2129,2130],{"class":101}," [write_statement(c) ",[35,2132,1002],{"class":97},[35,2134,2135],{"class":101}," c ",[35,2137,924],{"class":97},[35,2139,2140],{"class":101}," customers.index]\n",[35,2142,2144,2146],{"class":37,"line":2143},45,[35,2145,328],{"class":319},[35,2147,2148],{"class":101},"(paths)\n",[10,2150,2151,2152,2156,2157,2161],{},"The money format shows negative amounts — payments and credits — in red. Balances are written as values rather than formulas so the statement shows the same figures in every viewer, including mail previews; the ledger remains the source of truth. Landscape orientation and fit-to-width make the statement print or convert to PDF on one page across. A statement sent as PDF rather than as a workbook is a natural next step: see ",[14,2153,2155],{"href":2154},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002F","Convert Excel to PDF on Linux with LibreOffice",", and ",[14,2158,2160],{"href":2159},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fpassword-protect-a-pdf-exported-from-excel\u002F","Password-Protect a PDF Exported from Excel"," for locking it.",[542,2163,551,2169,551,2172,551,2175,551,2180,551,2184,551,2188,551,2192,551,2196,551,2199,551,2202,551,2205,551,2208,551,2212,551,2215,551,2219,551,2221,551,2224,551,2227,551,2230,551,2232,551,2235,551,2237,551,2240,551,2242,551,2244,551,2246,551,2248,551,2250,551,2252,551,2254,551,2257,551,2259,551,2261,551,2263,551,2265,551,2267,551,2269,551,2271,551,2273,551,2275,551,2278,551,2280,551,2282,551,2284,551,2286,551,2288,551,2290,551,2292,551,2294,551,2296,551,2298,551,2300,551,2303,551,2305,551,2307,551,2309,551,2311,551,2313,551,2315,551,2317,551,2319,551,2321,551,2323,551,2325,551,2328,551,2330,551,2332,551,2334,551,2336,551,2338,551,2340,551,2342,551,2344,551,2347,551,2351,551,2353,551,2355,551,2357,551,2359,551,2362,551,2364,551,2366,551,2369,551,2373,551,2375,551,2378,551,2382,551,2386,551,2388,551,2391,551,2393,551,2396],{"viewBox":2164,"role":545,"ariaLabelledBy":2165,"xmlns":549,"style":2168},"102 0 556 252",[2166,2167],"st-layout-t","st-layout-d","width:100%;max-width:556px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[553,2170,2171],{"id":2166},"The statement layout for Oakridge Farms",[557,2173,2174],{"id":2167},"A header with the customer and period, a table starting with the balance brought forward of 860, the three September transactions with running balance, the amount due of 1,760, and an ageing row with 1,540 current and 220 in the 1 to 30 days bucket.",[561,2176],{"x":2177,"y":564,"width":2178,"height":2179,"fill":567},"102","556","252",[561,2181],{"x":2182,"y":2183,"width":570,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"118.0","16.0",[561,2185],{"x":2186,"y":2183,"width":2187,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"150.0","116.8",[581,2189,586],{"x":2190,"y":2191,"style":585},"208.4","33.0",[561,2193],{"x":2194,"y":2183,"width":2195,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"266.8","90.9",[581,2197,594],{"x":2198,"y":2191,"style":585},"312.3",[561,2200],{"x":2201,"y":2183,"width":717,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"357.8",[581,2203,602],{"x":2204,"y":2191,"style":585},"414.2",[561,2206],{"x":2207,"y":2183,"width":744,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"470.5",[581,2209,2211],{"x":2210,"y":2191,"style":585},"518.5","D",[561,2213],{"x":2214,"y":2183,"width":726,"height":572,"rx":564,"fill":573,"stroke":574,"style":575},"566.5",[581,2216,2218],{"x":2217,"y":2191,"style":585},"604.2","E",[561,2220],{"x":2182,"y":571,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},[581,2222,610],{"x":2223,"y":721,"style":585},"134.0",[561,2225],{"x":2186,"y":571,"width":2187,"height":605,"rx":564,"fill":2226,"stroke":794,"style":774},"#f0f4ff",[581,2228,477],{"x":2190,"y":721,"style":2229},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[561,2231],{"x":2194,"y":571,"width":2195,"height":605,"rx":564,"fill":2226,"stroke":794,"style":774},[581,2233,2234],{"x":2198,"y":721,"style":2229},"Type",[561,2236],{"x":2201,"y":571,"width":717,"height":605,"rx":564,"fill":2226,"stroke":794,"style":774},[581,2238,2239],{"x":2204,"y":721,"style":2229},"Reference",[561,2241],{"x":2207,"y":571,"width":744,"height":605,"rx":564,"fill":2226,"stroke":794,"style":774},[581,2243,625],{"x":2210,"y":721,"style":2229},[561,2245],{"x":2214,"y":571,"width":726,"height":605,"rx":564,"fill":2226,"stroke":794,"style":774},[581,2247,730],{"x":2217,"y":721,"style":2229},[561,2249],{"x":2182,"y":733,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},[581,2251,334],{"x":2223,"y":736,"style":585},[561,2253],{"x":2186,"y":733,"width":2187,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2255,2256],{"x":2190,"y":736,"style":636},"01 Sep 2026",[561,2258],{"x":2194,"y":733,"width":2195,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[561,2260],{"x":2201,"y":733,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2262,737],{"x":2204,"y":736,"style":636},[561,2264],{"x":2207,"y":733,"width":744,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[561,2266],{"x":2214,"y":733,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2268,647],{"x":2217,"y":736,"style":636},[561,2270],{"x":2182,"y":744,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},[581,2272,654],{"x":2223,"y":747,"style":585},[561,2274],{"x":2186,"y":744,"width":2187,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2276,2277],{"x":2190,"y":747,"style":636},"05 Sep 2026",[561,2279],{"x":2194,"y":744,"width":2195,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2281,64],{"x":2198,"y":747,"style":636},[561,2283],{"x":2201,"y":744,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2285,664],{"x":2204,"y":747,"style":636},[561,2287],{"x":2207,"y":744,"width":744,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2289,669],{"x":2210,"y":747,"style":636},[561,2291],{"x":2214,"y":744,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2293,752],{"x":2217,"y":747,"style":636},[561,2295],{"x":2182,"y":755,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},[581,2297,676],{"x":2223,"y":758,"style":585},[561,2299],{"x":2186,"y":755,"width":2187,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2301,2302],{"x":2190,"y":758,"style":636},"18 Sep 2026",[561,2304],{"x":2194,"y":755,"width":2195,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2306,68],{"x":2198,"y":758,"style":636},[561,2308],{"x":2201,"y":755,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2310,686],{"x":2204,"y":758,"style":636},[561,2312],{"x":2207,"y":755,"width":744,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2314,691],{"x":2210,"y":758,"style":636},[561,2316],{"x":2214,"y":755,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2318,763],{"x":2217,"y":758,"style":636},[561,2320],{"x":2182,"y":766,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},[581,2322,698],{"x":2223,"y":769,"style":585},[561,2324],{"x":2186,"y":766,"width":2187,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2326,2327],{"x":2190,"y":769,"style":636},"25 Sep 2026",[561,2329],{"x":2194,"y":766,"width":2195,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2331,72],{"x":2198,"y":769,"style":636},[561,2333],{"x":2201,"y":766,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2335,708],{"x":2204,"y":769,"style":636},[561,2337],{"x":2207,"y":766,"width":744,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2339,713],{"x":2210,"y":769,"style":636},[561,2341],{"x":2214,"y":766,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2343,778],{"x":2217,"y":769,"style":636},[561,2345],{"x":2182,"y":2346,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"180.0",[581,2348,2350],{"x":2223,"y":2349,"style":585},"199.0","6",[561,2352],{"x":2186,"y":2346,"width":2187,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[561,2354],{"x":2194,"y":2346,"width":2195,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[561,2356],{"x":2201,"y":2346,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[561,2358],{"x":2207,"y":2346,"width":744,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2360,2361],{"x":2210,"y":2349,"style":636},"Amount due",[561,2363],{"x":2214,"y":2346,"width":726,"height":605,"rx":564,"fill":772,"stroke":773,"style":774},[581,2365,778],{"x":2217,"y":2349,"style":777},[561,2367],{"x":2182,"y":2368,"width":570,"height":605,"rx":564,"fill":573,"stroke":574,"style":575},"208.0",[581,2370,2372],{"x":2223,"y":2371,"style":585},"227.0","7",[561,2374],{"x":2186,"y":2368,"width":2187,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2376,2377],{"x":2190,"y":2371,"style":636},"Current 1,540.00",[561,2379],{"x":2194,"y":2368,"width":2195,"height":605,"rx":564,"fill":2380,"stroke":2381,"style":774},"#fdefd8","var(--gold,#b4740a)",[581,2383,2385],{"x":2198,"y":2371,"style":2384},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","1-30 220.00",[561,2387],{"x":2201,"y":2368,"width":717,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2389,2390],{"x":2204,"y":2371,"style":636},"31-60 0.00",[561,2392],{"x":2207,"y":2368,"width":744,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[581,2394,2395],{"x":2210,"y":2371,"style":636},"Over 60 0.00",[561,2397],{"x":2214,"y":2368,"width":726,"height":605,"rx":564,"fill":567,"stroke":574,"style":575},[20,2399,2401],{"id":2400},"step-5-reconcile-every-statement-with-the-ledger","Step 5 — Reconcile every statement with the ledger",[10,2403,2404],{},"Statements go to customers, so a mistake is public. Before sending, check that each one ties out — the closing balance equals the ledger total, the ageing adds up to the closing balance, and the last running balance matches:",[25,2406,2408],{"className":88,"code":2407,"language":90,"meta":30,"style":30},"from openpyxl import load_workbook\n\nproblems = []\nfor code in customers.index:\n    ledger_total = round(upto_end.loc[upto_end[\"Customer\"] == code, \"Amount\"].sum(), 2)\n    if round(closing[code], 2) != ledger_total:\n        problems.append(f\"{code}: closing {closing[code]} != ledger {ledger_total}\")\n    if round(ageing.loc[code].sum(), 2) != ledger_total:\n        problems.append(f\"{code}: ageing adds to {ageing.loc[code].sum()}\")\n    ws = load_workbook(f\"statements\u002F{code}_2026-09.xlsx\")[\"Statement\"]\n    due = next(ws.cell(r, 5).value for r in range(1, ws.max_row + 1) if ws.cell(r, 4).value == \"Amount due\")\n    if round(due, 2) != ledger_total:\n        problems.append(f\"{code}: workbook shows {due}\")\nprint(\"problems:\", problems or \"none\", \"| total due:\", round(closing.sum(), 2))\n",[32,2409,2410,2422,2426,2436,2448,2477,2498,2537,2554,2582,2609,2666,2683,2711],{"__ignoreMap":30},[35,2411,2412,2414,2417,2419],{"class":37,"line":38},[35,2413,1348],{"class":97},[35,2415,2416],{"class":101}," openpyxl ",[35,2418,98],{"class":97},[35,2420,2421],{"class":101}," load_workbook\n",[35,2423,2424],{"class":37,"line":111},[35,2425,115],{"emptyLinePlaceholder":114},[35,2427,2428,2431,2433],{"class":37,"line":118},[35,2429,2430],{"class":101},"problems ",[35,2432,124],{"class":97},[35,2434,2435],{"class":101}," []\n",[35,2437,2438,2440,2443,2445],{"class":37,"line":164},[35,2439,1002],{"class":97},[35,2441,2442],{"class":101}," code ",[35,2444,924],{"class":97},[35,2446,2447],{"class":101}," customers.index:\n",[35,2449,2450,2453,2455,2457,2460,2462,2464,2466,2469,2471,2473,2475],{"class":37,"line":187},[35,2451,2452],{"class":101},"    ledger_total ",[35,2454,124],{"class":97},[35,2456,963],{"class":319},[35,2458,2459],{"class":101},"(upto_end.loc[upto_end[",[35,2461,148],{"class":45},[35,2463,205],{"class":101},[35,2465,439],{"class":97},[35,2467,2468],{"class":101}," code, ",[35,2470,272],{"class":45},[35,2472,410],{"class":101},[35,2474,334],{"class":319},[35,2476,184],{"class":101},[35,2478,2479,2482,2484,2487,2489,2492,2495],{"class":37,"line":192},[35,2480,2481],{"class":97},"    if",[35,2483,963],{"class":319},[35,2485,2486],{"class":101},"(closing[code], ",[35,2488,334],{"class":319},[35,2490,2491],{"class":101},") ",[35,2493,2494],{"class":97},"!=",[35,2496,2497],{"class":101}," ledger_total:\n",[35,2499,2500,2503,2505,2507,2509,2511,2513,2516,2518,2521,2523,2526,2528,2531,2533,2535],{"class":37,"line":214},[35,2501,2502],{"class":101},"        problems.append(",[35,2504,1436],{"class":97},[35,2506,490],{"class":45},[35,2508,1442],{"class":493},[35,2510,32],{"class":101},[35,2512,1447],{"class":493},[35,2514,2515],{"class":45},": closing ",[35,2517,1442],{"class":493},[35,2519,2520],{"class":101},"closing[code]",[35,2522,1447],{"class":493},[35,2524,2525],{"class":45}," != ledger ",[35,2527,1442],{"class":493},[35,2529,2530],{"class":101},"ledger_total",[35,2532,1447],{"class":493},[35,2534,490],{"class":45},[35,2536,184],{"class":101},[35,2538,2539,2541,2543,2546,2548,2550,2552],{"class":37,"line":251},[35,2540,2481],{"class":97},[35,2542,963],{"class":319},[35,2544,2545],{"class":101},"(ageing.loc[code].sum(), ",[35,2547,334],{"class":319},[35,2549,2491],{"class":101},[35,2551,2494],{"class":97},[35,2553,2497],{"class":101},[35,2555,2556,2558,2560,2562,2564,2566,2568,2571,2573,2576,2578,2580],{"class":37,"line":256},[35,2557,2502],{"class":101},[35,2559,1436],{"class":97},[35,2561,490],{"class":45},[35,2563,1442],{"class":493},[35,2565,32],{"class":101},[35,2567,1447],{"class":493},[35,2569,2570],{"class":45},": ageing adds to ",[35,2572,1442],{"class":493},[35,2574,2575],{"class":101},"ageing.loc[code].sum()",[35,2577,1447],{"class":493},[35,2579,490],{"class":45},[35,2581,184],{"class":101},[35,2583,2584,2586,2588,2591,2593,2595,2597,2599,2601,2603,2605,2607],{"class":37,"line":278},[35,2585,1457],{"class":101},[35,2587,124],{"class":97},[35,2589,2590],{"class":101}," load_workbook(",[35,2592,1436],{"class":97},[35,2594,1439],{"class":45},[35,2596,1442],{"class":493},[35,2598,32],{"class":101},[35,2600,1447],{"class":493},[35,2602,1450],{"class":45},[35,2604,269],{"class":101},[35,2606,1465],{"class":45},[35,2608,301],{"class":101},[35,2610,2611,2614,2616,2619,2622,2624,2627,2629,2632,2634,2637,2639,2641,2644,2646,2648,2650,2652,2655,2657,2659,2661,2664],{"class":37,"line":304},[35,2612,2613],{"class":101},"    due ",[35,2615,124],{"class":97},[35,2617,2618],{"class":319}," next",[35,2620,2621],{"class":101},"(ws.cell(r, ",[35,2623,698],{"class":319},[35,2625,2626],{"class":101},").value ",[35,2628,1002],{"class":97},[35,2630,2631],{"class":101}," r ",[35,2633,924],{"class":97},[35,2635,2636],{"class":319}," range",[35,2638,520],{"class":101},[35,2640,610],{"class":319},[35,2642,2643],{"class":101},", ws.max_row ",[35,2645,383],{"class":97},[35,2647,2032],{"class":319},[35,2649,2491],{"class":101},[35,2651,1013],{"class":97},[35,2653,2654],{"class":101}," ws.cell(r, ",[35,2656,676],{"class":319},[35,2658,2626],{"class":101},[35,2660,439],{"class":97},[35,2662,2663],{"class":45}," \"Amount due\"",[35,2665,184],{"class":101},[35,2667,2668,2670,2672,2675,2677,2679,2681],{"class":37,"line":325},[35,2669,2481],{"class":97},[35,2671,963],{"class":319},[35,2673,2674],{"class":101},"(due, ",[35,2676,334],{"class":319},[35,2678,2491],{"class":101},[35,2680,2494],{"class":97},[35,2682,2497],{"class":101},[35,2684,2685,2687,2689,2691,2693,2695,2697,2700,2702,2705,2707,2709],{"class":37,"line":1110},[35,2686,2502],{"class":101},[35,2688,1436],{"class":97},[35,2690,490],{"class":45},[35,2692,1442],{"class":493},[35,2694,32],{"class":101},[35,2696,1447],{"class":493},[35,2698,2699],{"class":45},": workbook shows ",[35,2701,1442],{"class":493},[35,2703,2704],{"class":101},"due",[35,2706,1447],{"class":493},[35,2708,490],{"class":45},[35,2710,184],{"class":101},[35,2712,2713,2715,2717,2720,2723,2726,2729,2731,2734,2736,2738,2741,2743],{"class":37,"line":1121},[35,2714,328],{"class":319},[35,2716,520],{"class":101},[35,2718,2719],{"class":45},"\"problems:\"",[35,2721,2722],{"class":101},", problems ",[35,2724,2725],{"class":97},"or",[35,2727,2728],{"class":45}," \"none\"",[35,2730,133],{"class":101},[35,2732,2733],{"class":45},"\"| total due:\"",[35,2735,133],{"class":101},[35,2737,1030],{"class":319},[35,2739,2740],{"class":101},"(closing.sum(), ",[35,2742,334],{"class":319},[35,2744,513],{"class":101},[10,2746,2747,2748,2752],{},"The check recomputes each balance straight from the ledger rather than from the intermediate tables, reads the written workbook back, and confirms all three agree. The sum of all closing balances should also equal the receivables control account in the general ledger — a one-line comparison worth adding if you have that figure. Only when the problem list is empty do the statements move on to emailing; ",[14,2749,2751],{"href":2750},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report"," covers that loop.",[542,2754,551,2760,551,2763,551,2766,551,2770,551,2776,551,2787,551,2792,551,2797,551,2802,551,2805,551,2808,551,2815,551,2820,551,2823,551,2827,551,2830,551,2833,551,2840,551,2845,551,2848,551,2852,551,2855,551,2858,551,2865,551,2870],{"viewBox":2755,"role":545,"ariaLabelledBy":2756,"xmlns":549,"style":2759},"4 0 752 125",[2757,2758],"st-checks-t","st-checks-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[553,2761,2762],{"id":2757},"Three figures that must agree before sending",[557,2764,2765],{"id":2758},"For each customer, the ledger total up to the period end, the closing balance computed for the statement, and the amount due read back from the written workbook are compared, together with the sum of the ageing buckets. Only statements where all agree are sent.",[561,2767],{"x":676,"y":564,"width":2768,"height":2769,"fill":567},"752","125",[561,2771],{"x":2772,"y":2183,"width":2773,"height":2774,"rx":2775,"fill":573,"stroke":574,"style":795},"20.0","153.0","93.0","12",[2777,2778,2780],"g",{"transform":2779},"translate(84.5,29.0) scale(1.000)",[2781,2782],"path",{"d":2783,"fill":2784,"stroke":2785,"style":2786},"M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6 M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[581,2788,2791],{"x":2789,"y":726,"style":2790},"96.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Ledger total",[581,2793,2796],{"x":2789,"y":2794,"style":2795},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","sum up to period end",[37,2798],{"x1":2799,"y1":2800,"x2":2801,"y2":2800,"stroke":794,"style":795},"178.0","62.5","200.0",[797,2803],{"points":2804,"fill":800},"204.0,62.5 195.0,67.5 195.0,57.5",[561,2806],{"x":2807,"y":2183,"width":2773,"height":2774,"rx":2775,"fill":2226,"stroke":794,"style":795},"209.0",[2777,2809,2811],{"transform":2810},"translate(273.5,29.0) scale(1.000)",[2781,2812],{"d":2813,"fill":2784,"stroke":2814,"style":2786},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--brand-strong,#4338ca)",[581,2816,2819],{"x":2817,"y":726,"style":2818},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Closing balance",[581,2821,2822],{"x":2817,"y":2794,"style":2795},"opening + period",[37,2824],{"x1":2825,"y1":2800,"x2":2826,"y2":2800,"stroke":794,"style":795},"367.0","389.0",[797,2828],{"points":2829,"fill":800},"393.0,62.5 384.0,67.5 384.0,57.5",[561,2831],{"x":2832,"y":2183,"width":2773,"height":2774,"rx":2775,"fill":2380,"stroke":2381,"style":795},"398.0",[2777,2834,2836],{"transform":2835},"translate(462.5,29.0) scale(1.000)",[2781,2837],{"d":2838,"fill":2784,"stroke":2839,"style":2786},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--gold-ink,#7a4e06)",[581,2841,2844],{"x":2842,"y":726,"style":2843},"474.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Ageing sum",[581,2846,2847],{"x":2842,"y":2794,"style":2795},"buckets + unapplied",[37,2849],{"x1":2850,"y1":2800,"x2":2851,"y2":2800,"stroke":794,"style":795},"556.0","578.0",[797,2853],{"points":2854,"fill":800},"582.0,62.5 573.0,67.5 573.0,57.5",[561,2856],{"x":2857,"y":2183,"width":2773,"height":2774,"rx":2775,"fill":772,"stroke":773,"style":795},"587.0",[2777,2859,2861],{"transform":2860},"translate(651.5,29.0) scale(1.000)",[2781,2862],{"d":2863,"fill":2784,"stroke":2864,"style":2786},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[581,2866,2869],{"x":2867,"y":726,"style":2868},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Workbook",[581,2871,2872],{"x":2867,"y":2794,"style":2795},"amount due read back",[20,2874,2876],{"id":2875},"common-pitfalls","Common pitfalls",[2878,2879,2880,2888,2894,2900,2906],"ul",{},[2881,2882,2883,2887],"li",{},[2884,2885,2886],"strong",{},"Dropping customers with no prior balance."," Reindex the opening balances over all customers.",[2881,2889,2890,2893],{},[2884,2891,2892],{},"Unstable line order."," Sort by date and reference so same-day lines keep their order.",[2881,2895,2896,2899],{},[2884,2897,2898],{},"Ageing from invoice dates instead of due dates."," Overdue days start at the due date.",[2881,2901,2902,2905],{},[2884,2903,2904],{},"Ignoring unapplied credit."," Show it so the ageing still adds up to the balance.",[2881,2907,2908,2911],{},[2884,2909,2910],{},"Sending without reconciling."," Tie each statement back to the ledger first.",[20,2913,2915],{"id":2914},"conclusion","Conclusion",[10,2917,2918],{},"Customer statements come straight out of a receivables ledger: sum everything before the period into a balance brought forward, list the period's transactions with a grouped cumulative sum for the running balance, and age the closing balance by allocating payments and credits to the oldest invoices first. Write one formatted workbook per customer with XlsxWriter, and reconcile closing balance, ageing and the written amount due against the ledger before anything is sent.",[20,2920,2922],{"id":2921},"frequently-asked-questions","Frequently asked questions",[10,2924,2925,2928],{},[2884,2926,2927],{},"How do I calculate a running balance per customer in pandas?","\nSort the transactions by customer and date, take groupby on the customer with cumsum of the amount, and add each customer's opening balance mapped onto the rows.",[10,2930,2931,2934],{},[2884,2932,2933],{},"How do I work out the balance brought forward?","\nSum all transactions dated before the statement period for each customer, and reindex over the full customer list with a zero fill so customers without earlier activity start at zero.",[10,2936,2937,2940],{},[2884,2938,2939],{},"How are statement ageing buckets calculated?","\nAllocate payments and credits to the oldest invoices first, then place each invoice's remaining open amount in a bucket by the number of days past its due date at the statement date.",[10,2942,2943,2946],{},[2884,2944,2945],{},"How do I check generated statements are correct?","\nCompare each statement's closing balance and ageing total with the ledger total for that customer, and read the written workbook back to confirm the amount due it shows.",[20,2948,2950],{"id":2949},"related","Related",[2878,2952,2953,2959,2966,2973],{},[2881,2954,2955,2956,2958],{},"Up one level: ",[14,2957,17],{"href":16}," — documents built from spreadsheet data.",[2881,2960,2961,2965],{},[14,2962,2964],{"href":2963},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-accounts-receivable-aging-report\u002F","Build an Accounts Receivable Aging Report"," — ageing across all customers.",[2881,2967,2968,2972],{},[14,2969,2971],{"href":2970},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-trial-balance-from-a-general-ledger\u002F","Build a Trial Balance from a General Ledger"," — the ledger-to-report pattern for accounts.",[2881,2974,2975,2979],{},[14,2976,2978],{"href":2977},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-an-html-report-from-excel-data-with-jinja2\u002F","Generate an HTML Report from Excel Data with Jinja2"," — another per-audience output.",[2981,2982,2983],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":30,"searchDepth":111,"depth":111,"links":2985},[2986,2987,2988,2989,2990,2991,2992,2993,2994,2995],{"id":22,"depth":111,"text":23},{"id":76,"depth":111,"text":77},{"id":347,"depth":111,"text":348},{"id":816,"depth":111,"text":817},{"id":1328,"depth":111,"text":1329},{"id":2400,"depth":111,"text":2401},{"id":2875,"depth":111,"text":2876},{"id":2914,"depth":111,"text":2915},{"id":2921,"depth":111,"text":2922},{"id":2949,"depth":111,"text":2950},"2026-10-02","Produce a statement workbook for every customer from one receivables ledger with pandas and XlsxWriter: opening balance brought forward, the period transactions with a running balance, closing balance, an ageing summary built by allocating payments to the oldest invoices, a statement layout with formats, and reconciliation checks against the ledger.","md",[3000,3002,3004,3006],{"q":2927,"a":3001},"Sort the transactions by customer and date, take groupby on the customer with cumsum of the amount, and add each customer's opening balance mapped onto the rows.",{"q":2933,"a":3003},"Sum all transactions dated before the statement period for each customer, and reindex over the full customer list with a zero fill so customers without earlier activity start at zero.",{"q":2939,"a":3005},"Allocate payments and credits to the oldest invoices first, then place each invoice's remaining open amount in a bucket by the number of days past its due date at the statement date.",{"q":2945,"a":3007},"Compare each statement's closing balance and ageing total with the ledger total for that customer, and read the written workbook back to confirm the amount due it shows.","generate customer account statements from a ledger",null,{"breadcrumb":3011},[3012,3015,3018],{"name":3013,"item":3014},"Home","\u002F",{"name":3016,"item":3017},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger",{"title":3021,"description":3022},"Generate Customer Account Statements from a Ledger in Python","Build one Excel statement per customer from a sales ledger: balance brought forward, running balance, closing balance, FIFO payment allocation for ageing buckets, XlsxWriter layout and checks that tie back to the ledger.","generate-customer-account-statements-from-a-ledger","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-customer-account-statements-from-a-ledger\u002Findex",[3026,3027],"report-layouts","pdf-export","how-to","qKx6xJh-ibHS6qksPsSvj9nUEmtsZklD_W-DaRTuIDY",[3031,3034],{"title":2978,"path":3032,"stem":3033,"children":-1},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-an-html-report-from-excel-data-with-jinja2","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-an-html-report-from-excel-data-with-jinja2\u002Findex",{"title":3035,"path":3036,"stem":3037,"children":-1},"Generate Invoices from an Excel Template with Python","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-invoices-from-an-excel-template","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-invoices-from-an-excel-template\u002Findex",[3039,3041],{"path":3040,"title":17},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates",{"path":3042,"title":3016},"\u002Fautomating-reporting-workflows",[],[3045,3049,3052,3055,3058,3061],{"path":3046,"title":3047,"kind":3048},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports","Building Finance and Operations Reports in Excel with Python","guide",{"path":3050,"title":3051,"kind":3028},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments","Allocate Shared Costs Across Departments with Python",{"path":3053,"title":3054,"kind":3028},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report","Build a Bank Reconciliation Report with Python and Excel",{"path":3056,"title":3057,"kind":3028},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup","Build a Bill of Materials Cost Rollup",{"path":3059,"title":3060,"kind":3028},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-break-even-analysis-in-excel-with-python","Build a Break-Even Analysis in Excel with Python",{"path":3062,"title":3063,"kind":3064},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report","Build a Budget vs Actual Variance Report in Excel with Python","compare",1790972138026]