91 lines
4.5 KiB
Python
91 lines
4.5 KiB
Python
"""Сборка шаблона оформления ОСВ 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)
|