[{"data":1,"prerenderedAt":2702},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report":2667,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report":2676,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report":2681,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report":2682},{"id":4,"title":5,"body":6,"dateModified":2632,"datePublished":2632,"description":2633,"extension":2634,"faq":2635,"keyword":2644,"kind":2645,"meta":2646,"navigation":122,"path":2656,"seo":2657,"slug":2660,"stem":2661,"topics":2662,"type":2665,"__hash__":2666},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report\u002Findex.md","Consolidate Employee Expense Claims into a Report",{"type":7,"value":8,"toc":2620},"minimark",[9,19,24,53,60,64,67,515,530,766,770,777,989,999,1003,1006,1410,1413,1542,1546,1549,1823,1830,1834,1837,2368,2375,2502,2506,2548,2552,2555,2559,2565,2571,2577,2583,2587,2616],[10,11,12,13,18],"p",{},"Each month, every employee who travelled fills in the expense claim template — name, employee number and cost centre at the top, a table of receipts below — and emails it to finance. Someone then opens thirty workbooks, copies the lines into a master sheet, converts the euro and dollar receipts, checks each line against the expenses policy, and totals it all up for payroll. It is slow, it is where copy-paste errors creep in, and the policy checks are the first thing skipped when the deadline is close. A Python script can do all of it in seconds: read every form, combine the lines, convert currencies, apply the policy rules consistently, and produce a report with the totals to reimburse and a list of exceptions for the approver. 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,56,59],{},"The ",[32,57,58],{},"claims\u002F"," folder holds one form per employee in the company template: the claimant's name, employee ID and cost centre in cells B2–B4, and from row 7 a table with date, category, description, amount, currency and whether a receipt is attached (Y\u002FN). The policy used below is an example; replace its numbers with your own.",[20,61,63],{"id":62},"step-1-read-every-form-header-cells-and-the-claim-table","Step 1 — Read every form: header cells and the claim table",[10,65,66],{},"The form mixes two shapes: single values in fixed cells at the top and a table underneath. Read the header with openpyxl and the table with pandas, then attach the header values to every line:",[25,68,72],{"className":69,"code":70,"language":71,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\nimport pandas as pd\nfrom openpyxl import load_workbook\n\nHEADER_CELLS = {\"Employee\": \"B2\", \"Employee ID\": \"B3\", \"Cost centre\": \"B4\"}\n\ndef read_claim(path):\n    wb = load_workbook(path, read_only=True, data_only=True)\n    ws = wb[\"Claim\"]\n    header = {k: ws[cell].value for k, cell in HEADER_CELLS.items()}\n    wb.close()\n    lines = pd.read_excel(path, sheet_name=\"Claim\", header=6, dtype={\"Receipt\": str})\n    lines = lines.dropna(how=\"all\")\n    return lines.assign(**header, Source=path.name)\n\nclaims = pd.concat([read_claim(p) for p in sorted(Path(\"claims\").glob(\"*.xlsx\"))], ignore_index=True)\nclaims[\"Date\"] = pd.to_datetime(claims[\"Date\"])\nprint(len(claims), \"lines from\", claims[\"Source\"].nunique(), \"forms\")\nprint(claims[[\"Employee\", \"Date\", \"Category\", \"Amount\", \"Currency\"]].head(3).to_string(index=False))\n","python",[32,73,74,90,104,117,124,170,175,188,221,238,264,270,319,339,362,367,412,434,466],{"__ignoreMap":30},[35,75,76,80,84,87],{"class":37,"line":38},[35,77,79],{"class":78},"s-kum","from",[35,81,83],{"class":82},"skGVy"," pathlib ",[35,85,86],{"class":78},"import",[35,88,89],{"class":82}," Path\n",[35,91,93,95,98,101],{"class":37,"line":92},2,[35,94,86],{"class":78},[35,96,97],{"class":82}," pandas ",[35,99,100],{"class":78},"as",[35,102,103],{"class":82}," pd\n",[35,105,107,109,112,114],{"class":37,"line":106},3,[35,108,79],{"class":78},[35,110,111],{"class":82}," openpyxl ",[35,113,86],{"class":78},[35,115,116],{"class":82}," load_workbook\n",[35,118,120],{"class":37,"line":119},4,[35,121,123],{"emptyLinePlaceholder":122},true,"\n",[35,125,127,131,134,137,140,143,146,149,152,154,157,159,162,164,167],{"class":37,"line":126},5,[35,128,130],{"class":129},"sP0c6","HEADER_CELLS",[35,132,133],{"class":78}," =",[35,135,136],{"class":82}," {",[35,138,139],{"class":45},"\"Employee\"",[35,141,142],{"class":82},": ",[35,144,145],{"class":45},"\"B2\"",[35,147,148],{"class":82},", ",[35,150,151],{"class":45},"\"Employee ID\"",[35,153,142],{"class":82},[35,155,156],{"class":45},"\"B3\"",[35,158,148],{"class":82},[35,160,161],{"class":45},"\"Cost centre\"",[35,163,142],{"class":82},[35,165,166],{"class":45},"\"B4\"",[35,168,169],{"class":82},"}\n",[35,171,173],{"class":37,"line":172},6,[35,174,123],{"emptyLinePlaceholder":122},[35,176,178,181,185],{"class":37,"line":177},7,[35,179,180],{"class":78},"def",[35,182,184],{"class":183},"s_Opv"," read_claim",[35,186,187],{"class":82},"(path):\n",[35,189,191,194,197,200,204,206,209,211,214,216,218],{"class":37,"line":190},8,[35,192,193],{"class":82},"    wb ",[35,195,196],{"class":78},"=",[35,198,199],{"class":82}," load_workbook(path, ",[35,201,203],{"class":202},"sa561","read_only",[35,205,196],{"class":78},[35,207,208],{"class":129},"True",[35,210,148],{"class":82},[35,212,213],{"class":202},"data_only",[35,215,196],{"class":78},[35,217,208],{"class":129},[35,219,220],{"class":82},")\n",[35,222,224,227,229,232,235],{"class":37,"line":223},9,[35,225,226],{"class":82},"    ws ",[35,228,196],{"class":78},[35,230,231],{"class":82}," wb[",[35,233,234],{"class":45},"\"Claim\"",[35,236,237],{"class":82},"]\n",[35,239,241,244,246,249,252,255,258,261],{"class":37,"line":240},10,[35,242,243],{"class":82},"    header ",[35,245,196],{"class":78},[35,247,248],{"class":82}," {k: ws[cell].value ",[35,250,251],{"class":78},"for",[35,253,254],{"class":82}," k, cell ",[35,256,257],{"class":78},"in",[35,259,260],{"class":129}," HEADER_CELLS",[35,262,263],{"class":82},".items()}\n",[35,265,267],{"class":37,"line":266},11,[35,268,269],{"class":82},"    wb.close()\n",[35,271,273,276,278,281,284,286,288,290,293,295,298,300,303,305,308,311,313,316],{"class":37,"line":272},12,[35,274,275],{"class":82},"    lines ",[35,277,196],{"class":78},[35,279,280],{"class":82}," pd.read_excel(path, ",[35,282,283],{"class":202},"sheet_name",[35,285,196],{"class":78},[35,287,234],{"class":45},[35,289,148],{"class":82},[35,291,292],{"class":202},"header",[35,294,196],{"class":78},[35,296,297],{"class":129},"6",[35,299,148],{"class":82},[35,301,302],{"class":202},"dtype",[35,304,196],{"class":78},[35,306,307],{"class":82},"{",[35,309,310],{"class":45},"\"Receipt\"",[35,312,142],{"class":82},[35,314,315],{"class":129},"str",[35,317,318],{"class":82},"})\n",[35,320,322,324,326,329,332,334,337],{"class":37,"line":321},13,[35,323,275],{"class":82},[35,325,196],{"class":78},[35,327,328],{"class":82}," lines.dropna(",[35,330,331],{"class":202},"how",[35,333,196],{"class":78},[35,335,336],{"class":45},"\"all\"",[35,338,220],{"class":82},[35,340,342,345,348,351,354,357,359],{"class":37,"line":341},14,[35,343,344],{"class":78},"    return",[35,346,347],{"class":82}," lines.assign(",[35,349,350],{"class":78},"**",[35,352,353],{"class":82},"header, ",[35,355,356],{"class":202},"Source",[35,358,196],{"class":78},[35,360,361],{"class":82},"path.name)\n",[35,363,365],{"class":37,"line":364},15,[35,366,123],{"emptyLinePlaceholder":122},[35,368,370,373,375,378,380,383,385,388,391,394,397,400,403,406,408,410],{"class":37,"line":369},16,[35,371,372],{"class":82},"claims ",[35,374,196],{"class":78},[35,376,377],{"class":82}," pd.concat([read_claim(p) ",[35,379,251],{"class":78},[35,381,382],{"class":82}," p ",[35,384,257],{"class":78},[35,386,387],{"class":129}," sorted",[35,389,390],{"class":82},"(Path(",[35,392,393],{"class":45},"\"claims\"",[35,395,396],{"class":82},").glob(",[35,398,399],{"class":45},"\"*.xlsx\"",[35,401,402],{"class":82},"))], ",[35,404,405],{"class":202},"ignore_index",[35,407,196],{"class":78},[35,409,208],{"class":129},[35,411,220],{"class":82},[35,413,415,418,421,424,426,429,431],{"class":37,"line":414},17,[35,416,417],{"class":82},"claims[",[35,419,420],{"class":45},"\"Date\"",[35,422,423],{"class":82},"] ",[35,425,196],{"class":78},[35,427,428],{"class":82}," pd.to_datetime(claims[",[35,430,420],{"class":45},[35,432,433],{"class":82},"])\n",[35,435,437,440,443,446,449,452,455,458,461,464],{"class":37,"line":436},18,[35,438,439],{"class":129},"print",[35,441,442],{"class":82},"(",[35,444,445],{"class":129},"len",[35,447,448],{"class":82},"(claims), ",[35,450,451],{"class":45},"\"lines from\"",[35,453,454],{"class":82},", claims[",[35,456,457],{"class":45},"\"Source\"",[35,459,460],{"class":82},"].nunique(), ",[35,462,463],{"class":45},"\"forms\"",[35,465,220],{"class":82},[35,467,469,471,474,476,478,480,482,485,487,490,492,495,498,501,504,507,509,512],{"class":37,"line":468},19,[35,470,439],{"class":129},[35,472,473],{"class":82},"(claims[[",[35,475,139],{"class":45},[35,477,148],{"class":82},[35,479,420],{"class":45},[35,481,148],{"class":82},[35,483,484],{"class":45},"\"Category\"",[35,486,148],{"class":82},[35,488,489],{"class":45},"\"Amount\"",[35,491,148],{"class":82},[35,493,494],{"class":45},"\"Currency\"",[35,496,497],{"class":82},"]].head(",[35,499,500],{"class":129},"3",[35,502,503],{"class":82},").to_string(",[35,505,506],{"class":202},"index",[35,508,196],{"class":78},[35,510,511],{"class":129},"False",[35,513,514],{"class":82},"))\n",[10,516,517,520,521,524,525,529],{},[32,518,519],{},"header=6"," tells pandas the column headings are on row 7 (counted from zero), skipping the header block above. ",[32,522,523],{},"dropna(how=\"all\")"," removes blank rows at the end of a form, which templates often have. Keeping the source file name on each line means every exception in the report can be traced back to the form it came from. If some forms might be on an older template, check the heading row matches the expected columns before reading — ",[14,526,528],{"href":527},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002F","Find the Header Row in an Excel Sheet with a Shifting Layout"," covers locating headers dynamically.",[531,532,540,541,540,545,540,549,540,557,540,566,540,570,540,577,540,581,540,585,540,589,540,594,540,596,540,600,540,602,540,605,540,608,540,612,540,614,540,618,540,623,540,627,540,630,540,633,540,635,540,638,540,640,540,643,540,646,540,649,540,651,540,654,540,656,540,659,540,662,540,666,540,668,540,671,540,673,540,676,540,680,540,685,540,689,540,693,540,697,540,701,540,704,540,707,540,709,540,711,540,713,540,716,540,719,540,722,540,724,540,726,540,728,540,731,540,737,540,741,540,748,540,753,540,761],"svg",{"viewBox":533,"role":534,"ariaLabelledBy":535,"xmlns":538,"style":539},"4 0 752 220","img",[536,537],"ex-form-t","ex-form-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  ",[542,543,544],"title",{"id":536},"One claim form becomes rows in the combined table",[546,547,548],"desc",{"id":537},"The form has the claimant's name, employee ID and cost centre in cells B2 to B4 and a table of receipts from row 7. Each table row becomes a row in the combined table with the header values repeated and the source file name added.",[550,551],"rect",{"x":552,"y":553,"width":554,"height":555,"fill":556},"4","0","752","220","#ffffff",[550,558],{"x":559,"y":560,"width":561,"height":562,"rx":553,"fill":563,"stroke":564,"style":565},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[550,567],{"x":568,"y":560,"width":569,"height":562,"rx":553,"fill":563,"stroke":564,"style":565},"52.0","64.5",[571,572,576],"text",{"x":573,"y":574,"style":575},"84.3","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[550,578],{"x":579,"y":560,"width":580,"height":562,"rx":553,"fill":563,"stroke":564,"style":565},"116.5","61.6",[571,582,584],{"x":583,"y":574,"style":575},"147.3","B",[550,586],{"x":559,"y":587,"width":561,"height":588,"rx":553,"fill":563,"stroke":564,"style":565},"64.0","28.0",[571,590,593],{"x":591,"y":592,"style":575},"36.0","83.0","1",[550,595],{"x":568,"y":587,"width":569,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,597,599],{"x":573,"y":592,"style":598},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Cell",[550,601],{"x":579,"y":587,"width":580,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,603,604],{"x":583,"y":592,"style":598},"Value",[550,606],{"x":559,"y":607,"width":561,"height":588,"rx":553,"fill":563,"stroke":564,"style":565},"92.0",[571,609,611],{"x":591,"y":610,"style":575},"111.0","2",[550,613],{"x":568,"y":607,"width":569,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,615,617],{"x":573,"y":610,"style":616},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","B2 Name",[550,619],{"x":579,"y":607,"width":580,"height":588,"rx":553,"fill":620,"stroke":621,"style":622},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[571,624,626],{"x":583,"y":610,"style":625},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","B. Okafor",[550,628],{"x":559,"y":629,"width":561,"height":588,"rx":553,"fill":563,"stroke":564,"style":565},"120.0",[571,631,500],{"x":591,"y":632,"style":575},"139.0",[550,634],{"x":568,"y":629,"width":569,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,636,637],{"x":573,"y":632,"style":616},"B4 Centre",[550,639],{"x":579,"y":629,"width":580,"height":588,"rx":553,"fill":620,"stroke":621,"style":622},[571,641,642],{"x":583,"y":632,"style":625},"CC-310",[550,644],{"x":559,"y":645,"width":561,"height":588,"rx":553,"fill":563,"stroke":564,"style":565},"148.0",[571,647,552],{"x":591,"y":648,"style":575},"167.0",[550,650],{"x":568,"y":645,"width":569,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,652,653],{"x":573,"y":648,"style":616},"Travel",[550,655],{"x":579,"y":645,"width":580,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,657,658],{"x":583,"y":648,"style":616},"212 EUR",[550,660],{"x":559,"y":661,"width":561,"height":588,"rx":553,"fill":563,"stroke":564,"style":565},"176.0",[571,663,665],{"x":591,"y":664,"style":575},"195.0","5",[550,667],{"x":568,"y":661,"width":569,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,669,670],{"x":573,"y":664,"style":616},"Hotel",[550,672],{"x":579,"y":661,"width":580,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,674,675],{"x":583,"y":664,"style":616},"310 EUR",[550,677],{"x":678,"y":560,"width":679,"height":588,"rx":553,"fill":563,"stroke":564,"style":622},"561.1","66.1",[571,681,684],{"x":682,"y":683,"style":598},"594.2","59.0","Employee",[550,686],{"x":687,"y":560,"width":688,"height":588,"rx":553,"fill":563,"stroke":564,"style":622},"627.3","55.5",[571,690,692],{"x":691,"y":683,"style":598},"655.0","Centre",[550,694],{"x":695,"y":560,"width":696,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},"682.8","57.2",[571,698,700],{"x":699,"y":683,"style":598},"711.4","Amount",[550,702],{"x":678,"y":703,"width":679,"height":588,"rx":553,"fill":620,"stroke":621,"style":622},"68.0",[571,705,626],{"x":682,"y":706,"style":625},"87.0",[550,708],{"x":687,"y":703,"width":688,"height":588,"rx":553,"fill":620,"stroke":621,"style":622},[571,710,642],{"x":691,"y":706,"style":625},[550,712],{"x":695,"y":703,"width":696,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,714,715],{"x":699,"y":706,"style":616},"212",[550,717],{"x":678,"y":718,"width":679,"height":588,"rx":553,"fill":620,"stroke":621,"style":622},"96.0",[571,720,626],{"x":682,"y":721,"style":625},"115.0",[550,723],{"x":687,"y":718,"width":688,"height":588,"rx":553,"fill":620,"stroke":621,"style":622},[571,725,642],{"x":691,"y":721,"style":625},[550,727],{"x":695,"y":718,"width":696,"height":588,"rx":553,"fill":556,"stroke":564,"style":565},[571,729,730],{"x":699,"y":721,"style":616},"310",[571,732,736],{"x":733,"y":734,"style":735},"99.0","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","claim form",[571,738,740],{"x":739,"y":734,"style":735},"650.6","combined lines",[37,742],{"x1":743,"y1":744,"x2":745,"y2":744,"stroke":746,"style":747},"188.1","82.0","547.1","var(--brand,#5b5cf0)","stroke-width:2px",[749,750],"polygon",{"points":751,"fill":752},"551.1,82.0 542.1,87.0 542.1,77.0","#5b5cf0",[550,754],{"x":755,"y":756,"width":757,"height":758,"rx":759,"fill":556,"stroke":564,"style":760},"232.4","49.0","274.4","22.0","11","stroke-width:1.2px",[571,762,765],{"x":763,"y":587,"style":764},"369.6","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","header cells + read_excel(header=6)",[20,767,769],{"id":768},"step-2-convert-to-the-reporting-currency","Step 2 — Convert to the reporting currency",[10,771,772,773,776],{},"Claims in euros and dollars are reimbursed in pounds at a rate set by finance for the month. Keep the rates in a small table and convert with a merge, so a missing rate is caught rather than silently producing ",[32,774,775],{},"NaN",":",[25,778,780],{"className":69,"code":779,"language":71,"meta":30,"style":30},"rates = pd.DataFrame({\"Currency\": [\"GBP\", \"EUR\", \"USD\"], \"Rate to GBP\": [1.0, 0.86, 0.75]})\nclaims = claims.merge(rates, on=\"Currency\", how=\"left\", validate=\"many_to_one\")\nmissing = claims.loc[claims[\"Rate to GBP\"].isna(), \"Currency\"].unique()\nif len(missing):\n    raise ValueError(f\"no rate for {list(missing)}\")\nclaims[\"GBP\"] = (claims[\"Amount\"] * claims[\"Rate to GBP\"]).round(2)\nprint(claims.groupby(\"Currency\")[\"GBP\"].sum().round(2).to_dict())\n",[32,781,782,834,871,891,902,935,967],{"__ignoreMap":30},[35,783,784,787,789,792,794,797,800,802,805,807,810,813,816,818,821,823,826,828,831],{"class":37,"line":38},[35,785,786],{"class":82},"rates ",[35,788,196],{"class":78},[35,790,791],{"class":82}," pd.DataFrame({",[35,793,494],{"class":45},[35,795,796],{"class":82},": [",[35,798,799],{"class":45},"\"GBP\"",[35,801,148],{"class":82},[35,803,804],{"class":45},"\"EUR\"",[35,806,148],{"class":82},[35,808,809],{"class":45},"\"USD\"",[35,811,812],{"class":82},"], ",[35,814,815],{"class":45},"\"Rate to GBP\"",[35,817,796],{"class":82},[35,819,820],{"class":129},"1.0",[35,822,148],{"class":82},[35,824,825],{"class":129},"0.86",[35,827,148],{"class":82},[35,829,830],{"class":129},"0.75",[35,832,833],{"class":82},"]})\n",[35,835,836,838,840,843,846,848,850,852,854,856,859,861,864,866,869],{"class":37,"line":92},[35,837,372],{"class":82},[35,839,196],{"class":78},[35,841,842],{"class":82}," claims.merge(rates, ",[35,844,845],{"class":202},"on",[35,847,196],{"class":78},[35,849,494],{"class":45},[35,851,148],{"class":82},[35,853,331],{"class":202},[35,855,196],{"class":78},[35,857,858],{"class":45},"\"left\"",[35,860,148],{"class":82},[35,862,863],{"class":202},"validate",[35,865,196],{"class":78},[35,867,868],{"class":45},"\"many_to_one\"",[35,870,220],{"class":82},[35,872,873,876,878,881,883,886,888],{"class":37,"line":106},[35,874,875],{"class":82},"missing ",[35,877,196],{"class":78},[35,879,880],{"class":82}," claims.loc[claims[",[35,882,815],{"class":45},[35,884,885],{"class":82},"].isna(), ",[35,887,494],{"class":45},[35,889,890],{"class":82},"].unique()\n",[35,892,893,896,899],{"class":37,"line":119},[35,894,895],{"class":78},"if",[35,897,898],{"class":129}," len",[35,900,901],{"class":82},"(missing):\n",[35,903,904,907,910,912,915,918,921,924,927,930,933],{"class":37,"line":126},[35,905,906],{"class":78},"    raise",[35,908,909],{"class":129}," ValueError",[35,911,442],{"class":82},[35,913,914],{"class":78},"f",[35,916,917],{"class":45},"\"no rate for ",[35,919,307],{"class":920},"sSjpA",[35,922,923],{"class":129},"list",[35,925,926],{"class":82},"(missing)",[35,928,929],{"class":920},"}",[35,931,932],{"class":45},"\"",[35,934,220],{"class":82},[35,936,937,939,941,943,945,948,950,952,955,958,960,963,965],{"class":37,"line":172},[35,938,417],{"class":82},[35,940,799],{"class":45},[35,942,423],{"class":82},[35,944,196],{"class":78},[35,946,947],{"class":82}," (claims[",[35,949,489],{"class":45},[35,951,423],{"class":82},[35,953,954],{"class":78},"*",[35,956,957],{"class":82}," claims[",[35,959,815],{"class":45},[35,961,962],{"class":82},"]).round(",[35,964,611],{"class":129},[35,966,220],{"class":82},[35,968,969,971,974,976,979,981,984,986],{"class":37,"line":177},[35,970,439],{"class":129},[35,972,973],{"class":82},"(claims.groupby(",[35,975,494],{"class":45},[35,977,978],{"class":82},")[",[35,980,799],{"class":45},[35,982,983],{"class":82},"].sum().round(",[35,985,611],{"class":129},[35,987,988],{"class":82},").to_dict())\n",[10,990,991,994,995,18],{},[32,992,993],{},"validate=\"many_to_one\""," makes pandas raise an error if the rate table accidentally lists a currency twice, which would otherwise duplicate claim lines. Rounding each converted line to pennies before totalling matches how the payment will be made. For daily or transaction-date rates rather than one rate per month, use an as-of merge as in ",[14,996,998],{"href":997},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table\u002F","Convert Multi-Currency Transactions with an FX Rate Table",[20,1000,1002],{"id":1001},"step-3-apply-the-expenses-policy","Step 3 — Apply the expenses policy",[10,1004,1005],{},"The policy rules become vectorised checks, each producing a readable reason. Lines can fail several rules, and every reason is kept:",[25,1007,1009],{"className":69,"code":1008,"language":71,"meta":30,"style":30},"POLICY = {\n    \"limits\": {\"Meals\": 75.00, \"Hotel\": 180.00, \"Taxi\": 60.00},   # per line, GBP\n    \"receipt_over\": 25.00,\n    \"allowed\": {\"Travel\", \"Meals\", \"Hotel\", \"Taxi\"},\n}\n\nchecks = {\n    \"category not allowed\": ~claims[\"Category\"].isin(POLICY[\"allowed\"]),\n    \"over category limit\": claims[\"GBP\"] > claims[\"Category\"].map(POLICY[\"limits\"]).fillna(float(\"inf\")),\n    \"receipt missing\": (claims[\"Receipt\"].str.upper() != \"Y\") & (claims[\"GBP\"] > POLICY[\"receipt_over\"]),\n    \"possible duplicate\": claims.duplicated([\"Employee ID\", \"Date\", \"Category\", \"Amount\", \"Currency\"], keep=False),\n    \"weekend\": claims[\"Date\"].dt.dayofweek >= 5,\n}\nclaims[\"Issues\"] = [\n    \"; \".join(name for name, mask in checks.items() if mask.iloc[i]) for i in range(len(claims))\n]\nflagged = claims[claims[\"Issues\"] != \"\"]\nprint(flagged[[\"Employee\", \"Date\", \"Category\", \"GBP\", \"Issues\"]].to_string(index=False))\n",[32,1010,1011,1021,1064,1077,1102,1106,1110,1119,1147,1190,1233,1271,1291,1295,1309,1349,1353,1374],{"__ignoreMap":30},[35,1012,1013,1016,1018],{"class":37,"line":38},[35,1014,1015],{"class":129},"POLICY",[35,1017,133],{"class":78},[35,1019,1020],{"class":82}," {\n",[35,1022,1023,1026,1029,1032,1034,1037,1039,1042,1044,1047,1049,1052,1054,1057,1060],{"class":37,"line":92},[35,1024,1025],{"class":45},"    \"limits\"",[35,1027,1028],{"class":82},": {",[35,1030,1031],{"class":45},"\"Meals\"",[35,1033,142],{"class":82},[35,1035,1036],{"class":129},"75.00",[35,1038,148],{"class":82},[35,1040,1041],{"class":45},"\"Hotel\"",[35,1043,142],{"class":82},[35,1045,1046],{"class":129},"180.00",[35,1048,148],{"class":82},[35,1050,1051],{"class":45},"\"Taxi\"",[35,1053,142],{"class":82},[35,1055,1056],{"class":129},"60.00",[35,1058,1059],{"class":82},"},   ",[35,1061,1063],{"class":1062},"s-wDw","# per line, GBP\n",[35,1065,1066,1069,1071,1074],{"class":37,"line":106},[35,1067,1068],{"class":45},"    \"receipt_over\"",[35,1070,142],{"class":82},[35,1072,1073],{"class":129},"25.00",[35,1075,1076],{"class":82},",\n",[35,1078,1079,1082,1084,1087,1089,1091,1093,1095,1097,1099],{"class":37,"line":119},[35,1080,1081],{"class":45},"    \"allowed\"",[35,1083,1028],{"class":82},[35,1085,1086],{"class":45},"\"Travel\"",[35,1088,148],{"class":82},[35,1090,1031],{"class":45},[35,1092,148],{"class":82},[35,1094,1041],{"class":45},[35,1096,148],{"class":82},[35,1098,1051],{"class":45},[35,1100,1101],{"class":82},"},\n",[35,1103,1104],{"class":37,"line":126},[35,1105,169],{"class":82},[35,1107,1108],{"class":37,"line":172},[35,1109,123],{"emptyLinePlaceholder":122},[35,1111,1112,1115,1117],{"class":37,"line":177},[35,1113,1114],{"class":82},"checks ",[35,1116,196],{"class":78},[35,1118,1020],{"class":82},[35,1120,1121,1124,1126,1129,1131,1133,1136,1138,1141,1144],{"class":37,"line":190},[35,1122,1123],{"class":45},"    \"category not allowed\"",[35,1125,142],{"class":82},[35,1127,1128],{"class":78},"~",[35,1130,417],{"class":82},[35,1132,484],{"class":45},[35,1134,1135],{"class":82},"].isin(",[35,1137,1015],{"class":129},[35,1139,1140],{"class":82},"[",[35,1142,1143],{"class":45},"\"allowed\"",[35,1145,1146],{"class":82},"]),\n",[35,1148,1149,1152,1155,1157,1159,1162,1164,1166,1169,1171,1173,1176,1179,1182,1184,1187],{"class":37,"line":223},[35,1150,1151],{"class":45},"    \"over category limit\"",[35,1153,1154],{"class":82},": claims[",[35,1156,799],{"class":45},[35,1158,423],{"class":82},[35,1160,1161],{"class":78},">",[35,1163,957],{"class":82},[35,1165,484],{"class":45},[35,1167,1168],{"class":82},"].map(",[35,1170,1015],{"class":129},[35,1172,1140],{"class":82},[35,1174,1175],{"class":45},"\"limits\"",[35,1177,1178],{"class":82},"]).fillna(",[35,1180,1181],{"class":129},"float",[35,1183,442],{"class":82},[35,1185,1186],{"class":45},"\"inf\"",[35,1188,1189],{"class":82},")),\n",[35,1191,1192,1195,1198,1200,1203,1206,1209,1212,1215,1217,1219,1221,1223,1226,1228,1231],{"class":37,"line":240},[35,1193,1194],{"class":45},"    \"receipt missing\"",[35,1196,1197],{"class":82},": (claims[",[35,1199,310],{"class":45},[35,1201,1202],{"class":82},"].str.upper() ",[35,1204,1205],{"class":78},"!=",[35,1207,1208],{"class":45}," \"Y\"",[35,1210,1211],{"class":82},") ",[35,1213,1214],{"class":78},"&",[35,1216,947],{"class":82},[35,1218,799],{"class":45},[35,1220,423],{"class":82},[35,1222,1161],{"class":78},[35,1224,1225],{"class":129}," POLICY",[35,1227,1140],{"class":82},[35,1229,1230],{"class":45},"\"receipt_over\"",[35,1232,1146],{"class":82},[35,1234,1235,1238,1241,1243,1245,1247,1249,1251,1253,1255,1257,1259,1261,1264,1266,1268],{"class":37,"line":266},[35,1236,1237],{"class":45},"    \"possible duplicate\"",[35,1239,1240],{"class":82},": claims.duplicated([",[35,1242,151],{"class":45},[35,1244,148],{"class":82},[35,1246,420],{"class":45},[35,1248,148],{"class":82},[35,1250,484],{"class":45},[35,1252,148],{"class":82},[35,1254,489],{"class":45},[35,1256,148],{"class":82},[35,1258,494],{"class":45},[35,1260,812],{"class":82},[35,1262,1263],{"class":202},"keep",[35,1265,196],{"class":78},[35,1267,511],{"class":129},[35,1269,1270],{"class":82},"),\n",[35,1272,1273,1276,1278,1280,1283,1286,1289],{"class":37,"line":272},[35,1274,1275],{"class":45},"    \"weekend\"",[35,1277,1154],{"class":82},[35,1279,420],{"class":45},[35,1281,1282],{"class":82},"].dt.dayofweek ",[35,1284,1285],{"class":78},">=",[35,1287,1288],{"class":129}," 5",[35,1290,1076],{"class":82},[35,1292,1293],{"class":37,"line":321},[35,1294,169],{"class":82},[35,1296,1297,1299,1302,1304,1306],{"class":37,"line":341},[35,1298,417],{"class":82},[35,1300,1301],{"class":45},"\"Issues\"",[35,1303,423],{"class":82},[35,1305,196],{"class":78},[35,1307,1308],{"class":82}," [\n",[35,1310,1311,1314,1317,1319,1322,1324,1327,1329,1332,1334,1337,1339,1342,1344,1346],{"class":37,"line":364},[35,1312,1313],{"class":45},"    \"; \"",[35,1315,1316],{"class":82},".join(name ",[35,1318,251],{"class":78},[35,1320,1321],{"class":82}," name, mask ",[35,1323,257],{"class":78},[35,1325,1326],{"class":82}," checks.items() ",[35,1328,895],{"class":78},[35,1330,1331],{"class":82}," mask.iloc[i]) ",[35,1333,251],{"class":78},[35,1335,1336],{"class":82}," i ",[35,1338,257],{"class":78},[35,1340,1341],{"class":129}," range",[35,1343,442],{"class":82},[35,1345,445],{"class":129},[35,1347,1348],{"class":82},"(claims))\n",[35,1350,1351],{"class":37,"line":369},[35,1352,237],{"class":82},[35,1354,1355,1358,1360,1363,1365,1367,1369,1372],{"class":37,"line":414},[35,1356,1357],{"class":82},"flagged ",[35,1359,196],{"class":78},[35,1361,1362],{"class":82}," claims[claims[",[35,1364,1301],{"class":45},[35,1366,423],{"class":82},[35,1368,1205],{"class":78},[35,1370,1371],{"class":45}," \"\"",[35,1373,237],{"class":82},[35,1375,1376,1378,1381,1383,1385,1387,1389,1391,1393,1395,1397,1399,1402,1404,1406,1408],{"class":37,"line":436},[35,1377,439],{"class":129},[35,1379,1380],{"class":82},"(flagged[[",[35,1382,139],{"class":45},[35,1384,148],{"class":82},[35,1386,420],{"class":45},[35,1388,148],{"class":82},[35,1390,484],{"class":45},[35,1392,148],{"class":82},[35,1394,799],{"class":45},[35,1396,148],{"class":82},[35,1398,1301],{"class":45},[35,1400,1401],{"class":82},"]].to_string(",[35,1403,506],{"class":202},[35,1405,196],{"class":78},[35,1407,511],{"class":129},[35,1409,514],{"class":82},[10,1411,1412],{},"The rules are deliberately simple and transparent, and the run shows what each one catches: Ana's £142 client dinner is over the meal limit, receipts are missing for Ben's dinner and Chloé's mileage, Chloé's online course appears twice and is not an allowed category, and two meals fall on Saturdays. Ben's Lyon hotel is flagged too — at £266.60 it is over the £180 limit, but it covers two nights, so it is well within a nightly limit. That is a rule-design finding rather than a policy breach: the form should record the number of nights so the check can be per night. A flag is not a rejection — the weekend lunch may be perfectly legitimate — but it routes the line to a human instead of straight to payroll. Keeping the policy as data in one dictionary makes the annual policy update a one-line change, and the same table can be printed into the report so everyone sees which rules were applied.",[531,1414,540,1419,540,1422,540,1425,540,1428,540,1432,540,1437,540,1443,540,1446,540,1450,540,1454,540,1459,540,1461,540,1466,540,1468,540,1471,540,1474,540,1478,540,1480,540,1483,540,1485,540,1488,540,1491,540,1495,540,1497,540,1501,540,1503,540,1506,540,1509,540,1513,540,1515,540,1519,540,1521,540,1524,540,1527,540,1531,540,1533,540,1537,540,1539],{"viewBox":1415,"role":534,"ariaLabelledBy":1416,"xmlns":538,"style":539},"4 0 752 242",[1417,1418],"ex-rules-t","ex-rules-d",[542,1420,1421],{"id":1417},"Policy checks and what they caught",[546,1423,1424],{"id":1418},"Five checks run on every line. The category limits caught Ana Ruiz's client dinner and Ben Okafor's two-night hotel, the receipt rule caught Ben Okafor's dinner and Chloé Martin's mileage, the duplicate check caught Chloé's online course listed twice, the category rule caught software, and the weekend rule caught the Saturday lines.",[550,1426],{"x":552,"y":553,"width":554,"height":1427,"fill":556},"242",[550,1429],{"x":559,"y":1430,"width":1431,"height":560,"rx":553,"fill":556,"stroke":564,"style":565},"16.0","162.8",[550,1433],{"x":1434,"y":1430,"width":1435,"height":560,"rx":553,"fill":1436,"stroke":746,"style":760},"182.8","278.6","#f0f4ff",[571,1438,1442],{"x":1439,"y":1440,"style":1441},"322.1","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","rule",[550,1444],{"x":1445,"y":1430,"width":1435,"height":560,"rx":553,"fill":1436,"stroke":746,"style":760},"461.4",[571,1447,1449],{"x":1448,"y":1440,"style":1441},"600.7","caught",[550,1451],{"x":559,"y":1452,"width":1431,"height":1453,"rx":553,"fill":563,"stroke":564,"style":565},"56.0","34.0",[571,1455,1458],{"x":561,"y":1456,"style":1457},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","over category limit",[550,1460],{"x":1434,"y":1452,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1462,1465],{"x":1439,"y":1463,"style":1464},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Meals £75, Hotel £180",[550,1467],{"x":1445,"y":1452,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1469,1470],{"x":1448,"y":1463,"style":1464},"dinner £142, 2-night hotel",[550,1472],{"x":559,"y":1473,"width":1431,"height":1453,"rx":553,"fill":563,"stroke":564,"style":565},"90.0",[571,1475,1477],{"x":561,"y":1476,"style":1457},"112.0","receipt missing",[550,1479],{"x":1434,"y":1473,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1481,1482],{"x":1439,"y":610,"style":1464},"over £25 without receipt",[550,1484],{"x":1445,"y":1473,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1486,1487],{"x":1448,"y":610,"style":1464},"dinner, mileage",[550,1489],{"x":559,"y":1490,"width":1431,"height":1453,"rx":553,"fill":563,"stroke":564,"style":565},"124.0",[571,1492,1494],{"x":561,"y":1493,"style":1457},"146.0","possible duplicate",[550,1496],{"x":1434,"y":1490,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1498,1500],{"x":1439,"y":1499,"style":1464},"145.0","same day, category, amount",[550,1502],{"x":1445,"y":1490,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1504,1505],{"x":1448,"y":1499,"style":1464},"online course × 2",[550,1507],{"x":559,"y":1508,"width":1431,"height":1453,"rx":553,"fill":563,"stroke":564,"style":565},"158.0",[571,1510,1512],{"x":561,"y":1511,"style":1457},"180.0","category not allowed",[550,1514],{"x":1434,"y":1508,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1516,1518],{"x":1439,"y":1517,"style":1464},"179.0","Travel, Meals, Hotel, Taxi",[550,1520],{"x":1445,"y":1508,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1522,1523],{"x":1448,"y":1517,"style":1464},"software",[550,1525],{"x":559,"y":1526,"width":1431,"height":1453,"rx":553,"fill":563,"stroke":564,"style":565},"192.0",[571,1528,1530],{"x":561,"y":1529,"style":1457},"214.0","weekend",[550,1532],{"x":1434,"y":1526,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1534,1536],{"x":1439,"y":1535,"style":1464},"213.0","Saturday or Sunday",[550,1538],{"x":1445,"y":1526,"width":1435,"height":1453,"rx":553,"fill":556,"stroke":564,"style":565},[571,1540,1541],{"x":1448,"y":1535,"style":1464},"Sat 19 Sep, Sat 26 Sep",[20,1543,1545],{"id":1544},"step-4-summarise-for-approval-and-payment","Step 4 — Summarise for approval and payment",[10,1547,1548],{},"Approvers want a line per employee; finance wants the cost by cost centre and category. Lines with issues are held back from the amount to pay until someone approves them:",[25,1550,1552],{"className":69,"code":1551,"language":71,"meta":30,"style":30},"claims[\"Status\"] = claims[\"Issues\"].map(lambda s: \"Review\" if s else \"OK\")\nby_employee = (claims.pivot_table(index=[\"Employee ID\", \"Employee\", \"Cost centre\"], columns=\"Status\",\n                                  values=\"GBP\", aggfunc=\"sum\", fill_value=0)\n                     .reindex(columns=[\"OK\", \"Review\"], fill_value=0)\n                     .rename(columns={\"OK\": \"Payable now\", \"Review\": \"Held for review\"})\n                     .reset_index())\nby_cc = claims.pivot_table(index=\"Cost centre\", columns=\"Category\", values=\"GBP\", aggfunc=\"sum\",\n                           fill_value=0, margins=True, margins_name=\"Total\")\nprint(by_employee.to_string(index=False))\nprint(by_cc.round(2).to_string())\n",[32,1553,1554,1594,1631,1661,1689,1718,1723,1766,1796,1811],{"__ignoreMap":30},[35,1555,1556,1558,1561,1563,1565,1567,1569,1571,1574,1577,1580,1583,1586,1589,1592],{"class":37,"line":38},[35,1557,417],{"class":82},[35,1559,1560],{"class":45},"\"Status\"",[35,1562,423],{"class":82},[35,1564,196],{"class":78},[35,1566,957],{"class":82},[35,1568,1301],{"class":45},[35,1570,1168],{"class":82},[35,1572,1573],{"class":78},"lambda",[35,1575,1576],{"class":82}," s: ",[35,1578,1579],{"class":45},"\"Review\"",[35,1581,1582],{"class":78}," if",[35,1584,1585],{"class":82}," s ",[35,1587,1588],{"class":78},"else",[35,1590,1591],{"class":45}," \"OK\"",[35,1593,220],{"class":82},[35,1595,1596,1599,1601,1604,1606,1608,1610,1612,1614,1616,1618,1620,1622,1625,1627,1629],{"class":37,"line":92},[35,1597,1598],{"class":82},"by_employee ",[35,1600,196],{"class":78},[35,1602,1603],{"class":82}," (claims.pivot_table(",[35,1605,506],{"class":202},[35,1607,196],{"class":78},[35,1609,1140],{"class":82},[35,1611,151],{"class":45},[35,1613,148],{"class":82},[35,1615,139],{"class":45},[35,1617,148],{"class":82},[35,1619,161],{"class":45},[35,1621,812],{"class":82},[35,1623,1624],{"class":202},"columns",[35,1626,196],{"class":78},[35,1628,1560],{"class":45},[35,1630,1076],{"class":82},[35,1632,1633,1636,1638,1640,1642,1645,1647,1650,1652,1655,1657,1659],{"class":37,"line":106},[35,1634,1635],{"class":202},"                                  values",[35,1637,196],{"class":78},[35,1639,799],{"class":45},[35,1641,148],{"class":82},[35,1643,1644],{"class":202},"aggfunc",[35,1646,196],{"class":78},[35,1648,1649],{"class":45},"\"sum\"",[35,1651,148],{"class":82},[35,1653,1654],{"class":202},"fill_value",[35,1656,196],{"class":78},[35,1658,553],{"class":129},[35,1660,220],{"class":82},[35,1662,1663,1666,1668,1670,1672,1675,1677,1679,1681,1683,1685,1687],{"class":37,"line":119},[35,1664,1665],{"class":82},"                     .reindex(",[35,1667,1624],{"class":202},[35,1669,196],{"class":78},[35,1671,1140],{"class":82},[35,1673,1674],{"class":45},"\"OK\"",[35,1676,148],{"class":82},[35,1678,1579],{"class":45},[35,1680,812],{"class":82},[35,1682,1654],{"class":202},[35,1684,196],{"class":78},[35,1686,553],{"class":129},[35,1688,220],{"class":82},[35,1690,1691,1694,1696,1698,1700,1702,1704,1707,1709,1711,1713,1716],{"class":37,"line":126},[35,1692,1693],{"class":82},"                     .rename(",[35,1695,1624],{"class":202},[35,1697,196],{"class":78},[35,1699,307],{"class":82},[35,1701,1674],{"class":45},[35,1703,142],{"class":82},[35,1705,1706],{"class":45},"\"Payable now\"",[35,1708,148],{"class":82},[35,1710,1579],{"class":45},[35,1712,142],{"class":82},[35,1714,1715],{"class":45},"\"Held for review\"",[35,1717,318],{"class":82},[35,1719,1720],{"class":37,"line":172},[35,1721,1722],{"class":82},"                     .reset_index())\n",[35,1724,1725,1728,1730,1733,1735,1737,1739,1741,1743,1745,1747,1749,1752,1754,1756,1758,1760,1762,1764],{"class":37,"line":177},[35,1726,1727],{"class":82},"by_cc ",[35,1729,196],{"class":78},[35,1731,1732],{"class":82}," claims.pivot_table(",[35,1734,506],{"class":202},[35,1736,196],{"class":78},[35,1738,161],{"class":45},[35,1740,148],{"class":82},[35,1742,1624],{"class":202},[35,1744,196],{"class":78},[35,1746,484],{"class":45},[35,1748,148],{"class":82},[35,1750,1751],{"class":202},"values",[35,1753,196],{"class":78},[35,1755,799],{"class":45},[35,1757,148],{"class":82},[35,1759,1644],{"class":202},[35,1761,196],{"class":78},[35,1763,1649],{"class":45},[35,1765,1076],{"class":82},[35,1767,1768,1771,1773,1775,1777,1780,1782,1784,1786,1789,1791,1794],{"class":37,"line":190},[35,1769,1770],{"class":202},"                           fill_value",[35,1772,196],{"class":78},[35,1774,553],{"class":129},[35,1776,148],{"class":82},[35,1778,1779],{"class":202},"margins",[35,1781,196],{"class":78},[35,1783,208],{"class":129},[35,1785,148],{"class":82},[35,1787,1788],{"class":202},"margins_name",[35,1790,196],{"class":78},[35,1792,1793],{"class":45},"\"Total\"",[35,1795,220],{"class":82},[35,1797,1798,1800,1803,1805,1807,1809],{"class":37,"line":223},[35,1799,439],{"class":129},[35,1801,1802],{"class":82},"(by_employee.to_string(",[35,1804,506],{"class":202},[35,1806,196],{"class":78},[35,1808,511],{"class":129},[35,1810,514],{"class":82},[35,1812,1813,1815,1818,1820],{"class":37,"line":240},[35,1814,439],{"class":129},[35,1816,1817],{"class":82},"(by_cc.round(",[35,1819,611],{"class":129},[35,1821,1822],{"class":82},").to_string())\n",[10,1824,1825,1826,1829],{},"Each employee's total is split between what can be paid now and what is held. The cost-centre table, with ",[32,1827,1828],{},"margins=True"," adding row and column totals, is what the budget owners see. Both are computed from the same converted lines, so they add up to the same grand total — a check worth asserting in the script.",[20,1831,1833],{"id":1832},"step-5-write-the-report","Step 5 — Write the report",[10,1835,1836],{},"The workbook has the summary first, then the exceptions the approver must look at, then every line for reference:",[25,1838,1840],{"className":69,"code":1839,"language":71,"meta":30,"style":30},"from openpyxl.styles import Font, PatternFill\n\nwith pd.ExcelWriter(\"expense_report_2026-09.xlsx\", engine=\"openpyxl\") as xw:\n    by_employee.to_excel(xw, sheet_name=\"By employee\", index=False)\n    flagged[[\"Employee\", \"Source\", \"Date\", \"Category\", \"Description\", \"Amount\", \"Currency\", \"GBP\", \"Issues\"]] \\\n        .to_excel(xw, sheet_name=\"Exceptions\", index=False)\n    by_cc.round(2).to_excel(xw, sheet_name=\"By cost centre\")\n    claims.drop(columns=[\"Rate to GBP\"]).to_excel(xw, sheet_name=\"All lines\", index=False)\n    pd.DataFrame([(k, str(v)) for k, v in POLICY.items()], columns=[\"Rule\", \"Setting\"]) \\\n        .to_excel(xw, sheet_name=\"Policy\", index=False)\n    for ws in xw.book.worksheets:\n        for cell in ws[1]:\n            cell.font = Font(bold=True)\n        ws.freeze_panes = \"A2\"\n        for col in ws.columns:\n            ws.column_dimensions[col[0].column_letter].width = max(12, min(40, max(len(str(c.value or \"\")) for c in col) + 2))\n    for row in xw.book[\"Exceptions\"].iter_rows(min_row=2):\n        row[-1].fill = PatternFill(\"solid\", fgColor=\"FFEB9C\")\n\ntotal = claims[\"GBP\"].sum()\nprint(f\"total claimed £{total:,.2f} = payable £{by_employee['Payable now'].sum():,.2f} + held £{by_employee['Held for review'].sum():,.2f}\")\n",[32,1841,1842,1854,1858,1886,1908,1951,1973,1992,2025,2064,2085,2098,2116,2135,2145,2157,2229,2256,2289,2293,2308],{"__ignoreMap":30},[35,1843,1844,1846,1849,1851],{"class":37,"line":38},[35,1845,79],{"class":78},[35,1847,1848],{"class":82}," openpyxl.styles ",[35,1850,86],{"class":78},[35,1852,1853],{"class":82}," Font, PatternFill\n",[35,1855,1856],{"class":37,"line":92},[35,1857,123],{"emptyLinePlaceholder":122},[35,1859,1860,1863,1866,1869,1871,1874,1876,1879,1881,1883],{"class":37,"line":106},[35,1861,1862],{"class":78},"with",[35,1864,1865],{"class":82}," pd.ExcelWriter(",[35,1867,1868],{"class":45},"\"expense_report_2026-09.xlsx\"",[35,1870,148],{"class":82},[35,1872,1873],{"class":202},"engine",[35,1875,196],{"class":78},[35,1877,1878],{"class":45},"\"openpyxl\"",[35,1880,1211],{"class":82},[35,1882,100],{"class":78},[35,1884,1885],{"class":82}," xw:\n",[35,1887,1888,1891,1893,1895,1898,1900,1902,1904,1906],{"class":37,"line":119},[35,1889,1890],{"class":82},"    by_employee.to_excel(xw, ",[35,1892,283],{"class":202},[35,1894,196],{"class":78},[35,1896,1897],{"class":45},"\"By employee\"",[35,1899,148],{"class":82},[35,1901,506],{"class":202},[35,1903,196],{"class":78},[35,1905,511],{"class":129},[35,1907,220],{"class":82},[35,1909,1910,1913,1915,1917,1919,1921,1923,1925,1927,1929,1932,1934,1936,1938,1940,1942,1944,1946,1948],{"class":37,"line":126},[35,1911,1912],{"class":82},"    flagged[[",[35,1914,139],{"class":45},[35,1916,148],{"class":82},[35,1918,457],{"class":45},[35,1920,148],{"class":82},[35,1922,420],{"class":45},[35,1924,148],{"class":82},[35,1926,484],{"class":45},[35,1928,148],{"class":82},[35,1930,1931],{"class":45},"\"Description\"",[35,1933,148],{"class":82},[35,1935,489],{"class":45},[35,1937,148],{"class":82},[35,1939,494],{"class":45},[35,1941,148],{"class":82},[35,1943,799],{"class":45},[35,1945,148],{"class":82},[35,1947,1301],{"class":45},[35,1949,1950],{"class":82},"]] \\\n",[35,1952,1953,1956,1958,1960,1963,1965,1967,1969,1971],{"class":37,"line":172},[35,1954,1955],{"class":82},"        .to_excel(xw, ",[35,1957,283],{"class":202},[35,1959,196],{"class":78},[35,1961,1962],{"class":45},"\"Exceptions\"",[35,1964,148],{"class":82},[35,1966,506],{"class":202},[35,1968,196],{"class":78},[35,1970,511],{"class":129},[35,1972,220],{"class":82},[35,1974,1975,1978,1980,1983,1985,1987,1990],{"class":37,"line":177},[35,1976,1977],{"class":82},"    by_cc.round(",[35,1979,611],{"class":129},[35,1981,1982],{"class":82},").to_excel(xw, ",[35,1984,283],{"class":202},[35,1986,196],{"class":78},[35,1988,1989],{"class":45},"\"By cost centre\"",[35,1991,220],{"class":82},[35,1993,1994,1997,1999,2001,2003,2005,2008,2010,2012,2015,2017,2019,2021,2023],{"class":37,"line":190},[35,1995,1996],{"class":82},"    claims.drop(",[35,1998,1624],{"class":202},[35,2000,196],{"class":78},[35,2002,1140],{"class":82},[35,2004,815],{"class":45},[35,2006,2007],{"class":82},"]).to_excel(xw, ",[35,2009,283],{"class":202},[35,2011,196],{"class":78},[35,2013,2014],{"class":45},"\"All lines\"",[35,2016,148],{"class":82},[35,2018,506],{"class":202},[35,2020,196],{"class":78},[35,2022,511],{"class":129},[35,2024,220],{"class":82},[35,2026,2027,2030,2032,2035,2037,2040,2042,2044,2047,2049,2051,2053,2056,2058,2061],{"class":37,"line":223},[35,2028,2029],{"class":82},"    pd.DataFrame([(k, ",[35,2031,315],{"class":129},[35,2033,2034],{"class":82},"(v)) ",[35,2036,251],{"class":78},[35,2038,2039],{"class":82}," k, v ",[35,2041,257],{"class":78},[35,2043,1225],{"class":129},[35,2045,2046],{"class":82},".items()], ",[35,2048,1624],{"class":202},[35,2050,196],{"class":78},[35,2052,1140],{"class":82},[35,2054,2055],{"class":45},"\"Rule\"",[35,2057,148],{"class":82},[35,2059,2060],{"class":45},"\"Setting\"",[35,2062,2063],{"class":82},"]) \\\n",[35,2065,2066,2068,2070,2072,2075,2077,2079,2081,2083],{"class":37,"line":240},[35,2067,1955],{"class":82},[35,2069,283],{"class":202},[35,2071,196],{"class":78},[35,2073,2074],{"class":45},"\"Policy\"",[35,2076,148],{"class":82},[35,2078,506],{"class":202},[35,2080,196],{"class":78},[35,2082,511],{"class":129},[35,2084,220],{"class":82},[35,2086,2087,2090,2093,2095],{"class":37,"line":266},[35,2088,2089],{"class":78},"    for",[35,2091,2092],{"class":82}," ws ",[35,2094,257],{"class":78},[35,2096,2097],{"class":82}," xw.book.worksheets:\n",[35,2099,2100,2103,2106,2108,2111,2113],{"class":37,"line":272},[35,2101,2102],{"class":78},"        for",[35,2104,2105],{"class":82}," cell ",[35,2107,257],{"class":78},[35,2109,2110],{"class":82}," ws[",[35,2112,593],{"class":129},[35,2114,2115],{"class":82},"]:\n",[35,2117,2118,2121,2123,2126,2129,2131,2133],{"class":37,"line":321},[35,2119,2120],{"class":82},"            cell.font ",[35,2122,196],{"class":78},[35,2124,2125],{"class":82}," Font(",[35,2127,2128],{"class":202},"bold",[35,2130,196],{"class":78},[35,2132,208],{"class":129},[35,2134,220],{"class":82},[35,2136,2137,2140,2142],{"class":37,"line":341},[35,2138,2139],{"class":82},"        ws.freeze_panes ",[35,2141,196],{"class":78},[35,2143,2144],{"class":45}," \"A2\"\n",[35,2146,2147,2149,2152,2154],{"class":37,"line":364},[35,2148,2102],{"class":78},[35,2150,2151],{"class":82}," col ",[35,2153,257],{"class":78},[35,2155,2156],{"class":82}," ws.columns:\n",[35,2158,2159,2162,2164,2167,2169,2172,2174,2177,2179,2182,2184,2187,2189,2192,2194,2196,2198,2200,2203,2206,2208,2211,2213,2216,2218,2221,2224,2227],{"class":37,"line":369},[35,2160,2161],{"class":82},"            ws.column_dimensions[col[",[35,2163,553],{"class":129},[35,2165,2166],{"class":82},"].column_letter].width ",[35,2168,196],{"class":78},[35,2170,2171],{"class":129}," max",[35,2173,442],{"class":82},[35,2175,2176],{"class":129},"12",[35,2178,148],{"class":82},[35,2180,2181],{"class":129},"min",[35,2183,442],{"class":82},[35,2185,2186],{"class":129},"40",[35,2188,148],{"class":82},[35,2190,2191],{"class":129},"max",[35,2193,442],{"class":82},[35,2195,445],{"class":129},[35,2197,442],{"class":82},[35,2199,315],{"class":129},[35,2201,2202],{"class":82},"(c.value ",[35,2204,2205],{"class":78},"or",[35,2207,1371],{"class":45},[35,2209,2210],{"class":82},")) ",[35,2212,251],{"class":78},[35,2214,2215],{"class":82}," c ",[35,2217,257],{"class":78},[35,2219,2220],{"class":82}," col) ",[35,2222,2223],{"class":78},"+",[35,2225,2226],{"class":129}," 2",[35,2228,514],{"class":82},[35,2230,2231,2233,2236,2238,2241,2243,2246,2249,2251,2253],{"class":37,"line":414},[35,2232,2089],{"class":78},[35,2234,2235],{"class":82}," row ",[35,2237,257],{"class":78},[35,2239,2240],{"class":82}," xw.book[",[35,2242,1962],{"class":45},[35,2244,2245],{"class":82},"].iter_rows(",[35,2247,2248],{"class":202},"min_row",[35,2250,196],{"class":78},[35,2252,611],{"class":129},[35,2254,2255],{"class":82},"):\n",[35,2257,2258,2261,2264,2266,2269,2271,2274,2277,2279,2282,2284,2287],{"class":37,"line":436},[35,2259,2260],{"class":82},"        row[",[35,2262,2263],{"class":78},"-",[35,2265,593],{"class":129},[35,2267,2268],{"class":82},"].fill ",[35,2270,196],{"class":78},[35,2272,2273],{"class":82}," PatternFill(",[35,2275,2276],{"class":45},"\"solid\"",[35,2278,148],{"class":82},[35,2280,2281],{"class":202},"fgColor",[35,2283,196],{"class":78},[35,2285,2286],{"class":45},"\"FFEB9C\"",[35,2288,220],{"class":82},[35,2290,2291],{"class":37,"line":468},[35,2292,123],{"emptyLinePlaceholder":122},[35,2294,2296,2299,2301,2303,2305],{"class":37,"line":2295},20,[35,2297,2298],{"class":82},"total ",[35,2300,196],{"class":78},[35,2302,957],{"class":82},[35,2304,799],{"class":45},[35,2306,2307],{"class":82},"].sum()\n",[35,2309,2311,2313,2315,2317,2320,2322,2325,2328,2330,2333,2335,2338,2341,2344,2346,2348,2351,2353,2355,2358,2360,2362,2364,2366],{"class":37,"line":2310},21,[35,2312,439],{"class":129},[35,2314,442],{"class":82},[35,2316,914],{"class":78},[35,2318,2319],{"class":45},"\"total claimed £",[35,2321,307],{"class":920},[35,2323,2324],{"class":82},"total",[35,2326,2327],{"class":78},":,.2f",[35,2329,929],{"class":920},[35,2331,2332],{"class":45}," = payable £",[35,2334,307],{"class":920},[35,2336,2337],{"class":82},"by_employee[",[35,2339,2340],{"class":45},"'Payable now'",[35,2342,2343],{"class":82},"].sum()",[35,2345,2327],{"class":78},[35,2347,929],{"class":920},[35,2349,2350],{"class":45}," + held £",[35,2352,307],{"class":920},[35,2354,2337],{"class":82},[35,2356,2357],{"class":45},"'Held for review'",[35,2359,2343],{"class":82},[35,2361,2327],{"class":78},[35,2363,929],{"class":920},[35,2365,932],{"class":45},[35,2367,220],{"class":82},[10,2369,2370,2371,18],{},"The Exceptions sheet lists each flagged line with the form it came from, the original amount and currency, and the reasons in a highlighted column — the approver's to-do list. The Policy sheet records the rules in force, so a report opened in six months still explains why a line was held. The final line proves the split adds up: what is payable now plus what is held equals everything claimed. Emailing the report to approvers is covered in ",[14,2372,2374],{"href":2373},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report",[531,2376,540,2381,540,2384,540,2387,540,2390,540,2394,540,2405,540,2411,540,2415,540,2420,540,2423,540,2426,540,2433,540,2438,540,2441,540,2445,540,2448,540,2451,540,2458,540,2464,540,2468,540,2472,540,2476,540,2479,540,2484,540,2491,540,2496,540,2499],{"viewBox":2377,"role":534,"ariaLabelledBy":2378,"xmlns":538,"style":539},"4 0 752 142",[2379,2380],"ex-flow-t","ex-flow-d",[542,2382,2383],{"id":2379},"From claim forms to payment",[546,2385,2386],{"id":2380},"Claim forms are read and combined, converted to pounds, checked against the policy, split into lines payable now and lines held for review, and written to a report with summary, exceptions, all lines and the policy in force.",[550,2388],{"x":552,"y":553,"width":554,"height":2389,"fill":556},"142",[550,2391],{"x":559,"y":1430,"width":2392,"height":2393,"rx":2176,"fill":563,"stroke":564,"style":747},"153.0","110.5",[2395,2396,2398],"g",{"transform":2397},"translate(84.5,37.8) scale(1.000)",[2399,2400],"path",{"d":2401,"fill":2402,"stroke":2403,"style":2404},"M3 6h6l2 2h10v11H3z","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[571,2406,2410],{"x":2407,"y":2408,"style":2409},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Read forms",[571,2412,2414],{"x":2407,"y":2413,"style":1464},"103.8","header cells + table",[37,2416],{"x1":2417,"y1":2418,"x2":2419,"y2":2418,"stroke":746,"style":747},"178.0","71.2","200.0",[749,2421],{"points":2422,"fill":752},"204.0,71.2 195.0,76.2 195.0,66.2",[550,2424],{"x":2425,"y":1430,"width":2392,"height":2393,"rx":2176,"fill":1436,"stroke":746,"style":747},"209.0",[2395,2427,2429],{"transform":2428},"translate(273.5,37.8) scale(1.000)",[2399,2430],{"d":2431,"fill":2402,"stroke":2432,"style":2404},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--brand-strong,#4338ca)",[571,2434,2437],{"x":2435,"y":2408,"style":2436},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Convert",[571,2439,2440],{"x":2435,"y":2413,"style":1464},"month rates to GBP",[37,2442],{"x1":2443,"y1":2418,"x2":2444,"y2":2418,"stroke":746,"style":747},"367.0","389.0",[749,2446],{"points":2447,"fill":752},"393.0,71.2 384.0,76.2 384.0,66.2",[550,2449],{"x":2450,"y":1430,"width":2392,"height":2393,"rx":2176,"fill":620,"stroke":621,"style":747},"398.0",[2395,2452,2454],{"transform":2453},"translate(462.5,29.0) scale(1.000)",[2399,2455],{"d":2456,"fill":2402,"stroke":2457,"style":2404},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[571,2459,2463],{"x":2460,"y":2461,"style":2462},"474.5","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Policy checks",[571,2465,2467],{"x":2460,"y":2466,"style":1464},"95.0","limits, receipts,",[571,2469,2471],{"x":2460,"y":2470,"style":1464},"112.5","duplicates",[37,2473],{"x1":2474,"y1":2418,"x2":2475,"y2":2418,"stroke":746,"style":747},"556.0","578.0",[749,2477],{"points":2478,"fill":752},"582.0,71.2 573.0,76.2 573.0,66.2",[550,2480],{"x":2481,"y":1430,"width":2392,"height":2393,"rx":2176,"fill":2482,"stroke":2483,"style":747},"587.0","#d9f4f1","var(--teal,#0f9488)",[2395,2485,2487],{"transform":2486},"translate(651.5,29.0) scale(1.000)",[2399,2488],{"d":2489,"fill":2402,"stroke":2490,"style":2404},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--teal-ink,#0b6157)",[571,2492,2495],{"x":2493,"y":2461,"style":2494},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Report",[571,2497,2498],{"x":2493,"y":2466,"style":1464},"payable now · held ·",[571,2500,2501],{"x":2493,"y":2470,"style":1464},"exceptions",[20,2503,2505],{"id":2504},"common-pitfalls","Common pitfalls",[2507,2508,2509,2521,2530,2536,2542],"ul",{},[2510,2511,2512,2516,2517,2520],"li",{},[2513,2514,2515],"strong",{},"Reading the table without skipping the header block."," Set ",[32,2518,2519],{},"header="," to the heading row.",[2510,2522,2523,2529],{},[2513,2524,2525,2526,2528],{},"Silent ",[32,2527,775],{}," from a missing exchange rate."," Merge with a rate table and fail on gaps.",[2510,2531,2532,2535],{},[2513,2533,2534],{},"Rejecting instead of flagging."," Policy checks route lines for review; people decide.",[2510,2537,2538,2541],{},[2513,2539,2540],{},"Hard-coded limits scattered through the code."," Keep the policy in one table and print it in the report.",[2510,2543,2544,2547],{},[2513,2545,2546],{},"Totals that don't reconcile."," Assert payable plus held equals the total claimed.",[20,2549,2551],{"id":2550},"conclusion","Conclusion",[10,2553,2554],{},"Consolidating expense claims is a read-combine-check-summarise job: read the header cells and claim table from every form, convert foreign-currency lines with a validated rate table, apply the expenses policy as vectorised checks that produce readable reasons, and split each employee's total into payable now and held for review. A report with a summary, an exceptions sheet, every line and the policy in force gives approvers what they need and finance an auditable record.",[20,2556,2558],{"id":2557},"frequently-asked-questions","Frequently asked questions",[10,2560,2561,2564],{},[2513,2562,2563],{},"How do I combine many Excel expense forms into one table?","\nLoop over the files, read the fixed header cells with openpyxl and the claim table with pandas read_excel using the header row, add the header values and file name as columns, and concatenate the results.",[10,2566,2567,2570],{},[2513,2568,2569],{},"How do I check expense claims against a policy in Python?","\nExpress each rule as a boolean mask on the DataFrame, such as amount above a category limit or no receipt above a threshold, and join the names of the failing rules into an issues column.",[10,2572,2573,2576],{},[2513,2574,2575],{},"How do I find duplicate expense claims?","\nUse duplicated on employee, date, category, amount and currency with keep=False, which marks every line in a group of identical claims.",[10,2578,2579,2582],{},[2513,2580,2581],{},"How do I convert expense claims to one currency?","\nMerge a rate table on the currency column with validate many_to_one, raise an error if any rate is missing, and multiply the amount by the rate, rounding each line.",[20,2584,2586],{"id":2585},"related","Related",[2507,2588,2589,2595,2602,2609],{},[2510,2590,2591,2592,2594],{},"Up one level: ",[14,2593,17],{"href":16}," — finance and operations reports in Excel.",[2510,2596,2597,2601],{},[14,2598,2600],{"href":2599},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks\u002F","Consolidate Timesheets from Many Workbooks"," — the same pattern for hours.",[2510,2603,2604,2608],{},[14,2605,2607],{"href":2606},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python\u002F","Combine Multiple Excel Files into One"," — the basic folder merge.",[2510,2610,2611,2615],{},[14,2612,2614],{"href":2613},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-trial-balance-from-a-general-ledger\u002F","Build a Trial Balance from a General Ledger"," — where reimbursed expenses end up.",[2617,2618,2619],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":30,"searchDepth":92,"depth":92,"links":2621},[2622,2623,2624,2625,2626,2627,2628,2629,2630,2631],{"id":22,"depth":92,"text":23},{"id":62,"depth":92,"text":63},{"id":768,"depth":92,"text":769},{"id":1001,"depth":92,"text":1002},{"id":1544,"depth":92,"text":1545},{"id":1832,"depth":92,"text":1833},{"id":2504,"depth":92,"text":2505},{"id":2550,"depth":92,"text":2551},{"id":2557,"depth":92,"text":2558},{"id":2585,"depth":92,"text":2586},"2026-10-02","Turn a folder of employee expense claim forms into one checked report with pandas and openpyxl: read header cells and the claim table from each form, convert foreign-currency lines, apply policy rules such as category limits, missing receipts and duplicate claims, summarise by employee and cost centre, and write a workbook with an exceptions sheet for the approver.","md",[2636,2638,2640,2642],{"q":2563,"a":2637},"Loop over the files, read the fixed header cells with openpyxl and the claim table with pandas read_excel using the header row, add the header values and file name as columns, and concatenate the results.",{"q":2569,"a":2639},"Express each rule as a boolean mask on the DataFrame, such as amount above a category limit or no receipt above a threshold, and join the names of the failing rules into an issues column.",{"q":2575,"a":2641},"Use duplicated on employee, date, category, amount and currency with keep=False, which marks every line in a group of identical claims.",{"q":2581,"a":2643},"Merge a rate table on the currency column with validate many_to_one, raise an error if any rate is missing, and multiply the amount by the rate, rounding each line.","consolidate employee expense claims into a report",null,{"breadcrumb":2647},[2648,2651,2654],{"name":2649,"item":2650},"Home","\u002F",{"name":2652,"item":2653},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2655,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report",{"title":2658,"description":2659},"Consolidate Employee Expense Claims into an Excel Report","Combine expense claim workbooks with Python: header cells plus claim lines, currency conversion, policy checks for limits, receipts and duplicates, employee and cost-centre totals, and an exceptions sheet.","consolidate-employee-expense-claims-into-a-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report\u002Findex",[2663,2664],"report-layouts","sheets-and-files","how-to","UzC6dFsg0f_A4lKiIFFrfHusVLbuQipd--jH9VMjc3c",[2668,2672],{"title":2669,"path":2670,"stem":2671,"children":-1},"Compare Supplier Quotes with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcompare-supplier-quotes-with-python","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcompare-supplier-quotes-with-python\u002Findex",{"title":2673,"path":2674,"stem":2675,"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",[2677,2679],{"path":2678,"title":2655},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2680,"title":2652},"\u002Fautomating-reporting-workflows",[],[2683,2687,2690,2693,2696,2699],{"path":2684,"title":2685,"kind":2686},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2688,"title":2689,"kind":2665},"\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":2691,"title":2692,"kind":2665},"\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":2694,"title":2695,"kind":2665},"\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":2697,"title":2698,"kind":2665},"\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":2700,"title":2701,"kind":2665},"\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",1790972127945]