"""Сборка шаблона оформления ОСВ 1С (`osv_style.json`) из реального образца — один раз, результат коммитится. Запуск из корня worktree: uv run -q --with openpyxl==3.1.5 python -m synthgen.templates.build_osv_style [путь к образцу] В JSON попадают только оформление и неизменяемые тексты шапки (строки 1–11): ширины колонок, высоты и уровни группировки строк, шрифты, заливки, рамки, выравнивание, объединения. Название организации и период в шапке заменены плейсхолдерами `{org}` и `{title}`; ни одно значение данных образца (контрагенты, суммы) в шаблон не попадает. Сетка — «блок текста A:D + блок сумм E:Q»; колонка ИНН вставляется между ними при записи (`synthgen.osv`). """ import json import sys from pathlib import Path import openpyxl SAMPLE = Path("/Users/andrey.epifancev/projects/awp-ca/fixtures/osv/rb/2024-Q1/сч 60_ 1 кв 2024 г.xlsx") OUT = Path(__file__).with_name("osv_style.json") NCOLS = 17 # A…Q: дальше в образце только служебные хвосты HEADER_ROWS = 11 ROW_TYPES = {"account": 12, "sub": 13, "cp": 14} # образцы строк данных; «Итого» ищется по тексту PLACEHOLDERS = {(1, 1): "{org}", (2, 1): "{title}"} def _side(side) -> dict | None: if not side or not side.style: return None return {"style": side.style, "color": side.color.rgb if side.color is not None and isinstance(side.color.rgb, str) else None} def _style(cell) -> dict: font, fill, al, bd = cell.font, cell.fill, cell.alignment, cell.border color = font.color.rgb if font.color is not None and isinstance(font.color.rgb, str) else None return { "font": {"name": font.name, "sz": font.sz, "b": bool(font.b), "i": bool(font.i), "color": color}, "fill": fill.fgColor.rgb if fill.fill_type == "solid" else None, "border": {k: _side(getattr(bd, k)) for k in ("left", "right", "top", "bottom")}, "align": {"h": al.horizontal, "v": al.vertical, "wrap": bool(al.wrap_text), "indent": al.indent or 0}, "nf": cell.number_format, } def _row(ws, r: int) -> dict: dim = ws.row_dimensions[r] return { "height": dim.height, "outline": dim.outlineLevel or 0, "cells": [_style(ws.cell(r, c)) for c in range(1, NCOLS + 1)], } def build(sample: Path = SAMPLE, out: Path = OUT) -> dict: ws = openpyxl.load_workbook(sample).active total_row = next(r for r in range(ws.max_row, 0, -1) if ws.cell(r, 1).value == "Итого") spans = sorted( (m.min_col, m.max_col) for m in ws.merged_cells.ranges if m.min_row == 10 and m.max_row == 11 and m.min_col > 4 ) header = [] for r in range(1, HEADER_ROWS + 1): row = _row(ws, r) row["values"] = { str(c): PLACEHOLDERS.get((r, c), ws.cell(r, c).value) for c in range(1, NCOLS + 1) if ws.cell(r, c).value is not None } header.append(row) style = { "source": "образец 1С 8.3 (БП 3.0): ОСВ по счёту, лист Лист_1", "ncols": NCOLS, "text_span": [1, 4], # блок «Счет / Контрагенты»: A:D "sum_spans": [list(s) for s in spans], # шесть блоков сумм: сальдо нач. Дт/Кт, обороты Дт/Кт, сальдо кон. Дт/Кт "col_widths": { str(c): ws.column_dimensions[openpyxl.utils.get_column_letter(c)].width for c in range(1, NCOLS + 1) }, "inn_width": 11.0, "header": header, "header_merges": sorted([m.min_row, m.min_col, m.max_row, m.max_col] for m in ws.merged_cells.ranges if m.min_row <= HEADER_ROWS), "rows": {name: _row(ws, r) for name, r in ROW_TYPES.items()}, "total": _row(ws, total_row), "default_row_height": ws.sheet_format.defaultRowHeight, "summary_below": bool(ws.sheet_properties.outlinePr.summaryBelow), "grid_lines": ws.sheet_view.showGridLines is not False, } out.write_text(json.dumps(style, ensure_ascii=False, indent=1, sort_keys=True) + "\n", encoding="utf-8") return style if __name__ == "__main__": build(Path(sys.argv[1]) if len(sys.argv) > 1 else SAMPLE)