[{"data":1,"prerenderedAt":2938},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record":3,"surround:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record":2903,"ancestors:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record":2911,"children:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record":2916,"related:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record":2917},{"id":4,"title":5,"body":6,"dateModified":2869,"datePublished":2869,"description":2870,"extension":2871,"faq":2872,"keyword":2881,"kind":2882,"meta":2883,"navigation":193,"path":2892,"seo":2893,"slug":2896,"stem":2897,"topics":2898,"type":2901,"__hash__":2902},"docs\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record\u002Findex.md","Repeat a Template Block for Each Record in an Excel Report",{"type":7,"value":8,"toc":2857},"minimark",[9,19,121,126,152,163,885,889,896,1342,1360,1364,1375,1567,1570,1574,1577,1906,2126,2132,2425,2429,2432,2581,2589,2702,2706,2717,2721,2778,2782,2788,2792,2798,2809,2815,2821,2825,2854],[10,11,12,13,18],"p",{},"Some reports are not tables but a repeated layout: a statement per customer, a summary card per project, a label per parcel. The designer draws one block in Excel, and the report needs that block once per record, down the sheet, each with its own values. openpyxl has no \"copy range\" function, and a naive cell-by-cell copy loses merged titles, highlight rules and formulas that point at the wrong rows. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002F","Generating Excel Reports from Templates",", builds a stamping function that copies everything the block needs and grows it for records with more line items. The output was checked in LibreOffice for values, styles and page breaks.",[20,21,29,30,29,34,29,38,29,46,29,56,29,67,29,74,29,79,29,84,29,91,29,96,29,100,29,105,29,112,29,117],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"84 0 592 331","img",[25,26],"rb-bands-t","rb-bands-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"The template block, in three bands",[35,36,37],"desc",{"id":26},"The template block is split into three bands. Rows 1 to 3 are the header with the customer name, account and column headings, copied once per record. Row 4 is the item row, copied once for each line item. Row 5 is the total, copied once, with its SUM formula rewritten to cover the item rows actually created.",[39,40],"rect",{"x":41,"y":42,"width":43,"height":44,"fill":45},"84","0","592","331","#ffffff",[39,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"100.0","16.0","560.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,60],"g",{"transform":59},"translate(368.0,29.0) scale(1.000)",[61,62],"path",{"d":63,"fill":64,"stroke":65,"style":66},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[68,69,73],"text",{"x":70,"y":71,"style":72},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Rows 1–3: header",[68,75,78],{"x":70,"y":76,"style":77},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","{customer}, {account}, headings — once per record",[39,80],{"x":48,"y":81,"width":50,"height":51,"rx":52,"fill":82,"stroke":83,"style":55},"119.0","#fdefd8","var(--gold,#b4740a)",[57,85,87],{"transform":86},"translate(368.0,132.0) scale(1.000)",[61,88],{"d":89,"fill":64,"stroke":90,"style":66},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--gold-ink,#7a4e06)",[68,92,95],{"x":70,"y":93,"style":94},"178.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Row 4: item row",[68,97,99],{"x":70,"y":98,"style":77},"198.0","{item} {qty} {price} =B4*C4 — once per item",[39,101],{"x":48,"y":102,"width":50,"height":51,"rx":52,"fill":103,"stroke":104,"style":55},"222.0","#d9f4f1","var(--teal,#0f9488)",[57,106,108],{"transform":107},"translate(368.0,235.0) scale(1.000)",[61,109],{"d":110,"fill":64,"stroke":111,"style":66},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[68,113,116],{"x":70,"y":114,"style":115},"281.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Row 5: total",[68,118,120],{"x":70,"y":119,"style":77},"301.0","=SUM(…) rewritten for the rows created",[122,123,125],"h2",{"id":124},"prerequisites","Prerequisites",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[134,135,136],"code",{"__ignoreMap":132},[137,138,141,145,149],"span",{"class":139,"line":140},"line",1,[137,142,144],{"class":143},"sMTad","pip",[137,146,148],{"class":147},"srMev"," install",[137,150,151],{"class":147}," openpyxl\n",[10,153,154,155,158,159,162],{},"The template is a sheet named ",[134,156,157],{},"Block"," whose placeholders are whole-cell markers such as ",[134,160,161],{},"{customer}",". In practice a designer builds it in Excel; this creates an equivalent one, including a merged, filled title row and a highlight rule on the total:",[127,164,168],{"className":165,"code":166,"language":167,"meta":132,"style":132},"language-python shiki shiki-themes github-light github-dark-high-contrast","import copy\nimport re\n\nfrom openpyxl import Workbook, load_workbook\nfrom openpyxl.styles import Font, PatternFill, Border, Side\nfrom openpyxl.formatting.rule import CellIsRule\nfrom openpyxl.formula.translate import Translator\nfrom openpyxl.worksheet.pagebreak import Break\n\ndef make_template(path=\"statement_template.xlsx\"):\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Block\"\n    thin = Side(style=\"thin\", color=\"FF9AA5B1\")\n    ws[\"A1\"] = \"{customer}\"\n    ws.merge_cells(\"A1:D1\")\n    ws[\"A1\"].font = Font(size=14, bold=True, color=\"FFFFFFFF\")\n    ws[\"A1\"].fill = PatternFill(\"solid\", fgColor=\"FF1F4E79\")\n    ws.row_dimensions[1].height = 24\n    ws.append([\"Account\", \"{account}\", \"Period\", \"{period}\"])\n    ws.append([\"Item\", \"Qty\", \"Price\", \"Amount\"])\n    for cell in ws[3]:\n        cell.font = Font(bold=True)\n        cell.border = Border(bottom=thin)\n    ws.append([\"{item}\", \"{qty}\", \"{price}\", \"=B4*C4\"])                  # the repeating row\n    ws.append([None, None, \"Total\", \"=SUM(D4:D4)\"])\n    for ref in (\"C4\", \"D4\", \"D5\"):\n        ws[ref].number_format = \"#,##0.00\"\n    ws[\"C5\"].font = ws[\"D5\"].font = Font(bold=True)\n    ws[\"D5\"].border = Border(top=thin)\n    ws.conditional_formatting.add(\"D5\", CellIsRule(operator=\"greaterThan\", formula=[\"100\"],\n                                                   fill=PatternFill(\"solid\", fgColor=\"FFFCE4D6\")))\n    for col, width in zip(\"ABCD\", [28, 8, 12, 14]):\n        ws.column_dimensions[col].width = width\n    wb.save(path)\n\nmake_template()\n","python",[134,169,170,180,188,195,209,222,235,248,261,266,288,299,310,321,355,378,389,434,464,481,517,542,563,581,600,642,666,694,705,735,756,791,816,857,868,874,879],{"__ignoreMap":132},[137,171,172,176],{"class":139,"line":140},[137,173,175],{"class":174},"s-kum","import",[137,177,179],{"class":178},"skGVy"," copy\n",[137,181,183,185],{"class":139,"line":182},2,[137,184,175],{"class":174},[137,186,187],{"class":178}," re\n",[137,189,191],{"class":139,"line":190},3,[137,192,194],{"emptyLinePlaceholder":193},true,"\n",[137,196,198,201,204,206],{"class":139,"line":197},4,[137,199,200],{"class":174},"from",[137,202,203],{"class":178}," openpyxl ",[137,205,175],{"class":174},[137,207,208],{"class":178}," Workbook, load_workbook\n",[137,210,212,214,217,219],{"class":139,"line":211},5,[137,213,200],{"class":174},[137,215,216],{"class":178}," openpyxl.styles ",[137,218,175],{"class":174},[137,220,221],{"class":178}," Font, PatternFill, Border, Side\n",[137,223,225,227,230,232],{"class":139,"line":224},6,[137,226,200],{"class":174},[137,228,229],{"class":178}," openpyxl.formatting.rule ",[137,231,175],{"class":174},[137,233,234],{"class":178}," CellIsRule\n",[137,236,238,240,243,245],{"class":139,"line":237},7,[137,239,200],{"class":174},[137,241,242],{"class":178}," openpyxl.formula.translate ",[137,244,175],{"class":174},[137,246,247],{"class":178}," Translator\n",[137,249,251,253,256,258],{"class":139,"line":250},8,[137,252,200],{"class":174},[137,254,255],{"class":178}," openpyxl.worksheet.pagebreak ",[137,257,175],{"class":174},[137,259,260],{"class":178}," Break\n",[137,262,264],{"class":139,"line":263},9,[137,265,194],{"emptyLinePlaceholder":193},[137,267,269,272,276,279,282,285],{"class":139,"line":268},10,[137,270,271],{"class":174},"def",[137,273,275],{"class":274},"s_Opv"," make_template",[137,277,278],{"class":178},"(path",[137,280,281],{"class":174},"=",[137,283,284],{"class":147},"\"statement_template.xlsx\"",[137,286,287],{"class":178},"):\n",[137,289,291,294,296],{"class":139,"line":290},11,[137,292,293],{"class":178},"    wb ",[137,295,281],{"class":174},[137,297,298],{"class":178}," Workbook()\n",[137,300,302,305,307],{"class":139,"line":301},12,[137,303,304],{"class":178},"    ws ",[137,306,281],{"class":174},[137,308,309],{"class":178}," wb.active\n",[137,311,313,316,318],{"class":139,"line":312},13,[137,314,315],{"class":178},"    ws.title ",[137,317,281],{"class":174},[137,319,320],{"class":147}," \"Block\"\n",[137,322,324,327,329,332,336,338,341,344,347,349,352],{"class":139,"line":323},14,[137,325,326],{"class":178},"    thin ",[137,328,281],{"class":174},[137,330,331],{"class":178}," Side(",[137,333,335],{"class":334},"sa561","style",[137,337,281],{"class":174},[137,339,340],{"class":147},"\"thin\"",[137,342,343],{"class":178},", ",[137,345,346],{"class":334},"color",[137,348,281],{"class":174},[137,350,351],{"class":147},"\"FF9AA5B1\"",[137,353,354],{"class":178},")\n",[137,356,358,361,364,367,369,372,375],{"class":139,"line":357},15,[137,359,360],{"class":178},"    ws[",[137,362,363],{"class":147},"\"A1\"",[137,365,366],{"class":178},"] ",[137,368,281],{"class":174},[137,370,371],{"class":147}," \"",[137,373,161],{"class":374},"sSjpA",[137,376,377],{"class":147},"\"\n",[137,379,381,384,387],{"class":139,"line":380},16,[137,382,383],{"class":178},"    ws.merge_cells(",[137,385,386],{"class":147},"\"A1:D1\"",[137,388,354],{"class":178},[137,390,392,394,396,399,401,404,407,409,413,415,418,420,423,425,427,429,432],{"class":139,"line":391},17,[137,393,360],{"class":178},[137,395,363],{"class":147},[137,397,398],{"class":178},"].font ",[137,400,281],{"class":174},[137,402,403],{"class":178}," Font(",[137,405,406],{"class":334},"size",[137,408,281],{"class":174},[137,410,412],{"class":411},"sP0c6","14",[137,414,343],{"class":178},[137,416,417],{"class":334},"bold",[137,419,281],{"class":174},[137,421,422],{"class":411},"True",[137,424,343],{"class":178},[137,426,346],{"class":334},[137,428,281],{"class":174},[137,430,431],{"class":147},"\"FFFFFFFF\"",[137,433,354],{"class":178},[137,435,437,439,441,444,446,449,452,454,457,459,462],{"class":139,"line":436},18,[137,438,360],{"class":178},[137,440,363],{"class":147},[137,442,443],{"class":178},"].fill ",[137,445,281],{"class":174},[137,447,448],{"class":178}," PatternFill(",[137,450,451],{"class":147},"\"solid\"",[137,453,343],{"class":178},[137,455,456],{"class":334},"fgColor",[137,458,281],{"class":174},[137,460,461],{"class":147},"\"FF1F4E79\"",[137,463,354],{"class":178},[137,465,467,470,473,476,478],{"class":139,"line":466},19,[137,468,469],{"class":178},"    ws.row_dimensions[",[137,471,472],{"class":411},"1",[137,474,475],{"class":178},"].height ",[137,477,281],{"class":174},[137,479,480],{"class":411}," 24\n",[137,482,484,487,490,492,495,498,500,502,505,507,509,512,514],{"class":139,"line":483},20,[137,485,486],{"class":178},"    ws.append([",[137,488,489],{"class":147},"\"Account\"",[137,491,343],{"class":178},[137,493,494],{"class":147},"\"",[137,496,497],{"class":374},"{account}",[137,499,494],{"class":147},[137,501,343],{"class":178},[137,503,504],{"class":147},"\"Period\"",[137,506,343],{"class":178},[137,508,494],{"class":147},[137,510,511],{"class":374},"{period}",[137,513,494],{"class":147},[137,515,516],{"class":178},"])\n",[137,518,520,522,525,527,530,532,535,537,540],{"class":139,"line":519},21,[137,521,486],{"class":178},[137,523,524],{"class":147},"\"Item\"",[137,526,343],{"class":178},[137,528,529],{"class":147},"\"Qty\"",[137,531,343],{"class":178},[137,533,534],{"class":147},"\"Price\"",[137,536,343],{"class":178},[137,538,539],{"class":147},"\"Amount\"",[137,541,516],{"class":178},[137,543,545,548,551,554,557,560],{"class":139,"line":544},22,[137,546,547],{"class":174},"    for",[137,549,550],{"class":178}," cell ",[137,552,553],{"class":174},"in",[137,555,556],{"class":178}," ws[",[137,558,559],{"class":411},"3",[137,561,562],{"class":178},"]:\n",[137,564,566,569,571,573,575,577,579],{"class":139,"line":565},23,[137,567,568],{"class":178},"        cell.font ",[137,570,281],{"class":174},[137,572,403],{"class":178},[137,574,417],{"class":334},[137,576,281],{"class":174},[137,578,422],{"class":411},[137,580,354],{"class":178},[137,582,584,587,589,592,595,597],{"class":139,"line":583},24,[137,585,586],{"class":178},"        cell.border ",[137,588,281],{"class":174},[137,590,591],{"class":178}," Border(",[137,593,594],{"class":334},"bottom",[137,596,281],{"class":174},[137,598,599],{"class":178},"thin)\n",[137,601,603,605,607,610,612,614,616,619,621,623,625,628,630,632,635,638],{"class":139,"line":602},25,[137,604,486],{"class":178},[137,606,494],{"class":147},[137,608,609],{"class":374},"{item}",[137,611,494],{"class":147},[137,613,343],{"class":178},[137,615,494],{"class":147},[137,617,618],{"class":374},"{qty}",[137,620,494],{"class":147},[137,622,343],{"class":178},[137,624,494],{"class":147},[137,626,627],{"class":374},"{price}",[137,629,494],{"class":147},[137,631,343],{"class":178},[137,633,634],{"class":147},"\"=B4*C4\"",[137,636,637],{"class":178},"])                  ",[137,639,641],{"class":640},"s-wDw","# the repeating row\n",[137,643,645,647,650,652,654,656,659,661,664],{"class":139,"line":644},26,[137,646,486],{"class":178},[137,648,649],{"class":411},"None",[137,651,343],{"class":178},[137,653,649],{"class":411},[137,655,343],{"class":178},[137,657,658],{"class":147},"\"Total\"",[137,660,343],{"class":178},[137,662,663],{"class":147},"\"=SUM(D4:D4)\"",[137,665,516],{"class":178},[137,667,669,671,674,676,679,682,684,687,689,692],{"class":139,"line":668},27,[137,670,547],{"class":174},[137,672,673],{"class":178}," ref ",[137,675,553],{"class":174},[137,677,678],{"class":178}," (",[137,680,681],{"class":147},"\"C4\"",[137,683,343],{"class":178},[137,685,686],{"class":147},"\"D4\"",[137,688,343],{"class":178},[137,690,691],{"class":147},"\"D5\"",[137,693,287],{"class":178},[137,695,697,700,702],{"class":139,"line":696},28,[137,698,699],{"class":178},"        ws[ref].number_format ",[137,701,281],{"class":174},[137,703,704],{"class":147}," \"#,##0.00\"\n",[137,706,708,710,713,715,717,719,721,723,725,727,729,731,733],{"class":139,"line":707},29,[137,709,360],{"class":178},[137,711,712],{"class":147},"\"C5\"",[137,714,398],{"class":178},[137,716,281],{"class":174},[137,718,556],{"class":178},[137,720,691],{"class":147},[137,722,398],{"class":178},[137,724,281],{"class":174},[137,726,403],{"class":178},[137,728,417],{"class":334},[137,730,281],{"class":174},[137,732,422],{"class":411},[137,734,354],{"class":178},[137,736,738,740,742,745,747,749,752,754],{"class":139,"line":737},30,[137,739,360],{"class":178},[137,741,691],{"class":147},[137,743,744],{"class":178},"].border ",[137,746,281],{"class":174},[137,748,591],{"class":178},[137,750,751],{"class":334},"top",[137,753,281],{"class":174},[137,755,599],{"class":178},[137,757,759,762,764,767,770,772,775,777,780,782,785,788],{"class":139,"line":758},31,[137,760,761],{"class":178},"    ws.conditional_formatting.add(",[137,763,691],{"class":147},[137,765,766],{"class":178},", CellIsRule(",[137,768,769],{"class":334},"operator",[137,771,281],{"class":174},[137,773,774],{"class":147},"\"greaterThan\"",[137,776,343],{"class":178},[137,778,779],{"class":334},"formula",[137,781,281],{"class":174},[137,783,784],{"class":178},"[",[137,786,787],{"class":147},"\"100\"",[137,789,790],{"class":178},"],\n",[137,792,794,797,799,802,804,806,808,810,813],{"class":139,"line":793},32,[137,795,796],{"class":334},"                                                   fill",[137,798,281],{"class":174},[137,800,801],{"class":178},"PatternFill(",[137,803,451],{"class":147},[137,805,343],{"class":178},[137,807,456],{"class":334},[137,809,281],{"class":174},[137,811,812],{"class":147},"\"FFFCE4D6\"",[137,814,815],{"class":178},")))\n",[137,817,819,821,824,826,829,832,835,838,841,843,846,848,850,852,854],{"class":139,"line":818},33,[137,820,547],{"class":174},[137,822,823],{"class":178}," col, width ",[137,825,553],{"class":174},[137,827,828],{"class":411}," zip",[137,830,831],{"class":178},"(",[137,833,834],{"class":147},"\"ABCD\"",[137,836,837],{"class":178},", [",[137,839,840],{"class":411},"28",[137,842,343],{"class":178},[137,844,845],{"class":411},"8",[137,847,343],{"class":178},[137,849,52],{"class":411},[137,851,343],{"class":178},[137,853,412],{"class":411},[137,855,856],{"class":178},"]):\n",[137,858,860,863,865],{"class":139,"line":859},34,[137,861,862],{"class":178},"        ws.column_dimensions[col].width ",[137,864,281],{"class":174},[137,866,867],{"class":178}," width\n",[137,869,871],{"class":139,"line":870},35,[137,872,873],{"class":178},"    wb.save(path)\n",[137,875,877],{"class":139,"line":876},36,[137,878,194],{"emptyLinePlaceholder":193},[137,880,882],{"class":139,"line":881},37,[137,883,884],{"class":178},"make_template()\n",[122,886,888],{"id":887},"step-1-copy-a-band-of-rows-with-everything-attached","Step 1 — Copy a band of rows with everything attached",[10,890,891,892,895],{},"A band copy has five parts: values, styles, row heights, merged ranges and conditional formatting. Placeholders are replaced with typed values from the record, and formulas are moved to their new position with ",[134,893,894],{},"Translator",", which adjusts relative references the same way Excel does when you copy a cell:",[127,897,899],{"className":165,"code":898,"language":167,"meta":132,"style":132},"PLACEHOLDER = re.compile(r\"^\\{(\\w+)\\}$\")\n\ndef copy_rows(src, first, last, dst, top, values):\n    offset = top - first\n    for row in src.iter_rows(min_row=first, max_row=last):\n        for cell in row:\n            target = dst.cell(cell.row + offset, cell.column)\n            value = cell.value\n            if isinstance(value, str) and (m := PLACEHOLDER.match(value)):\n                value = values[m.group(1)]\n            elif isinstance(value, str) and value.startswith(\"=\"):\n                value = Translator(value, origin=cell.coordinate).translate_formula(target.coordinate)\n            target.value = value\n            if cell.has_style:\n                target._style = copy.copy(cell._style)\n    for r in range(first, last + 1):\n        if src.row_dimensions[r].height:\n            dst.row_dimensions[r + offset].height = src.row_dimensions[r].height\n    for rng in src.merged_cells.ranges:\n        if first \u003C= rng.min_row and rng.max_row \u003C= last:\n            moved = copy.copy(rng); moved.shift(0, offset)\n            dst.merge_cells(moved.coord)\n    for cf in src.conditional_formatting:\n        for rng in cf.sqref.ranges:\n            if first \u003C= rng.min_row and rng.max_row \u003C= last:\n                moved = copy.copy(rng); moved.shift(0, offset)\n                for rule in cf.rules:\n                    dst.conditional_formatting.add(moved.coord, copy.copy(rule))\n    return last + offset + 1\n",[134,900,901,943,947,957,973,1001,1013,1028,1038,1070,1085,1108,1125,1135,1142,1152,1174,1182,1197,1209,1232,1247,1252,1264,1275,1293,1306,1319,1324],{"__ignoreMap":132},[137,902,903,906,909,912,915,917,920,924,927,930,933,936,939,941],{"class":139,"line":140},[137,904,905],{"class":411},"PLACEHOLDER",[137,907,908],{"class":174}," =",[137,910,911],{"class":178}," re.compile(",[137,913,914],{"class":174},"r",[137,916,494],{"class":147},[137,918,919],{"class":411},"^",[137,921,923],{"class":922},"s_b0D","\\{",[137,925,926],{"class":411},"(\\w",[137,928,929],{"class":174},"+",[137,931,932],{"class":411},")",[137,934,935],{"class":922},"\\}",[137,937,938],{"class":411},"$",[137,940,494],{"class":147},[137,942,354],{"class":178},[137,944,945],{"class":139,"line":182},[137,946,194],{"emptyLinePlaceholder":193},[137,948,949,951,954],{"class":139,"line":190},[137,950,271],{"class":174},[137,952,953],{"class":274}," copy_rows",[137,955,956],{"class":178},"(src, first, last, dst, top, values):\n",[137,958,959,962,964,967,970],{"class":139,"line":197},[137,960,961],{"class":178},"    offset ",[137,963,281],{"class":174},[137,965,966],{"class":178}," top ",[137,968,969],{"class":174},"-",[137,971,972],{"class":178}," first\n",[137,974,975,977,980,982,985,988,990,993,996,998],{"class":139,"line":211},[137,976,547],{"class":174},[137,978,979],{"class":178}," row ",[137,981,553],{"class":174},[137,983,984],{"class":178}," src.iter_rows(",[137,986,987],{"class":334},"min_row",[137,989,281],{"class":174},[137,991,992],{"class":178},"first, ",[137,994,995],{"class":334},"max_row",[137,997,281],{"class":174},[137,999,1000],{"class":178},"last):\n",[137,1002,1003,1006,1008,1010],{"class":139,"line":224},[137,1004,1005],{"class":174},"        for",[137,1007,550],{"class":178},[137,1009,553],{"class":174},[137,1011,1012],{"class":178}," row:\n",[137,1014,1015,1018,1020,1023,1025],{"class":139,"line":237},[137,1016,1017],{"class":178},"            target ",[137,1019,281],{"class":174},[137,1021,1022],{"class":178}," dst.cell(cell.row ",[137,1024,929],{"class":174},[137,1026,1027],{"class":178}," offset, cell.column)\n",[137,1029,1030,1033,1035],{"class":139,"line":250},[137,1031,1032],{"class":178},"            value ",[137,1034,281],{"class":174},[137,1036,1037],{"class":178}," cell.value\n",[137,1039,1040,1043,1046,1049,1052,1055,1058,1061,1064,1067],{"class":139,"line":263},[137,1041,1042],{"class":174},"            if",[137,1044,1045],{"class":411}," isinstance",[137,1047,1048],{"class":178},"(value, ",[137,1050,1051],{"class":411},"str",[137,1053,1054],{"class":178},") ",[137,1056,1057],{"class":174},"and",[137,1059,1060],{"class":178}," (m ",[137,1062,1063],{"class":174},":=",[137,1065,1066],{"class":411}," PLACEHOLDER",[137,1068,1069],{"class":178},".match(value)):\n",[137,1071,1072,1075,1077,1080,1082],{"class":139,"line":268},[137,1073,1074],{"class":178},"                value ",[137,1076,281],{"class":174},[137,1078,1079],{"class":178}," values[m.group(",[137,1081,472],{"class":411},[137,1083,1084],{"class":178},")]\n",[137,1086,1087,1090,1092,1094,1096,1098,1100,1103,1106],{"class":139,"line":290},[137,1088,1089],{"class":174},"            elif",[137,1091,1045],{"class":411},[137,1093,1048],{"class":178},[137,1095,1051],{"class":411},[137,1097,1054],{"class":178},[137,1099,1057],{"class":174},[137,1101,1102],{"class":178}," value.startswith(",[137,1104,1105],{"class":147},"\"=\"",[137,1107,287],{"class":178},[137,1109,1110,1112,1114,1117,1120,1122],{"class":139,"line":301},[137,1111,1074],{"class":178},[137,1113,281],{"class":174},[137,1115,1116],{"class":178}," Translator(value, ",[137,1118,1119],{"class":334},"origin",[137,1121,281],{"class":174},[137,1123,1124],{"class":178},"cell.coordinate).translate_formula(target.coordinate)\n",[137,1126,1127,1130,1132],{"class":139,"line":312},[137,1128,1129],{"class":178},"            target.value ",[137,1131,281],{"class":174},[137,1133,1134],{"class":178}," value\n",[137,1136,1137,1139],{"class":139,"line":323},[137,1138,1042],{"class":174},[137,1140,1141],{"class":178}," cell.has_style:\n",[137,1143,1144,1147,1149],{"class":139,"line":357},[137,1145,1146],{"class":178},"                target._style ",[137,1148,281],{"class":174},[137,1150,1151],{"class":178}," copy.copy(cell._style)\n",[137,1153,1154,1156,1159,1161,1164,1167,1169,1172],{"class":139,"line":380},[137,1155,547],{"class":174},[137,1157,1158],{"class":178}," r ",[137,1160,553],{"class":174},[137,1162,1163],{"class":411}," range",[137,1165,1166],{"class":178},"(first, last ",[137,1168,929],{"class":174},[137,1170,1171],{"class":411}," 1",[137,1173,287],{"class":178},[137,1175,1176,1179],{"class":139,"line":391},[137,1177,1178],{"class":174},"        if",[137,1180,1181],{"class":178}," src.row_dimensions[r].height:\n",[137,1183,1184,1187,1189,1192,1194],{"class":139,"line":436},[137,1185,1186],{"class":178},"            dst.row_dimensions[r ",[137,1188,929],{"class":174},[137,1190,1191],{"class":178}," offset].height ",[137,1193,281],{"class":174},[137,1195,1196],{"class":178}," src.row_dimensions[r].height\n",[137,1198,1199,1201,1204,1206],{"class":139,"line":466},[137,1200,547],{"class":174},[137,1202,1203],{"class":178}," rng ",[137,1205,553],{"class":174},[137,1207,1208],{"class":178}," src.merged_cells.ranges:\n",[137,1210,1211,1213,1216,1219,1222,1224,1227,1229],{"class":139,"line":483},[137,1212,1178],{"class":174},[137,1214,1215],{"class":178}," first ",[137,1217,1218],{"class":174},"\u003C=",[137,1220,1221],{"class":178}," rng.min_row ",[137,1223,1057],{"class":174},[137,1225,1226],{"class":178}," rng.max_row ",[137,1228,1218],{"class":174},[137,1230,1231],{"class":178}," last:\n",[137,1233,1234,1237,1239,1242,1244],{"class":139,"line":519},[137,1235,1236],{"class":178},"            moved ",[137,1238,281],{"class":174},[137,1240,1241],{"class":178}," copy.copy(rng); moved.shift(",[137,1243,42],{"class":411},[137,1245,1246],{"class":178},", offset)\n",[137,1248,1249],{"class":139,"line":544},[137,1250,1251],{"class":178},"            dst.merge_cells(moved.coord)\n",[137,1253,1254,1256,1259,1261],{"class":139,"line":565},[137,1255,547],{"class":174},[137,1257,1258],{"class":178}," cf ",[137,1260,553],{"class":174},[137,1262,1263],{"class":178}," src.conditional_formatting:\n",[137,1265,1266,1268,1270,1272],{"class":139,"line":583},[137,1267,1005],{"class":174},[137,1269,1203],{"class":178},[137,1271,553],{"class":174},[137,1273,1274],{"class":178}," cf.sqref.ranges:\n",[137,1276,1277,1279,1281,1283,1285,1287,1289,1291],{"class":139,"line":602},[137,1278,1042],{"class":174},[137,1280,1215],{"class":178},[137,1282,1218],{"class":174},[137,1284,1221],{"class":178},[137,1286,1057],{"class":174},[137,1288,1226],{"class":178},[137,1290,1218],{"class":174},[137,1292,1231],{"class":178},[137,1294,1295,1298,1300,1302,1304],{"class":139,"line":644},[137,1296,1297],{"class":178},"                moved ",[137,1299,281],{"class":174},[137,1301,1241],{"class":178},[137,1303,42],{"class":411},[137,1305,1246],{"class":178},[137,1307,1308,1311,1314,1316],{"class":139,"line":668},[137,1309,1310],{"class":174},"                for",[137,1312,1313],{"class":178}," rule ",[137,1315,553],{"class":174},[137,1317,1318],{"class":178}," cf.rules:\n",[137,1320,1321],{"class":139,"line":696},[137,1322,1323],{"class":178},"                    dst.conditional_formatting.add(moved.coord, copy.copy(rule))\n",[137,1325,1326,1329,1332,1334,1337,1339],{"class":139,"line":707},[137,1327,1328],{"class":174},"    return",[137,1330,1331],{"class":178}," last ",[137,1333,929],{"class":174},[137,1335,1336],{"class":178}," offset ",[137,1338,929],{"class":174},[137,1340,1341],{"class":411}," 1\n",[10,1343,1344,1347,1348,1350,1351,1354,1355,1359],{},[134,1345,1346],{},"cell._style"," is an index into the workbook's shared style table, so copying it is cheap and exact — but only within the same workbook. That is why the output sheet is created inside the template workbook and the ",[134,1349,157],{}," sheet is removed at the end; copying ",[134,1352,1353],{},"_style"," into a different workbook points at the wrong styles. Replacing whole-cell placeholders with typed values, rather than substituting text, is covered in ",[14,1356,1358],{"href":1357},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python\u002F","Replace Placeholders in an Excel Template with Python",".",[122,1361,1363],{"id":1362},"step-2-grow-the-block-for-each-records-items","Step 2 — Grow the block for each record's items",[10,1365,1366,1367,1370,1371,1374],{},"Records rarely have the same number of lines. Copy the header band once, the item row once per item, and the total band once — then rewrite the total's ",[134,1368,1369],{},"SUM"," to cover exactly the item rows created, because a translated ",[134,1372,1373],{},"=SUM(D4:D4)"," would only ever cover one row:",[127,1376,1378],{"className":165,"code":1377,"language":167,"meta":132,"style":132},"def stamp_record(src, dst, top, record):\n    top = copy_rows(src, 1, 3, dst, top, record)                        # header\n    first_item = top\n    for item in record[\"items\"]:\n        top = copy_rows(src, 4, 4, dst, top, item)                      # one row per item\n    total_row = top\n    top = copy_rows(src, 5, 5, dst, top, record)                        # total\n    dst[f\"D{total_row}\"] = f\"=SUM(D{first_item}:D{total_row - 1})\" if record[\"items\"] else 0\n    return top\n",[134,1379,1380,1390,1412,1422,1439,1461,1470,1490,1561],{"__ignoreMap":132},[137,1381,1382,1384,1387],{"class":139,"line":140},[137,1383,271],{"class":174},[137,1385,1386],{"class":274}," stamp_record",[137,1388,1389],{"class":178},"(src, dst, top, record):\n",[137,1391,1392,1395,1397,1400,1402,1404,1406,1409],{"class":139,"line":182},[137,1393,1394],{"class":178},"    top ",[137,1396,281],{"class":174},[137,1398,1399],{"class":178}," copy_rows(src, ",[137,1401,472],{"class":411},[137,1403,343],{"class":178},[137,1405,559],{"class":411},[137,1407,1408],{"class":178},", dst, top, record)                        ",[137,1410,1411],{"class":640},"# header\n",[137,1413,1414,1417,1419],{"class":139,"line":190},[137,1415,1416],{"class":178},"    first_item ",[137,1418,281],{"class":174},[137,1420,1421],{"class":178}," top\n",[137,1423,1424,1426,1429,1431,1434,1437],{"class":139,"line":197},[137,1425,547],{"class":174},[137,1427,1428],{"class":178}," item ",[137,1430,553],{"class":174},[137,1432,1433],{"class":178}," record[",[137,1435,1436],{"class":147},"\"items\"",[137,1438,562],{"class":178},[137,1440,1441,1444,1446,1448,1451,1453,1455,1458],{"class":139,"line":211},[137,1442,1443],{"class":178},"        top ",[137,1445,281],{"class":174},[137,1447,1399],{"class":178},[137,1449,1450],{"class":411},"4",[137,1452,343],{"class":178},[137,1454,1450],{"class":411},[137,1456,1457],{"class":178},", dst, top, item)                      ",[137,1459,1460],{"class":640},"# one row per item\n",[137,1462,1463,1466,1468],{"class":139,"line":224},[137,1464,1465],{"class":178},"    total_row ",[137,1467,281],{"class":174},[137,1469,1421],{"class":178},[137,1471,1472,1474,1476,1478,1481,1483,1485,1487],{"class":139,"line":237},[137,1473,1394],{"class":178},[137,1475,281],{"class":174},[137,1477,1399],{"class":178},[137,1479,1480],{"class":411},"5",[137,1482,343],{"class":178},[137,1484,1480],{"class":411},[137,1486,1408],{"class":178},[137,1488,1489],{"class":640},"# total\n",[137,1491,1492,1495,1498,1501,1504,1507,1510,1512,1514,1516,1519,1522,1524,1527,1529,1532,1534,1537,1539,1541,1543,1546,1549,1551,1553,1555,1558],{"class":139,"line":250},[137,1493,1494],{"class":178},"    dst[",[137,1496,1497],{"class":174},"f",[137,1499,1500],{"class":147},"\"D",[137,1502,1503],{"class":374},"{",[137,1505,1506],{"class":178},"total_row",[137,1508,1509],{"class":374},"}",[137,1511,494],{"class":147},[137,1513,366],{"class":178},[137,1515,281],{"class":174},[137,1517,1518],{"class":174}," f",[137,1520,1521],{"class":147},"\"=SUM(D",[137,1523,1503],{"class":374},[137,1525,1526],{"class":178},"first_item",[137,1528,1509],{"class":374},[137,1530,1531],{"class":147},":D",[137,1533,1503],{"class":374},[137,1535,1536],{"class":178},"total_row ",[137,1538,969],{"class":174},[137,1540,1171],{"class":411},[137,1542,1509],{"class":374},[137,1544,1545],{"class":147},")\"",[137,1547,1548],{"class":174}," if",[137,1550,1433],{"class":178},[137,1552,1436],{"class":147},[137,1554,366],{"class":178},[137,1556,1557],{"class":174},"else",[137,1559,1560],{"class":411}," 0\n",[137,1562,1563,1565],{"class":139,"line":263},[137,1564,1328],{"class":174},[137,1566,1421],{"class":178},[10,1568,1569],{},"With no items the range would be backwards, so the total is written as 0 instead. The same idea extends to any band structure: a sub-header per group, a notes row that appears only when a record has notes.",[122,1571,1573],{"id":1572},"step-3-stamp-every-record-one-per-printed-page","Step 3 — Stamp every record, one per printed page",[10,1575,1576],{},"The records here are three customers with two, one and four items. After each record, a row break starts the next one on a new page, and a blank row separates them on screen:",[127,1578,1580],{"className":165,"code":1579,"language":167,"meta":132,"style":132},"records = [\n    {\"customer\": \"Harbour Stationers Ltd\", \"account\": \"C-1041\", \"period\": \"Sep 2026\",\n     \"items\": [{\"item\": \"Paper A4\", \"qty\": 3, \"price\": 4.13}, {\"item\": \"Toner\", \"qty\": 1, \"price\": 38.43}]},\n    {\"customer\": \"Northfield Primary\", \"account\": \"C-1042\", \"period\": \"Sep 2026\",\n     \"items\": [{\"item\": \"Labels\", \"qty\": 7, \"price\": 2.37}]},\n    {\"customer\": \"Quayside Dental\", \"account\": \"C-1043\", \"period\": \"Sep 2026\",\n     \"items\": [{\"item\": \"Stapler\", \"qty\": 1, \"price\": 7.99}, {\"item\": \"Paper A4\", \"qty\": 30, \"price\": 4.13},\n               {\"item\": \"Pens\", \"qty\": 2, \"price\": 3.07}, {\"item\": \"Labels\", \"qty\": 4, \"price\": 2.37}]},\n]\n",[134,1581,1582,1592,1629,1694,1724,1757,1787,1845,1901],{"__ignoreMap":132},[137,1583,1584,1587,1589],{"class":139,"line":140},[137,1585,1586],{"class":178},"records ",[137,1588,281],{"class":174},[137,1590,1591],{"class":178}," [\n",[137,1593,1594,1597,1600,1603,1606,1608,1611,1613,1616,1618,1621,1623,1626],{"class":139,"line":182},[137,1595,1596],{"class":178},"    {",[137,1598,1599],{"class":147},"\"customer\"",[137,1601,1602],{"class":178},": ",[137,1604,1605],{"class":147},"\"Harbour Stationers Ltd\"",[137,1607,343],{"class":178},[137,1609,1610],{"class":147},"\"account\"",[137,1612,1602],{"class":178},[137,1614,1615],{"class":147},"\"C-1041\"",[137,1617,343],{"class":178},[137,1619,1620],{"class":147},"\"period\"",[137,1622,1602],{"class":178},[137,1624,1625],{"class":147},"\"Sep 2026\"",[137,1627,1628],{"class":178},",\n",[137,1630,1631,1634,1637,1640,1642,1645,1647,1650,1652,1654,1656,1659,1661,1664,1667,1669,1671,1674,1676,1678,1680,1682,1684,1686,1688,1691],{"class":139,"line":190},[137,1632,1633],{"class":147},"     \"items\"",[137,1635,1636],{"class":178},": [{",[137,1638,1639],{"class":147},"\"item\"",[137,1641,1602],{"class":178},[137,1643,1644],{"class":147},"\"Paper A4\"",[137,1646,343],{"class":178},[137,1648,1649],{"class":147},"\"qty\"",[137,1651,1602],{"class":178},[137,1653,559],{"class":411},[137,1655,343],{"class":178},[137,1657,1658],{"class":147},"\"price\"",[137,1660,1602],{"class":178},[137,1662,1663],{"class":411},"4.13",[137,1665,1666],{"class":178},"}, {",[137,1668,1639],{"class":147},[137,1670,1602],{"class":178},[137,1672,1673],{"class":147},"\"Toner\"",[137,1675,343],{"class":178},[137,1677,1649],{"class":147},[137,1679,1602],{"class":178},[137,1681,472],{"class":411},[137,1683,343],{"class":178},[137,1685,1658],{"class":147},[137,1687,1602],{"class":178},[137,1689,1690],{"class":411},"38.43",[137,1692,1693],{"class":178},"}]},\n",[137,1695,1696,1698,1700,1702,1705,1707,1709,1711,1714,1716,1718,1720,1722],{"class":139,"line":197},[137,1697,1596],{"class":178},[137,1699,1599],{"class":147},[137,1701,1602],{"class":178},[137,1703,1704],{"class":147},"\"Northfield Primary\"",[137,1706,343],{"class":178},[137,1708,1610],{"class":147},[137,1710,1602],{"class":178},[137,1712,1713],{"class":147},"\"C-1042\"",[137,1715,343],{"class":178},[137,1717,1620],{"class":147},[137,1719,1602],{"class":178},[137,1721,1625],{"class":147},[137,1723,1628],{"class":178},[137,1725,1726,1728,1730,1732,1734,1737,1739,1741,1743,1746,1748,1750,1752,1755],{"class":139,"line":211},[137,1727,1633],{"class":147},[137,1729,1636],{"class":178},[137,1731,1639],{"class":147},[137,1733,1602],{"class":178},[137,1735,1736],{"class":147},"\"Labels\"",[137,1738,343],{"class":178},[137,1740,1649],{"class":147},[137,1742,1602],{"class":178},[137,1744,1745],{"class":411},"7",[137,1747,343],{"class":178},[137,1749,1658],{"class":147},[137,1751,1602],{"class":178},[137,1753,1754],{"class":411},"2.37",[137,1756,1693],{"class":178},[137,1758,1759,1761,1763,1765,1768,1770,1772,1774,1777,1779,1781,1783,1785],{"class":139,"line":224},[137,1760,1596],{"class":178},[137,1762,1599],{"class":147},[137,1764,1602],{"class":178},[137,1766,1767],{"class":147},"\"Quayside Dental\"",[137,1769,343],{"class":178},[137,1771,1610],{"class":147},[137,1773,1602],{"class":178},[137,1775,1776],{"class":147},"\"C-1043\"",[137,1778,343],{"class":178},[137,1780,1620],{"class":147},[137,1782,1602],{"class":178},[137,1784,1625],{"class":147},[137,1786,1628],{"class":178},[137,1788,1789,1791,1793,1795,1797,1800,1802,1804,1806,1808,1810,1812,1814,1817,1819,1821,1823,1825,1827,1829,1831,1834,1836,1838,1840,1842],{"class":139,"line":237},[137,1790,1633],{"class":147},[137,1792,1636],{"class":178},[137,1794,1639],{"class":147},[137,1796,1602],{"class":178},[137,1798,1799],{"class":147},"\"Stapler\"",[137,1801,343],{"class":178},[137,1803,1649],{"class":147},[137,1805,1602],{"class":178},[137,1807,472],{"class":411},[137,1809,343],{"class":178},[137,1811,1658],{"class":147},[137,1813,1602],{"class":178},[137,1815,1816],{"class":411},"7.99",[137,1818,1666],{"class":178},[137,1820,1639],{"class":147},[137,1822,1602],{"class":178},[137,1824,1644],{"class":147},[137,1826,343],{"class":178},[137,1828,1649],{"class":147},[137,1830,1602],{"class":178},[137,1832,1833],{"class":411},"30",[137,1835,343],{"class":178},[137,1837,1658],{"class":147},[137,1839,1602],{"class":178},[137,1841,1663],{"class":411},[137,1843,1844],{"class":178},"},\n",[137,1846,1847,1850,1852,1854,1857,1859,1861,1863,1866,1868,1870,1872,1875,1877,1879,1881,1883,1885,1887,1889,1891,1893,1895,1897,1899],{"class":139,"line":250},[137,1848,1849],{"class":178},"               {",[137,1851,1639],{"class":147},[137,1853,1602],{"class":178},[137,1855,1856],{"class":147},"\"Pens\"",[137,1858,343],{"class":178},[137,1860,1649],{"class":147},[137,1862,1602],{"class":178},[137,1864,1865],{"class":411},"2",[137,1867,343],{"class":178},[137,1869,1658],{"class":147},[137,1871,1602],{"class":178},[137,1873,1874],{"class":411},"3.07",[137,1876,1666],{"class":178},[137,1878,1639],{"class":147},[137,1880,1602],{"class":178},[137,1882,1736],{"class":147},[137,1884,343],{"class":178},[137,1886,1649],{"class":147},[137,1888,1602],{"class":178},[137,1890,1450],{"class":411},[137,1892,343],{"class":178},[137,1894,1658],{"class":147},[137,1896,1602],{"class":178},[137,1898,1754],{"class":411},[137,1900,1693],{"class":178},[137,1902,1903],{"class":139,"line":263},[137,1904,1905],{"class":178},"]\n",[127,1907,1909],{"className":165,"code":1908,"language":167,"meta":132,"style":132},"wb = load_workbook(\"statement_template.xlsx\")\nblock = wb[\"Block\"]\nout = wb.create_sheet(\"Statements\")\nfor col, dim in block.column_dimensions.items():\n    out.column_dimensions[col].width = dim.width\ntop = 1\nfor i, record in enumerate(records):\n    top = stamp_record(block, out, top, record)\n    if i \u003C len(records) - 1:\n        out.row_breaks.append(Break(id=top - 1))                       # new page per record\n        top += 1                                                        # blank spacer row\nwb.remove(block)\nwb.save(\"statements.xlsx\")\nprint([b.id for b in out.row_breaks.brk], out.max_row, sorted(str(r) for r in out.merged_cells.ranges))\n# [6, 12] 21 ['A14:D14', 'A1:D1', 'A8:D8']\n",[134,1910,1911,1925,1940,1955,1968,1978,1987,2002,2011,2035,2057,2069,2074,2084,2121],{"__ignoreMap":132},[137,1912,1913,1916,1918,1921,1923],{"class":139,"line":140},[137,1914,1915],{"class":178},"wb ",[137,1917,281],{"class":174},[137,1919,1920],{"class":178}," load_workbook(",[137,1922,284],{"class":147},[137,1924,354],{"class":178},[137,1926,1927,1930,1932,1935,1938],{"class":139,"line":182},[137,1928,1929],{"class":178},"block ",[137,1931,281],{"class":174},[137,1933,1934],{"class":178}," wb[",[137,1936,1937],{"class":147},"\"Block\"",[137,1939,1905],{"class":178},[137,1941,1942,1945,1947,1950,1953],{"class":139,"line":190},[137,1943,1944],{"class":178},"out ",[137,1946,281],{"class":174},[137,1948,1949],{"class":178}," wb.create_sheet(",[137,1951,1952],{"class":147},"\"Statements\"",[137,1954,354],{"class":178},[137,1956,1957,1960,1963,1965],{"class":139,"line":197},[137,1958,1959],{"class":174},"for",[137,1961,1962],{"class":178}," col, dim ",[137,1964,553],{"class":174},[137,1966,1967],{"class":178}," block.column_dimensions.items():\n",[137,1969,1970,1973,1975],{"class":139,"line":211},[137,1971,1972],{"class":178},"    out.column_dimensions[col].width ",[137,1974,281],{"class":174},[137,1976,1977],{"class":178}," dim.width\n",[137,1979,1980,1983,1985],{"class":139,"line":224},[137,1981,1982],{"class":178},"top ",[137,1984,281],{"class":174},[137,1986,1341],{"class":411},[137,1988,1989,1991,1994,1996,1999],{"class":139,"line":237},[137,1990,1959],{"class":174},[137,1992,1993],{"class":178}," i, record ",[137,1995,553],{"class":174},[137,1997,1998],{"class":411}," enumerate",[137,2000,2001],{"class":178},"(records):\n",[137,2003,2004,2006,2008],{"class":139,"line":250},[137,2005,1394],{"class":178},[137,2007,281],{"class":174},[137,2009,2010],{"class":178}," stamp_record(block, out, top, record)\n",[137,2012,2013,2016,2019,2022,2025,2028,2030,2032],{"class":139,"line":263},[137,2014,2015],{"class":174},"    if",[137,2017,2018],{"class":178}," i ",[137,2020,2021],{"class":174},"\u003C",[137,2023,2024],{"class":411}," len",[137,2026,2027],{"class":178},"(records) ",[137,2029,969],{"class":174},[137,2031,1171],{"class":411},[137,2033,2034],{"class":178},":\n",[137,2036,2037,2040,2043,2045,2047,2049,2051,2054],{"class":139,"line":268},[137,2038,2039],{"class":178},"        out.row_breaks.append(Break(",[137,2041,2042],{"class":334},"id",[137,2044,281],{"class":174},[137,2046,1982],{"class":178},[137,2048,969],{"class":174},[137,2050,1171],{"class":411},[137,2052,2053],{"class":178},"))                       ",[137,2055,2056],{"class":640},"# new page per record\n",[137,2058,2059,2061,2064,2066],{"class":139,"line":290},[137,2060,1443],{"class":178},[137,2062,2063],{"class":174},"+=",[137,2065,1171],{"class":411},[137,2067,2068],{"class":640},"                                                        # blank spacer row\n",[137,2070,2071],{"class":139,"line":301},[137,2072,2073],{"class":178},"wb.remove(block)\n",[137,2075,2076,2079,2082],{"class":139,"line":312},[137,2077,2078],{"class":178},"wb.save(",[137,2080,2081],{"class":147},"\"statements.xlsx\"",[137,2083,354],{"class":178},[137,2085,2086,2089,2092,2094,2097,2099,2102,2105,2107,2109,2112,2114,2116,2118],{"class":139,"line":323},[137,2087,2088],{"class":411},"print",[137,2090,2091],{"class":178},"([b.id ",[137,2093,1959],{"class":174},[137,2095,2096],{"class":178}," b ",[137,2098,553],{"class":174},[137,2100,2101],{"class":178}," out.row_breaks.brk], out.max_row, ",[137,2103,2104],{"class":411},"sorted",[137,2106,831],{"class":178},[137,2108,1051],{"class":411},[137,2110,2111],{"class":178},"(r) ",[137,2113,1959],{"class":174},[137,2115,1158],{"class":178},[137,2117,553],{"class":174},[137,2119,2120],{"class":178}," out.merged_cells.ranges))\n",[137,2122,2123],{"class":139,"line":357},[137,2124,2125],{"class":640},"# [6, 12] 21 ['A14:D14', 'A1:D1', 'A8:D8']\n",[10,2127,2128,2131],{},[134,2129,2130],{},"Break(id=6)"," puts a page break after row 6, the last row of the first statement. Converted with LibreOffice, the sheet produced three pages, one per customer, and every total was recalculated correctly: 50.82, 16.59 and 147.51.",[20,2133,29,2139,29,2142,29,2145,29,2150,29,2158,29,2162,29,2168,29,2172,29,2176,29,2180,29,2184,29,2188,29,2192,29,2196,29,2200,29,2204,29,2208,29,2210,29,2212,29,2215,29,2217,29,2219,29,2221,29,2223,29,2225,29,2227,29,2230,29,2233,29,2235,29,2238,29,2241,29,2245,29,2247,29,2249,29,2251,29,2254,29,2257,29,2259,29,2262,29,2264,29,2267,29,2269,29,2271,29,2273,29,2276,29,2279,29,2282,29,2284,29,2287,29,2289,29,2291,29,2293,29,2296,29,2298,29,2302,29,2305,29,2308,29,2310,29,2312,29,2314,29,2317,29,2319,29,2321,29,2323,29,2326,29,2329,29,2331,29,2333,29,2335,29,2337,29,2339,29,2341,29,2343,29,2346,29,2349,29,2352,29,2354,29,2356,29,2358,29,2361,29,2363,29,2365,29,2367,29,2370,29,2373,29,2375,29,2378,29,2380,29,2383,29,2385,29,2387,29,2389,29,2392,29,2395,29,2399,29,2401,29,2404,29,2406,29,2408,29,2410,29,2412,29,2416,29,2420],{"viewBox":2134,"role":23,"ariaLabelledBy":2135,"xmlns":27,"style":2138},"100 0 560 334",[2136,2137],"rb-output-t","rb-output-d","width:100%;max-width:560px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,2140,2141],{"id":2136},"Three records stamped down one sheet",[35,2143,2144],{"id":2137},"The Statements sheet after stamping. Harbour Stationers occupies rows 1 to 6 with two items and a total of 50.82. Northfield Primary occupies rows 8 to 12 with one item and a total of 16.59. Quayside Dental starts at row 14 with four items and a total of 147.51, which the copied highlight rule marks because it is over 100. Page breaks follow rows 6 and 12.",[39,2146],{"x":2147,"y":42,"width":2148,"height":2149,"fill":45},"100","560","334",[39,2151],{"x":2152,"y":49,"width":2153,"height":2154,"rx":42,"fill":2155,"stroke":2156,"style":2157},"153.5","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[39,2159],{"x":2160,"y":49,"width":2161,"height":2154,"rx":42,"fill":2155,"stroke":2156,"style":2157},"185.5","59.5",[68,2163,2167],{"x":2164,"y":2165,"style":2166},"215.3","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[39,2169],{"x":2170,"y":49,"width":2171,"height":2154,"rx":42,"fill":2155,"stroke":2156,"style":2157},"245.0","163.6",[68,2173,2175],{"x":2174,"y":2165,"style":2166},"326.8","B",[39,2177],{"x":2178,"y":49,"width":2179,"height":2154,"rx":42,"fill":2155,"stroke":2156,"style":2157},"408.6","44.0",[68,2181,2183],{"x":2182,"y":2165,"style":2166},"430.6","C",[39,2185],{"x":2186,"y":49,"width":2187,"height":2154,"rx":42,"fill":2155,"stroke":2156,"style":2157},"452.6","55.5",[68,2189,2191],{"x":2190,"y":2165,"style":2166},"480.4","D",[39,2193],{"x":2194,"y":49,"width":2195,"height":2154,"rx":42,"fill":2155,"stroke":2156,"style":2157},"508.1","98.4",[68,2197,2199],{"x":2198,"y":2165,"style":2166},"557.3","E",[39,2201],{"x":2152,"y":2202,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"40.0","28.0",[68,2205,472],{"x":2206,"y":2207,"style":2166},"169.5","59.0",[39,2209],{"x":2160,"y":2202,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2211],{"x":2170,"y":2202,"width":2171,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2213,2167],{"x":2174,"y":2207,"style":2214},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[39,2216],{"x":2178,"y":2202,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2218,2175],{"x":2182,"y":2207,"style":2214},[39,2220],{"x":2186,"y":2202,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2222,2183],{"x":2190,"y":2207,"style":2214},[39,2224],{"x":2194,"y":2202,"width":2195,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2226,2191],{"x":2198,"y":2207,"style":2214},[39,2228],{"x":2152,"y":2229,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"68.0",[68,2231,1865],{"x":2206,"y":2232,"style":2166},"87.0",[39,2234],{"x":2160,"y":2229,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2236,472],{"x":2164,"y":2232,"style":2237},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[39,2239],{"x":2170,"y":2229,"width":2171,"height":2203,"rx":42,"fill":53,"stroke":54,"style":2240},"stroke-width:1.5px",[68,2242,2244],{"x":2174,"y":2232,"style":2243},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Harbour Stationers Ltd",[39,2246],{"x":2178,"y":2229,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2248],{"x":2186,"y":2229,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2250],{"x":2194,"y":2229,"width":2195,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2252],{"x":2152,"y":2253,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"96.0",[68,2255,559],{"x":2206,"y":2256,"style":2166},"115.0",[39,2258],{"x":2160,"y":2253,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2260,2261],{"x":2164,"y":2256,"style":2237},"4–5",[39,2263],{"x":2170,"y":2253,"width":2171,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2265,2266],{"x":2174,"y":2256,"style":2237},"2 items",[39,2268],{"x":2178,"y":2253,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2270],{"x":2186,"y":2253,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2272],{"x":2194,"y":2253,"width":2195,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2274,2275],{"x":2198,"y":2256,"style":2237},"=B4*C4 …",[39,2277],{"x":2152,"y":2278,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"124.0",[68,2280,1450],{"x":2206,"y":2281,"style":2166},"143.0",[39,2283],{"x":2160,"y":2278,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2285,2286],{"x":2164,"y":2281,"style":2237},"6",[39,2288],{"x":2170,"y":2278,"width":2171,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2290],{"x":2178,"y":2278,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2292],{"x":2186,"y":2278,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2294,2295],{"x":2190,"y":2281,"style":2237},"Total",[39,2297],{"x":2194,"y":2278,"width":2195,"height":2203,"rx":42,"fill":103,"stroke":104,"style":2240},[68,2299,2301],{"x":2198,"y":2281,"style":2300},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","50.82",[39,2303],{"x":2152,"y":2304,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"152.0",[68,2306,1480],{"x":2206,"y":2307,"style":2166},"171.0",[39,2309],{"x":2160,"y":2304,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2311,845],{"x":2164,"y":2307,"style":2237},[39,2313],{"x":2170,"y":2304,"width":2171,"height":2203,"rx":42,"fill":53,"stroke":54,"style":2240},[68,2315,2316],{"x":2174,"y":2307,"style":2243},"Northfield Primary",[39,2318],{"x":2178,"y":2304,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2320],{"x":2186,"y":2304,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2322],{"x":2194,"y":2304,"width":2195,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2324],{"x":2152,"y":2325,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"180.0",[68,2327,2286],{"x":2206,"y":2328,"style":2166},"199.0",[39,2330],{"x":2160,"y":2325,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2332,52],{"x":2164,"y":2328,"style":2237},[39,2334],{"x":2170,"y":2325,"width":2171,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2336],{"x":2178,"y":2325,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2338],{"x":2186,"y":2325,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2340,2295],{"x":2190,"y":2328,"style":2237},[39,2342],{"x":2194,"y":2325,"width":2195,"height":2203,"rx":42,"fill":103,"stroke":104,"style":2240},[68,2344,2345],{"x":2198,"y":2328,"style":2300},"16.59",[39,2347],{"x":2152,"y":2348,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"208.0",[68,2350,1745],{"x":2206,"y":2351,"style":2166},"227.0",[39,2353],{"x":2160,"y":2348,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2355,412],{"x":2164,"y":2351,"style":2237},[39,2357],{"x":2170,"y":2348,"width":2171,"height":2203,"rx":42,"fill":53,"stroke":54,"style":2240},[68,2359,2360],{"x":2174,"y":2351,"style":2243},"Quayside Dental",[39,2362],{"x":2178,"y":2348,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2364],{"x":2186,"y":2348,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2366],{"x":2194,"y":2348,"width":2195,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2368],{"x":2152,"y":2369,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"236.0",[68,2371,845],{"x":2206,"y":2372,"style":2166},"255.0",[39,2374],{"x":2160,"y":2369,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2376,2377],{"x":2164,"y":2372,"style":2237},"17–20",[39,2379],{"x":2170,"y":2369,"width":2171,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2381,2382],{"x":2174,"y":2372,"style":2237},"4 items",[39,2384],{"x":2178,"y":2369,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2386],{"x":2186,"y":2369,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2388],{"x":2194,"y":2369,"width":2195,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2390,2391],{"x":2198,"y":2372,"style":2237},"=B17*C17 …",[39,2393],{"x":2152,"y":2394,"width":2153,"height":2203,"rx":42,"fill":2155,"stroke":2156,"style":2157},"264.0",[68,2396,2398],{"x":2206,"y":2397,"style":2166},"283.0","9",[39,2400],{"x":2160,"y":2394,"width":2161,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2402,2403],{"x":2164,"y":2397,"style":2237},"21",[39,2405],{"x":2170,"y":2394,"width":2171,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2407],{"x":2178,"y":2394,"width":2179,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[39,2409],{"x":2186,"y":2394,"width":2187,"height":2203,"rx":42,"fill":45,"stroke":2156,"style":2157},[68,2411,2295],{"x":2190,"y":2397,"style":2237},[39,2413],{"x":2194,"y":2394,"width":2195,"height":2203,"rx":42,"fill":2414,"stroke":2415,"style":2240},"#fce9e9","var(--danger,#dc2626)",[68,2417,2419],{"x":2198,"y":2397,"style":2418},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","147.51",[68,2421,2424],{"x":70,"y":2422,"style":2423},"314.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","each total's SUM covers its own item rows; only 147.51 is over the highlight threshold",[122,2426,2428],{"id":2427},"step-4-check-the-result-before-sending-it","Step 4 — Check the result before sending it",[10,2430,2431],{},"Stamped layouts fail in quiet ways: a total that sums the wrong rows, a record that lost its merged title. Read the output back and check the structure against the records:",[127,2433,2435],{"className":165,"code":2434,"language":167,"meta":132,"style":132},"check = load_workbook(\"statements.xlsx\")[\"Statements\"]\ntitles = [check.cell(r.min_row, 1).value for r in sorted(check.merged_cells.ranges, key=lambda r: r.min_row)]\nassert titles == [r[\"customer\"] for r in records], titles\n\ntotals = [c.value for c in check[\"D\"] if isinstance(c.value, str) and c.value.startswith(\"=SUM\")]\nprint(totals)\n# ['=SUM(D4:D5)', '=SUM(D11:D11)', '=SUM(D17:D20)']\n",[134,2436,2437,2455,2491,2518,2522,2569,2576],{"__ignoreMap":132},[137,2438,2439,2442,2444,2446,2448,2451,2453],{"class":139,"line":140},[137,2440,2441],{"class":178},"check ",[137,2443,281],{"class":174},[137,2445,1920],{"class":178},[137,2447,2081],{"class":147},[137,2449,2450],{"class":178},")[",[137,2452,1952],{"class":147},[137,2454,1905],{"class":178},[137,2456,2457,2460,2462,2465,2467,2470,2472,2474,2476,2479,2482,2485,2488],{"class":139,"line":182},[137,2458,2459],{"class":178},"titles ",[137,2461,281],{"class":174},[137,2463,2464],{"class":178}," [check.cell(r.min_row, ",[137,2466,472],{"class":411},[137,2468,2469],{"class":178},").value ",[137,2471,1959],{"class":174},[137,2473,1158],{"class":178},[137,2475,553],{"class":174},[137,2477,2478],{"class":411}," sorted",[137,2480,2481],{"class":178},"(check.merged_cells.ranges, ",[137,2483,2484],{"class":334},"key",[137,2486,2487],{"class":174},"=lambda",[137,2489,2490],{"class":178}," r: r.min_row)]\n",[137,2492,2493,2496,2499,2502,2505,2507,2509,2511,2513,2515],{"class":139,"line":190},[137,2494,2495],{"class":174},"assert",[137,2497,2498],{"class":178}," titles ",[137,2500,2501],{"class":174},"==",[137,2503,2504],{"class":178}," [r[",[137,2506,1599],{"class":147},[137,2508,366],{"class":178},[137,2510,1959],{"class":174},[137,2512,1158],{"class":178},[137,2514,553],{"class":174},[137,2516,2517],{"class":178}," records], titles\n",[137,2519,2520],{"class":139,"line":197},[137,2521,194],{"emptyLinePlaceholder":193},[137,2523,2524,2527,2529,2532,2534,2537,2539,2542,2545,2547,2550,2552,2555,2557,2559,2561,2564,2567],{"class":139,"line":211},[137,2525,2526],{"class":178},"totals ",[137,2528,281],{"class":174},[137,2530,2531],{"class":178}," [c.value ",[137,2533,1959],{"class":174},[137,2535,2536],{"class":178}," c ",[137,2538,553],{"class":174},[137,2540,2541],{"class":178}," check[",[137,2543,2544],{"class":147},"\"D\"",[137,2546,366],{"class":178},[137,2548,2549],{"class":174},"if",[137,2551,1045],{"class":411},[137,2553,2554],{"class":178},"(c.value, ",[137,2556,1051],{"class":411},[137,2558,1054],{"class":178},[137,2560,1057],{"class":174},[137,2562,2563],{"class":178}," c.value.startswith(",[137,2565,2566],{"class":147},"\"=SUM\"",[137,2568,1084],{"class":178},[137,2570,2571,2573],{"class":139,"line":224},[137,2572,2088],{"class":411},[137,2574,2575],{"class":178},"(totals)\n",[137,2577,2578],{"class":139,"line":237},[137,2579,2580],{"class":640},"# ['=SUM(D4:D5)', '=SUM(D11:D11)', '=SUM(D17:D20)']\n",[10,2582,2583,2584,2588],{},"openpyxl does not calculate formulas, so check the calculated totals after a LibreOffice or Excel recalculation when the numbers matter — or write totals as values computed in Python, as ",[14,2585,2587],{"href":2586},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-invoices-from-an-excel-template\u002F","Generate Invoices from an Excel Template with Python"," does.",[20,2590,29,2596,29,2599,29,2602,29,2606,29,2610,29,2615,29,2618,29,2622,29,2626,29,2629,29,2634,29,2642,29,2647,29,2652,29,2656,29,2660,29,2662,29,2665,29,2669,29,2672,29,2675,29,2678,29,2681,29,2683,29,2686,29,2690,29,2693,29,2696,29,2699],{"viewBox":2591,"role":23,"ariaLabelledBy":2592,"xmlns":27,"style":2595},"4 0 752 238",[2593,2594],"rb-choose-t","rb-choose-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,2597,2598],{"id":2593},"Blocks on one sheet, a sheet each, or a file each?",[35,2600,2601],{"id":2594},"Use stamped blocks on one sheet when records are short and readers scroll or print them together. Use one sheet per record, made with copy_worksheet, when records are long or need their own tab, remembering to re-add conditional formatting. Use one file per record when each goes to a different recipient.",[39,2603],{"x":1450,"y":42,"width":2604,"height":2605,"fill":45},"752","238",[39,2607],{"x":2608,"y":49,"width":2609,"height":71,"rx":52,"fill":53,"stroke":54,"style":55},"240.0","280.0",[57,2611,2612],{"transform":59},[61,2613],{"d":2614,"fill":64,"stroke":65,"style":66},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z",[68,2616,2617],{"x":70,"y":71,"style":72},"How will each record be read?",[139,2619],{"x1":70,"y1":2620,"x2":70,"y2":2621,"stroke":54,"style":55},"91.5","111.5",[139,2623],{"x1":2624,"y1":2621,"x2":2625,"y2":2621,"stroke":54,"style":55},"134.7","625.3",[139,2627],{"x1":2624,"y1":2621,"x2":2624,"y2":2628,"stroke":54,"style":55},"157.5",[2630,2631],"polygon",{"points":2632,"fill":2633},"134.7,161.5 129.7,152.5 139.7,152.5","#5b5cf0",[39,2635],{"x":2636,"y":2637,"width":2638,"height":2639,"rx":2640,"fill":45,"stroke":2156,"style":2641},"100.3","121.5","68.7","21.0","10","stroke-width:1.2px",[68,2643,2646],{"x":2624,"y":2644,"style":2645},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","together",[39,2648],{"x":2649,"y":2650,"width":2651,"height":2207,"rx":52,"fill":53,"stroke":54,"style":55},"20.0","163.5","229.3",[68,2653,2655],{"x":2624,"y":2654,"style":72},"189.0","blocks on one sheet",[68,2657,2659],{"x":2624,"y":2658,"style":77},"208.5","row break per record",[139,2661],{"x1":70,"y1":2621,"x2":70,"y2":2628,"stroke":54,"style":55},[2630,2663],{"points":2664,"fill":2633},"380.0,161.5 375.0,152.5 385.0,152.5",[39,2666],{"x":2667,"y":2637,"width":2668,"height":2639,"rx":2640,"fill":45,"stroke":2156,"style":2641},"323.5","113.1",[68,2670,2671],{"x":70,"y":2644,"style":2645},"on their own tab",[39,2673],{"x":2674,"y":2650,"width":2651,"height":2207,"rx":52,"fill":82,"stroke":83,"style":55},"265.3",[68,2676,2677],{"x":70,"y":2654,"style":94},"copy_worksheet",[68,2679,2680],{"x":70,"y":2658,"style":77},"re-add CF rules",[139,2682],{"x1":2625,"y1":2621,"x2":2625,"y2":2628,"stroke":54,"style":55},[2630,2684],{"points":2685,"fill":2633},"625.3,161.5 620.3,152.5 630.3,152.5",[39,2687],{"x":2688,"y":2637,"width":2689,"height":2639,"rx":2640,"fill":45,"stroke":2156,"style":2641},"570.6","109.4",[68,2691,2692],{"x":2625,"y":2644,"style":2645},"sent separately",[39,2694],{"x":2695,"y":2650,"width":2651,"height":2207,"rx":52,"fill":103,"stroke":104,"style":55},"510.7",[68,2697,2698],{"x":2625,"y":2654,"style":115},"one file each",[68,2700,2701],{"x":2625,"y":2658,"style":77},"loop over the template",[122,2703,2705],{"id":2704},"other-ways-to-repeat-a-layout","Other ways to repeat a layout",[10,2707,2708,2709,2712,2713,1359],{},"When each record deserves its own tab, ",[134,2710,2711],{},"wb.copy_worksheet(wb[\"Block\"])"," duplicates the whole sheet within the workbook. In a test it kept values, styles, merged cells, row heights and column widths, but the copy had no conditional formatting rules, so re-add them with the loop from Step 1. When each record goes to a different person, one workbook per record is simpler still — see ",[14,2714,2716],{"href":2715},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","Generate One Excel Report per Region in a Loop",[122,2718,2720],{"id":2719},"common-pitfalls","Common pitfalls",[2722,2723,2724,2752,2758,2772],"ul",{},[2725,2726,2727,2731,2732,2734,2735,343,2738,343,2741,343,2744,2747,2748,2751],"li",{},[2728,2729,2730],"strong",{},"Copying styles across workbooks."," ",[134,2733,1353],{}," indices only make sense in the workbook they came from. Stamp inside the template workbook, or copy ",[134,2736,2737],{},"font",[134,2739,2740],{},"fill",[134,2742,2743],{},"border",[134,2745,2746],{},"alignment"," and ",[134,2749,2750],{},"number_format"," individually.",[2725,2753,2754,2757],{},[2728,2755,2756],{},"Data validation and images."," The band copy above does not move drop-down lists or pictures. Re-create data validation for each band's range, and anchor images again at the new rows.",[2725,2759,2760,2763,2764,2767,2768,2771],{},[2728,2761,2762],{},"Formulas pointing outside the block."," A reference such as ",[134,2765,2766],{},"=$G$1*D4"," keeps ",[134,2769,2770],{},"$G$1"," fixed, which is usually right for a rate cell; a relative reference to a cell outside the block moves with it and ends up pointing at another record.",[2725,2773,2774,2777],{},[2728,2775,2776],{},"Very long outputs."," Thousands of stamped records make a slow, heavy sheet. Past a few hundred, one file or sheet per record is easier on everyone.",[122,2779,2781],{"id":2780},"conclusion","Conclusion",[10,2783,2784,2785,2787],{},"Split the template block into bands, copy each band with its values, styles, heights, merged ranges and conditional formatting, move formulas with ",[134,2786,894],{},", repeat the item row per line and rewrite the total's range, and add a row break after each record. Work inside the template workbook so styles copy exactly, and check the stamped structure before sending it.",[122,2789,2791],{"id":2790},"frequently-asked-questions","Frequently asked questions",[10,2793,2794,2797],{},[2728,2795,2796],{},"How do I copy a range of rows with formatting in openpyxl?","\nopenpyxl has no built-in range copy. Loop over the source cells, write each value to the target cell and copy the style with target._style = copy.copy(cell._style), then shift and re-create merged ranges, row heights and conditional formatting yourself. Do it within one workbook, because style indices belong to the workbook.",[10,2799,2800,2803,2804,2808],{},[2728,2801,2802],{},"How do I adjust formulas when copying template rows?","\nUse openpyxl.formula.translate.Translator. Translator(\"=B4",[2805,2806,2807],"em",{},"C4\", origin=\"D4\").translate_formula(\"D11\") returns \"=B11","C11\", moving relative references the same way Excel does when you copy a cell, while absolute references with $ stay fixed.",[10,2810,2811,2814],{},[2728,2812,2813],{},"How do I handle records with different numbers of line items?","\nSplit the template block into a header, one repeating item row and a footer. Copy the header once, the item row once per item and the footer once, then write the footer's SUM formula over the rows you actually created.",[10,2816,2817,2820],{},[2728,2818,2819],{},"Does copy_worksheet copy conditional formatting?","\nNo. In a test, wb.copy_worksheet kept values, styles, merged cells, row heights and column widths but the copy had no conditional formatting rules. Re-add them to the copy if the block depends on them.",[122,2822,2824],{"id":2823},"related","Related",[2722,2826,2827,2833,2840,2847],{},[2725,2828,2829,2830,2832],{},"Up one level: ",[14,2831,17],{"href":16}," — filling designed workbooks from data.",[2725,2834,2835,2839],{},[14,2836,2838],{"href":2837},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill an Excel Template with Python and openpyxl"," — named cells and a single data table.",[2725,2841,2842,2846],{},[14,2843,2845],{"href":2844},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel Template Without Losing Formatting"," — what survives an openpyxl re-save.",[2725,2848,2849,2853],{},[14,2850,2852],{"href":2851},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-an-entire-row-based-on-a-cell-value-with-openpyxl\u002F","Highlight an Entire Row Based on a Cell Value with openpyxl"," — rules worth copying with each block.",[335,2855,2856],{},"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 .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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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}",{"title":132,"searchDepth":182,"depth":182,"links":2858},[2859,2860,2861,2862,2863,2864,2865,2866,2867,2868],{"id":124,"depth":182,"text":125},{"id":887,"depth":182,"text":888},{"id":1362,"depth":182,"text":1363},{"id":1572,"depth":182,"text":1573},{"id":2427,"depth":182,"text":2428},{"id":2704,"depth":182,"text":2705},{"id":2719,"depth":182,"text":2720},{"id":2780,"depth":182,"text":2781},{"id":2790,"depth":182,"text":2791},{"id":2823,"depth":182,"text":2824},"2026-09-23","Stamp a designed block of rows — a customer statement, a card, a mini-report — once per record down a single sheet with openpyxl, copying styles, merged cells, row heights and conditional formatting, translating formulas to their new rows, growing the block for records with more line items and starting each record on a new printed page.","md",[2873,2875,2877,2879],{"q":2796,"a":2874},"openpyxl has no built-in range copy. Loop over the source cells, write each value to the target cell and copy the style with target._style = copy.copy(cell._style), then shift and re-create merged ranges, row heights and conditional formatting yourself. Do it within one workbook, because style indices belong to the workbook.",{"q":2802,"a":2876},"Use openpyxl.formula.translate.Translator. Translator(\"=B4*C4\", origin=\"D4\").translate_formula(\"D11\") returns \"=B11*C11\", moving relative references the same way Excel does when you copy a cell, while absolute references with $ stay fixed.",{"q":2813,"a":2878},"Split the template block into a header, one repeating item row and a footer. Copy the header once, the item row once per item and the footer once, then write the footer's SUM formula over the rows you actually created.",{"q":2819,"a":2880},"No. In a test, wb.copy_worksheet kept values, styles, merged cells, row heights and column widths but the copy had no conditional formatting rules. Re-add them to the copy if the block depends on them.","repeat a template block for each record in an excel report",null,{"breadcrumb":2884},[2885,2888,2891],{"name":2886,"item":2887},"Home","\u002F",{"name":2889,"item":2890},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record",{"title":2894,"description":2895},"Repeat an Excel Template Block per Record with Python (openpyxl)","Copy a styled range of template rows for every record with openpyxl — placeholders filled with typed values, formulas moved with Translator, merged cells and conditional formatting shifted, a repeating item row for variable-length records, rebuilt SUM ranges and a page break per record, verified in LibreOffice.","repeat-a-template-block-for-each-record","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record\u002Findex",[2899,2900],"report-layouts","openpyxl","how-to","pctLqDjIngXn7vePNcTdameK-GPTZLi11Dx5Ne529E0",[2904,2908],{"title":2905,"path":2906,"stem":2907,"children":-1},"Print Mailing Labels from an Excel Address List","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fprint-mailing-labels-from-an-excel-address-list","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fprint-mailing-labels-from-an-excel-address-list\u002Findex",{"title":1358,"path":2909,"stem":2910,"children":-1},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python\u002Findex",[2912,2914],{"path":2913,"title":17},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates",{"path":2915,"title":2889},"\u002Fautomating-reporting-workflows",[],[2918,2922,2925,2928,2931,2934],{"path":2919,"title":2920,"kind":2921},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports","Building Finance and Operations Reports in Excel with Python","guide",{"path":2923,"title":2924,"kind":2901},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments","Allocate Shared Costs Across Departments with Python",{"path":2926,"title":2927,"kind":2901},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report","Build a Bank Reconciliation Report with Python and Excel",{"path":2929,"title":2930,"kind":2901},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup","Build a Bill of Materials Cost Rollup",{"path":2932,"title":2933,"kind":2901},"\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":2935,"title":2936,"kind":2937},"\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",1790972138260]