[{"data":1,"prerenderedAt":2443},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table":2408,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table":2417,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table":2422,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table":2423},{"id":4,"title":5,"body":6,"dateModified":2374,"datePublished":2374,"description":2375,"extension":2376,"faq":2377,"keyword":2386,"kind":2387,"meta":2388,"navigation":302,"path":2397,"seo":2398,"slug":2401,"stem":2402,"topics":2403,"type":2406,"__hash__":2407},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table\u002Findex.md","Convert Multi-Currency Transactions with an FX Rate Table",{"type":7,"value":8,"toc":2361},"minimark",[9,19,24,56,72,263,267,274,710,713,717,732,993,1004,1008,1011,1229,1232,1236,1246,1396,1399,1503,1507,1510,1719,1722,1839,1843,1846,2223,2232,2236,2280,2284,2291,2295,2301,2307,2313,2319,2323,2357],[10,11,12,13,18],"p",{},"A business that invoices in euros, dollars and yen but reports in pounds needs every figure converted before it can be added up, and \"converted\" hides several decisions: which rate source, which day's rate, which direction the rate is quoted in, and whether a balance still open at month end should be revalued. Getting any of them wrong produces totals that look plausible and do not match the accounts. This page builds the conversion step by step with pandas, from a rate table to an Excel report with the unrealised gain or loss on open invoices. It is part of ",[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 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,59,62,63,67,68,71],{},"Two files. ",[32,60,61],{},"eur_reference_rates.xlsx"," is a daily rate table in the format central banks publish: one row per business day, one column per currency, each value meaning ",[64,65,66],"em",{},"how many units of that currency one euro buys",". There is no row for weekends or for the 31 August bank holiday. ",[32,69,70],{},"invoices_q3.xlsx"," lists sales invoices for July to September 2026 in EUR, USD, JPY and GBP, with whether each is paid or still open. The reporting currency is GBP.",[73,74,82,83,82,87,82,91,82,99,82,109,82,120,82,127,82,131,82,135,82,140,82,146,82,151,82,155,82,162,82,167,82,171,82,175,82,178,82,181,82,187,82,192,82,196,82,200,82,204,82,207,82,212,82,219,82,224,82,227,82,230,82,234,82,237,82,242,82,249,82,254,82,257,82,260],"svg",{"viewBox":75,"role":76,"ariaLabelledBy":77,"xmlns":80,"style":81},"4 0 752 160","img",[78,79],"fx-flow-t","fx-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  ",[84,85,86],"title",{"id":78},"From a euro rate table to base-currency figures",[88,89,90],"desc",{"id":79},"Euro reference rates are turned into sterling rates for each currency by triangulation. Each invoice is matched to the rate in force on its date. Amounts are converted and rounded to pence. Monthly averages feed the P&L and the closing rate revalues invoices still open.",[92,93],"rect",{"x":94,"y":95,"width":96,"height":97,"fill":98},"4","0","752","160","#ffffff",[92,100],{"x":101,"y":102,"width":103,"height":104,"rx":105,"fill":106,"stroke":107,"style":108},"20.0","16.0","115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[110,111,113],"g",{"transform":112},"translate(65.6,29.0) scale(1.000)",[114,115],"path",{"d":116,"fill":117,"stroke":118,"style":119},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[121,122,126],"text",{"x":123,"y":124,"style":125},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","EUR",[121,128,130],{"x":123,"y":129,"style":125},"93.0","reference",[121,132,134],{"x":123,"y":133,"style":125},"110.5","rates",[121,136,139],{"x":123,"y":137,"style":138},"130.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","1 EUR = x CCY",[37,141],{"x1":142,"y1":143,"x2":144,"y2":143,"stroke":145,"style":108},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[147,148],"polygon",{"points":149,"fill":150},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[92,152],{"x":153,"y":102,"width":103,"height":104,"rx":105,"fill":154,"stroke":145,"style":108},"171.2","#f0f4ff",[110,156,158],{"transform":157},"translate(216.8,46.5) scale(1.000)",[114,159],{"d":160,"fill":117,"stroke":161,"style":119},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--brand-strong,#4338ca)",[121,163,166],{"x":164,"y":129,"style":165},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Triangulate",[121,168,170],{"x":164,"y":169,"style":138},"112.5","GBP per 1 CCY",[37,172],{"x1":173,"y1":143,"x2":174,"y2":143,"stroke":145,"style":108},"291.4","313.4",[147,176],{"points":177,"fill":150},"317.4,80.0 308.4,85.0 308.4,75.0",[92,179],{"x":180,"y":102,"width":103,"height":104,"rx":105,"fill":154,"stroke":145,"style":108},"322.4",[110,182,184],{"transform":183},"translate(368.0,37.8) scale(1.000)",[114,185],{"d":186,"fill":117,"stroke":161,"style":119},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4",[121,188,191],{"x":189,"y":190,"style":165},"380.0","84.2","Match by",[121,193,195],{"x":189,"y":194,"style":165},"101.8","date",[121,197,199],{"x":189,"y":198,"style":138},"121.2","rate in force",[37,201],{"x1":202,"y1":143,"x2":203,"y2":143,"stroke":145,"style":108},"442.6","464.6",[147,205],{"points":206,"fill":150},"468.6,80.0 459.6,85.0 459.6,75.0",[92,208],{"x":209,"y":102,"width":103,"height":104,"rx":105,"fill":210,"stroke":211,"style":108},"473.6","#d9f4f1","var(--teal,#0f9488)",[110,213,215],{"transform":214},"translate(519.2,37.8) scale(1.000)",[114,216],{"d":217,"fill":117,"stroke":218,"style":119},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--teal-ink,#0b6157)",[121,220,223],{"x":221,"y":190,"style":222},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Convert and",[121,225,226],{"x":221,"y":194,"style":222},"round",[121,228,229],{"x":221,"y":198,"style":138},"to pence",[37,231],{"x1":232,"y1":143,"x2":233,"y2":143,"stroke":145,"style":108},"593.8","615.8",[147,235],{"points":236,"fill":150},"619.8,80.0 610.8,85.0 610.8,75.0",[92,238],{"x":239,"y":102,"width":103,"height":104,"rx":105,"fill":240,"stroke":241,"style":108},"624.8","#fdefd8","var(--gold,#b4740a)",[110,243,245],{"transform":244},"translate(670.4,29.0) scale(1.000)",[114,246],{"d":247,"fill":117,"stroke":248,"style":119},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--gold-ink,#7a4e06)",[121,250,253],{"x":251,"y":124,"style":252},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","P&L and",[121,255,256],{"x":251,"y":129,"style":252},"balances",[121,258,259],{"x":251,"y":169,"style":138},"average and",[121,261,262],{"x":251,"y":137,"style":138},"closing",[20,264,266],{"id":265},"step-1-get-the-rates-the-right-way-round","Step 1 — Get the rates the right way round",[10,268,269,270,273],{},"The table says 1 EUR = 0.858 GBP and 1 EUR = 1.162 USD. What the conversion needs is ",[64,271,272],{},"pounds per one unit of each currency",". For the euro that is the GBP column itself; for any other currency it goes through the euro: 1 USD = 0.858 \u002F 1.162 GBP. This is triangulation, and it is how reporting systems derive cross rates from a single-base table:",[25,275,279],{"className":276,"code":277,"language":278,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nrates = pd.read_excel(\"eur_reference_rates.xlsx\", parse_dates=[\"Date\"])\nBASE = \"GBP\"\n\nlong = rates.melt(id_vars=\"Date\", var_name=\"Currency\", value_name=\"PerEUR\")\ngbp_per_eur = rates.set_index(\"Date\")[BASE]\nlong[\"BasePerUnit\"] = long[\"Date\"].map(gbp_per_eur) \u002F long[\"PerEUR\"]\neur_rows = pd.DataFrame({\"Date\": rates[\"Date\"], \"Currency\": \"EUR\", \"PerEUR\": 1.0, \"BasePerUnit\": gbp_per_eur.values})\nfx = pd.concat([long[long[\"Currency\"] != BASE], eur_rows], ignore_index=True)\nfx = fx[[\"Date\", \"Currency\", \"BasePerUnit\"]].sort_values(\"Date\")\nprint(fx[fx[\"Date\"] == \"2026-07-01\"].round({\"BasePerUnit\": 6}).to_string(index=False))\n\nusd = fx.loc[fx[\"Currency\"] == \"USD\", \"BasePerUnit\"].iloc[0]\nassert 0.5 \u003C usd \u003C 1, \"USD rate looks inverted\"\nprint(f\"sanity check: 100 USD = {100 * usd:.2f} GBP\")\n","python",[32,280,281,297,304,337,350,355,396,417,453,498,540,567,610,615,646,672],{"__ignoreMap":30},[35,282,283,287,291,294],{"class":37,"line":38},[35,284,286],{"class":285},"s-kum","import",[35,288,290],{"class":289},"skGVy"," pandas ",[35,292,293],{"class":285},"as",[35,295,296],{"class":289}," pd\n",[35,298,300],{"class":37,"line":299},2,[35,301,303],{"emptyLinePlaceholder":302},true,"\n",[35,305,307,310,313,316,319,322,326,328,331,334],{"class":37,"line":306},3,[35,308,309],{"class":289},"rates ",[35,311,312],{"class":285},"=",[35,314,315],{"class":289}," pd.read_excel(",[35,317,318],{"class":45},"\"eur_reference_rates.xlsx\"",[35,320,321],{"class":289},", ",[35,323,325],{"class":324},"sa561","parse_dates",[35,327,312],{"class":285},[35,329,330],{"class":289},"[",[35,332,333],{"class":45},"\"Date\"",[35,335,336],{"class":289},"])\n",[35,338,340,344,347],{"class":37,"line":339},4,[35,341,343],{"class":342},"sP0c6","BASE",[35,345,346],{"class":285}," =",[35,348,349],{"class":45}," \"GBP\"\n",[35,351,353],{"class":37,"line":352},5,[35,354,303],{"emptyLinePlaceholder":302},[35,356,358,361,363,366,369,371,373,375,378,380,383,385,388,390,393],{"class":37,"line":357},6,[35,359,360],{"class":324},"long",[35,362,346],{"class":285},[35,364,365],{"class":289}," rates.melt(",[35,367,368],{"class":324},"id_vars",[35,370,312],{"class":285},[35,372,333],{"class":45},[35,374,321],{"class":289},[35,376,377],{"class":324},"var_name",[35,379,312],{"class":285},[35,381,382],{"class":45},"\"Currency\"",[35,384,321],{"class":289},[35,386,387],{"class":324},"value_name",[35,389,312],{"class":285},[35,391,392],{"class":45},"\"PerEUR\"",[35,394,395],{"class":289},")\n",[35,397,399,402,404,407,409,412,414],{"class":37,"line":398},7,[35,400,401],{"class":289},"gbp_per_eur ",[35,403,312],{"class":285},[35,405,406],{"class":289}," rates.set_index(",[35,408,333],{"class":45},[35,410,411],{"class":289},")[",[35,413,343],{"class":342},[35,415,416],{"class":289},"]\n",[35,418,420,422,424,427,430,432,435,437,439,442,445,447,449,451],{"class":37,"line":419},8,[35,421,360],{"class":324},[35,423,330],{"class":289},[35,425,426],{"class":45},"\"BasePerUnit\"",[35,428,429],{"class":289},"] ",[35,431,312],{"class":285},[35,433,434],{"class":324}," long",[35,436,330],{"class":289},[35,438,333],{"class":45},[35,440,441],{"class":289},"].map(gbp_per_eur) ",[35,443,444],{"class":285},"\u002F",[35,446,434],{"class":324},[35,448,330],{"class":289},[35,450,392],{"class":45},[35,452,416],{"class":289},[35,454,456,459,461,464,466,469,471,474,476,479,482,484,486,488,491,493,495],{"class":37,"line":455},9,[35,457,458],{"class":289},"eur_rows ",[35,460,312],{"class":285},[35,462,463],{"class":289}," pd.DataFrame({",[35,465,333],{"class":45},[35,467,468],{"class":289},": rates[",[35,470,333],{"class":45},[35,472,473],{"class":289},"], ",[35,475,382],{"class":45},[35,477,478],{"class":289},": ",[35,480,481],{"class":45},"\"EUR\"",[35,483,321],{"class":289},[35,485,392],{"class":45},[35,487,478],{"class":289},[35,489,490],{"class":342},"1.0",[35,492,321],{"class":289},[35,494,426],{"class":45},[35,496,497],{"class":289},": gbp_per_eur.values})\n",[35,499,501,504,506,509,511,513,515,517,519,521,524,527,530,533,535,538],{"class":37,"line":500},10,[35,502,503],{"class":289},"fx ",[35,505,312],{"class":285},[35,507,508],{"class":289}," pd.concat([",[35,510,360],{"class":324},[35,512,330],{"class":289},[35,514,360],{"class":324},[35,516,330],{"class":289},[35,518,382],{"class":45},[35,520,429],{"class":289},[35,522,523],{"class":285},"!=",[35,525,526],{"class":342}," BASE",[35,528,529],{"class":289},"], eur_rows], ",[35,531,532],{"class":324},"ignore_index",[35,534,312],{"class":285},[35,536,537],{"class":342},"True",[35,539,395],{"class":289},[35,541,543,545,547,550,552,554,556,558,560,563,565],{"class":37,"line":542},11,[35,544,503],{"class":289},[35,546,312],{"class":285},[35,548,549],{"class":289}," fx[[",[35,551,333],{"class":45},[35,553,321],{"class":289},[35,555,382],{"class":45},[35,557,321],{"class":289},[35,559,426],{"class":45},[35,561,562],{"class":289},"]].sort_values(",[35,564,333],{"class":45},[35,566,395],{"class":289},[35,568,570,573,576,578,580,583,586,589,591,593,596,599,602,604,607],{"class":37,"line":569},12,[35,571,572],{"class":342},"print",[35,574,575],{"class":289},"(fx[fx[",[35,577,333],{"class":45},[35,579,429],{"class":289},[35,581,582],{"class":285},"==",[35,584,585],{"class":45}," \"2026-07-01\"",[35,587,588],{"class":289},"].round({",[35,590,426],{"class":45},[35,592,478],{"class":289},[35,594,595],{"class":342},"6",[35,597,598],{"class":289},"}).to_string(",[35,600,601],{"class":324},"index",[35,603,312],{"class":285},[35,605,606],{"class":342},"False",[35,608,609],{"class":289},"))\n",[35,611,613],{"class":37,"line":612},13,[35,614,303],{"emptyLinePlaceholder":302},[35,616,618,621,623,626,628,630,632,635,637,639,642,644],{"class":37,"line":617},14,[35,619,620],{"class":289},"usd ",[35,622,312],{"class":285},[35,624,625],{"class":289}," fx.loc[fx[",[35,627,382],{"class":45},[35,629,429],{"class":289},[35,631,582],{"class":285},[35,633,634],{"class":45}," \"USD\"",[35,636,321],{"class":289},[35,638,426],{"class":45},[35,640,641],{"class":289},"].iloc[",[35,643,95],{"class":342},[35,645,416],{"class":289},[35,647,649,652,655,658,661,664,667,669],{"class":37,"line":648},15,[35,650,651],{"class":285},"assert",[35,653,654],{"class":342}," 0.5",[35,656,657],{"class":285}," \u003C",[35,659,660],{"class":289}," usd ",[35,662,663],{"class":285},"\u003C",[35,665,666],{"class":342}," 1",[35,668,321],{"class":289},[35,670,671],{"class":45},"\"USD rate looks inverted\"\n",[35,673,675,677,680,683,686,690,693,696,699,702,705,708],{"class":37,"line":674},16,[35,676,572],{"class":342},[35,678,679],{"class":289},"(",[35,681,682],{"class":285},"f",[35,684,685],{"class":45},"\"sanity check: 100 USD = ",[35,687,689],{"class":688},"sSjpA","{",[35,691,692],{"class":342},"100",[35,694,695],{"class":285}," *",[35,697,698],{"class":289}," usd",[35,700,701],{"class":285},":.2f",[35,703,704],{"class":688},"}",[35,706,707],{"class":45}," GBP\"",[35,709,395],{"class":289},[10,711,712],{},"Getting the direction wrong is the classic error, and it is easy to catch: converting 100 USD should give fewer pounds, roughly 74, not 135. The script asserts it, so an inverted rate stops the run instead of reaching a report. The base currency itself needs no rate; it is handled in Step 2.",[20,714,716],{"id":715},"step-2-attach-the-rate-in-force-on-each-date","Step 2 — Attach the rate in force on each date",[10,718,719,720,723,724,727,728,731],{},"Invoices dated on a Saturday or a holiday have no rate of their own; the convention is the latest published rate on or before the transaction date. ",[32,721,722],{},"merge_asof"," does this per currency with ",[32,725,726],{},"by=",", and ",[32,729,730],{},"tolerance"," stops a missing week of rates from silently reusing a stale one:",[25,733,735],{"className":276,"code":734,"language":278,"meta":30,"style":30},"tx = pd.read_excel(\"invoices_q3.xlsx\", parse_dates=[\"Date\"]).sort_values(\"Date\")\n\nconv = pd.merge_asof(tx, fx.rename(columns={\"Date\": \"RateDate\"}), left_on=\"Date\", right_on=\"RateDate\",\n                     by=\"Currency\", direction=\"backward\", tolerance=pd.Timedelta(days=5))\nconv.loc[conv[\"Currency\"] == BASE, [\"BasePerUnit\", \"RateDate\"]] = [1.0, pd.NaT]\nmissing = conv[conv[\"BasePerUnit\"].isna()]\nassert missing.empty, f\"no rate for: {missing[['Invoice', 'Currency', 'Date']].to_dict('records')}\"\nprint(conv[[\"Invoice\", \"Date\", \"Currency\", \"RateDate\", \"BasePerUnit\"]].round({\"BasePerUnit\": 6}).to_string(index=False))\n",[32,736,737,766,770,816,854,889,904,948],{"__ignoreMap":30},[35,738,739,742,744,746,749,751,753,755,757,759,762,764],{"class":37,"line":38},[35,740,741],{"class":289},"tx ",[35,743,312],{"class":285},[35,745,315],{"class":289},[35,747,748],{"class":45},"\"invoices_q3.xlsx\"",[35,750,321],{"class":289},[35,752,325],{"class":324},[35,754,312],{"class":285},[35,756,330],{"class":289},[35,758,333],{"class":45},[35,760,761],{"class":289},"]).sort_values(",[35,763,333],{"class":45},[35,765,395],{"class":289},[35,767,768],{"class":37,"line":299},[35,769,303],{"emptyLinePlaceholder":302},[35,771,772,775,777,780,783,785,787,789,791,794,797,800,802,804,806,809,811,813],{"class":37,"line":306},[35,773,774],{"class":289},"conv ",[35,776,312],{"class":285},[35,778,779],{"class":289}," pd.merge_asof(tx, fx.rename(",[35,781,782],{"class":324},"columns",[35,784,312],{"class":285},[35,786,689],{"class":289},[35,788,333],{"class":45},[35,790,478],{"class":289},[35,792,793],{"class":45},"\"RateDate\"",[35,795,796],{"class":289},"}), ",[35,798,799],{"class":324},"left_on",[35,801,312],{"class":285},[35,803,333],{"class":45},[35,805,321],{"class":289},[35,807,808],{"class":324},"right_on",[35,810,312],{"class":285},[35,812,793],{"class":45},[35,814,815],{"class":289},",\n",[35,817,818,821,823,825,827,830,832,835,837,839,841,844,847,849,852],{"class":37,"line":339},[35,819,820],{"class":324},"                     by",[35,822,312],{"class":285},[35,824,382],{"class":45},[35,826,321],{"class":289},[35,828,829],{"class":324},"direction",[35,831,312],{"class":285},[35,833,834],{"class":45},"\"backward\"",[35,836,321],{"class":289},[35,838,730],{"class":324},[35,840,312],{"class":285},[35,842,843],{"class":289},"pd.Timedelta(",[35,845,846],{"class":324},"days",[35,848,312],{"class":285},[35,850,851],{"class":342},"5",[35,853,609],{"class":289},[35,855,856,859,861,863,865,867,870,872,874,876,879,881,884,886],{"class":37,"line":352},[35,857,858],{"class":289},"conv.loc[conv[",[35,860,382],{"class":45},[35,862,429],{"class":289},[35,864,582],{"class":285},[35,866,526],{"class":342},[35,868,869],{"class":289},", [",[35,871,426],{"class":45},[35,873,321],{"class":289},[35,875,793],{"class":45},[35,877,878],{"class":289},"]] ",[35,880,312],{"class":285},[35,882,883],{"class":289}," [",[35,885,490],{"class":342},[35,887,888],{"class":289},", pd.NaT]\n",[35,890,891,894,896,899,901],{"class":37,"line":357},[35,892,893],{"class":289},"missing ",[35,895,312],{"class":285},[35,897,898],{"class":289}," conv[conv[",[35,900,426],{"class":45},[35,902,903],{"class":289},"].isna()]\n",[35,905,906,908,911,913,916,918,921,924,926,929,931,934,937,940,943,945],{"class":37,"line":398},[35,907,651],{"class":285},[35,909,910],{"class":289}," missing.empty, ",[35,912,682],{"class":285},[35,914,915],{"class":45},"\"no rate for: ",[35,917,689],{"class":688},[35,919,920],{"class":289},"missing[[",[35,922,923],{"class":45},"'Invoice'",[35,925,321],{"class":289},[35,927,928],{"class":45},"'Currency'",[35,930,321],{"class":289},[35,932,933],{"class":45},"'Date'",[35,935,936],{"class":289},"]].to_dict(",[35,938,939],{"class":45},"'records'",[35,941,942],{"class":289},")",[35,944,704],{"class":688},[35,946,947],{"class":45},"\"\n",[35,949,950,952,955,958,960,962,964,966,968,970,972,974,977,979,981,983,985,987,989,991],{"class":37,"line":419},[35,951,572],{"class":342},[35,953,954],{"class":289},"(conv[[",[35,956,957],{"class":45},"\"Invoice\"",[35,959,321],{"class":289},[35,961,333],{"class":45},[35,963,321],{"class":289},[35,965,382],{"class":45},[35,967,321],{"class":289},[35,969,793],{"class":45},[35,971,321],{"class":289},[35,973,426],{"class":45},[35,975,976],{"class":289},"]].round({",[35,978,426],{"class":45},[35,980,478],{"class":289},[35,982,595],{"class":342},[35,984,598],{"class":289},[35,986,601],{"class":324},[35,988,312],{"class":285},[35,990,606],{"class":342},[35,992,609],{"class":289},[10,994,995,996,1000,1001,1003],{},"INV-8808, dated Monday 31 August, uses Friday 28 August's rate because the bank holiday had none. ",[14,997,999],{"href":998},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmatch-rows-by-nearest-date-with-merge-asof\u002F","Match Rows by Nearest Date with merge_asof"," covers the options and the sorting errors ",[32,1002,722],{}," raises.",[20,1005,1007],{"id":1006},"step-3-convert-and-round-to-the-right-precision","Step 3 — Convert and round to the right precision",[10,1009,1010],{},"Currencies have different minor units: pence and cents have two decimals, yen has none. Round converted amounts to the base currency's precision, and display source amounts in their own:",[25,1012,1014],{"className":276,"code":1013,"language":278,"meta":30,"style":30},"MINOR_UNITS = {\"GBP\": 2, \"EUR\": 2, \"USD\": 2, \"CHF\": 2, \"JPY\": 0}\n\nconv[\"BaseAmount\"] = (conv[\"Amount\"] * conv[\"BasePerUnit\"]).round(MINOR_UNITS[BASE])\nconv[\"Month\"] = conv[\"Date\"].dt.to_period(\"M\")\nsummary = conv.pivot_table(index=\"Currency\", columns=\"Month\", values=\"BaseAmount\", aggfunc=\"sum\", fill_value=0)\nsummary[\"Q3\"] = summary.sum(axis=1)\nprint(summary.round(2).to_string())\n",[32,1015,1016,1072,1076,1115,1138,1192,1217],{"__ignoreMap":30},[35,1017,1018,1021,1023,1026,1029,1031,1034,1036,1038,1040,1042,1044,1047,1049,1051,1053,1056,1058,1060,1062,1065,1067,1069],{"class":37,"line":38},[35,1019,1020],{"class":342},"MINOR_UNITS",[35,1022,346],{"class":285},[35,1024,1025],{"class":289}," {",[35,1027,1028],{"class":45},"\"GBP\"",[35,1030,478],{"class":289},[35,1032,1033],{"class":342},"2",[35,1035,321],{"class":289},[35,1037,481],{"class":45},[35,1039,478],{"class":289},[35,1041,1033],{"class":342},[35,1043,321],{"class":289},[35,1045,1046],{"class":45},"\"USD\"",[35,1048,478],{"class":289},[35,1050,1033],{"class":342},[35,1052,321],{"class":289},[35,1054,1055],{"class":45},"\"CHF\"",[35,1057,478],{"class":289},[35,1059,1033],{"class":342},[35,1061,321],{"class":289},[35,1063,1064],{"class":45},"\"JPY\"",[35,1066,478],{"class":289},[35,1068,95],{"class":342},[35,1070,1071],{"class":289},"}\n",[35,1073,1074],{"class":37,"line":299},[35,1075,303],{"emptyLinePlaceholder":302},[35,1077,1078,1081,1084,1086,1088,1091,1094,1096,1099,1102,1104,1107,1109,1111,1113],{"class":37,"line":306},[35,1079,1080],{"class":289},"conv[",[35,1082,1083],{"class":45},"\"BaseAmount\"",[35,1085,429],{"class":289},[35,1087,312],{"class":285},[35,1089,1090],{"class":289}," (conv[",[35,1092,1093],{"class":45},"\"Amount\"",[35,1095,429],{"class":289},[35,1097,1098],{"class":285},"*",[35,1100,1101],{"class":289}," conv[",[35,1103,426],{"class":45},[35,1105,1106],{"class":289},"]).round(",[35,1108,1020],{"class":342},[35,1110,330],{"class":289},[35,1112,343],{"class":342},[35,1114,336],{"class":289},[35,1116,1117,1119,1122,1124,1126,1128,1130,1133,1136],{"class":37,"line":339},[35,1118,1080],{"class":289},[35,1120,1121],{"class":45},"\"Month\"",[35,1123,429],{"class":289},[35,1125,312],{"class":285},[35,1127,1101],{"class":289},[35,1129,333],{"class":45},[35,1131,1132],{"class":289},"].dt.to_period(",[35,1134,1135],{"class":45},"\"M\"",[35,1137,395],{"class":289},[35,1139,1140,1143,1145,1148,1150,1152,1154,1156,1158,1160,1162,1164,1167,1169,1171,1173,1176,1178,1181,1183,1186,1188,1190],{"class":37,"line":352},[35,1141,1142],{"class":289},"summary ",[35,1144,312],{"class":285},[35,1146,1147],{"class":289}," conv.pivot_table(",[35,1149,601],{"class":324},[35,1151,312],{"class":285},[35,1153,382],{"class":45},[35,1155,321],{"class":289},[35,1157,782],{"class":324},[35,1159,312],{"class":285},[35,1161,1121],{"class":45},[35,1163,321],{"class":289},[35,1165,1166],{"class":324},"values",[35,1168,312],{"class":285},[35,1170,1083],{"class":45},[35,1172,321],{"class":289},[35,1174,1175],{"class":324},"aggfunc",[35,1177,312],{"class":285},[35,1179,1180],{"class":45},"\"sum\"",[35,1182,321],{"class":289},[35,1184,1185],{"class":324},"fill_value",[35,1187,312],{"class":285},[35,1189,95],{"class":342},[35,1191,395],{"class":289},[35,1193,1194,1197,1200,1202,1204,1207,1210,1212,1215],{"class":37,"line":357},[35,1195,1196],{"class":289},"summary[",[35,1198,1199],{"class":45},"\"Q3\"",[35,1201,429],{"class":289},[35,1203,312],{"class":285},[35,1205,1206],{"class":289}," summary.sum(",[35,1208,1209],{"class":324},"axis",[35,1211,312],{"class":285},[35,1213,1214],{"class":342},"1",[35,1216,395],{"class":289},[35,1218,1219,1221,1224,1226],{"class":37,"line":398},[35,1220,572],{"class":342},[35,1222,1223],{"class":289},"(summary.round(",[35,1225,1033],{"class":342},[35,1227,1228],{"class":289},").to_string())\n",[10,1230,1231],{},"Round once, at the converted amount. Rounding the rate first changes large amounts noticeably: the yen rate on 24 September was 0.005022 pounds; cut to four decimal places it becomes 0.0050, and the 4,125,000-yen invoice drops by about 90 pounds.",[20,1233,1235],{"id":1234},"step-4-average-rates-for-the-pl-closing-rates-for-balances","Step 4 — Average rates for the P&L, closing rates for balances",[10,1237,1238,1239,1242,1243,1245],{},"Many groups translate income at the month's ",[64,1240,1241],{},"average"," rate rather than each day's, so a month of sales is converted consistently; and they value balances still open at the month end at the ",[64,1244,262],{}," rate. Both come from the same table:",[25,1247,1249],{"className":276,"code":1248,"language":278,"meta":30,"style":30},"fx[\"Month\"] = fx[\"Date\"].dt.to_period(\"M\")\naverage = fx.groupby([\"Currency\", \"Month\"])[\"BasePerUnit\"].mean().rename(\"AvgRate\")\nclosing = fx.sort_values(\"Date\").groupby([\"Currency\", \"Month\"])[\"BasePerUnit\"].last().rename(\"ClosingRate\")\nrate_table = pd.concat([average, closing], axis=1).reset_index()\nprint(rate_table[rate_table[\"Month\"] == \"2026-09\"].round({\"AvgRate\": 6, \"ClosingRate\": 6}).to_string(index=False))\n",[32,1250,1251,1273,1302,1335,1354],{"__ignoreMap":30},[35,1252,1253,1256,1258,1260,1262,1265,1267,1269,1271],{"class":37,"line":38},[35,1254,1255],{"class":289},"fx[",[35,1257,1121],{"class":45},[35,1259,429],{"class":289},[35,1261,312],{"class":285},[35,1263,1264],{"class":289}," fx[",[35,1266,333],{"class":45},[35,1268,1132],{"class":289},[35,1270,1135],{"class":45},[35,1272,395],{"class":289},[35,1274,1275,1278,1280,1283,1285,1287,1289,1292,1294,1297,1300],{"class":37,"line":299},[35,1276,1277],{"class":289},"average ",[35,1279,312],{"class":285},[35,1281,1282],{"class":289}," fx.groupby([",[35,1284,382],{"class":45},[35,1286,321],{"class":289},[35,1288,1121],{"class":45},[35,1290,1291],{"class":289},"])[",[35,1293,426],{"class":45},[35,1295,1296],{"class":289},"].mean().rename(",[35,1298,1299],{"class":45},"\"AvgRate\"",[35,1301,395],{"class":289},[35,1303,1304,1307,1309,1312,1314,1317,1319,1321,1323,1325,1327,1330,1333],{"class":37,"line":306},[35,1305,1306],{"class":289},"closing ",[35,1308,312],{"class":285},[35,1310,1311],{"class":289}," fx.sort_values(",[35,1313,333],{"class":45},[35,1315,1316],{"class":289},").groupby([",[35,1318,382],{"class":45},[35,1320,321],{"class":289},[35,1322,1121],{"class":45},[35,1324,1291],{"class":289},[35,1326,426],{"class":45},[35,1328,1329],{"class":289},"].last().rename(",[35,1331,1332],{"class":45},"\"ClosingRate\"",[35,1334,395],{"class":289},[35,1336,1337,1340,1342,1345,1347,1349,1351],{"class":37,"line":339},[35,1338,1339],{"class":289},"rate_table ",[35,1341,312],{"class":285},[35,1343,1344],{"class":289}," pd.concat([average, closing], ",[35,1346,1209],{"class":324},[35,1348,312],{"class":285},[35,1350,1214],{"class":342},[35,1352,1353],{"class":289},").reset_index()\n",[35,1355,1356,1358,1361,1363,1365,1367,1370,1372,1374,1376,1378,1380,1382,1384,1386,1388,1390,1392,1394],{"class":37,"line":352},[35,1357,572],{"class":342},[35,1359,1360],{"class":289},"(rate_table[rate_table[",[35,1362,1121],{"class":45},[35,1364,429],{"class":289},[35,1366,582],{"class":285},[35,1368,1369],{"class":45}," \"2026-09\"",[35,1371,588],{"class":289},[35,1373,1299],{"class":45},[35,1375,478],{"class":289},[35,1377,595],{"class":342},[35,1379,321],{"class":289},[35,1381,1332],{"class":45},[35,1383,478],{"class":289},[35,1385,595],{"class":342},[35,1387,598],{"class":289},[35,1389,601],{"class":324},[35,1391,312],{"class":285},[35,1393,606],{"class":342},[35,1395,609],{"class":289},[10,1397,1398],{},"Which method applies is an accounting policy, not a technical choice; the script's job is to make both available and label which one each figure used. In September the dollar's average rate was 0.7224 and its closing rate 0.7166, so a dollar balance valued at the average would be overstated by almost 1% at the quarter end. Differences of that size are why auditors ask which rate each line used.",[73,1400,82,1405,82,1408,82,1411,82,1414,82,1419,82,1425,82,1431,82,1437,82,1442,82,1446,82,1450,82,1453,82,1459,82,1463,82,1467,82,1470,82,1473,82,1476,82,1479,82,1485,82,1490,82,1494,82,1497,82,1500],{"viewBox":1401,"role":76,"ariaLabelledBy":1402,"xmlns":80,"style":81},"4 0 752 207",[1403,1404],"fx-which-t","fx-which-d",[84,1406,1407],{"id":1403},"Which rate for which figure",[88,1409,1410],{"id":1404},"Three panels. The transaction rate, the rate on the invoice date, records each sale when it happens. The monthly average rate translates a month's income and expenses consistently. The closing rate, the last rate of the month, values balances such as open invoices at the reporting date.",[92,1412],{"x":94,"y":95,"width":96,"height":1413,"fill":98},"207",[92,1415],{"x":101,"y":102,"width":1416,"height":1417,"rx":1418,"fill":154,"stroke":145,"style":108},"228.0","175.0","14",[110,1420,1422],{"transform":1421},"translate(121.0,29.0) scale(1.083)",[114,1423],{"d":186,"fill":117,"stroke":161,"style":1424},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[121,1426,1430],{"x":1427,"y":1428,"style":1429},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Transaction rate",[37,1432],{"x1":1433,"y1":1434,"x2":1435,"y2":1434,"stroke":145,"style":1436},"36.0","91.0","232.0","stroke-width:1px",[121,1438,1441],{"x":1427,"y":1439,"style":1440},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","rate on the invoice date",[121,1443,1445],{"x":1427,"y":1444,"style":1440},"137.0","books each sale",[121,1447,1449],{"x":1427,"y":1448,"style":1440},"163.0","Step 2",[92,1451],{"x":1452,"y":102,"width":1416,"height":1417,"rx":1418,"fill":210,"stroke":211,"style":108},"266.0",[110,1454,1456],{"transform":1455},"translate(367.0,29.0) scale(1.083)",[114,1457],{"d":1458,"fill":117,"stroke":218,"style":1424},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8",[121,1460,1462],{"x":189,"y":1428,"style":1461},"font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Monthly average",[37,1464],{"x1":1465,"y1":1434,"x2":1466,"y2":1434,"stroke":211,"style":1436},"282.0","478.0",[121,1468,1469],{"x":189,"y":1439,"style":1440},"mean of the month's rates",[121,1471,1472],{"x":189,"y":1444,"style":1440},"P&L translation",[121,1474,1475],{"x":189,"y":1448,"style":1440},"one rate per month",[92,1477],{"x":1478,"y":102,"width":1416,"height":1417,"rx":1418,"fill":240,"stroke":241,"style":108},"512.0",[110,1480,1482],{"transform":1481},"translate(613.0,29.0) scale(1.083)",[114,1483],{"d":1484,"fill":117,"stroke":248,"style":1424},"M5 12.5l4.5 4.5L19 7",[121,1486,1489],{"x":1487,"y":1428,"style":1488},"626.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Closing rate",[37,1491],{"x1":1492,"y1":1434,"x2":1493,"y2":1434,"stroke":241,"style":1436},"528.0","724.0",[121,1495,1496],{"x":1487,"y":1439,"style":1440},"last rate of the month",[121,1498,1499],{"x":1487,"y":1444,"style":1440},"open balances",[121,1501,1502],{"x":1487,"y":1448,"style":1440},"revaluation",[20,1504,1506],{"id":1505},"step-5-revalue-open-invoices","Step 5 — Revalue open invoices",[10,1508,1509],{},"An invoice booked at one rate and still unpaid at the month end is worth a different amount in pounds at the closing rate. The difference is an unrealised exchange gain or loss, reported until the invoice is paid, when it becomes realised:",[25,1511,1513],{"className":276,"code":1512,"language":278,"meta":30,"style":30},"open_inv = conv[conv[\"Status\"] == \"Open\"].copy()\nq3_close = rate_table[rate_table[\"Month\"] == \"2026-09\"].set_index(\"Currency\")[\"ClosingRate\"]\nopen_inv[\"ClosingRate\"] = open_inv[\"Currency\"].map(q3_close).fillna(1.0)\nopen_inv[\"ValueAtClose\"] = (open_inv[\"Amount\"] * open_inv[\"ClosingRate\"]).round(2)\nopen_inv[\"FX gain\u002F(loss)\"] = (open_inv[\"ValueAtClose\"] - open_inv[\"BaseAmount\"]).round(2)\nprint(open_inv[[\"Invoice\", \"Currency\", \"Amount\", \"BaseAmount\", \"ValueAtClose\", \"FX gain\u002F(loss)\"]].to_string(index=False))\nprint(f\"net unrealised FX: {open_inv['FX gain\u002F(loss)'].sum():,.2f} GBP\")\n",[32,1514,1515,1537,1566,1589,1619,1649,1689],{"__ignoreMap":30},[35,1516,1517,1520,1522,1524,1527,1529,1531,1534],{"class":37,"line":38},[35,1518,1519],{"class":289},"open_inv ",[35,1521,312],{"class":285},[35,1523,898],{"class":289},[35,1525,1526],{"class":45},"\"Status\"",[35,1528,429],{"class":289},[35,1530,582],{"class":285},[35,1532,1533],{"class":45}," \"Open\"",[35,1535,1536],{"class":289},"].copy()\n",[35,1538,1539,1542,1544,1547,1549,1551,1553,1555,1558,1560,1562,1564],{"class":37,"line":299},[35,1540,1541],{"class":289},"q3_close ",[35,1543,312],{"class":285},[35,1545,1546],{"class":289}," rate_table[rate_table[",[35,1548,1121],{"class":45},[35,1550,429],{"class":289},[35,1552,582],{"class":285},[35,1554,1369],{"class":45},[35,1556,1557],{"class":289},"].set_index(",[35,1559,382],{"class":45},[35,1561,411],{"class":289},[35,1563,1332],{"class":45},[35,1565,416],{"class":289},[35,1567,1568,1571,1573,1575,1577,1580,1582,1585,1587],{"class":37,"line":306},[35,1569,1570],{"class":289},"open_inv[",[35,1572,1332],{"class":45},[35,1574,429],{"class":289},[35,1576,312],{"class":285},[35,1578,1579],{"class":289}," open_inv[",[35,1581,382],{"class":45},[35,1583,1584],{"class":289},"].map(q3_close).fillna(",[35,1586,490],{"class":342},[35,1588,395],{"class":289},[35,1590,1591,1593,1596,1598,1600,1603,1605,1607,1609,1611,1613,1615,1617],{"class":37,"line":339},[35,1592,1570],{"class":289},[35,1594,1595],{"class":45},"\"ValueAtClose\"",[35,1597,429],{"class":289},[35,1599,312],{"class":285},[35,1601,1602],{"class":289}," (open_inv[",[35,1604,1093],{"class":45},[35,1606,429],{"class":289},[35,1608,1098],{"class":285},[35,1610,1579],{"class":289},[35,1612,1332],{"class":45},[35,1614,1106],{"class":289},[35,1616,1033],{"class":342},[35,1618,395],{"class":289},[35,1620,1621,1623,1626,1628,1630,1632,1634,1636,1639,1641,1643,1645,1647],{"class":37,"line":352},[35,1622,1570],{"class":289},[35,1624,1625],{"class":45},"\"FX gain\u002F(loss)\"",[35,1627,429],{"class":289},[35,1629,312],{"class":285},[35,1631,1602],{"class":289},[35,1633,1595],{"class":45},[35,1635,429],{"class":289},[35,1637,1638],{"class":285},"-",[35,1640,1579],{"class":289},[35,1642,1083],{"class":45},[35,1644,1106],{"class":289},[35,1646,1033],{"class":342},[35,1648,395],{"class":289},[35,1650,1651,1653,1656,1658,1660,1662,1664,1666,1668,1670,1672,1674,1676,1678,1681,1683,1685,1687],{"class":37,"line":357},[35,1652,572],{"class":342},[35,1654,1655],{"class":289},"(open_inv[[",[35,1657,957],{"class":45},[35,1659,321],{"class":289},[35,1661,382],{"class":45},[35,1663,321],{"class":289},[35,1665,1093],{"class":45},[35,1667,321],{"class":289},[35,1669,1083],{"class":45},[35,1671,321],{"class":289},[35,1673,1595],{"class":45},[35,1675,321],{"class":289},[35,1677,1625],{"class":45},[35,1679,1680],{"class":289},"]].to_string(",[35,1682,601],{"class":324},[35,1684,312],{"class":285},[35,1686,606],{"class":342},[35,1688,609],{"class":289},[35,1690,1691,1693,1695,1697,1700,1702,1704,1707,1710,1713,1715,1717],{"class":37,"line":398},[35,1692,572],{"class":342},[35,1694,679],{"class":289},[35,1696,682],{"class":285},[35,1698,1699],{"class":45},"\"net unrealised FX: ",[35,1701,689],{"class":688},[35,1703,1570],{"class":289},[35,1705,1706],{"class":45},"'FX gain\u002F(loss)'",[35,1708,1709],{"class":289},"].sum()",[35,1711,1712],{"class":285},":,.2f",[35,1714,704],{"class":688},[35,1716,707],{"class":45},[35,1718,395],{"class":289},[10,1720,1721],{},"The GBP invoice shows zero, as it must. The largest movement is on the dollar invoice from 18 September: the dollar weakened against sterling over the last fortnight of the quarter, from 0.7248 to 0.7166 pounds, so 30,400 dollars are worth 247 pounds less than when they were booked. Next month the revaluation is repeated from the new closing rate, and when an invoice is paid the difference between its booked value and the pounds actually received becomes a realised gain or loss.",[73,1723,82,1729,82,1732,82,1735,82,1740,82,1746,82,1752,82,1756,82,1760,82,1764,82,1768,82,1772,82,1776,82,1779,82,1784,82,1786,82,1789,82,1791,82,1794,82,1796,82,1799,82,1802,82,1805,82,1807,82,1810,82,1812,82,1815,82,1817,82,1820,82,1823,82,1827,82,1829,82,1831,82,1835],{"viewBox":1724,"role":76,"ariaLabelledBy":1725,"xmlns":80,"style":1728},"171 0 417 144",[1726,1727],"fx-reval-t","fx-reval-d","width:100%;max-width:417px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[84,1730,1731],{"id":1726},"Revaluing the open dollar invoice",[88,1733,1734],{"id":1727},"A worked example for INV-8811, 30,400 dollars invoiced on 18 September. Booked at 0.724755 pounds per dollar it was worth 22,032.56 pounds; at the 30 September closing rate of 0.716628 it is worth 21,785.48, an unrealised loss of 247.08.",[92,1736],{"x":1737,"y":95,"width":1738,"height":1739,"fill":98},"171","417","144",[92,1741],{"x":1742,"y":102,"width":1743,"height":1744,"rx":95,"fill":154,"stroke":145,"style":1745},"187.9","110.7","28.0","stroke-width:1.5px",[121,1747,1751],{"x":1748,"y":1749,"style":1750},"243.2","35.0","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","INV-8811",[92,1753],{"x":1754,"y":102,"width":1755,"height":1744,"rx":95,"fill":154,"stroke":145,"style":1745},"298.6","93.6",[121,1757,1759],{"x":1758,"y":1749,"style":1750},"345.4","30,400 USD",[92,1761],{"x":1762,"y":102,"width":1763,"height":1744,"rx":95,"fill":154,"stroke":145,"style":1745},"392.2","96.5",[121,1765,1767],{"x":1766,"y":1749,"style":1750},"440.4","GBP per USD",[92,1769],{"x":1770,"y":102,"width":1771,"height":1744,"rx":95,"fill":154,"stroke":145,"style":1745},"488.7","83.5",[121,1773,1775],{"x":1774,"y":1749,"style":1750},"530.4","GBP value",[92,1777],{"x":1742,"y":1778,"width":1743,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},"44.0",[121,1780,1783],{"x":1748,"y":1781,"style":1782},"63.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Booked",[92,1785],{"x":1754,"y":1778,"width":1755,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[121,1787,1788],{"x":1758,"y":1781,"style":1782},"18 Sep",[92,1790],{"x":1762,"y":1778,"width":1763,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[121,1792,1793],{"x":1766,"y":1781,"style":1782},"0.724755",[92,1795],{"x":1770,"y":1778,"width":1771,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[121,1797,1798],{"x":1774,"y":1781,"style":1782},"22,032.56",[92,1800],{"x":1742,"y":1801,"width":1743,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},"72.0",[121,1803,1804],{"x":1748,"y":1434,"style":1782},"Valued at close",[92,1806],{"x":1754,"y":1801,"width":1755,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[121,1808,1809],{"x":1758,"y":1434,"style":1782},"30 Sep",[92,1811],{"x":1762,"y":1801,"width":1763,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[121,1813,1814],{"x":1766,"y":1434,"style":1782},"0.716628",[92,1816],{"x":1770,"y":1801,"width":1771,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[121,1818,1819],{"x":1774,"y":1434,"style":1782},"21,785.48",[92,1821],{"x":1742,"y":1822,"width":1743,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},"100.0",[121,1824,1826],{"x":1748,"y":1825,"style":1782},"119.0","Unrealised loss",[92,1828],{"x":1754,"y":1822,"width":1755,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[92,1830],{"x":1762,"y":1822,"width":1763,"height":1744,"rx":95,"fill":98,"stroke":107,"style":1436},[92,1832],{"x":1770,"y":1822,"width":1771,"height":1744,"rx":95,"fill":1833,"stroke":1834,"style":1745},"#fce9e9","var(--danger,#dc2626)",[121,1836,1838],{"x":1774,"y":1825,"style":1837},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","-247.08",[20,1840,1842],{"id":1841},"step-6-write-the-report","Step 6 — Write the report",[10,1844,1845],{},"Keep the rates alongside the results: anyone checking a figure needs the rate and its date. Write the converted invoices, the rate table and the revaluation to one workbook:",[25,1847,1849],{"className":276,"code":1848,"language":278,"meta":30,"style":30},"with pd.ExcelWriter(\"fx_report_q3_2026.xlsx\", engine=\"xlsxwriter\") as xw:\n    out = conv.drop(columns=[\"Month\"]).copy()\n    out.to_excel(xw, sheet_name=\"Invoices in GBP\", index=False)\n    rate_table.assign(Month=rate_table[\"Month\"].astype(str)).to_excel(xw, sheet_name=\"Rates\", index=False)\n    open_inv.drop(columns=[\"Month\"]).to_excel(xw, sheet_name=\"Revaluation\", index=False)\n    book = xw.book\n    rate_fmt = book.add_format({\"num_format\": \"0.000000\"})\n    money = book.add_format({\"num_format\": \"#,##0.00;[Red](#,##0.00)\"})\n    date_fmt = book.add_format({\"num_format\": \"yyyy-mm-dd\"})\n    ws = xw.sheets[\"Invoices in GBP\"]\n    ws.set_column(\"B:B\", 12, date_fmt)\n    ws.set_column(\"C:C\", 14)\n    ws.set_column(\"E:E\", 14, book.add_format({\"num_format\": \"#,##0.##\"}))\n    ws.set_column(\"G:G\", 12, date_fmt)\n    ws.set_column(\"H:H\", 12, rate_fmt)\n    ws.set_column(\"I:I\", 13, money)\n    xw.sheets[\"Rates\"].set_column(\"C:D\", 12, rate_fmt)\n    xw.sheets[\"Revaluation\"].set_column(\"I:L\", 14, money)\nprint(\"written\")\n",[32,1850,1851,1880,1901,1924,1965,1998,2008,2029,2047,2065,2079,2094,2107,2131,2144,2158,2173,2193,2211],{"__ignoreMap":30},[35,1852,1853,1856,1859,1862,1864,1867,1869,1872,1875,1877],{"class":37,"line":38},[35,1854,1855],{"class":285},"with",[35,1857,1858],{"class":289}," pd.ExcelWriter(",[35,1860,1861],{"class":45},"\"fx_report_q3_2026.xlsx\"",[35,1863,321],{"class":289},[35,1865,1866],{"class":324},"engine",[35,1868,312],{"class":285},[35,1870,1871],{"class":45},"\"xlsxwriter\"",[35,1873,1874],{"class":289},") ",[35,1876,293],{"class":285},[35,1878,1879],{"class":289}," xw:\n",[35,1881,1882,1885,1887,1890,1892,1894,1896,1898],{"class":37,"line":299},[35,1883,1884],{"class":289},"    out ",[35,1886,312],{"class":285},[35,1888,1889],{"class":289}," conv.drop(",[35,1891,782],{"class":324},[35,1893,312],{"class":285},[35,1895,330],{"class":289},[35,1897,1121],{"class":45},[35,1899,1900],{"class":289},"]).copy()\n",[35,1902,1903,1906,1909,1911,1914,1916,1918,1920,1922],{"class":37,"line":306},[35,1904,1905],{"class":289},"    out.to_excel(xw, ",[35,1907,1908],{"class":324},"sheet_name",[35,1910,312],{"class":285},[35,1912,1913],{"class":45},"\"Invoices in GBP\"",[35,1915,321],{"class":289},[35,1917,601],{"class":324},[35,1919,312],{"class":285},[35,1921,606],{"class":342},[35,1923,395],{"class":289},[35,1925,1926,1929,1932,1934,1937,1939,1942,1945,1948,1950,1952,1955,1957,1959,1961,1963],{"class":37,"line":339},[35,1927,1928],{"class":289},"    rate_table.assign(",[35,1930,1931],{"class":324},"Month",[35,1933,312],{"class":285},[35,1935,1936],{"class":289},"rate_table[",[35,1938,1121],{"class":45},[35,1940,1941],{"class":289},"].astype(",[35,1943,1944],{"class":342},"str",[35,1946,1947],{"class":289},")).to_excel(xw, ",[35,1949,1908],{"class":324},[35,1951,312],{"class":285},[35,1953,1954],{"class":45},"\"Rates\"",[35,1956,321],{"class":289},[35,1958,601],{"class":324},[35,1960,312],{"class":285},[35,1962,606],{"class":342},[35,1964,395],{"class":289},[35,1966,1967,1970,1972,1974,1976,1978,1981,1983,1985,1988,1990,1992,1994,1996],{"class":37,"line":352},[35,1968,1969],{"class":289},"    open_inv.drop(",[35,1971,782],{"class":324},[35,1973,312],{"class":285},[35,1975,330],{"class":289},[35,1977,1121],{"class":45},[35,1979,1980],{"class":289},"]).to_excel(xw, ",[35,1982,1908],{"class":324},[35,1984,312],{"class":285},[35,1986,1987],{"class":45},"\"Revaluation\"",[35,1989,321],{"class":289},[35,1991,601],{"class":324},[35,1993,312],{"class":285},[35,1995,606],{"class":342},[35,1997,395],{"class":289},[35,1999,2000,2003,2005],{"class":37,"line":357},[35,2001,2002],{"class":289},"    book ",[35,2004,312],{"class":285},[35,2006,2007],{"class":289}," xw.book\n",[35,2009,2010,2013,2015,2018,2021,2023,2026],{"class":37,"line":398},[35,2011,2012],{"class":289},"    rate_fmt ",[35,2014,312],{"class":285},[35,2016,2017],{"class":289}," book.add_format({",[35,2019,2020],{"class":45},"\"num_format\"",[35,2022,478],{"class":289},[35,2024,2025],{"class":45},"\"0.000000\"",[35,2027,2028],{"class":289},"})\n",[35,2030,2031,2034,2036,2038,2040,2042,2045],{"class":37,"line":419},[35,2032,2033],{"class":289},"    money ",[35,2035,312],{"class":285},[35,2037,2017],{"class":289},[35,2039,2020],{"class":45},[35,2041,478],{"class":289},[35,2043,2044],{"class":45},"\"#,##0.00;[Red](#,##0.00)\"",[35,2046,2028],{"class":289},[35,2048,2049,2052,2054,2056,2058,2060,2063],{"class":37,"line":455},[35,2050,2051],{"class":289},"    date_fmt ",[35,2053,312],{"class":285},[35,2055,2017],{"class":289},[35,2057,2020],{"class":45},[35,2059,478],{"class":289},[35,2061,2062],{"class":45},"\"yyyy-mm-dd\"",[35,2064,2028],{"class":289},[35,2066,2067,2070,2072,2075,2077],{"class":37,"line":500},[35,2068,2069],{"class":289},"    ws ",[35,2071,312],{"class":285},[35,2073,2074],{"class":289}," xw.sheets[",[35,2076,1913],{"class":45},[35,2078,416],{"class":289},[35,2080,2081,2084,2087,2089,2091],{"class":37,"line":542},[35,2082,2083],{"class":289},"    ws.set_column(",[35,2085,2086],{"class":45},"\"B:B\"",[35,2088,321],{"class":289},[35,2090,105],{"class":342},[35,2092,2093],{"class":289},", date_fmt)\n",[35,2095,2096,2098,2101,2103,2105],{"class":37,"line":569},[35,2097,2083],{"class":289},[35,2099,2100],{"class":45},"\"C:C\"",[35,2102,321],{"class":289},[35,2104,1418],{"class":342},[35,2106,395],{"class":289},[35,2108,2109,2111,2114,2116,2118,2121,2123,2125,2128],{"class":37,"line":612},[35,2110,2083],{"class":289},[35,2112,2113],{"class":45},"\"E:E\"",[35,2115,321],{"class":289},[35,2117,1418],{"class":342},[35,2119,2120],{"class":289},", book.add_format({",[35,2122,2020],{"class":45},[35,2124,478],{"class":289},[35,2126,2127],{"class":45},"\"#,##0.##\"",[35,2129,2130],{"class":289},"}))\n",[35,2132,2133,2135,2138,2140,2142],{"class":37,"line":617},[35,2134,2083],{"class":289},[35,2136,2137],{"class":45},"\"G:G\"",[35,2139,321],{"class":289},[35,2141,105],{"class":342},[35,2143,2093],{"class":289},[35,2145,2146,2148,2151,2153,2155],{"class":37,"line":648},[35,2147,2083],{"class":289},[35,2149,2150],{"class":45},"\"H:H\"",[35,2152,321],{"class":289},[35,2154,105],{"class":342},[35,2156,2157],{"class":289},", rate_fmt)\n",[35,2159,2160,2162,2165,2167,2170],{"class":37,"line":674},[35,2161,2083],{"class":289},[35,2163,2164],{"class":45},"\"I:I\"",[35,2166,321],{"class":289},[35,2168,2169],{"class":342},"13",[35,2171,2172],{"class":289},", money)\n",[35,2174,2176,2179,2181,2184,2187,2189,2191],{"class":37,"line":2175},17,[35,2177,2178],{"class":289},"    xw.sheets[",[35,2180,1954],{"class":45},[35,2182,2183],{"class":289},"].set_column(",[35,2185,2186],{"class":45},"\"C:D\"",[35,2188,321],{"class":289},[35,2190,105],{"class":342},[35,2192,2157],{"class":289},[35,2194,2196,2198,2200,2202,2205,2207,2209],{"class":37,"line":2195},18,[35,2197,2178],{"class":289},[35,2199,1987],{"class":45},[35,2201,2183],{"class":289},[35,2203,2204],{"class":45},"\"I:L\"",[35,2206,321],{"class":289},[35,2208,1418],{"class":342},[35,2210,2172],{"class":289},[35,2212,2214,2216,2218,2221],{"class":37,"line":2213},19,[35,2215,572],{"class":342},[35,2217,679],{"class":289},[35,2219,2220],{"class":45},"\"written\"",[35,2222,395],{"class":289},[10,2224,2225,2226,2229,2230,18],{},"The source-amount format ",[32,2227,2228],{},"#,##0.##"," shows yen without decimals and euros with them; for strict per-currency formatting, write each row with a format chosen from ",[32,2231,1020],{},[20,2233,2235],{"id":2234},"common-pitfalls","Common pitfalls",[2237,2238,2239,2247,2256,2262,2268,2274],"ul",{},[2240,2241,2242,2246],"li",{},[2243,2244,2245],"strong",{},"Inverted rates."," Know whether the table says units of currency per euro or euros per unit, and sanity-check a known conversion.",[2240,2248,2249,2252,2253,2255],{},[2243,2250,2251],{},"Exact date joins."," Weekend and holiday transactions have no rate row; use ",[32,2254,722],{}," with a tolerance.",[2240,2257,2258,2261],{},[2243,2259,2260],{},"The base currency."," It has no row in the rate table; give it a rate of 1 explicitly.",[2240,2263,2264,2267],{},[2243,2265,2266],{},"Rounding the rate."," Round the converted amount, not the rate.",[2240,2269,2270,2273],{},[2243,2271,2272],{},"Mixing methods."," Label which figures use transaction, average or closing rates; mixing them in one total produces unexplained differences.",[2240,2275,2276,2279],{},[2243,2277,2278],{},"Revaluing paid invoices."," Only balances still open at the reporting date are revalued; paid ones produce a realised difference at payment.",[20,2281,2283],{"id":2282},"conclusion","Conclusion",[10,2285,2286,2287,2290],{},"Triangulate a single-base rate table into base-currency rates per unit, attach the rate in force on each transaction date with ",[32,2288,2289],{},"merge_asof(..., by=\"Currency\", tolerance=...)",", give the base currency a rate of 1, and round converted amounts to the base currency's minor unit. Derive monthly average and closing rates from the same table for P&L translation and balance revaluation, calculate unrealised gains and losses on open invoices, and keep the rates and their dates in the report.",[20,2292,2294],{"id":2293},"frequently-asked-questions","Frequently asked questions",[10,2296,2297,2300],{},[2243,2298,2299],{},"How do I convert currencies using ECB euro reference rates?","\nThe rates say how many units of each currency one euro buys. To convert into a non-euro base currency, divide the base currency's rate by the source currency's rate for the same day; for euro amounts, multiply by the base currency's rate.",[10,2302,2303,2306],{},[2243,2304,2305],{},"Which exchange rate should a weekend transaction use?","\nUsually the latest published rate on or before the transaction date, typically Friday's. merge_asof with direction=\"backward\" implements that rule.",[10,2308,2309,2312],{},[2243,2310,2311],{},"What is the difference between average and closing rates?","\nAverage rates, the mean of a month's daily rates, translate income and expenses for the month; closing rates, the last rate of the month, value balances that are still open at that date.",[10,2314,2315,2318],{},[2243,2316,2317],{},"How do I calculate an unrealised FX gain or loss?","\nFor each open foreign-currency balance, convert it at the closing rate and subtract the amount it was booked at in the base currency. A positive result is a gain.",[20,2320,2322],{"id":2321},"related","Related",[2237,2324,2325,2331,2336,2343,2350],{},[2240,2326,2327,2328,2330],{},"Up one level: ",[14,2329,17],{"href":16}," — the rest of the finance reports.",[2240,2332,2333,2335],{},[14,2334,999],{"href":998}," — the join in detail.",[2240,2337,2338,2342],{},[14,2339,2341],{"href":2340},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-accounts-receivable-aging-report\u002F","Build an Accounts Receivable Aging Report"," — the open invoices, by age.",[2240,2344,2345,2349],{},[14,2346,2348],{"href":2347},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fjoin-rows-to-effective-date-ranges-with-pandas\u002F","Join Rows to Effective Date Ranges with pandas"," — rates valid between two dates.",[2240,2351,2352,2356],{},[14,2353,2355],{"href":2354},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger\u002F","Build a Profit and Loss Statement from a General Ledger"," — where translated income lands.",[2358,2359,2360],"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}",{"title":30,"searchDepth":299,"depth":299,"links":2362},[2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373],{"id":22,"depth":299,"text":23},{"id":265,"depth":299,"text":266},{"id":715,"depth":299,"text":716},{"id":1006,"depth":299,"text":1007},{"id":1234,"depth":299,"text":1235},{"id":1505,"depth":299,"text":1506},{"id":1841,"depth":299,"text":1842},{"id":2234,"depth":299,"text":2235},{"id":2282,"depth":299,"text":2283},{"id":2293,"depth":299,"text":2294},{"id":2321,"depth":299,"text":2322},"2026-09-29","Report transactions in several currencies in one base currency: turn euro-based reference rates into base-currency rates by triangulation, apply the rate in force on each transaction date, round to each currency's minor units, use monthly average rates for the P&L and closing rates for balances, and calculate the unrealised gain or loss on open invoices.","md",[2378,2380,2382,2384],{"q":2299,"a":2379},"The rates say how many units of each currency one euro buys. To convert into a non-euro base currency, divide the base currency's rate by the source currency's rate for the same day; for euro amounts, multiply by the base currency's rate.",{"q":2305,"a":2381},"Usually the latest published rate on or before the transaction date, typically Friday's. merge_asof with direction=\"backward\" implements that rule.",{"q":2311,"a":2383},"Average rates, the mean of a month's daily rates, translate income and expenses for the month; closing rates, the last rate of the month, value balances that are still open at that date.",{"q":2317,"a":2385},"For each open foreign-currency balance, convert it at the closing rate and subtract the amount it was booked at in the base currency. A positive result is a gain.","convert multi-currency transactions to a base currency",null,{"breadcrumb":2389},[2390,2392,2395],{"name":2391,"item":444},"Home",{"name":2393,"item":2394},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2396,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table",{"title":2399,"description":2400},"Convert Multi-Currency Transactions to a Base Currency in Python","Convert EUR, USD and JPY transactions to GBP with pandas: triangulate ECB-style EUR reference rates, merge_asof by currency, currency minor units, monthly average vs closing rates, and FX revaluation of open receivables in Excel.","convert-multi-currency-transactions-with-an-fx-rate-table","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table\u002Findex",[2404,2405],"joins-and-lookups","report-layouts","how-to","NJv1ESHjUsducVV86vNEqyhRS6FFJIq5Jrmr4OtEcSU",[2409,2413],{"title":2410,"path":2411,"stem":2412,"children":-1},"Consolidate Timesheets from Many Workbooks with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks\u002Findex",{"title":2414,"path":2415,"stem":2416,"children":-1},"Generate a Staff Rota in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fgenerate-a-staff-rota-in-excel-with-python","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fgenerate-a-staff-rota-in-excel-with-python\u002Findex",[2418,2420],{"path":2419,"title":2396},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2421,"title":2393},"\u002Fautomating-reporting-workflows",[],[2424,2428,2431,2434,2437,2440],{"path":2425,"title":2426,"kind":2427},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes","Merging and Joining Excel DataFrames with Pandas","guide",{"path":2429,"title":2430,"kind":2406},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fappend-new-rows-to-a-master-excel-file-without-duplicates","Append New Rows to a Master Excel File Without Duplicates",{"path":2432,"title":2433,"kind":2406},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fconcatenate-excel-sheets-with-different-columns","Concatenate Excel Sheets with Different Columns",{"path":2435,"title":2436,"kind":2406},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join","Create Every Combination of Two Lists with a Cross Join in pandas",{"path":2438,"title":2439,"kind":2406},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another","Find Rows in One Excel File Missing from Another",{"path":2441,"title":2442,"kind":2406},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python","Fuzzy Match Names Between Two Excel Files with Python",1790972127991]