[{"data":1,"prerenderedAt":2594},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows":3,"surround:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows":2558,"ancestors:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows":2567,"children:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows":2572,"related:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows":2573},{"id":4,"title":5,"body":6,"dateModified":2525,"datePublished":2525,"description":2526,"extension":2527,"faq":2528,"keyword":2537,"kind":2538,"meta":2539,"navigation":254,"path":2547,"seo":2548,"slug":2551,"stem":2552,"topics":2553,"type":2556,"__hash__":2557},"docs\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows\u002Findex.md","Generate Word Documents from Excel Rows with Python",{"type":7,"value":8,"toc":2511},"minimark",[9,28,163,168,199,219,223,226,302,315,757,769,773,779,976,979,1089,1093,1096,1230,1233,1237,1240,1880,1890,1894,1897,2014,2157,2161,2164,2334,2351,2355,2414,2418,2424,2428,2435,2439,2450,2464,2470,2476,2480,2507],[10,11,12,13,17,18,21,22,27],"p",{},"A spreadsheet of customers, employees or course attendees often has to become a set of documents: a statement per account, an offer letter per candidate, a certificate per attendee. Word's own mail merge works for simple letters but is awkward to automate and struggles with repeating rows such as a list of invoices. Python can fill a Word template directly: ",[14,15,16],"code",{},"python-docx"," opens the ",[14,19,20],{},".docx",", replaces placeholders with values from each row and saves one document per row. This page, part of ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002F","Generating Excel Reports from Templates",", builds account statements from two sheets — one row per account and one per open invoice.",[29,30,38,39,38,43,38,47,38,55,38,65,38,76,38,83,38,88,38,95,38,101,38,107,38,112,38,115,38,120,38,123,38,128,38,131,38,135,38,138,38,141,38,144,38,149,38,153,38,157,38,160],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"4 0 752 332","img",[34,35],"docx-fan-t","docx-fan-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  ",[40,41,42],"title",{"id":34},"One template, one document per spreadsheet row",[44,45,46],"desc",{"id":35},"A Word template with placeholders and the Accounts sheet are combined into one statement per account: Acme Ltd, Brightside and Co, and Corex GmbH, each with their own open invoices listed in a table.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"4","0","752","332","#ffffff",[48,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"20.0","119.5","220.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[66,67,69],"g",{"transform":68},"translate(118.0,132.5) scale(1.000)",[70,71],"path",{"d":72,"fill":73,"stroke":74,"style":75},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[77,78,82],"text",{"x":79,"y":80,"style":81},"130.0","179.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","statement_template.docx",[77,84,87],{"x":79,"y":85,"style":86},"198.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","{{placeholders}} + Accounts sheet",[48,89],{"x":90,"y":91,"width":59,"height":92,"rx":61,"fill":93,"stroke":94,"style":64},"520.0","16.0","92.0","#d9f4f1","var(--teal,#0f9488)",[66,96,98],{"transform":97},"translate(618.0,29.0) scale(1.000)",[70,99],{"d":72,"fill":73,"stroke":100,"style":75},"var(--teal-ink,#0b6157)",[77,102,106],{"x":103,"y":104,"style":105},"630.0","75.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Acme Ltd",[77,108,111],{"x":103,"y":109,"style":110},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","2 invoices",[70,113],{"d":114,"fill":73,"stroke":63,"style":64},"M244.0,166.0 C379.0,166.0 379.0,62.0 510.0,62.0",[116,117],"polygon",{"points":118,"fill":119},"514.0,62.0 505.0,67.0 505.0,57.0","#5b5cf0",[48,121],{"x":90,"y":122,"width":59,"height":92,"rx":61,"fill":93,"stroke":94,"style":64},"120.0",[66,124,126],{"transform":125},"translate(618.0,133.0) scale(1.000)",[70,127],{"d":72,"fill":73,"stroke":100,"style":75},[77,129,130],{"x":103,"y":80,"style":105},"Brightside & Co",[77,132,134],{"x":103,"y":133,"style":110},"198.0","1 invoice",[70,136],{"d":137,"fill":73,"stroke":63,"style":64},"M244.0,166.0 C379.0,166.0 379.0,166.0 510.0,166.0",[116,139],{"points":140,"fill":119},"514.0,166.0 505.0,171.0 505.0,161.0",[48,142],{"x":90,"y":143,"width":59,"height":92,"rx":61,"fill":93,"stroke":94,"style":64},"224.0",[66,145,147],{"transform":146},"translate(618.0,237.0) scale(1.000)",[70,148],{"d":72,"fill":73,"stroke":100,"style":75},[77,150,152],{"x":103,"y":151,"style":105},"283.0","Corex GmbH",[77,154,156],{"x":103,"y":155,"style":110},"302.0","3 invoices",[70,158],{"d":159,"fill":73,"stroke":63,"style":64},"M244.0,166.0 C379.0,166.0 379.0,270.0 510.0,270.0",[116,161],{"points":162,"fill":119},"514.0,270.0 505.0,275.0 505.0,265.0",[164,165,167],"h2",{"id":166},"prerequisites","Prerequisites",[169,170,175],"pre",{"className":171,"code":172,"language":173,"meta":174,"style":174},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install python-docx pandas openpyxl\n","bash","",[14,176,177],{"__ignoreMap":174},[178,179,182,186,190,193,196],"span",{"class":180,"line":181},"line",1,[178,183,185],{"class":184},"sMTad","pip",[178,187,189],{"class":188},"srMev"," install",[178,191,192],{"class":188}," python-docx",[178,194,195],{"class":188}," pandas",[178,197,198],{"class":188}," openpyxl\n",[10,200,201,203,204,207,208,207,211,214,215,218],{},[14,202,82],{}," is an ordinary Word document with placeholders in double braces — ",[14,205,206],{},"{{contact}}",", ",[14,209,210],{},"{{balance}}",[14,212,213],{},"{{due_date}}"," — in the body, the header, the footer and a table row. ",[14,216,217],{},"statements.xlsx"," has an Accounts sheet, one row per account, and an Invoices sheet with each account's open invoices. For LibreOffice PDF conversion at the end, LibreOffice must be installed.",[164,220,222],{"id":221},"step-1-replace-placeholders-even-when-word-splits-them","Step 1 — Replace placeholders even when Word splits them",[10,224,225],{},"The obvious approach — replace text inside each run — fails as soon as Word has split a placeholder across runs, which it does whenever formatting changes mid-word, after edits, or for spell-checking markers. Look at the template's second paragraph:",[169,227,231],{"className":228,"code":229,"language":230,"meta":174,"style":174},"language-python shiki shiki-themes github-light github-dark-high-contrast","from docx import Document\n\ntemplate = Document(\"statement_template.docx\")\nprint([r.text for r in template.paragraphs[2].runs])\n","python",[14,232,233,249,256,274],{"__ignoreMap":174},[178,234,235,239,243,246],{"class":180,"line":181},[178,236,238],{"class":237},"s-kum","from",[178,240,242],{"class":241},"skGVy"," docx ",[178,244,245],{"class":237},"import",[178,247,248],{"class":241}," Document\n",[178,250,252],{"class":180,"line":251},2,[178,253,255],{"emptyLinePlaceholder":254},true,"\n",[178,257,259,262,265,268,271],{"class":180,"line":258},3,[178,260,261],{"class":241},"template ",[178,263,264],{"class":237},"=",[178,266,267],{"class":241}," Document(",[178,269,270],{"class":188},"\"statement_template.docx\"",[178,272,273],{"class":241},")\n",[178,275,277,281,284,287,290,293,296,299],{"class":180,"line":276},4,[178,278,280],{"class":279},"sP0c6","print",[178,282,283],{"class":241},"([r.text ",[178,285,286],{"class":237},"for",[178,288,289],{"class":241}," r ",[178,291,292],{"class":237},"in",[178,294,295],{"class":241}," template.paragraphs[",[178,297,298],{"class":279},"2",[178,300,301],{"class":241},"].runs])\n",[10,303,304,306,307,310,311,314],{},[14,305,210],{}," is stored as two runs, ",[14,308,309],{},"{{bal"," and ",[14,312,313],{},"ance}}",", so no single run contains it. The fix is to search the paragraph's full text, then write the replacement into the runs the match spans — the value goes into the first of them, keeping its formatting, and the rest of the placeholder is removed from the others:",[169,316,318],{"className":228,"code":317,"language":230,"meta":174,"style":174},"import re\n\nPLACEHOLDER = re.compile(r\"\\{\\{\\s*(\\w+)\\s*\\}\\}\")\n\ndef fill_paragraph(paragraph, values):\n    start = 0\n    while True:\n        text = \"\".join(run.text for run in paragraph.runs)\n        m = PLACEHOLDER.search(text, start)\n        if not m:\n            return\n        value = str(values[m.group(1)])\n        pos, first = 0, True\n        for run in paragraph.runs:\n            run_start, run_end = pos, pos + len(run.text)\n            pos = run_end\n            if run_end \u003C= m.start() or run_start >= m.end():\n                continue\n            a, b = max(m.start(), run_start) - run_start, min(m.end(), run_end) - run_start\n            run.text = run.text[:a] + (value if first else \"\") + run.text[b:]\n            first = False\n        start = m.start() + len(value)\n\np = template.paragraphs[2]\nfill_paragraph(p, {\"balance\": \"11,600.00\", \"due_date\": \"15 October 2026\"})\nprint([r.text for r in p.runs], \"| bold:\", [r.bold for r in p.runs])\n",[14,319,320,327,331,376,380,393,404,416,440,454,466,472,492,508,521,540,551,578,584,615,650,661,678,683,698,726],{"__ignoreMap":174},[178,321,322,324],{"class":180,"line":181},[178,323,245],{"class":237},[178,325,326],{"class":241}," re\n",[178,328,329],{"class":180,"line":251},[178,330,255],{"emptyLinePlaceholder":254},[178,332,333,336,339,342,345,348,352,355,358,361,364,367,369,372,374],{"class":180,"line":258},[178,334,335],{"class":279},"PLACEHOLDER",[178,337,338],{"class":237}," =",[178,340,341],{"class":241}," re.compile(",[178,343,344],{"class":237},"r",[178,346,347],{"class":188},"\"",[178,349,351],{"class":350},"s_b0D","\\{\\{",[178,353,354],{"class":279},"\\s",[178,356,357],{"class":237},"*",[178,359,360],{"class":279},"(\\w",[178,362,363],{"class":237},"+",[178,365,366],{"class":279},")\\s",[178,368,357],{"class":237},[178,370,371],{"class":350},"\\}\\}",[178,373,347],{"class":188},[178,375,273],{"class":241},[178,377,378],{"class":180,"line":276},[178,379,255],{"emptyLinePlaceholder":254},[178,381,383,386,390],{"class":180,"line":382},5,[178,384,385],{"class":237},"def",[178,387,389],{"class":388},"s_Opv"," fill_paragraph",[178,391,392],{"class":241},"(paragraph, values):\n",[178,394,396,399,401],{"class":180,"line":395},6,[178,397,398],{"class":241},"    start ",[178,400,264],{"class":237},[178,402,403],{"class":279}," 0\n",[178,405,407,410,413],{"class":180,"line":406},7,[178,408,409],{"class":237},"    while",[178,411,412],{"class":279}," True",[178,414,415],{"class":241},":\n",[178,417,419,422,424,427,430,432,435,437],{"class":180,"line":418},8,[178,420,421],{"class":241},"        text ",[178,423,264],{"class":237},[178,425,426],{"class":188}," \"\"",[178,428,429],{"class":241},".join(run.text ",[178,431,286],{"class":237},[178,433,434],{"class":241}," run ",[178,436,292],{"class":237},[178,438,439],{"class":241}," paragraph.runs)\n",[178,441,443,446,448,451],{"class":180,"line":442},9,[178,444,445],{"class":241},"        m ",[178,447,264],{"class":237},[178,449,450],{"class":279}," PLACEHOLDER",[178,452,453],{"class":241},".search(text, start)\n",[178,455,457,460,463],{"class":180,"line":456},10,[178,458,459],{"class":237},"        if",[178,461,462],{"class":237}," not",[178,464,465],{"class":241}," m:\n",[178,467,469],{"class":180,"line":468},11,[178,470,471],{"class":237},"            return\n",[178,473,475,478,480,483,486,489],{"class":180,"line":474},12,[178,476,477],{"class":241},"        value ",[178,479,264],{"class":237},[178,481,482],{"class":279}," str",[178,484,485],{"class":241},"(values[m.group(",[178,487,488],{"class":279},"1",[178,490,491],{"class":241},")])\n",[178,493,495,498,500,503,505],{"class":180,"line":494},13,[178,496,497],{"class":241},"        pos, first ",[178,499,264],{"class":237},[178,501,502],{"class":279}," 0",[178,504,207],{"class":241},[178,506,507],{"class":279},"True\n",[178,509,511,514,516,518],{"class":180,"line":510},14,[178,512,513],{"class":237},"        for",[178,515,434],{"class":241},[178,517,292],{"class":237},[178,519,520],{"class":241}," paragraph.runs:\n",[178,522,524,527,529,532,534,537],{"class":180,"line":523},15,[178,525,526],{"class":241},"            run_start, run_end ",[178,528,264],{"class":237},[178,530,531],{"class":241}," pos, pos ",[178,533,363],{"class":237},[178,535,536],{"class":279}," len",[178,538,539],{"class":241},"(run.text)\n",[178,541,543,546,548],{"class":180,"line":542},16,[178,544,545],{"class":241},"            pos ",[178,547,264],{"class":237},[178,549,550],{"class":241}," run_end\n",[178,552,554,557,560,563,566,569,572,575],{"class":180,"line":553},17,[178,555,556],{"class":237},"            if",[178,558,559],{"class":241}," run_end ",[178,561,562],{"class":237},"\u003C=",[178,564,565],{"class":241}," m.start() ",[178,567,568],{"class":237},"or",[178,570,571],{"class":241}," run_start ",[178,573,574],{"class":237},">=",[178,576,577],{"class":241}," m.end():\n",[178,579,581],{"class":180,"line":580},18,[178,582,583],{"class":237},"                continue\n",[178,585,587,590,592,595,598,601,604,607,610,612],{"class":180,"line":586},19,[178,588,589],{"class":241},"            a, b ",[178,591,264],{"class":237},[178,593,594],{"class":279}," max",[178,596,597],{"class":241},"(m.start(), run_start) ",[178,599,600],{"class":237},"-",[178,602,603],{"class":241}," run_start, ",[178,605,606],{"class":279},"min",[178,608,609],{"class":241},"(m.end(), run_end) ",[178,611,600],{"class":237},[178,613,614],{"class":241}," run_start\n",[178,616,618,621,623,626,628,631,634,637,640,642,645,647],{"class":180,"line":617},20,[178,619,620],{"class":241},"            run.text ",[178,622,264],{"class":237},[178,624,625],{"class":241}," run.text[:a] ",[178,627,363],{"class":237},[178,629,630],{"class":241}," (value ",[178,632,633],{"class":237},"if",[178,635,636],{"class":241}," first ",[178,638,639],{"class":237},"else",[178,641,426],{"class":188},[178,643,644],{"class":241},") ",[178,646,363],{"class":237},[178,648,649],{"class":241}," run.text[b:]\n",[178,651,653,656,658],{"class":180,"line":652},21,[178,654,655],{"class":241},"            first ",[178,657,264],{"class":237},[178,659,660],{"class":279}," False\n",[178,662,664,667,669,671,673,675],{"class":180,"line":663},22,[178,665,666],{"class":241},"        start ",[178,668,264],{"class":237},[178,670,565],{"class":241},[178,672,363],{"class":237},[178,674,536],{"class":279},[178,676,677],{"class":241},"(value)\n",[178,679,681],{"class":180,"line":680},23,[178,682,255],{"emptyLinePlaceholder":254},[178,684,686,689,691,693,695],{"class":180,"line":685},24,[178,687,688],{"class":241},"p ",[178,690,264],{"class":237},[178,692,295],{"class":241},[178,694,298],{"class":279},[178,696,697],{"class":241},"]\n",[178,699,701,704,707,710,713,715,718,720,723],{"class":180,"line":700},25,[178,702,703],{"class":241},"fill_paragraph(p, {",[178,705,706],{"class":188},"\"balance\"",[178,708,709],{"class":241},": ",[178,711,712],{"class":188},"\"11,600.00\"",[178,714,207],{"class":241},[178,716,717],{"class":188},"\"due_date\"",[178,719,709],{"class":241},[178,721,722],{"class":188},"\"15 October 2026\"",[178,724,725],{"class":241},"})\n",[178,727,729,731,733,735,737,739,742,745,748,750,752,754],{"class":180,"line":728},26,[178,730,280],{"class":279},[178,732,283],{"class":241},[178,734,286],{"class":237},[178,736,289],{"class":241},[178,738,292],{"class":237},[178,740,741],{"class":241}," p.runs], ",[178,743,744],{"class":188},"\"| bold:\"",[178,746,747],{"class":241},", [r.bold ",[178,749,286],{"class":237},[178,751,289],{"class":241},[178,753,292],{"class":237},[178,755,756],{"class":241}," p.runs])\n",[10,758,759,760,764,765,768],{},"The balance lands in the first bold run, so it stays bold. Searching again from ",[761,762,763],"em",{},"after"," the inserted value means a value that happens to contain braces is never treated as a new placeholder. A missing key raises ",[14,766,767],{},"KeyError"," with the placeholder's name — which is what you want: a letter with an unfilled field should not be produced.",[164,770,772],{"id":771},"step-2-walk-the-body-tables-headers-and-footers","Step 2 — Walk the body, tables, headers and footers",[10,774,775,778],{},[14,776,777],{},"doc.paragraphs"," is only the body text. Placeholders in table cells, headers and footers need their own loop. Collect every paragraph in the document into one generator:",[169,780,782],{"className":228,"code":781,"language":230,"meta":174,"style":174},"def all_paragraphs(doc):\n    yield from doc.paragraphs\n    for table in doc.tables:\n        for row in table.rows:\n            for cell in row.cells:\n                yield from cell.paragraphs\n    for section in doc.sections:\n        for part in (section.header, section.footer):\n            yield from part.paragraphs\n            for table in part.tables:\n                for row in table.rows:\n                    for cell in row.cells:\n                        yield from cell.paragraphs\n\nfresh = Document(\"statement_template.docx\")\nprint(sorted({m.group(1) for p in all_paragraphs(fresh) for m in PLACEHOLDER.finditer(p.text)}))\n",[14,783,784,794,802,815,827,840,848,860,872,880,891,902,913,920,924,937],{"__ignoreMap":174},[178,785,786,788,791],{"class":180,"line":181},[178,787,385],{"class":237},[178,789,790],{"class":388}," all_paragraphs",[178,792,793],{"class":241},"(doc):\n",[178,795,796,799],{"class":180,"line":251},[178,797,798],{"class":237},"    yield from",[178,800,801],{"class":241}," doc.paragraphs\n",[178,803,804,807,810,812],{"class":180,"line":258},[178,805,806],{"class":237},"    for",[178,808,809],{"class":241}," table ",[178,811,292],{"class":237},[178,813,814],{"class":241}," doc.tables:\n",[178,816,817,819,822,824],{"class":180,"line":276},[178,818,513],{"class":237},[178,820,821],{"class":241}," row ",[178,823,292],{"class":237},[178,825,826],{"class":241}," table.rows:\n",[178,828,829,832,835,837],{"class":180,"line":382},[178,830,831],{"class":237},"            for",[178,833,834],{"class":241}," cell ",[178,836,292],{"class":237},[178,838,839],{"class":241}," row.cells:\n",[178,841,842,845],{"class":180,"line":395},[178,843,844],{"class":237},"                yield from",[178,846,847],{"class":241}," cell.paragraphs\n",[178,849,850,852,855,857],{"class":180,"line":406},[178,851,806],{"class":237},[178,853,854],{"class":241}," section ",[178,856,292],{"class":237},[178,858,859],{"class":241}," doc.sections:\n",[178,861,862,864,867,869],{"class":180,"line":418},[178,863,513],{"class":237},[178,865,866],{"class":241}," part ",[178,868,292],{"class":237},[178,870,871],{"class":241}," (section.header, section.footer):\n",[178,873,874,877],{"class":180,"line":442},[178,875,876],{"class":237},"            yield from",[178,878,879],{"class":241}," part.paragraphs\n",[178,881,882,884,886,888],{"class":180,"line":456},[178,883,831],{"class":237},[178,885,809],{"class":241},[178,887,292],{"class":237},[178,889,890],{"class":241}," part.tables:\n",[178,892,893,896,898,900],{"class":180,"line":468},[178,894,895],{"class":237},"                for",[178,897,821],{"class":241},[178,899,292],{"class":237},[178,901,826],{"class":241},[178,903,904,907,909,911],{"class":180,"line":474},[178,905,906],{"class":237},"                    for",[178,908,834],{"class":241},[178,910,292],{"class":237},[178,912,839],{"class":241},[178,914,915,918],{"class":180,"line":494},[178,916,917],{"class":237},"                        yield from",[178,919,847],{"class":241},[178,921,922],{"class":180,"line":510},[178,923,255],{"emptyLinePlaceholder":254},[178,925,926,929,931,933,935],{"class":180,"line":523},[178,927,928],{"class":241},"fresh ",[178,930,264],{"class":237},[178,932,267],{"class":241},[178,934,270],{"class":188},[178,936,273],{"class":241},[178,938,939,941,944,947,950,952,954,956,959,961,964,966,969,971,973],{"class":180,"line":542},[178,940,280],{"class":279},[178,942,943],{"class":241},"(",[178,945,946],{"class":279},"sorted",[178,948,949],{"class":241},"({m.group(",[178,951,488],{"class":279},[178,953,644],{"class":241},[178,955,286],{"class":237},[178,957,958],{"class":241}," p ",[178,960,292],{"class":237},[178,962,963],{"class":241}," all_paragraphs(fresh) ",[178,965,286],{"class":237},[178,967,968],{"class":241}," m ",[178,970,292],{"class":237},[178,972,450],{"class":279},[178,974,975],{"class":241},".finditer(p.text)}))\n",[10,977,978],{},"Listing every placeholder in the template is also a useful check before a run: each name must be a column in the spreadsheet, a computed field, or a line-item field.",[29,980,38,986,38,989,38,992,38,997,38,1003,38,1009,38,1015,38,1019,38,1023,38,1026,38,1029,38,1034,38,1039,38,1042,38,1046,38,1049,38,1052,38,1058,38,1062,38,1066,38,1070,38,1073,38,1076,38,1081,38,1085],{"viewBox":981,"role":32,"ariaLabelledBy":982,"xmlns":36,"style":985},"84 0 592 512",[983,984],"docx-parts-t","docx-parts-d","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,987,988],{"id":983},"Where placeholders can live in a Word document",[44,990,991],{"id":984},"A Word document has several places to search for placeholders. The header, such as the statement number. The body paragraphs, such as the contact and balance. Table cells, such as the invoice line. The footer, such as the account number. doc.paragraphs covers only the body, so each part needs its own loop.",[48,993],{"x":994,"y":51,"width":995,"height":996,"fill":54},"84","592","512",[48,998],{"x":999,"y":91,"width":1000,"height":60,"rx":61,"fill":1001,"stroke":1002,"style":64},"100.0","560.0","#e7ebef","var(--line,#cdd5e6)",[66,1004,1006],{"transform":1005},"translate(368.0,29.0) scale(1.000)",[70,1007],{"d":72,"fill":73,"stroke":1008,"style":75},"var(--text,#172033)",[77,1010,1014],{"x":1011,"y":1012,"style":1013},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Header",[77,1016,1018],{"x":1011,"y":1017,"style":86},"95.0","{{statement_no}}",[180,1020],{"x1":1011,"y1":1021,"x2":1011,"y2":1022,"stroke":63,"style":64},"114.0","136.0",[116,1024],{"points":1025,"fill":119},"380.0,140.0 375.0,131.0 385.0,131.0",[48,1027],{"x":999,"y":1028,"width":1000,"height":60,"rx":61,"fill":93,"stroke":94,"style":64},"145.0",[66,1030,1032],{"transform":1031},"translate(368.0,158.0) scale(1.000)",[70,1033],{"d":72,"fill":73,"stroke":100,"style":75},[77,1035,1038],{"x":1011,"y":1036,"style":1037},"204.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Body paragraphs",[77,1040,1041],{"x":1011,"y":143,"style":86},"{{contact}}, {{balance}}",[180,1043],{"x1":1011,"y1":1044,"x2":1011,"y2":1045,"stroke":63,"style":64},"243.0","265.0",[116,1047],{"points":1048,"fill":119},"380.0,269.0 375.0,260.0 385.0,260.0",[48,1050],{"x":999,"y":1051,"width":1000,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"274.0",[66,1053,1055],{"transform":1054},"translate(368.0,287.0) scale(1.000)",[70,1056],{"d":1057,"fill":73,"stroke":74,"style":75},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14",[77,1059,1061],{"x":1011,"y":1060,"style":81},"333.5","Table cells",[77,1063,1065],{"x":1011,"y":1064,"style":86},"353.0","{{invoice}}, {{amount}}",[180,1067],{"x1":1011,"y1":1068,"x2":1011,"y2":1069,"stroke":63,"style":64},"372.0","394.0",[116,1071],{"points":1072,"fill":119},"380.0,398.0 375.0,389.0 385.0,389.0",[48,1074],{"x":999,"y":1075,"width":1000,"height":60,"rx":61,"fill":1001,"stroke":1002,"style":64},"403.0",[66,1077,1079],{"transform":1078},"translate(368.0,416.0) scale(1.000)",[70,1080],{"d":72,"fill":73,"stroke":1008,"style":75},[77,1082,1084],{"x":1011,"y":1083,"style":1013},"462.5","Footer",[77,1086,1088],{"x":1011,"y":1087,"style":86},"482.0","{{account}}",[164,1090,1092],{"id":1091},"step-3-repeat-a-table-row-for-each-line-item","Step 3 — Repeat a table row for each line item",[10,1094,1095],{},"The template's table has a header row and one row of line-item placeholders. For each account, copy that row once per invoice, fill each copy, and remove the original. python-docx has no \"duplicate row\" method, but a row is an XML element that can be deep-copied:",[169,1097,1099],{"className":228,"code":1098,"language":230,"meta":174,"style":174},"import copy\n\ndef fill_line_items(table, items, template_row=1):\n    proto = table.rows[template_row]._tr\n    for item in items:\n        new_tr = copy.deepcopy(proto)\n        proto.addprevious(new_tr)\n    new_rows = table.rows[template_row:template_row + len(items)]\n    for row, item in zip(new_rows, items):\n        for cell in row.cells:\n            for paragraph in cell.paragraphs:\n                fill_paragraph(paragraph, item)\n    proto.getparent().remove(proto)\n",[14,1100,1101,1108,1112,1129,1139,1151,1161,1166,1183,1198,1208,1220,1225],{"__ignoreMap":174},[178,1102,1103,1105],{"class":180,"line":181},[178,1104,245],{"class":237},[178,1106,1107],{"class":241}," copy\n",[178,1109,1110],{"class":180,"line":251},[178,1111,255],{"emptyLinePlaceholder":254},[178,1113,1114,1116,1119,1122,1124,1126],{"class":180,"line":258},[178,1115,385],{"class":237},[178,1117,1118],{"class":388}," fill_line_items",[178,1120,1121],{"class":241},"(table, items, template_row",[178,1123,264],{"class":237},[178,1125,488],{"class":279},[178,1127,1128],{"class":241},"):\n",[178,1130,1131,1134,1136],{"class":180,"line":276},[178,1132,1133],{"class":241},"    proto ",[178,1135,264],{"class":237},[178,1137,1138],{"class":241}," table.rows[template_row]._tr\n",[178,1140,1141,1143,1146,1148],{"class":180,"line":382},[178,1142,806],{"class":237},[178,1144,1145],{"class":241}," item ",[178,1147,292],{"class":237},[178,1149,1150],{"class":241}," items:\n",[178,1152,1153,1156,1158],{"class":180,"line":395},[178,1154,1155],{"class":241},"        new_tr ",[178,1157,264],{"class":237},[178,1159,1160],{"class":241}," copy.deepcopy(proto)\n",[178,1162,1163],{"class":180,"line":406},[178,1164,1165],{"class":241},"        proto.addprevious(new_tr)\n",[178,1167,1168,1171,1173,1176,1178,1180],{"class":180,"line":418},[178,1169,1170],{"class":241},"    new_rows ",[178,1172,264],{"class":237},[178,1174,1175],{"class":241}," table.rows[template_row:template_row ",[178,1177,363],{"class":237},[178,1179,536],{"class":279},[178,1181,1182],{"class":241},"(items)]\n",[178,1184,1185,1187,1190,1192,1195],{"class":180,"line":442},[178,1186,806],{"class":237},[178,1188,1189],{"class":241}," row, item ",[178,1191,292],{"class":237},[178,1193,1194],{"class":279}," zip",[178,1196,1197],{"class":241},"(new_rows, items):\n",[178,1199,1200,1202,1204,1206],{"class":180,"line":456},[178,1201,513],{"class":237},[178,1203,834],{"class":241},[178,1205,292],{"class":237},[178,1207,839],{"class":241},[178,1209,1210,1212,1215,1217],{"class":180,"line":468},[178,1211,831],{"class":237},[178,1213,1214],{"class":241}," paragraph ",[178,1216,292],{"class":237},[178,1218,1219],{"class":241}," cell.paragraphs:\n",[178,1221,1222],{"class":180,"line":474},[178,1223,1224],{"class":241},"                fill_paragraph(paragraph, item)\n",[178,1226,1227],{"class":180,"line":494},[178,1228,1229],{"class":241},"    proto.getparent().remove(proto)\n",[10,1231,1232],{},"Copying the row element keeps its borders, shading and fonts. Inserting each copy before the prototype keeps the invoices in order, and removing the prototype at the end leaves no row of raw placeholders behind.",[164,1234,1236],{"id":1235},"step-4-format-values-and-generate-every-document","Step 4 — Format values and generate every document",[10,1238,1239],{},"Values are formatted for a reader before they go into the document: money with thousands separators and two decimals, dates written out. Then for each account, fill the line items first — so the table's placeholders are gone — and the remaining fields after:",[169,1241,1243],{"className":228,"code":1242,"language":230,"meta":174,"style":174},"from pathlib import Path\nimport pandas as pd\n\naccounts = pd.read_excel(\"statements.xlsx\", sheet_name=\"Accounts\", parse_dates=[\"due_date\"])\ninvoices = pd.read_excel(\"statements.xlsx\", sheet_name=\"Invoices\", parse_dates=[\"invoice_date\"])\nout_dir = Path(\"statements\"); out_dir.mkdir(exist_ok=True)\n\ndef money(x):\n    return f\"{x:,.2f}\"\n\ndef long_date(d):\n    return f\"{d.day} {d:%B %Y}\"\n\ndef safe(name):\n    return re.sub(r\"[^\\w\\-]+\", \"_\", name).strip(\"_\")\n\nmade = []\nfor n, acct in enumerate(accounts.itertuples(index=False), start=1):\n    lines = invoices[invoices[\"account\"] == acct.account].sort_values(\"invoice_date\")\n    doc = Document(\"statement_template.docx\")\n    fill_line_items(doc.tables[0], [{\"invoice\": r.invoice, \"invoice_date\": long_date(r.invoice_date), \"amount\": money(r.amount)}\n                                    for r in lines.itertuples()])\n    values = {\"statement_no\": f\"2026-09-{n:03d}\", \"date\": long_date(pd.Timestamp(\"2026-09-30\")),\n              \"contact\": acct.contact, \"balance\": money(lines[\"amount\"].sum()), \"due_date\": long_date(acct.due_date),\n              \"account_manager\": acct.account_manager, \"account\": acct.account}\n    for paragraph in all_paragraphs(doc):\n        fill_paragraph(paragraph, values)\n    leftover = [p.text for p in all_paragraphs(doc) if \"{{\" in p.text]\n    assert not leftover, f\"{acct.account}: unfilled {leftover}\"\n    path = out_dir \u002F f\"statement_{acct.account}_{safe(acct.company)}.docx\"\n    doc.save(path)\n    made.append(path)\nprint([p.name for p in made])\n",[14,1244,1245,1257,1270,1274,1313,1346,1372,1376,1386,1412,1416,1426,1462,1466,1476,1517,1521,1531,1566,1592,1605,1632,1644,1690,1713,1726,1737,1743,1779,1813,1851,1857,1863],{"__ignoreMap":174},[178,1246,1247,1249,1252,1254],{"class":180,"line":181},[178,1248,238],{"class":237},[178,1250,1251],{"class":241}," pathlib ",[178,1253,245],{"class":237},[178,1255,1256],{"class":241}," Path\n",[178,1258,1259,1261,1264,1267],{"class":180,"line":251},[178,1260,245],{"class":237},[178,1262,1263],{"class":241}," pandas ",[178,1265,1266],{"class":237},"as",[178,1268,1269],{"class":241}," pd\n",[178,1271,1272],{"class":180,"line":258},[178,1273,255],{"emptyLinePlaceholder":254},[178,1275,1276,1279,1281,1284,1287,1289,1293,1295,1298,1300,1303,1305,1308,1310],{"class":180,"line":276},[178,1277,1278],{"class":241},"accounts ",[178,1280,264],{"class":237},[178,1282,1283],{"class":241}," pd.read_excel(",[178,1285,1286],{"class":188},"\"statements.xlsx\"",[178,1288,207],{"class":241},[178,1290,1292],{"class":1291},"sa561","sheet_name",[178,1294,264],{"class":237},[178,1296,1297],{"class":188},"\"Accounts\"",[178,1299,207],{"class":241},[178,1301,1302],{"class":1291},"parse_dates",[178,1304,264],{"class":237},[178,1306,1307],{"class":241},"[",[178,1309,717],{"class":188},[178,1311,1312],{"class":241},"])\n",[178,1314,1315,1318,1320,1322,1324,1326,1328,1330,1333,1335,1337,1339,1341,1344],{"class":180,"line":382},[178,1316,1317],{"class":241},"invoices ",[178,1319,264],{"class":237},[178,1321,1283],{"class":241},[178,1323,1286],{"class":188},[178,1325,207],{"class":241},[178,1327,1292],{"class":1291},[178,1329,264],{"class":237},[178,1331,1332],{"class":188},"\"Invoices\"",[178,1334,207],{"class":241},[178,1336,1302],{"class":1291},[178,1338,264],{"class":237},[178,1340,1307],{"class":241},[178,1342,1343],{"class":188},"\"invoice_date\"",[178,1345,1312],{"class":241},[178,1347,1348,1351,1353,1356,1359,1362,1365,1367,1370],{"class":180,"line":395},[178,1349,1350],{"class":241},"out_dir ",[178,1352,264],{"class":237},[178,1354,1355],{"class":241}," Path(",[178,1357,1358],{"class":188},"\"statements\"",[178,1360,1361],{"class":241},"); out_dir.mkdir(",[178,1363,1364],{"class":1291},"exist_ok",[178,1366,264],{"class":237},[178,1368,1369],{"class":279},"True",[178,1371,273],{"class":241},[178,1373,1374],{"class":180,"line":406},[178,1375,255],{"emptyLinePlaceholder":254},[178,1377,1378,1380,1383],{"class":180,"line":418},[178,1379,385],{"class":237},[178,1381,1382],{"class":388}," money",[178,1384,1385],{"class":241},"(x):\n",[178,1387,1388,1391,1394,1396,1400,1403,1406,1409],{"class":180,"line":442},[178,1389,1390],{"class":237},"    return",[178,1392,1393],{"class":237}," f",[178,1395,347],{"class":188},[178,1397,1399],{"class":1398},"sSjpA","{",[178,1401,1402],{"class":241},"x",[178,1404,1405],{"class":237},":,.2f",[178,1407,1408],{"class":1398},"}",[178,1410,1411],{"class":188},"\"\n",[178,1413,1414],{"class":180,"line":456},[178,1415,255],{"emptyLinePlaceholder":254},[178,1417,1418,1420,1423],{"class":180,"line":468},[178,1419,385],{"class":237},[178,1421,1422],{"class":388}," long_date",[178,1424,1425],{"class":241},"(d):\n",[178,1427,1428,1430,1432,1434,1436,1439,1441,1444,1447,1450,1453,1455,1458,1460],{"class":180,"line":474},[178,1429,1390],{"class":237},[178,1431,1393],{"class":237},[178,1433,347],{"class":188},[178,1435,1399],{"class":1398},[178,1437,1438],{"class":241},"d.day",[178,1440,1408],{"class":1398},[178,1442,1443],{"class":1398}," {",[178,1445,1446],{"class":241},"d:",[178,1448,1449],{"class":237},"%",[178,1451,1452],{"class":241},"B ",[178,1454,1449],{"class":237},[178,1456,1457],{"class":241},"Y",[178,1459,1408],{"class":1398},[178,1461,1411],{"class":188},[178,1463,1464],{"class":180,"line":494},[178,1465,255],{"emptyLinePlaceholder":254},[178,1467,1468,1470,1473],{"class":180,"line":510},[178,1469,385],{"class":237},[178,1471,1472],{"class":388}," safe",[178,1474,1475],{"class":241},"(name):\n",[178,1477,1478,1480,1483,1485,1487,1489,1492,1495,1498,1501,1503,1505,1507,1510,1513,1515],{"class":180,"line":523},[178,1479,1390],{"class":237},[178,1481,1482],{"class":241}," re.sub(",[178,1484,344],{"class":237},[178,1486,347],{"class":188},[178,1488,1307],{"class":279},[178,1490,1491],{"class":237},"^",[178,1493,1494],{"class":279},"\\w",[178,1496,1497],{"class":350},"\\-",[178,1499,1500],{"class":279},"]",[178,1502,363],{"class":237},[178,1504,347],{"class":188},[178,1506,207],{"class":241},[178,1508,1509],{"class":188},"\"_\"",[178,1511,1512],{"class":241},", name).strip(",[178,1514,1509],{"class":188},[178,1516,273],{"class":241},[178,1518,1519],{"class":180,"line":542},[178,1520,255],{"emptyLinePlaceholder":254},[178,1522,1523,1526,1528],{"class":180,"line":553},[178,1524,1525],{"class":241},"made ",[178,1527,264],{"class":237},[178,1529,1530],{"class":241}," []\n",[178,1532,1533,1535,1538,1540,1543,1546,1549,1551,1554,1557,1560,1562,1564],{"class":180,"line":580},[178,1534,286],{"class":237},[178,1536,1537],{"class":241}," n, acct ",[178,1539,292],{"class":237},[178,1541,1542],{"class":279}," enumerate",[178,1544,1545],{"class":241},"(accounts.itertuples(",[178,1547,1548],{"class":1291},"index",[178,1550,264],{"class":237},[178,1552,1553],{"class":279},"False",[178,1555,1556],{"class":241},"), ",[178,1558,1559],{"class":1291},"start",[178,1561,264],{"class":237},[178,1563,488],{"class":279},[178,1565,1128],{"class":241},[178,1567,1568,1571,1573,1576,1579,1582,1585,1588,1590],{"class":180,"line":586},[178,1569,1570],{"class":241},"    lines ",[178,1572,264],{"class":237},[178,1574,1575],{"class":241}," invoices[invoices[",[178,1577,1578],{"class":188},"\"account\"",[178,1580,1581],{"class":241},"] ",[178,1583,1584],{"class":237},"==",[178,1586,1587],{"class":241}," acct.account].sort_values(",[178,1589,1343],{"class":188},[178,1591,273],{"class":241},[178,1593,1594,1597,1599,1601,1603],{"class":180,"line":617},[178,1595,1596],{"class":241},"    doc ",[178,1598,264],{"class":237},[178,1600,267],{"class":241},[178,1602,270],{"class":188},[178,1604,273],{"class":241},[178,1606,1607,1610,1612,1615,1618,1621,1623,1626,1629],{"class":180,"line":652},[178,1608,1609],{"class":241},"    fill_line_items(doc.tables[",[178,1611,51],{"class":279},[178,1613,1614],{"class":241},"], [{",[178,1616,1617],{"class":188},"\"invoice\"",[178,1619,1620],{"class":241},": r.invoice, ",[178,1622,1343],{"class":188},[178,1624,1625],{"class":241},": long_date(r.invoice_date), ",[178,1627,1628],{"class":188},"\"amount\"",[178,1630,1631],{"class":241},": money(r.amount)}\n",[178,1633,1634,1637,1639,1641],{"class":180,"line":663},[178,1635,1636],{"class":237},"                                    for",[178,1638,289],{"class":241},[178,1640,292],{"class":237},[178,1642,1643],{"class":241}," lines.itertuples()])\n",[178,1645,1646,1649,1651,1653,1656,1658,1661,1664,1666,1669,1672,1674,1676,1678,1681,1684,1687],{"class":180,"line":680},[178,1647,1648],{"class":241},"    values ",[178,1650,264],{"class":237},[178,1652,1443],{"class":241},[178,1654,1655],{"class":188},"\"statement_no\"",[178,1657,709],{"class":241},[178,1659,1660],{"class":237},"f",[178,1662,1663],{"class":188},"\"2026-09-",[178,1665,1399],{"class":1398},[178,1667,1668],{"class":241},"n",[178,1670,1671],{"class":237},":03d",[178,1673,1408],{"class":1398},[178,1675,347],{"class":188},[178,1677,207],{"class":241},[178,1679,1680],{"class":188},"\"date\"",[178,1682,1683],{"class":241},": long_date(pd.Timestamp(",[178,1685,1686],{"class":188},"\"2026-09-30\"",[178,1688,1689],{"class":241},")),\n",[178,1691,1692,1695,1698,1700,1703,1705,1708,1710],{"class":180,"line":685},[178,1693,1694],{"class":188},"              \"contact\"",[178,1696,1697],{"class":241},": acct.contact, ",[178,1699,706],{"class":188},[178,1701,1702],{"class":241},": money(lines[",[178,1704,1628],{"class":188},[178,1706,1707],{"class":241},"].sum()), ",[178,1709,717],{"class":188},[178,1711,1712],{"class":241},": long_date(acct.due_date),\n",[178,1714,1715,1718,1721,1723],{"class":180,"line":700},[178,1716,1717],{"class":188},"              \"account_manager\"",[178,1719,1720],{"class":241},": acct.account_manager, ",[178,1722,1578],{"class":188},[178,1724,1725],{"class":241},": acct.account}\n",[178,1727,1728,1730,1732,1734],{"class":180,"line":728},[178,1729,806],{"class":237},[178,1731,1214],{"class":241},[178,1733,292],{"class":237},[178,1735,1736],{"class":241}," all_paragraphs(doc):\n",[178,1738,1740],{"class":180,"line":1739},27,[178,1741,1742],{"class":241},"        fill_paragraph(paragraph, values)\n",[178,1744,1746,1749,1751,1754,1756,1758,1760,1763,1765,1768,1771,1773,1776],{"class":180,"line":1745},28,[178,1747,1748],{"class":241},"    leftover ",[178,1750,264],{"class":237},[178,1752,1753],{"class":241}," [p.text ",[178,1755,286],{"class":237},[178,1757,958],{"class":241},[178,1759,292],{"class":237},[178,1761,1762],{"class":241}," all_paragraphs(doc) ",[178,1764,633],{"class":237},[178,1766,1767],{"class":188}," \"",[178,1769,1770],{"class":1398},"{{",[178,1772,347],{"class":188},[178,1774,1775],{"class":237}," in",[178,1777,1778],{"class":241}," p.text]\n",[178,1780,1782,1785,1787,1790,1792,1794,1796,1799,1801,1804,1806,1809,1811],{"class":180,"line":1781},29,[178,1783,1784],{"class":237},"    assert",[178,1786,462],{"class":237},[178,1788,1789],{"class":241}," leftover, ",[178,1791,1660],{"class":237},[178,1793,347],{"class":188},[178,1795,1399],{"class":1398},[178,1797,1798],{"class":241},"acct.account",[178,1800,1408],{"class":1398},[178,1802,1803],{"class":188},": unfilled ",[178,1805,1399],{"class":1398},[178,1807,1808],{"class":241},"leftover",[178,1810,1408],{"class":1398},[178,1812,1411],{"class":188},[178,1814,1816,1819,1821,1824,1827,1829,1832,1834,1836,1838,1841,1843,1846,1848],{"class":180,"line":1815},30,[178,1817,1818],{"class":241},"    path ",[178,1820,264],{"class":237},[178,1822,1823],{"class":241}," out_dir ",[178,1825,1826],{"class":237},"\u002F",[178,1828,1393],{"class":237},[178,1830,1831],{"class":188},"\"statement_",[178,1833,1399],{"class":1398},[178,1835,1798],{"class":241},[178,1837,1408],{"class":1398},[178,1839,1840],{"class":188},"_",[178,1842,1399],{"class":1398},[178,1844,1845],{"class":241},"safe(acct.company)",[178,1847,1408],{"class":1398},[178,1849,1850],{"class":188},".docx\"\n",[178,1852,1854],{"class":180,"line":1853},31,[178,1855,1856],{"class":241},"    doc.save(path)\n",[178,1858,1860],{"class":180,"line":1859},32,[178,1861,1862],{"class":241},"    made.append(path)\n",[178,1864,1866,1868,1871,1873,1875,1877],{"class":180,"line":1865},33,[178,1867,280],{"class":279},[178,1869,1870],{"class":241},"([p.name ",[178,1872,286],{"class":237},[178,1874,958],{"class":241},[178,1876,292],{"class":237},[178,1878,1879],{"class":241}," made])\n",[10,1881,1882,1883,1885,1886,1889],{},"The final check scans the finished document for any remaining ",[14,1884,1770],{}," — a placeholder with a typo, or one added to the template without a matching value — and stops rather than sending a letter with a gap in it. File names come from the account number and a sanitised company name, so \"Brightside & Co\" becomes ",[14,1887,1888],{},"Brightside_Co",".",[164,1891,1893],{"id":1892},"step-5-check-a-finished-document","Step 5 — Check a finished document",[10,1895,1896],{},"Read one generated statement back to confirm what a recipient will see:",[169,1898,1900],{"className":228,"code":1899,"language":230,"meta":174,"style":174},"check = Document(made[2])\nprint(check.sections[0].header.paragraphs[0].text)\nfor p in check.paragraphs:\n    if p.text:\n        print(p.text)\nfor row in check.tables[0].rows:\n    print(\" | \".join(c.text for c in row.cells))\nprint(check.sections[0].footer.paragraphs[0].text)\n",[14,1901,1902,1916,1933,1944,1952,1960,1976,1999],{"__ignoreMap":174},[178,1903,1904,1907,1909,1912,1914],{"class":180,"line":181},[178,1905,1906],{"class":241},"check ",[178,1908,264],{"class":237},[178,1910,1911],{"class":241}," Document(made[",[178,1913,298],{"class":279},[178,1915,1312],{"class":241},[178,1917,1918,1920,1923,1925,1928,1930],{"class":180,"line":251},[178,1919,280],{"class":279},[178,1921,1922],{"class":241},"(check.sections[",[178,1924,51],{"class":279},[178,1926,1927],{"class":241},"].header.paragraphs[",[178,1929,51],{"class":279},[178,1931,1932],{"class":241},"].text)\n",[178,1934,1935,1937,1939,1941],{"class":180,"line":258},[178,1936,286],{"class":237},[178,1938,958],{"class":241},[178,1940,292],{"class":237},[178,1942,1943],{"class":241}," check.paragraphs:\n",[178,1945,1946,1949],{"class":180,"line":276},[178,1947,1948],{"class":237},"    if",[178,1950,1951],{"class":241}," p.text:\n",[178,1953,1954,1957],{"class":180,"line":382},[178,1955,1956],{"class":279},"        print",[178,1958,1959],{"class":241},"(p.text)\n",[178,1961,1962,1964,1966,1968,1971,1973],{"class":180,"line":395},[178,1963,286],{"class":237},[178,1965,821],{"class":241},[178,1967,292],{"class":237},[178,1969,1970],{"class":241}," check.tables[",[178,1972,51],{"class":279},[178,1974,1975],{"class":241},"].rows:\n",[178,1977,1978,1981,1983,1986,1989,1991,1994,1996],{"class":180,"line":406},[178,1979,1980],{"class":279},"    print",[178,1982,943],{"class":241},[178,1984,1985],{"class":188},"\" | \"",[178,1987,1988],{"class":241},".join(c.text ",[178,1990,286],{"class":237},[178,1992,1993],{"class":241}," c ",[178,1995,292],{"class":237},[178,1997,1998],{"class":241}," row.cells))\n",[178,2000,2001,2003,2005,2007,2010,2012],{"class":180,"line":418},[178,2002,280],{"class":279},[178,2004,1922],{"class":241},[178,2006,51],{"class":279},[178,2008,2009],{"class":241},"].footer.paragraphs[",[178,2011,51],{"class":279},[178,2013,1932],{"class":241},[29,2015,38,2021,38,2024,38,2027,38,2032,38,2037,38,2044,38,2047,38,2053,38,2057,38,2061,38,2065,38,2069,38,2073,38,2077,38,2079,38,2083,38,2085,38,2088,38,2090,38,2093,38,2095,38,2098,38,2100,38,2104,38,2106,38,2109,38,2111,38,2114,38,2117,38,2121,38,2123,38,2126,38,2128,38,2131,38,2133,38,2136,38,2139,38,2142,38,2144,38,2147,38,2149,38,2152,38,2154],{"viewBox":2016,"role":32,"ariaLabelledBy":2017,"xmlns":36,"style":2020},"173 0 414 200",[2018,2019],"docx-table-t","docx-table-d","width:100%;max-width:414px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,2022,2023],{"id":2018},"The invoice table in the Corex GmbH statement",[44,2025,2026],{"id":2019},"The table in the generated statement for Corex GmbH after filling: a header row, then one row per open invoice in date order — INV-2071 on 30 May 2026 for 1,800.00, INV-2120 on 25 July 2026 for 6,900.00 and INV-2230 on 2 September 2026 for 450.50. The placeholder row from the template has been removed.",[48,2028],{"x":2029,"y":51,"width":2030,"height":2031,"fill":54},"173","414","200",[77,2033,2036],{"x":1011,"y":2034,"style":2035},"31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","statement_C-307_Corex_GmbH.docx — invoice table",[48,2038],{"x":2039,"y":2040,"width":2041,"height":2042,"rx":51,"fill":1001,"stroke":1002,"style":2043},"211.0","48.0","32.0","24.0","stroke-width:1px",[48,2045],{"x":1044,"y":2040,"width":2046,"height":2042,"rx":51,"fill":1001,"stroke":1002,"style":2043},"85.0",[77,2048,2052],{"x":2049,"y":2050,"style":2051},"285.5","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,2054],{"x":2055,"y":2040,"width":2056,"height":2042,"rx":51,"fill":1001,"stroke":1002,"style":2043},"327.9","138.4",[77,2058,2060],{"x":2059,"y":2050,"style":2051},"397.1","B",[48,2062],{"x":2063,"y":2040,"width":2064,"height":2042,"rx":51,"fill":1001,"stroke":1002,"style":2043},"466.3","82.7",[77,2066,2068],{"x":2067,"y":2050,"style":2051},"507.7","C",[48,2070],{"x":2039,"y":2071,"width":2041,"height":2072,"rx":51,"fill":1001,"stroke":1002,"style":2043},"72.0","28.0",[77,2074,488],{"x":2075,"y":2076,"style":2051},"227.0","91.0",[48,2078],{"x":1044,"y":2071,"width":2046,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2080,2082],{"x":2049,"y":2076,"style":2081},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Invoice",[48,2084],{"x":2055,"y":2071,"width":2056,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2086,2087],{"x":2059,"y":2076,"style":2081},"Date",[48,2089],{"x":2063,"y":2071,"width":2064,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2091,2092],{"x":2067,"y":2076,"style":2081},"Amount",[48,2094],{"x":2039,"y":999,"width":2041,"height":2072,"rx":51,"fill":1001,"stroke":1002,"style":2043},[77,2096,298],{"x":2075,"y":2097,"style":2051},"119.0",[48,2099],{"x":1044,"y":999,"width":2046,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2101,2103],{"x":2049,"y":2097,"style":2102},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","INV-2071",[48,2105],{"x":2055,"y":999,"width":2056,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2107,2108],{"x":2059,"y":2097,"style":2102},"30 May 2026",[48,2110],{"x":2063,"y":999,"width":2064,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2112,2113],{"x":2067,"y":2097,"style":2102},"1,800.00",[48,2115],{"x":2039,"y":2116,"width":2041,"height":2072,"rx":51,"fill":1001,"stroke":1002,"style":2043},"128.0",[77,2118,2120],{"x":2075,"y":2119,"style":2051},"147.0","3",[48,2122],{"x":1044,"y":2116,"width":2046,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2124,2125],{"x":2049,"y":2119,"style":2102},"INV-2120",[48,2127],{"x":2055,"y":2116,"width":2056,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2129,2130],{"x":2059,"y":2119,"style":2102},"25 July 2026",[48,2132],{"x":2063,"y":2116,"width":2064,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2134,2135],{"x":2067,"y":2119,"style":2102},"6,900.00",[48,2137],{"x":2039,"y":2138,"width":2041,"height":2072,"rx":51,"fill":1001,"stroke":1002,"style":2043},"156.0",[77,2140,50],{"x":2075,"y":2141,"style":2051},"175.0",[48,2143],{"x":1044,"y":2138,"width":2046,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2145,2146],{"x":2049,"y":2141,"style":2102},"INV-2230",[48,2148],{"x":2055,"y":2138,"width":2056,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2150,2151],{"x":2059,"y":2141,"style":2102},"2 September 2026",[48,2153],{"x":2063,"y":2138,"width":2064,"height":2072,"rx":51,"fill":54,"stroke":1002,"style":2043},[77,2155,2156],{"x":2067,"y":2141,"style":2102},"450.50",[164,2158,2160],{"id":2159},"step-6-convert-the-documents-to-pdf","Step 6 — Convert the documents to PDF",[10,2162,2163],{},"Statements are usually sent as PDFs. LibreOffice converts a whole folder of documents in one headless call:",[169,2165,2167],{"className":228,"code":2166,"language":230,"meta":174,"style":174},"import shutil\nimport subprocess\n\nsoffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\nif soffice:\n    subprocess.run([soffice, \"--headless\", \"--convert-to\", \"pdf\", \"--outdir\", str(out_dir), *map(str, made)],\n                   check=True, capture_output=True, timeout=180)\n    print(sorted(p.name for p in out_dir.glob(\"*.pdf\")))\nelse:\n    print(\"LibreOffice not found — skipping PDF conversion\")\n",[14,2168,2169,2176,2183,2187,2211,2218,2261,2291,2317,2323],{"__ignoreMap":174},[178,2170,2171,2173],{"class":180,"line":181},[178,2172,245],{"class":237},[178,2174,2175],{"class":241}," shutil\n",[178,2177,2178,2180],{"class":180,"line":251},[178,2179,245],{"class":237},[178,2181,2182],{"class":241}," subprocess\n",[178,2184,2185],{"class":180,"line":258},[178,2186,255],{"emptyLinePlaceholder":254},[178,2188,2189,2192,2194,2197,2200,2202,2204,2206,2209],{"class":180,"line":276},[178,2190,2191],{"class":241},"soffice ",[178,2193,264],{"class":237},[178,2195,2196],{"class":241}," shutil.which(",[178,2198,2199],{"class":188},"\"soffice\"",[178,2201,644],{"class":241},[178,2203,568],{"class":237},[178,2205,2196],{"class":241},[178,2207,2208],{"class":188},"\"libreoffice\"",[178,2210,273],{"class":241},[178,2212,2213,2215],{"class":180,"line":382},[178,2214,633],{"class":237},[178,2216,2217],{"class":241}," soffice:\n",[178,2219,2220,2223,2226,2228,2231,2233,2236,2238,2241,2243,2246,2249,2251,2254,2256,2258],{"class":180,"line":395},[178,2221,2222],{"class":241},"    subprocess.run([soffice, ",[178,2224,2225],{"class":188},"\"--headless\"",[178,2227,207],{"class":241},[178,2229,2230],{"class":188},"\"--convert-to\"",[178,2232,207],{"class":241},[178,2234,2235],{"class":188},"\"pdf\"",[178,2237,207],{"class":241},[178,2239,2240],{"class":188},"\"--outdir\"",[178,2242,207],{"class":241},[178,2244,2245],{"class":279},"str",[178,2247,2248],{"class":241},"(out_dir), ",[178,2250,357],{"class":237},[178,2252,2253],{"class":279},"map",[178,2255,943],{"class":241},[178,2257,2245],{"class":279},[178,2259,2260],{"class":241},", made)],\n",[178,2262,2263,2266,2268,2270,2272,2275,2277,2279,2281,2284,2286,2289],{"class":180,"line":406},[178,2264,2265],{"class":1291},"                   check",[178,2267,264],{"class":237},[178,2269,1369],{"class":279},[178,2271,207],{"class":241},[178,2273,2274],{"class":1291},"capture_output",[178,2276,264],{"class":237},[178,2278,1369],{"class":279},[178,2280,207],{"class":241},[178,2282,2283],{"class":1291},"timeout",[178,2285,264],{"class":237},[178,2287,2288],{"class":279},"180",[178,2290,273],{"class":241},[178,2292,2293,2295,2297,2299,2302,2304,2306,2308,2311,2314],{"class":180,"line":418},[178,2294,1980],{"class":279},[178,2296,943],{"class":241},[178,2298,946],{"class":279},[178,2300,2301],{"class":241},"(p.name ",[178,2303,286],{"class":237},[178,2305,958],{"class":241},[178,2307,292],{"class":237},[178,2309,2310],{"class":241}," out_dir.glob(",[178,2312,2313],{"class":188},"\"*.pdf\"",[178,2315,2316],{"class":241},")))\n",[178,2318,2319,2321],{"class":180,"line":442},[178,2320,639],{"class":237},[178,2322,415],{"class":241},[178,2324,2325,2327,2329,2332],{"class":180,"line":456},[178,2326,1980],{"class":279},[178,2328,943],{"class":241},[178,2330,2331],{"class":188},"\"LibreOffice not found — skipping PDF conversion\"",[178,2333,273],{"class":241},[10,2335,2336,2337,2340,2341,2345,2346,2350],{},"On Windows with Word installed, ",[14,2338,2339],{},"docx2pdf"," drives Word itself for exact Word rendering. ",[23,2342,2344],{"href":2343},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF"," compares the conversion routes, and ",[23,2347,2349],{"href":2348},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report"," covers emailing each statement to its account.",[164,2352,2354],{"id":2353},"common-pitfalls","Common pitfalls",[2356,2357,2358,2366,2374,2387,2396,2408],"ul",{},[2359,2360,2361,2365],"li",{},[2362,2363,2364],"strong",{},"Replacing inside single runs."," Word splits placeholders; search the paragraph and rewrite the spanned runs.",[2359,2367,2368,2373],{},[2362,2369,2370,2371,1889],{},"Only searching ",[14,2372,777],{}," Tables, headers and footers need their own loops.",[2359,2375,2376,2379,2380,310,2383,2386],{},[2362,2377,2378],{},"Unformatted values."," ",[14,2381,2382],{},"11600.0",[14,2384,2385],{},"2026-10-15 00:00:00"," look wrong in a letter; format first.",[2359,2388,2389,2392,2393,2395],{},[2362,2390,2391],{},"Unfilled placeholders."," Scan the finished document for ",[14,2394,1770],{}," and stop if any remain.",[2359,2397,2398,2401,2402,207,2405,2407],{},[2362,2399,2400],{},"Unsafe file names."," Company names contain ",[14,2403,2404],{},"&",[14,2406,1826],{}," and spaces; sanitise them.",[2359,2409,2410,2413],{},[2362,2411,2412],{},"Editing the template in place."," Always open the template fresh for each document.",[164,2415,2417],{"id":2416},"performance-notes","Performance notes",[10,2419,2420,2421,2423],{},"Opening and saving a small ",[14,2422,20],{}," takes a few milliseconds, so thousands of documents take seconds. PDF conversion is the slow step: batch many documents into one LibreOffice call rather than starting it once per file.",[164,2425,2427],{"id":2426},"conclusion","Conclusion",[10,2429,2430,2431,2434],{},"Put ",[14,2432,2433],{},"{{placeholders}}"," in an ordinary Word template, and fill them with a function that searches each paragraph's full text and rewrites the runs a match spans, so placeholders split by Word are still found. Apply it to body paragraphs, table cells, headers and footers; repeat a template table row for each line item by deep-copying it; format values for readers; and refuse to save a document that still contains a placeholder. Convert the finished documents to PDF in one LibreOffice call.",[164,2436,2438],{"id":2437},"frequently-asked-questions","Frequently asked questions",[10,2440,2441,2444,2445,2449],{},[2362,2442,2443],{},"How do I create a Word document for each row in Excel?","\nDesign a .docx template with placeholders such as ",[2446,2447],"binding",{"value":2448},"name",", read the spreadsheet with pandas, and for each row open the template with python-docx, replace every placeholder with the row's values, and save it under a name built from the row.",[10,2451,2452,2455,2456,2459,2460,2463],{},[2362,2453,2454],{},"Why does python-docx not replace my placeholder?","\nWord often splits text into several runs — when formatting changes, after an edit, or for spell-checking — so ",[2446,2457],{"value":2458},"balance"," may be stored as \"",[2446,2461],{"value":2462},"bal\" and \"ance","\". Replacing inside each run misses it. Join the runs' text, find the placeholder there, and write the replacement back into the runs it spans.",[10,2465,2466,2469],{},[2362,2467,2468],{},"How do I fill placeholders in headers, footers and tables?","\ndoc.paragraphs only covers the body text. Also walk every table's cells, and each section's header and footer, and apply the same paragraph replacement to all of them.",[10,2471,2472,2475],{},[2362,2473,2474],{},"Should I use python-docx or docxtpl?","\npython-docx is enough for simple placeholders and repeating table rows, as shown here. docxtpl adds Jinja templating — loops, conditions and filters written in the Word document itself — which is worth it when non-programmers maintain complicated templates.",[164,2477,2479],{"id":2478},"related","Related",[2356,2481,2482,2488,2495,2502],{},[2359,2483,2484,2485,2487],{},"Up one level: ",[23,2486,26],{"href":25}," — templates for reports.",[2359,2489,2490,2494],{},[23,2491,2493],{"href":2492},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill Excel Template with Python openpyxl"," — the same idea for workbooks.",[2359,2496,2497,2501],{},[23,2498,2500],{"href":2499},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-accounts-receivable-aging-report\u002F","Build an Accounts Receivable Aging Report"," — where the open invoices come from.",[2359,2503,2504,2506],{},[23,2505,2344],{"href":2343}," — PDF conversion options.",[2508,2509,2510],"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_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}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}",{"title":174,"searchDepth":251,"depth":251,"links":2512},[2513,2514,2515,2516,2517,2518,2519,2520,2521,2522,2523,2524],{"id":166,"depth":251,"text":167},{"id":221,"depth":251,"text":222},{"id":771,"depth":251,"text":772},{"id":1091,"depth":251,"text":1092},{"id":1235,"depth":251,"text":1236},{"id":1892,"depth":251,"text":1893},{"id":2159,"depth":251,"text":2160},{"id":2353,"depth":251,"text":2354},{"id":2416,"depth":251,"text":2417},{"id":2426,"depth":251,"text":2427},{"id":2437,"depth":251,"text":2438},{"id":2478,"depth":251,"text":2479},"2026-09-25","Do a mail merge in Python: one Word letter, statement or certificate per row of a spreadsheet, filled from a .docx template. Replace placeholders even when Word has split them across formatting runs, fill header, footer and table placeholders, add a row per line item, format money and dates, catch unfilled fields and convert the results to PDF.","md",[2529,2531,2533,2535],{"q":2443,"a":2530},"Design a .docx template with placeholders such as {{name}}, read the spreadsheet with pandas, and for each row open the template with python-docx, replace every placeholder with the row's values, and save it under a name built from the row.",{"q":2454,"a":2532},"Word often splits text into several runs — when formatting changes, after an edit, or for spell-checking — so {{balance}} may be stored as \"{{bal\" and \"ance}}\". Replacing inside each run misses it. Join the runs' text, find the placeholder there, and write the replacement back into the runs it spans.",{"q":2468,"a":2534},"doc.paragraphs only covers the body text. Also walk every table's cells, and each section's header and footer, and apply the same paragraph replacement to all of them.",{"q":2474,"a":2536},"python-docx is enough for simple placeholders and repeating table rows, as shown here. docxtpl adds Jinja templating — loops, conditions and filters written in the Word document itself — which is worth it when non-programmers maintain complicated templates.","generate word documents from excel rows",null,{"breadcrumb":2540},[2541,2543,2546],{"name":2542,"item":1826},"Home",{"name":2544,"item":2545},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":26,"item":25},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows",{"title":2549,"description":2550},"Generate Word Documents from Excel Data with python-docx","Mail merge Excel rows into Word with python-docx: double-brace placeholder templates, placeholders split across runs, headers, footers and tables, repeating line-item rows, number and date formatting, unfilled-field checks, safe file names and PDF conversion with LibreOffice.","generate-word-documents-from-excel-rows","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows\u002Findex",[2554,2555],"report-layouts","pdf-export","how-to","RL1wsy1dBQIJjB6cHoLuW3-C1m4dlGJMKLt5s8EcH4w",[2559,2563],{"title":2560,"path":2561,"stem":2562,"children":-1},"Generate PowerPoint Slides from Excel Data","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-powerpoint-slides-from-excel-data","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-powerpoint-slides-from-excel-data\u002Findex",{"title":2564,"path":2565,"stem":2566,"children":-1},"Keep Charts and Images When Filling an Excel Template","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template\u002Findex",[2568,2570],{"path":2569,"title":26},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates",{"path":2571,"title":2544},"\u002Fautomating-reporting-workflows",[],[2574,2578,2581,2584,2587,2590],{"path":2575,"title":2576,"kind":2577},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports","Building Finance and Operations Reports in Excel with Python","guide",{"path":2579,"title":2580,"kind":2556},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments","Allocate Shared Costs Across Departments with Python",{"path":2582,"title":2583,"kind":2556},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report","Build a Bank Reconciliation Report with Python and Excel",{"path":2585,"title":2586,"kind":2556},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup","Build a Bill of Materials Cost Rollup",{"path":2588,"title":2589,"kind":2556},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-break-even-analysis-in-excel-with-python","Build a Break-Even Analysis in Excel with Python",{"path":2591,"title":2592,"kind":2593},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report","Build a Budget vs Actual Variance Report in Excel with Python","compare",1790972138154]