[{"data":1,"prerenderedAt":2194},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements":2159,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements":2168,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements":2173,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements":2174},{"id":4,"title":5,"body":6,"dateModified":2124,"datePublished":2124,"description":2125,"extension":2126,"faq":2127,"keyword":2136,"kind":2137,"meta":2138,"navigation":88,"path":2148,"seo":2149,"slug":2152,"stem":2153,"topics":2154,"type":2157,"__hash__":2158},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements\u002Findex.md","Build a Cash Position Report from Bank Statements",{"type":7,"value":8,"toc":2113},"minimark",[9,19,24,53,56,60,660,668,825,829,1086,1089,1155,1159,1549,1560,1752,1756,1986,1993,1997,2032,2036,2039,2043,2049,2055,2072,2078,2082,2109],[10,11,12,13,18],"p",{},"Every morning, someone in treasury or finance answers the same question: how much cash do we have, where is it, and is any account close to its limit? The answer lives in yesterday's bank statements, which arrive as CSV exports from each bank's portal — every bank with its own columns, date format and sign convention. Pasting them into a spreadsheet each day is slow and error-prone, and a mistake in a cash report is the kind that gets noticed when a payment bounces. This page builds the daily cash position with pandas: one small mapping per bank turns its export into a standard table, each statement is checked to add up from its opening to its closing balance, balances are converted to sterling, every account is compared with its minimum balance and overdraft limit, and the result is a one-page Excel report with the day's largest movements. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55],{},"Three statement exports for 1 October 2026: the main sterling account at one bank, with separate debit and credit columns and day-first dates; the payroll account at another, semicolon-separated with decimal commas and signed amounts; and a euro account at a German bank with German headings and a currency column. Each starts with an opening balance row, and each row carries the running balance, as most bank exports do.",[20,57,59],{"id":58},"step-1-one-mapping-per-bank-one-standard-table","Step 1 — One mapping per bank, one standard table",[25,61,65],{"className":62,"code":63,"language":64,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nBANKS = {\n    \"barclays_main.csv\": {\"account\": \"GBP Main\", \"currency\": \"GBP\", \"read\": {},\n                          \"date\": (\"Date\", \"%d\u002F%m\u002F%Y\"), \"text\": \"Narrative\",\n                          \"amount\": lambda d: d[\"Credit\"].fillna(0) - d[\"Debit\"].fillna(0), \"balance\": \"Balance\"},\n    \"hsbc_payroll.csv\":  {\"account\": \"GBP Payroll\", \"currency\": \"GBP\", \"read\": {\"sep\": \";\", \"decimal\": \",\"},\n                          \"date\": (\"Date\", \"%Y-%m-%d\"), \"text\": \"Description\",\n                          \"amount\": lambda d: d[\"Amount\"].fillna(0), \"balance\": \"Running balance\"},\n    \"deutsche_eur.csv\":  {\"account\": \"EUR Berlin\", \"currency\": \"EUR\", \"read\": {},\n                          \"date\": (\"Buchungstag\", \"%d.%m.%Y\"), \"text\": \"Verwendungszweck\",\n                          \"amount\": lambda d: d[\"Betrag\"].fillna(0), \"balance\": \"Saldo\"},\n}\n\nframes = []\nfor file, m in BANKS.items():\n    raw = pd.read_csv(f\"statements\u002F{file}\", **m[\"read\"])\n    col, fmt = m[\"date\"]\n    frames.append(pd.DataFrame({\"Account\": m[\"account\"], \"Currency\": m[\"currency\"],\n                                \"Date\": pd.to_datetime(raw[col], format=fmt), \"Text\": raw[m[\"text\"]],\n                                \"Amount\": m[\"amount\"](raw).round(2), \"Balance\": raw[m[\"balance\"]].round(2)}))\ntx = pd.concat(frames, ignore_index=True)\nprint(tx.to_string(index=False))\n","python",[32,66,67,83,90,103,140,178,230,280,309,338,368,398,427,433,438,450,472,513,530,557,585,618,640],{"__ignoreMap":30},[35,68,69,73,77,80],{"class":37,"line":38},[35,70,72],{"class":71},"s-kum","import",[35,74,76],{"class":75},"skGVy"," pandas ",[35,78,79],{"class":71},"as",[35,81,82],{"class":75}," pd\n",[35,84,86],{"class":37,"line":85},2,[35,87,89],{"emptyLinePlaceholder":88},true,"\n",[35,91,93,97,100],{"class":37,"line":92},3,[35,94,96],{"class":95},"sP0c6","BANKS",[35,98,99],{"class":71}," =",[35,101,102],{"class":75}," {\n",[35,104,106,109,112,115,118,121,124,127,129,132,134,137],{"class":37,"line":105},4,[35,107,108],{"class":45},"    \"barclays_main.csv\"",[35,110,111],{"class":75},": {",[35,113,114],{"class":45},"\"account\"",[35,116,117],{"class":75},": ",[35,119,120],{"class":45},"\"GBP Main\"",[35,122,123],{"class":75},", ",[35,125,126],{"class":45},"\"currency\"",[35,128,117],{"class":75},[35,130,131],{"class":45},"\"GBP\"",[35,133,123],{"class":75},[35,135,136],{"class":45},"\"read\"",[35,138,139],{"class":75},": {},\n",[35,141,143,146,149,152,154,157,161,164,167,170,172,175],{"class":37,"line":142},5,[35,144,145],{"class":45},"                          \"date\"",[35,147,148],{"class":75},": (",[35,150,151],{"class":45},"\"Date\"",[35,153,123],{"class":75},[35,155,156],{"class":45},"\"",[35,158,160],{"class":159},"sSjpA","%d",[35,162,163],{"class":45},"\u002F%m\u002F%Y\"",[35,165,166],{"class":75},"), ",[35,168,169],{"class":45},"\"text\"",[35,171,117],{"class":75},[35,173,174],{"class":45},"\"Narrative\"",[35,176,177],{"class":75},",\n",[35,179,181,184,186,189,192,195,198,201,204,207,210,213,215,217,219,222,224,227],{"class":37,"line":180},6,[35,182,183],{"class":45},"                          \"amount\"",[35,185,117],{"class":75},[35,187,188],{"class":71},"lambda",[35,190,191],{"class":75}," d: d[",[35,193,194],{"class":45},"\"Credit\"",[35,196,197],{"class":75},"].fillna(",[35,199,200],{"class":95},"0",[35,202,203],{"class":75},") ",[35,205,206],{"class":71},"-",[35,208,209],{"class":75}," d[",[35,211,212],{"class":45},"\"Debit\"",[35,214,197],{"class":75},[35,216,200],{"class":95},[35,218,166],{"class":75},[35,220,221],{"class":45},"\"balance\"",[35,223,117],{"class":75},[35,225,226],{"class":45},"\"Balance\"",[35,228,229],{"class":75},"},\n",[35,231,233,236,239,241,243,246,248,250,252,254,256,258,260,263,265,268,270,273,275,278],{"class":37,"line":232},7,[35,234,235],{"class":45},"    \"hsbc_payroll.csv\"",[35,237,238],{"class":75},":  {",[35,240,114],{"class":45},[35,242,117],{"class":75},[35,244,245],{"class":45},"\"GBP Payroll\"",[35,247,123],{"class":75},[35,249,126],{"class":45},[35,251,117],{"class":75},[35,253,131],{"class":45},[35,255,123],{"class":75},[35,257,136],{"class":45},[35,259,111],{"class":75},[35,261,262],{"class":45},"\"sep\"",[35,264,117],{"class":75},[35,266,267],{"class":45},"\";\"",[35,269,123],{"class":75},[35,271,272],{"class":45},"\"decimal\"",[35,274,117],{"class":75},[35,276,277],{"class":45},"\",\"",[35,279,229],{"class":75},[35,281,283,285,287,289,291,294,296,298,300,302,304,307],{"class":37,"line":282},8,[35,284,145],{"class":45},[35,286,148],{"class":75},[35,288,151],{"class":45},[35,290,123],{"class":75},[35,292,293],{"class":45},"\"%Y-%m-",[35,295,160],{"class":159},[35,297,156],{"class":45},[35,299,166],{"class":75},[35,301,169],{"class":45},[35,303,117],{"class":75},[35,305,306],{"class":45},"\"Description\"",[35,308,177],{"class":75},[35,310,312,314,316,318,320,323,325,327,329,331,333,336],{"class":37,"line":311},9,[35,313,183],{"class":45},[35,315,117],{"class":75},[35,317,188],{"class":71},[35,319,191],{"class":75},[35,321,322],{"class":45},"\"Amount\"",[35,324,197],{"class":75},[35,326,200],{"class":95},[35,328,166],{"class":75},[35,330,221],{"class":45},[35,332,117],{"class":75},[35,334,335],{"class":45},"\"Running balance\"",[35,337,229],{"class":75},[35,339,341,344,346,348,350,353,355,357,359,362,364,366],{"class":37,"line":340},10,[35,342,343],{"class":45},"    \"deutsche_eur.csv\"",[35,345,238],{"class":75},[35,347,114],{"class":45},[35,349,117],{"class":75},[35,351,352],{"class":45},"\"EUR Berlin\"",[35,354,123],{"class":75},[35,356,126],{"class":45},[35,358,117],{"class":75},[35,360,361],{"class":45},"\"EUR\"",[35,363,123],{"class":75},[35,365,136],{"class":45},[35,367,139],{"class":75},[35,369,371,373,375,378,380,382,384,387,389,391,393,396],{"class":37,"line":370},11,[35,372,145],{"class":45},[35,374,148],{"class":75},[35,376,377],{"class":45},"\"Buchungstag\"",[35,379,123],{"class":75},[35,381,156],{"class":45},[35,383,160],{"class":159},[35,385,386],{"class":45},".%m.%Y\"",[35,388,166],{"class":75},[35,390,169],{"class":45},[35,392,117],{"class":75},[35,394,395],{"class":45},"\"Verwendungszweck\"",[35,397,177],{"class":75},[35,399,401,403,405,407,409,412,414,416,418,420,422,425],{"class":37,"line":400},12,[35,402,183],{"class":45},[35,404,117],{"class":75},[35,406,188],{"class":71},[35,408,191],{"class":75},[35,410,411],{"class":45},"\"Betrag\"",[35,413,197],{"class":75},[35,415,200],{"class":95},[35,417,166],{"class":75},[35,419,221],{"class":45},[35,421,117],{"class":75},[35,423,424],{"class":45},"\"Saldo\"",[35,426,229],{"class":75},[35,428,430],{"class":37,"line":429},13,[35,431,432],{"class":75},"}\n",[35,434,436],{"class":37,"line":435},14,[35,437,89],{"emptyLinePlaceholder":88},[35,439,441,444,447],{"class":37,"line":440},15,[35,442,443],{"class":75},"frames ",[35,445,446],{"class":71},"=",[35,448,449],{"class":75}," []\n",[35,451,453,456,460,463,466,469],{"class":37,"line":452},16,[35,454,455],{"class":71},"for",[35,457,459],{"class":458},"sa561"," file",[35,461,462],{"class":75},", m ",[35,464,465],{"class":71},"in",[35,467,468],{"class":95}," BANKS",[35,470,471],{"class":75},".items():\n",[35,473,475,478,480,483,486,489,492,495,498,500,502,505,508,510],{"class":37,"line":474},17,[35,476,477],{"class":75},"    raw ",[35,479,446],{"class":71},[35,481,482],{"class":75}," pd.read_csv(",[35,484,485],{"class":71},"f",[35,487,488],{"class":45},"\"statements\u002F",[35,490,491],{"class":159},"{",[35,493,494],{"class":458},"file",[35,496,497],{"class":159},"}",[35,499,156],{"class":45},[35,501,123],{"class":75},[35,503,504],{"class":71},"**",[35,506,507],{"class":75},"m[",[35,509,136],{"class":45},[35,511,512],{"class":75},"])\n",[35,514,516,519,521,524,527],{"class":37,"line":515},18,[35,517,518],{"class":75},"    col, fmt ",[35,520,446],{"class":71},[35,522,523],{"class":75}," m[",[35,525,526],{"class":45},"\"date\"",[35,528,529],{"class":75},"]\n",[35,531,533,536,539,542,544,547,550,552,554],{"class":37,"line":532},19,[35,534,535],{"class":75},"    frames.append(pd.DataFrame({",[35,537,538],{"class":45},"\"Account\"",[35,540,541],{"class":75},": m[",[35,543,114],{"class":45},[35,545,546],{"class":75},"], ",[35,548,549],{"class":45},"\"Currency\"",[35,551,541],{"class":75},[35,553,126],{"class":45},[35,555,556],{"class":75},"],\n",[35,558,560,563,566,569,571,574,577,580,582],{"class":37,"line":559},20,[35,561,562],{"class":45},"                                \"Date\"",[35,564,565],{"class":75},": pd.to_datetime(raw[col], ",[35,567,568],{"class":458},"format",[35,570,446],{"class":71},[35,572,573],{"class":75},"fmt), ",[35,575,576],{"class":45},"\"Text\"",[35,578,579],{"class":75},": raw[m[",[35,581,169],{"class":45},[35,583,584],{"class":75},"]],\n",[35,586,588,591,593,596,599,602,604,606,608,610,613,615],{"class":37,"line":587},21,[35,589,590],{"class":45},"                                \"Amount\"",[35,592,541],{"class":75},[35,594,595],{"class":45},"\"amount\"",[35,597,598],{"class":75},"](raw).round(",[35,600,601],{"class":95},"2",[35,603,166],{"class":75},[35,605,226],{"class":45},[35,607,579],{"class":75},[35,609,221],{"class":45},[35,611,612],{"class":75},"]].round(",[35,614,601],{"class":95},[35,616,617],{"class":75},")}))\n",[35,619,621,624,626,629,632,634,637],{"class":37,"line":620},22,[35,622,623],{"class":75},"tx ",[35,625,446],{"class":71},[35,627,628],{"class":75}," pd.concat(frames, ",[35,630,631],{"class":458},"ignore_index",[35,633,446],{"class":71},[35,635,636],{"class":95},"True",[35,638,639],{"class":75},")\n",[35,641,643,646,649,652,654,657],{"class":37,"line":642},23,[35,644,645],{"class":95},"print",[35,647,648],{"class":75},"(tx.to_string(",[35,650,651],{"class":458},"index",[35,653,446],{"class":71},[35,655,656],{"class":95},"False",[35,658,659],{"class":75},"))\n",[10,661,662,663,667],{},"Each bank gets a small dictionary that says how to read its file and where each standard field comes from: the date column and its format, the description, how to compute a signed amount — credits minus debits for the bank that splits them, the amount as it stands for the others — and the running balance. The reading options handle the European formatting of the payroll export, with semicolons between fields and commas as decimal separators. Dates are parsed with an explicit format for each bank, so ",[664,665,666],"em",{},"01\u002F10\u002F2026"," is read as 1 October and never as 10 January. Adding a fourth bank means adding one entry, not changing the code, and a bank that changes its export layout — which happens more often than it should — is fixed in one place. The result is one table with the same six columns for every account, which is all the rest of the report needs.",[669,670,678,679,678,683,678,687,678,694,678,704,678,711,678,716,678,722,678,727,678,731,678,737,678,740,678,743,678,747,678,751,678,754,678,757,678,762,678,766,678,770,678,774,678,777,678,782,678,788,678,791,678,795,678,798,678,802,678,806,678,809,678,814,678,819,678,822],"svg",{"viewBox":671,"role":672,"ariaLabelledBy":673,"xmlns":676,"style":677},"4 0 752 144","img",[674,675],"cash-flow-t","cash-flow-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  ",[680,681,682],"title",{"id":674},"From bank exports to a cash position",[684,685,686],"desc",{"id":675},"Each bank's CSV is read with its own options and mapped to a standard table of account, currency, date, text, signed amount and balance. Each statement is checked to add up, balances are converted to sterling and compared with limits, and the report is written.",[688,689],"rect",{"x":690,"y":200,"width":691,"height":692,"fill":693},"4","752","144","#ffffff",[688,695],{"x":696,"y":697,"width":698,"height":699,"rx":700,"fill":701,"stroke":702,"style":703},"20.0","16.0","115.2","111.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[705,706,710],"text",{"x":707,"y":708,"style":709},"77.6","67.8","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Bank CSVs",[705,712,715],{"x":707,"y":713,"style":714},"87.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","three layouts",[37,717],{"x1":718,"y1":719,"x2":720,"y2":719,"stroke":721,"style":703},"140.2","71.8","162.2","var(--brand,#5b5cf0)",[723,724],"polygon",{"points":725,"fill":726},"166.2,71.8 157.2,76.8 157.2,66.8","#5b5cf0",[688,728],{"x":729,"y":697,"width":698,"height":699,"rx":700,"fill":730,"stroke":721,"style":703},"171.2","#f0f4ff",[705,732,736],{"x":733,"y":734,"style":735},"228.8","50.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Map per",[705,738,739],{"x":733,"y":708,"style":735},"bank",[705,741,742],{"x":733,"y":713,"style":714},"date format,",[705,744,746],{"x":733,"y":745,"style":714},"104.8","signs",[37,748],{"x1":749,"y1":719,"x2":750,"y2":719,"stroke":721,"style":703},"291.4","313.4",[723,752],{"points":753,"fill":726},"317.4,71.8 308.4,76.8 308.4,66.8",[688,755],{"x":756,"y":697,"width":698,"height":699,"rx":700,"fill":730,"stroke":721,"style":703},"322.4",[705,758,761],{"x":759,"y":760,"style":735},"380.0","59.0","Standard",[705,763,765],{"x":759,"y":764,"style":735},"76.5","table",[705,767,769],{"x":759,"y":768,"style":714},"96.0","six columns",[37,771],{"x1":772,"y1":719,"x2":773,"y2":719,"stroke":721,"style":703},"442.6","464.6",[723,775],{"points":776,"fill":726},"468.6,71.8 459.6,76.8 459.6,66.8",[688,778],{"x":779,"y":697,"width":698,"height":699,"rx":700,"fill":780,"stroke":781,"style":703},"473.6","#fdefd8","var(--gold,#b4740a)",[705,783,787],{"x":784,"y":785,"style":786},"531.2","41.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Check each",[705,789,790],{"x":784,"y":760,"style":786},"statement",[705,792,794],{"x":784,"y":793,"style":714},"78.5","opening +",[705,796,797],{"x":784,"y":768,"style":714},"movements =",[705,799,801],{"x":784,"y":800,"style":714},"113.5","closing",[37,803],{"x1":804,"y1":719,"x2":805,"y2":719,"stroke":721,"style":703},"593.8","615.8",[723,807],{"points":808,"fill":726},"619.8,71.8 610.8,76.8 610.8,66.8",[688,810],{"x":811,"y":697,"width":698,"height":699,"rx":700,"fill":812,"stroke":813,"style":703},"624.8","#d9f4f1","var(--teal,#0f9488)",[705,815,818],{"x":816,"y":760,"style":817},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Position in",[705,820,821],{"x":816,"y":764,"style":817},"GBP",[705,823,824],{"x":816,"y":768,"style":714},"against limits",[20,826,828],{"id":827},"step-2-prove-each-statement-adds-up","Step 2 — Prove each statement adds up",[25,830,832],{"className":62,"code":831,"language":64,"meta":30,"style":30},"checks = []\nfor account, g in tx.groupby(\"Account\", sort=False):\n    opening = g[\"Balance\"].iloc[0]\n    movements = g[\"Amount\"].iloc[1:].sum().round(2)\n    closing = g[\"Balance\"].iloc[-1]\n    running_ok = (g[\"Balance\"].diff().iloc[1:].round(2) == g[\"Amount\"].iloc[1:]).all()\n    checks.append({\"Account\": account, \"Opening\": opening, \"Movements\": movements, \"Closing\": closing,\n                   \"Adds up\": round(opening + movements, 2) == closing, \"Running balance OK\": bool(running_ok)})\nchecks = pd.DataFrame(checks)\nprint(checks.to_string(index=False))\nassert checks[\"Adds up\"].all() and checks[\"Running balance OK\"].all(), \"a statement does not reconcile\"\n",[32,833,834,843,869,888,911,930,968,996,1035,1044,1059],{"__ignoreMap":30},[35,835,836,839,841],{"class":37,"line":38},[35,837,838],{"class":75},"checks ",[35,840,446],{"class":71},[35,842,449],{"class":75},[35,844,845,847,850,852,855,857,859,862,864,866],{"class":37,"line":85},[35,846,455],{"class":71},[35,848,849],{"class":75}," account, g ",[35,851,465],{"class":71},[35,853,854],{"class":75}," tx.groupby(",[35,856,538],{"class":45},[35,858,123],{"class":75},[35,860,861],{"class":458},"sort",[35,863,446],{"class":71},[35,865,656],{"class":95},[35,867,868],{"class":75},"):\n",[35,870,871,874,876,879,881,884,886],{"class":37,"line":92},[35,872,873],{"class":75},"    opening ",[35,875,446],{"class":71},[35,877,878],{"class":75}," g[",[35,880,226],{"class":45},[35,882,883],{"class":75},"].iloc[",[35,885,200],{"class":95},[35,887,529],{"class":75},[35,889,890,893,895,897,899,901,904,907,909],{"class":37,"line":105},[35,891,892],{"class":75},"    movements ",[35,894,446],{"class":71},[35,896,878],{"class":75},[35,898,322],{"class":45},[35,900,883],{"class":75},[35,902,903],{"class":95},"1",[35,905,906],{"class":75},":].sum().round(",[35,908,601],{"class":95},[35,910,639],{"class":75},[35,912,913,916,918,920,922,924,926,928],{"class":37,"line":142},[35,914,915],{"class":75},"    closing ",[35,917,446],{"class":71},[35,919,878],{"class":75},[35,921,226],{"class":45},[35,923,883],{"class":75},[35,925,206],{"class":71},[35,927,903],{"class":95},[35,929,529],{"class":75},[35,931,932,935,937,940,942,945,947,950,952,954,957,959,961,963,965],{"class":37,"line":180},[35,933,934],{"class":75},"    running_ok ",[35,936,446],{"class":71},[35,938,939],{"class":75}," (g[",[35,941,226],{"class":45},[35,943,944],{"class":75},"].diff().iloc[",[35,946,903],{"class":95},[35,948,949],{"class":75},":].round(",[35,951,601],{"class":95},[35,953,203],{"class":75},[35,955,956],{"class":71},"==",[35,958,878],{"class":75},[35,960,322],{"class":45},[35,962,883],{"class":75},[35,964,903],{"class":95},[35,966,967],{"class":75},":]).all()\n",[35,969,970,973,975,978,981,984,987,990,993],{"class":37,"line":232},[35,971,972],{"class":75},"    checks.append({",[35,974,538],{"class":45},[35,976,977],{"class":75},": account, ",[35,979,980],{"class":45},"\"Opening\"",[35,982,983],{"class":75},": opening, ",[35,985,986],{"class":45},"\"Movements\"",[35,988,989],{"class":75},": movements, ",[35,991,992],{"class":45},"\"Closing\"",[35,994,995],{"class":75},": closing,\n",[35,997,998,1001,1003,1006,1009,1012,1015,1017,1019,1021,1024,1027,1029,1032],{"class":37,"line":282},[35,999,1000],{"class":45},"                   \"Adds up\"",[35,1002,117],{"class":75},[35,1004,1005],{"class":95},"round",[35,1007,1008],{"class":75},"(opening ",[35,1010,1011],{"class":71},"+",[35,1013,1014],{"class":75}," movements, ",[35,1016,601],{"class":95},[35,1018,203],{"class":75},[35,1020,956],{"class":71},[35,1022,1023],{"class":75}," closing, ",[35,1025,1026],{"class":45},"\"Running balance OK\"",[35,1028,117],{"class":75},[35,1030,1031],{"class":95},"bool",[35,1033,1034],{"class":75},"(running_ok)})\n",[35,1036,1037,1039,1041],{"class":37,"line":311},[35,1038,838],{"class":75},[35,1040,446],{"class":71},[35,1042,1043],{"class":75}," pd.DataFrame(checks)\n",[35,1045,1046,1048,1051,1053,1055,1057],{"class":37,"line":340},[35,1047,645],{"class":95},[35,1049,1050],{"class":75},"(checks.to_string(",[35,1052,651],{"class":458},[35,1054,446],{"class":71},[35,1056,656],{"class":95},[35,1058,659],{"class":75},[35,1060,1061,1064,1067,1070,1073,1076,1078,1080,1083],{"class":37,"line":370},[35,1062,1063],{"class":71},"assert",[35,1065,1066],{"class":75}," checks[",[35,1068,1069],{"class":45},"\"Adds up\"",[35,1071,1072],{"class":75},"].all() ",[35,1074,1075],{"class":71},"and",[35,1077,1066],{"class":75},[35,1079,1026],{"class":45},[35,1081,1082],{"class":75},"].all(), ",[35,1084,1085],{"class":45},"\"a statement does not reconcile\"\n",[10,1087,1088],{},"A cash report is only as good as its inputs, and bank exports can be truncated, filtered or downloaded for the wrong date range. Two checks catch that. The opening balance plus all the day's movements must equal the closing balance. And each row's change in running balance must equal its amount, which catches a missing or duplicated line even when the totals happen to agree. If either check fails, the report stops instead of publishing a wrong position, and the message names the account so the right export can be downloaded again. Here all three statements reconcile: the main account goes from 412,300.55 to 41,372.15, the payroll account from 15,400.00 to 11,189.63, and the euro account from 88,200.00 to 100,100.00.",[669,1090,678,1095,678,1098,678,1101,678,1104,678,1109,678,1115,678,1121,678,1126,678,1130,678,1134,678,1137,678,1142,678,1146,678,1149,678,1152],{"viewBox":1091,"role":672,"ariaLabelledBy":1092,"xmlns":676,"style":677},"4 0 752 173",[1093,1094],"cash-checks-t","cash-checks-d",[680,1096,1097],{"id":1093},"Two checks on every statement",[684,1099,1100],{"id":1094},"The totals check compares the opening balance plus all movements with the closing balance and catches truncated or filtered exports. The running balance check compares each row's change in balance with its amount and catches a missing or duplicated line even when totals agree.",[688,1102],{"x":690,"y":200,"width":691,"height":1103,"fill":693},"173",[688,1105],{"x":696,"y":697,"width":1106,"height":1107,"rx":1108,"fill":730,"stroke":721,"style":703},"351.0","141.0","14",[705,1110,1114],{"x":1111,"y":1112,"style":1113},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Totals check",[37,1116],{"x1":1117,"y1":1118,"x2":1119,"y2":1118,"stroke":721,"style":1120},"36.0","57.0","355.0","stroke-width:1px",[705,1122,1125],{"x":1111,"y":1123,"style":1124},"77.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","opening + movements = closing",[705,1127,1129],{"x":1111,"y":1128,"style":1124},"103.0","catches truncated exports",[705,1131,1133],{"x":1111,"y":1132,"style":1124},"129.0","catches wrong date ranges",[688,1135],{"x":1136,"y":697,"width":1106,"height":1107,"rx":1108,"fill":812,"stroke":813,"style":703},"389.0",[705,1138,1141],{"x":1139,"y":1112,"style":1140},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Running balance check",[37,1143],{"x1":1144,"y1":1118,"x2":1145,"y2":1118,"stroke":813,"style":1120},"405.0","724.0",[705,1147,1148],{"x":1139,"y":1123,"style":1124},"balance change = amount, row by row",[705,1150,1151],{"x":1139,"y":1128,"style":1124},"catches a missing line",[705,1153,1154],{"x":1139,"y":1132,"style":1124},"catches a duplicated line",[20,1156,1158],{"id":1157},"step-3-the-position-in-sterling-against-limits","Step 3 — The position in sterling, against limits",[25,1160,1162],{"className":62,"code":1161,"language":64,"meta":30,"style":30},"fx = {\"GBP\": 1.0, \"EUR\": 0.8612}                                  # closing rate, GBP per unit\nlimits = pd.DataFrame({\"Account\": [\"GBP Main\", \"GBP Payroll\", \"EUR Berlin\"],\n                       \"Minimum balance\": [150_000, 10_000, 20_000], \"Overdraft limit\": [250_000, 0, 0]})\n\nposition = (checks[[\"Account\", \"Opening\", \"Closing\"]]\n            .merge(tx.groupby(\"Account\")[\"Currency\"].first().reset_index(), on=\"Account\")\n            .merge(limits, on=\"Account\"))\nposition[\"Closing GBP\"] = (position[\"Closing\"] * position[\"Currency\"].map(fx)).round(2)\nposition[\"Headroom over minimum\"] = (position[\"Closing\"] - position[\"Minimum balance\"]).round(2)\nposition[\"Available incl. overdraft\"] = (position[\"Closing\"] + position[\"Overdraft limit\"]).round(2)\nposition[\"Status\"] = position[\"Headroom over minimum\"].map(lambda h: \"BELOW MINIMUM\" if h \u003C 0 else (\"TIGHT\" if h \u003C 5_000 else \"OK\"))\nprint(position[[\"Account\", \"Currency\", \"Closing\", \"Closing GBP\", \"Headroom over minimum\", \"Status\"]].to_string(index=False))\nprint(\"total cash (GBP):\", position[\"Closing GBP\"].sum().round(2))\n",[32,1163,1164,1197,1224,1265,1269,1292,1316,1329,1364,1395,1424,1487,1527],{"__ignoreMap":30},[35,1165,1166,1169,1171,1174,1176,1178,1181,1183,1185,1187,1190,1193],{"class":37,"line":38},[35,1167,1168],{"class":75},"fx ",[35,1170,446],{"class":71},[35,1172,1173],{"class":75}," {",[35,1175,131],{"class":45},[35,1177,117],{"class":75},[35,1179,1180],{"class":95},"1.0",[35,1182,123],{"class":75},[35,1184,361],{"class":45},[35,1186,117],{"class":75},[35,1188,1189],{"class":95},"0.8612",[35,1191,1192],{"class":75},"}                                  ",[35,1194,1196],{"class":1195},"s-wDw","# closing rate, GBP per unit\n",[35,1198,1199,1202,1204,1207,1209,1212,1214,1216,1218,1220,1222],{"class":37,"line":85},[35,1200,1201],{"class":75},"limits ",[35,1203,446],{"class":71},[35,1205,1206],{"class":75}," pd.DataFrame({",[35,1208,538],{"class":45},[35,1210,1211],{"class":75},": [",[35,1213,120],{"class":45},[35,1215,123],{"class":75},[35,1217,245],{"class":45},[35,1219,123],{"class":75},[35,1221,352],{"class":45},[35,1223,556],{"class":75},[35,1225,1226,1229,1231,1234,1236,1239,1241,1244,1246,1249,1251,1254,1256,1258,1260,1262],{"class":37,"line":92},[35,1227,1228],{"class":45},"                       \"Minimum balance\"",[35,1230,1211],{"class":75},[35,1232,1233],{"class":95},"150_000",[35,1235,123],{"class":75},[35,1237,1238],{"class":95},"10_000",[35,1240,123],{"class":75},[35,1242,1243],{"class":95},"20_000",[35,1245,546],{"class":75},[35,1247,1248],{"class":45},"\"Overdraft limit\"",[35,1250,1211],{"class":75},[35,1252,1253],{"class":95},"250_000",[35,1255,123],{"class":75},[35,1257,200],{"class":95},[35,1259,123],{"class":75},[35,1261,200],{"class":95},[35,1263,1264],{"class":75},"]})\n",[35,1266,1267],{"class":37,"line":105},[35,1268,89],{"emptyLinePlaceholder":88},[35,1270,1271,1274,1276,1279,1281,1283,1285,1287,1289],{"class":37,"line":142},[35,1272,1273],{"class":75},"position ",[35,1275,446],{"class":71},[35,1277,1278],{"class":75}," (checks[[",[35,1280,538],{"class":45},[35,1282,123],{"class":75},[35,1284,980],{"class":45},[35,1286,123],{"class":75},[35,1288,992],{"class":45},[35,1290,1291],{"class":75},"]]\n",[35,1293,1294,1297,1299,1302,1304,1307,1310,1312,1314],{"class":37,"line":180},[35,1295,1296],{"class":75},"            .merge(tx.groupby(",[35,1298,538],{"class":45},[35,1300,1301],{"class":75},")[",[35,1303,549],{"class":45},[35,1305,1306],{"class":75},"].first().reset_index(), ",[35,1308,1309],{"class":458},"on",[35,1311,446],{"class":71},[35,1313,538],{"class":45},[35,1315,639],{"class":75},[35,1317,1318,1321,1323,1325,1327],{"class":37,"line":232},[35,1319,1320],{"class":75},"            .merge(limits, ",[35,1322,1309],{"class":458},[35,1324,446],{"class":71},[35,1326,538],{"class":45},[35,1328,659],{"class":75},[35,1330,1331,1334,1337,1340,1342,1345,1347,1349,1352,1355,1357,1360,1362],{"class":37,"line":282},[35,1332,1333],{"class":75},"position[",[35,1335,1336],{"class":45},"\"Closing GBP\"",[35,1338,1339],{"class":75},"] ",[35,1341,446],{"class":71},[35,1343,1344],{"class":75}," (position[",[35,1346,992],{"class":45},[35,1348,1339],{"class":75},[35,1350,1351],{"class":71},"*",[35,1353,1354],{"class":75}," position[",[35,1356,549],{"class":45},[35,1358,1359],{"class":75},"].map(fx)).round(",[35,1361,601],{"class":95},[35,1363,639],{"class":75},[35,1365,1366,1368,1371,1373,1375,1377,1379,1381,1383,1385,1388,1391,1393],{"class":37,"line":311},[35,1367,1333],{"class":75},[35,1369,1370],{"class":45},"\"Headroom over minimum\"",[35,1372,1339],{"class":75},[35,1374,446],{"class":71},[35,1376,1344],{"class":75},[35,1378,992],{"class":45},[35,1380,1339],{"class":75},[35,1382,206],{"class":71},[35,1384,1354],{"class":75},[35,1386,1387],{"class":45},"\"Minimum balance\"",[35,1389,1390],{"class":75},"]).round(",[35,1392,601],{"class":95},[35,1394,639],{"class":75},[35,1396,1397,1399,1402,1404,1406,1408,1410,1412,1414,1416,1418,1420,1422],{"class":37,"line":340},[35,1398,1333],{"class":75},[35,1400,1401],{"class":45},"\"Available incl. overdraft\"",[35,1403,1339],{"class":75},[35,1405,446],{"class":71},[35,1407,1344],{"class":75},[35,1409,992],{"class":45},[35,1411,1339],{"class":75},[35,1413,1011],{"class":71},[35,1415,1354],{"class":75},[35,1417,1248],{"class":45},[35,1419,1390],{"class":75},[35,1421,601],{"class":95},[35,1423,639],{"class":75},[35,1425,1426,1428,1431,1433,1435,1437,1439,1442,1444,1447,1450,1453,1456,1459,1462,1465,1468,1471,1473,1475,1477,1480,1482,1485],{"class":37,"line":370},[35,1427,1333],{"class":75},[35,1429,1430],{"class":45},"\"Status\"",[35,1432,1339],{"class":75},[35,1434,446],{"class":71},[35,1436,1354],{"class":75},[35,1438,1370],{"class":45},[35,1440,1441],{"class":75},"].map(",[35,1443,188],{"class":71},[35,1445,1446],{"class":75}," h: ",[35,1448,1449],{"class":45},"\"BELOW MINIMUM\"",[35,1451,1452],{"class":71}," if",[35,1454,1455],{"class":75}," h ",[35,1457,1458],{"class":71},"\u003C",[35,1460,1461],{"class":95}," 0",[35,1463,1464],{"class":71}," else",[35,1466,1467],{"class":75}," (",[35,1469,1470],{"class":45},"\"TIGHT\"",[35,1472,1452],{"class":71},[35,1474,1455],{"class":75},[35,1476,1458],{"class":71},[35,1478,1479],{"class":95}," 5_000",[35,1481,1464],{"class":71},[35,1483,1484],{"class":45}," \"OK\"",[35,1486,659],{"class":75},[35,1488,1489,1491,1494,1496,1498,1500,1502,1504,1506,1508,1510,1512,1514,1516,1519,1521,1523,1525],{"class":37,"line":400},[35,1490,645],{"class":95},[35,1492,1493],{"class":75},"(position[[",[35,1495,538],{"class":45},[35,1497,123],{"class":75},[35,1499,549],{"class":45},[35,1501,123],{"class":75},[35,1503,992],{"class":45},[35,1505,123],{"class":75},[35,1507,1336],{"class":45},[35,1509,123],{"class":75},[35,1511,1370],{"class":45},[35,1513,123],{"class":75},[35,1515,1430],{"class":45},[35,1517,1518],{"class":75},"]].to_string(",[35,1520,651],{"class":458},[35,1522,446],{"class":71},[35,1524,656],{"class":95},[35,1526,659],{"class":75},[35,1528,1529,1531,1534,1537,1540,1542,1545,1547],{"class":37,"line":429},[35,1530,645],{"class":95},[35,1532,1533],{"class":75},"(",[35,1535,1536],{"class":45},"\"total cash (GBP):\"",[35,1538,1539],{"class":75},", position[",[35,1541,1336],{"class":45},[35,1543,1544],{"class":75},"].sum().round(",[35,1546,601],{"class":95},[35,1548,659],{"class":75},[10,1550,1551,1552,1555,1556,1559],{},"Each account's closing balance is converted to sterling at the closing rate for the consolidated total, while headroom is measured in the account's own currency against its own limits — a minimum balance the company has set itself, and an overdraft facility where one exists. Two accounts need attention. The main account paid the supplier run and the VAT and transferred 180,000 to fund the payroll, and now holds 41,372.15 — 108,627.85 below the 150,000 minimum the company keeps, though still well inside its overdraft facility, so it is flagged ",[664,1553,1554],{},"BELOW MINIMUM"," rather than at risk of bouncing payments. The payroll account, after the October payroll, holds 11,189.63, only 1,189.63 above its minimum, and is flagged ",[664,1557,1558],{},"TIGHT",". The euro account is comfortably above its minimum. The total cash position across all three accounts, 138,767.90 in sterling, is the headline figure of the report. The 180,000 transfer appears on both the main and the payroll statements, once out and once in, so it cancels in the total — internal transfers move cash between accounts without changing the position, and it is worth labelling them as such in the movement list so nobody mistakes the payroll funding for a receipt.",[669,1561,678,1567,678,1570,678,1573,678,1578,678,1583,678,1587,678,1593,678,1597,678,1601,678,1605,678,1609,678,1613,678,1617,678,1621,678,1625,678,1629,678,1632,678,1635,678,1639,678,1641,678,1644,678,1646,678,1649,678,1651,678,1654,678,1656,678,1659,678,1662,678,1665,678,1667,678,1670,678,1672,678,1675,678,1677,678,1679,678,1681,678,1684,678,1688,678,1691,678,1693,678,1697,678,1699,678,1702,678,1704,678,1707,678,1709,678,1711,678,1713,678,1716,678,1718,678,1721,678,1724,678,1727,678,1729,678,1732,678,1734,678,1737,678,1739,678,1742,678,1744,678,1747,678,1749],{"viewBox":1562,"role":672,"ariaLabelledBy":1563,"xmlns":676,"style":1566},"81 0 598 168",[1564,1565],"cash-position-t","cash-position-d","width:100%;max-width:598px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[680,1568,1569],{"id":1564},"Cash position at close on 1 October",[684,1571,1572],{"id":1565},"The main sterling account closes at 41,372.15, 108,627.85 below its 150,000 minimum but within its overdraft. The payroll account closes at 11,189.63, only 1,189.63 above its 10,000 minimum, and is flagged tight. The euro account closes at 100,100 euros, worth 86,206.12 pounds.",[688,1574],{"x":1575,"y":200,"width":1576,"height":1577,"fill":693},"81","598","168",[688,1579],{"x":1580,"y":697,"width":1581,"height":1582,"rx":200,"fill":701,"stroke":702,"style":1120},"97.4","32.0","24.0",[688,1584],{"x":1585,"y":697,"width":1586,"height":1582,"rx":200,"fill":701,"stroke":702,"style":1120},"129.4","95.7",[705,1588,1592],{"x":1589,"y":1590,"style":1591},"177.2","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[688,1594],{"x":1595,"y":697,"width":1596,"height":1582,"rx":200,"fill":701,"stroke":702,"style":1120},"225.0","102.9",[705,1598,1600],{"x":1599,"y":1590,"style":1591},"276.5","B",[688,1602],{"x":1603,"y":697,"width":1604,"height":1582,"rx":200,"fill":701,"stroke":702,"style":1120},"327.9","86.8",[705,1606,1608],{"x":1607,"y":1590,"style":1591},"371.3","C",[688,1610],{"x":1611,"y":697,"width":1612,"height":1582,"rx":200,"fill":701,"stroke":702,"style":1120},"414.7","113.9",[705,1614,1616],{"x":1615,"y":1590,"style":1591},"471.7","D",[688,1618],{"x":1619,"y":697,"width":1620,"height":1582,"rx":200,"fill":701,"stroke":702,"style":1120},"528.6","134.0",[705,1622,1624],{"x":1623,"y":1590,"style":1591},"595.6","E",[688,1626],{"x":1580,"y":1627,"width":1581,"height":1628,"rx":200,"fill":701,"stroke":702,"style":1120},"40.0","28.0",[705,1630,903],{"x":1631,"y":760,"style":1591},"113.4",[688,1633],{"x":1585,"y":1627,"width":1586,"height":1628,"rx":200,"fill":730,"stroke":721,"style":1634},"stroke-width:1.5px",[705,1636,1638],{"x":1589,"y":760,"style":1637},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Account",[688,1640],{"x":1595,"y":1627,"width":1596,"height":1628,"rx":200,"fill":730,"stroke":721,"style":1634},[705,1642,1643],{"x":1599,"y":760,"style":1637},"Closing",[688,1645],{"x":1603,"y":1627,"width":1604,"height":1628,"rx":200,"fill":730,"stroke":721,"style":1634},[705,1647,1648],{"x":1607,"y":760,"style":1637},"In GBP",[688,1650],{"x":1611,"y":1627,"width":1612,"height":1628,"rx":200,"fill":730,"stroke":721,"style":1634},[705,1652,1653],{"x":1615,"y":760,"style":1637},"Over minimum",[688,1655],{"x":1619,"y":1627,"width":1620,"height":1628,"rx":200,"fill":730,"stroke":721,"style":1634},[705,1657,1658],{"x":1623,"y":760,"style":1637},"Status",[688,1660],{"x":1580,"y":1661,"width":1581,"height":1628,"rx":200,"fill":701,"stroke":702,"style":1120},"68.0",[705,1663,601],{"x":1631,"y":1664,"style":1591},"87.0",[688,1666],{"x":1585,"y":1661,"width":1586,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1668,1669],{"x":1589,"y":1664,"style":1124},"GBP Main",[688,1671],{"x":1595,"y":1661,"width":1596,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1673,1674],{"x":1599,"y":1664,"style":1124},"41,372.15",[688,1676],{"x":1603,"y":1661,"width":1604,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1678,1674],{"x":1607,"y":1664,"style":1124},[688,1680],{"x":1611,"y":1661,"width":1612,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1682,1683],{"x":1615,"y":1664,"style":1124},"−108,627.85",[688,1685],{"x":1619,"y":1661,"width":1620,"height":1628,"rx":200,"fill":1686,"stroke":1687,"style":1634},"#fce9e9","var(--danger,#dc2626)",[705,1689,1554],{"x":1623,"y":1664,"style":1690},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[688,1692],{"x":1580,"y":768,"width":1581,"height":1628,"rx":200,"fill":701,"stroke":702,"style":1120},[705,1694,1696],{"x":1631,"y":1695,"style":1591},"115.0","3",[688,1698],{"x":1585,"y":768,"width":1586,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1700,1701],{"x":1589,"y":1695,"style":1124},"GBP Payroll",[688,1703],{"x":1595,"y":768,"width":1596,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1705,1706],{"x":1599,"y":1695,"style":1124},"11,189.63",[688,1708],{"x":1603,"y":768,"width":1604,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1710,1706],{"x":1607,"y":1695,"style":1124},[688,1712],{"x":1611,"y":768,"width":1612,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1714,1715],{"x":1615,"y":1695,"style":1124},"1,189.63",[688,1717],{"x":1619,"y":768,"width":1620,"height":1628,"rx":200,"fill":780,"stroke":781,"style":1634},[705,1719,1558],{"x":1623,"y":1695,"style":1720},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[688,1722],{"x":1580,"y":1723,"width":1581,"height":1628,"rx":200,"fill":701,"stroke":702,"style":1120},"124.0",[705,1725,690],{"x":1631,"y":1726,"style":1591},"143.0",[688,1728],{"x":1585,"y":1723,"width":1586,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1730,1731],{"x":1589,"y":1726,"style":1124},"EUR Berlin",[688,1733],{"x":1595,"y":1723,"width":1596,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1735,1736],{"x":1599,"y":1726,"style":1124},"€100,100.00",[688,1738],{"x":1603,"y":1723,"width":1604,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1740,1741],{"x":1607,"y":1726,"style":1124},"86,206.12",[688,1743],{"x":1611,"y":1723,"width":1612,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1745,1746],{"x":1615,"y":1726,"style":1124},"€80,100.00",[688,1748],{"x":1619,"y":1723,"width":1620,"height":1628,"rx":200,"fill":693,"stroke":702,"style":1120},[705,1750,1751],{"x":1623,"y":1726,"style":1124},"OK",[20,1753,1755],{"id":1754},"step-4-the-one-page-report","Step 4 — The one-page report",[25,1757,1759],{"className":62,"code":1758,"language":64,"meta":30,"style":30},"top = tx[tx[\"Text\"].str.lower().str.contains(\"opening|anfangssaldo\") == False]\ntop = top.assign(Abs=top[\"Amount\"].abs()).nlargest(5, \"Abs\").drop(columns=[\"Abs\", \"Balance\"])\n\nwith pd.ExcelWriter(\"cash_position_2026-10-01.xlsx\") as xw:\n    position.to_excel(xw, sheet_name=\"Position\", index=False)\n    top.assign(Date=top[\"Date\"].dt.date).to_excel(xw, sheet_name=\"Largest movements\", index=False)\n    checks.to_excel(xw, sheet_name=\"Statement checks\", index=False)\n    tx.assign(Date=tx[\"Date\"].dt.date).to_excel(xw, sheet_name=\"All transactions\", index=False)\nprint(top.to_string(index=False))\n",[32,1760,1761,1788,1837,1841,1859,1882,1916,1938,1971],{"__ignoreMap":30},[35,1762,1763,1766,1768,1771,1773,1776,1779,1781,1783,1786],{"class":37,"line":38},[35,1764,1765],{"class":75},"top ",[35,1767,446],{"class":71},[35,1769,1770],{"class":75}," tx[tx[",[35,1772,576],{"class":45},[35,1774,1775],{"class":75},"].str.lower().str.contains(",[35,1777,1778],{"class":45},"\"opening|anfangssaldo\"",[35,1780,203],{"class":75},[35,1782,956],{"class":71},[35,1784,1785],{"class":95}," False",[35,1787,529],{"class":75},[35,1789,1790,1792,1794,1797,1800,1802,1805,1807,1810,1813,1815,1818,1821,1824,1826,1829,1831,1833,1835],{"class":37,"line":85},[35,1791,1765],{"class":75},[35,1793,446],{"class":71},[35,1795,1796],{"class":75}," top.assign(",[35,1798,1799],{"class":458},"Abs",[35,1801,446],{"class":71},[35,1803,1804],{"class":75},"top[",[35,1806,322],{"class":45},[35,1808,1809],{"class":75},"].abs()).nlargest(",[35,1811,1812],{"class":95},"5",[35,1814,123],{"class":75},[35,1816,1817],{"class":45},"\"Abs\"",[35,1819,1820],{"class":75},").drop(",[35,1822,1823],{"class":458},"columns",[35,1825,446],{"class":71},[35,1827,1828],{"class":75},"[",[35,1830,1817],{"class":45},[35,1832,123],{"class":75},[35,1834,226],{"class":45},[35,1836,512],{"class":75},[35,1838,1839],{"class":37,"line":92},[35,1840,89],{"emptyLinePlaceholder":88},[35,1842,1843,1846,1849,1852,1854,1856],{"class":37,"line":105},[35,1844,1845],{"class":71},"with",[35,1847,1848],{"class":75}," pd.ExcelWriter(",[35,1850,1851],{"class":45},"\"cash_position_2026-10-01.xlsx\"",[35,1853,203],{"class":75},[35,1855,79],{"class":71},[35,1857,1858],{"class":75}," xw:\n",[35,1860,1861,1864,1867,1869,1872,1874,1876,1878,1880],{"class":37,"line":142},[35,1862,1863],{"class":75},"    position.to_excel(xw, ",[35,1865,1866],{"class":458},"sheet_name",[35,1868,446],{"class":71},[35,1870,1871],{"class":45},"\"Position\"",[35,1873,123],{"class":75},[35,1875,651],{"class":458},[35,1877,446],{"class":71},[35,1879,656],{"class":95},[35,1881,639],{"class":75},[35,1883,1884,1887,1890,1892,1894,1896,1899,1901,1903,1906,1908,1910,1912,1914],{"class":37,"line":180},[35,1885,1886],{"class":75},"    top.assign(",[35,1888,1889],{"class":458},"Date",[35,1891,446],{"class":71},[35,1893,1804],{"class":75},[35,1895,151],{"class":45},[35,1897,1898],{"class":75},"].dt.date).to_excel(xw, ",[35,1900,1866],{"class":458},[35,1902,446],{"class":71},[35,1904,1905],{"class":45},"\"Largest movements\"",[35,1907,123],{"class":75},[35,1909,651],{"class":458},[35,1911,446],{"class":71},[35,1913,656],{"class":95},[35,1915,639],{"class":75},[35,1917,1918,1921,1923,1925,1928,1930,1932,1934,1936],{"class":37,"line":232},[35,1919,1920],{"class":75},"    checks.to_excel(xw, ",[35,1922,1866],{"class":458},[35,1924,446],{"class":71},[35,1926,1927],{"class":45},"\"Statement checks\"",[35,1929,123],{"class":75},[35,1931,651],{"class":458},[35,1933,446],{"class":71},[35,1935,656],{"class":95},[35,1937,639],{"class":75},[35,1939,1940,1943,1945,1947,1950,1952,1954,1956,1958,1961,1963,1965,1967,1969],{"class":37,"line":282},[35,1941,1942],{"class":75},"    tx.assign(",[35,1944,1889],{"class":458},[35,1946,446],{"class":71},[35,1948,1949],{"class":75},"tx[",[35,1951,151],{"class":45},[35,1953,1898],{"class":75},[35,1955,1866],{"class":458},[35,1957,446],{"class":71},[35,1959,1960],{"class":45},"\"All transactions\"",[35,1962,123],{"class":75},[35,1964,651],{"class":458},[35,1966,446],{"class":71},[35,1968,656],{"class":95},[35,1970,639],{"class":75},[35,1972,1973,1975,1978,1980,1982,1984],{"class":37,"line":311},[35,1974,645],{"class":95},[35,1976,1977],{"class":75},"(top.to_string(",[35,1979,651],{"class":458},[35,1981,446],{"class":71},[35,1983,656],{"class":95},[35,1985,659],{"class":75},[10,1987,1988,1989,18],{},"The report opens on the position by account, with sterling values, headroom and status, followed by the five largest movements of the day — here the supplier payment run, the payroll, the two sides of the transfer into the payroll account and the VAT direct debit — which are usually what anyone reading a cash report asks about first. The statement checks and the full list of transactions sit behind it for anyone who needs to trace a figure. Run every morning from a scheduled job, with the exports dropped into the folder by the banks' SFTP delivery or a person, it turns a half-hour task into a check of one page. The same standard transaction table feeds the bank reconciliation, described in ",[14,1990,1992],{"href":1991},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report\u002F","Build a Bank Reconciliation Report with Python and Excel",[20,1994,1996],{"id":1995},"common-pitfalls","Common pitfalls",[1998,1999,2000,2008,2014,2020,2026],"ul",{},[2001,2002,2003,2007],"li",{},[2004,2005,2006],"strong",{},"Guessing date formats."," Parse each bank's dates with an explicit format.",[2001,2009,2010,2013],{},[2004,2011,2012],{},"Mixed sign conventions."," Convert every layout to one signed amount: receipts positive.",[2001,2015,2016,2019],{},[2004,2017,2018],{},"Unchecked statements."," Check opening plus movements equals closing, and the running balance row by row.",[2001,2021,2022,2025],{},[2004,2023,2024],{},"Converting before comparing with limits."," Measure headroom in the account's own currency.",[2001,2027,2028,2031],{},[2004,2029,2030],{},"One mapping in code per bank, copied."," Keep the per-bank differences in a dictionary.",[20,2033,2035],{"id":2034},"conclusion","Conclusion",[10,2037,2038],{},"To build a daily cash position from bank statements, describe each bank's export in a small mapping — reading options, date format, sign convention — and turn every file into one standard table. Check each statement adds up from opening to closing balance and row by row, convert closing balances to the reporting currency for the total, measure headroom against minimums and overdrafts in each account's own currency, and publish a one-page position with the day's largest movements.",[20,2040,2042],{"id":2041},"frequently-asked-questions","Frequently asked questions",[10,2044,2045,2048],{},[2004,2046,2047],{},"How do I combine bank statements with different CSV formats in pandas?","\nWrite a small mapping per bank with its read options, date format and how to compute a signed amount, and use it to build one standard DataFrame per file before concatenating.",[10,2050,2051,2054],{},[2004,2052,2053],{},"How do I check that a bank statement export is complete?","\nConfirm that the opening balance plus the sum of transactions equals the closing balance, and that each row's change in running balance equals its amount.",[10,2056,2057,2060,2061,2064,2065,2068,2069,18],{},[2004,2058,2059],{},"How do I read a CSV with decimal commas in pandas?","\nPass ",[32,2062,2063],{},"sep=\";\""," and ",[32,2066,2067],{},"decimal=\",\""," to ",[32,2070,2071],{},"read_csv",[10,2073,2074,2077],{},[2004,2075,2076],{},"Should cash headroom be calculated in the reporting currency?","\nNo; compare each account with its limits in its own currency, and convert only for the consolidated total.",[20,2079,2081],{"id":2080},"related","Related",[1998,2083,2084,2090,2095,2102],{},[2001,2085,2086,2087,2089],{},"Up one level: ",[14,2088,17],{"href":16}," — finance reports from Python.",[2001,2091,2092,2094],{},[14,2093,1992],{"href":1991}," — matching the same transactions to the ledger.",[2001,2096,2097,2101],{},[14,2098,2100],{"href":2099},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-flow-forecast-report\u002F","Build a Cash Flow Forecast Report"," — where the position goes next.",[2001,2103,2104,2108],{},[14,2105,2107],{"href":2106},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server\u002F","Download New Excel Files from an SFTP Server"," — collecting the exports.",[2110,2111,2112],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":30,"searchDepth":85,"depth":85,"links":2114},[2115,2116,2117,2118,2119,2120,2121,2122,2123],{"id":22,"depth":85,"text":23},{"id":58,"depth":85,"text":59},{"id":827,"depth":85,"text":828},{"id":1157,"depth":85,"text":1158},{"id":1754,"depth":85,"text":1755},{"id":1995,"depth":85,"text":1996},{"id":2034,"depth":85,"text":2035},{"id":2041,"depth":85,"text":2042},{"id":2080,"depth":85,"text":2081},"2026-10-02","Produce a daily cash position across several banks and currencies from their statement exports with pandas: map each bank's own CSV layout to one standard, prove every statement adds up from opening to closing balance, convert to the reporting currency, compare each account with its minimum balance and overdraft limit, and write a one-page position with the largest movements of the day.","md",[2128,2130,2132,2134],{"q":2047,"a":2129},"Write a small mapping per bank with its read options, date format and how to compute a signed amount, and use it to build one standard DataFrame per file before concatenating.",{"q":2053,"a":2131},"Confirm that the opening balance plus the sum of transactions equals the closing balance, and that each row's change in running balance equals its amount.",{"q":2059,"a":2133},"Pass sep=\";\" and decimal=\",\" to read_csv.",{"q":2076,"a":2135},"No; compare each account with its limits in its own currency, and convert only for the consolidated total.","cash position report python",null,{"breadcrumb":2139},[2140,2143,2146],{"name":2141,"item":2142},"Home","\u002F",{"name":2144,"item":2145},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2147,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements",{"title":2150,"description":2151},"Build a Cash Position Report from Bank Statements in Python","Daily cash position with pandas: bank CSVs in different layouts mapped to one standard, opening-to-closing balance checks, conversion to GBP, headroom against limits and the day's largest movements in Excel.","build-a-cash-position-report-from-bank-statements","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-position-report-from-bank-statements\u002Findex",[2155,2156],"report-layouts","data-sources","how-to","m84G54sJ0wkkyM11XIvn1DJU7GMRlA_fnuOrdxPILBY",[2160,2164],{"title":2161,"path":2162,"stem":2163,"children":-1},"Build a 13-Week Cash Flow Forecast Report in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-flow-forecast-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-flow-forecast-report\u002Findex",{"title":2165,"path":2166,"stem":2167,"children":-1},"Build a Credit Control Dunning Letter Run","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run\u002Findex",[2169,2171],{"path":2170,"title":2147},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2172,"title":2144},"\u002Fautomating-reporting-workflows",[],[2175,2179,2182,2185,2188,2191],{"path":2176,"title":2177,"kind":2178},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2180,"title":2181,"kind":2157},"\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":2183,"title":2184,"kind":2157},"\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":2186,"title":2187,"kind":2157},"\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":2189,"title":2190,"kind":2157},"\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":2192,"title":2193,"kind":2157},"\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",1790972121256]