"""ББ и ОФР в Excel по шаблону 1С (спека §6.2): копия книги образца (`templates/bb_1c.xlsx`, `ofr_1c.xlsx`) с подстановкой реквизитов и значений. Значения ищутся по колонке «Коды» (и номерам колонок под заголовком), а не по номерам строк; положение реквизитов шапки — таблица `HEADER_CELLS` (шаблон наш, собран `templates/build_forms_templates.py`). Значения — целые тыс. руб., расходы ОФР — отрицательные числа (решение 12); ноль пишется `0` с форматом `[=0]"-";General`, остальное — `#,##0`, как в выгрузке 1С. """ from pathlib import Path import openpyxl from synthgen import xlsx from synthgen.forms import Form from synthgen.rng import stable_hash TEMPLATES = Path(__file__).with_name("templates") TEMPLATE_FILES = {"BALANCE": "bb_1c.xlsx", "INCOME": "ofr_1c.xlsx"} ZERO_FORMAT = '[=0]"-";General' NUMBER_FORMAT = "#,##0" HEADER_CELLS = { "BALANCE": { "title": "B3", "org": "G7", "okpo": "R7", "inn": "R8", "activity": "G9", "okved": "R9", "legal_form": "G10", "okopf": "R10", "okfs": "S10", "address": "G13", "date": ("R6", "S6", "T6"), "head": "Q80", "accountant": "Q83", }, "INCOME": { "title": "B3", "org": "G7", "okpo": "P7", "inn": "P8", "activity": "G9", "okved": "P9", "legal_form": "G10", "okopf": "P10", "okfs": "Q10", "address": None, "date": ("P6", "Q6", "R6"), "head": "O49", "accountant": "O52", }, } # fmt: skip ACTIVITY = { "агро": ("Сельское хозяйство", "01.11.11"), "торговля": ("Торговля оптовая", "46.90"), "логистика": ("Деятельность автомобильного грузового транспорта", "49.41"), "производство": ("Производство прочих изделий", "25.99"), "строительство": ("Строительство зданий", "41.20"), "услуги": ("Деятельность консультативная", "70.22"), } # fmt: skip LEGAL_FORM = {"ООО": ("ООО - общество с ограниченной ответственностью", "65"), "АО": ("АО - акционерное общество", "47")} STREETS = ("Ленина", "Советская", "Мира", "Заводская", "Промышленная", "Центральная", "Садовая", "Новая") CITIES = ("г. Тверь", "г. Липецк", "г. Курск", "г. Тула", "г. Рязань", "г. Орёл", "г. Тамбов", "г. Калуга") SURNAMES = ("Ветров", "Озеров", "Лугов", "Дубравин", "Ключников", "Ракитин", "Соловьёв", "Березин", "Туманов", "Зорин") FEMALE_SURNAMES = ("Ветрова", "Озерова", "Лугова", "Дубравина", "Ключникова", "Ракитина", "Соловьёва", "Березина", "Туманова", "Зорина") MALE_NAMES = ("Андрей Павлович", "Игорь Сергеевич", "Олег Николаевич", "Максим Андреевич", "Денис Олегович") FEMALE_NAMES = ("Анна Игоревна", "Ольга Павловна", "Елена Сергеевна", "Марина Андреевна", "Татьяна Олеговна") def file_name(form: Form) -> str: prefix = "ББ" if form.kind == "BALANCE" else "ОФР" return f"{prefix}_{form.period.label_1c[:-1]}.xlsx" # метка периода кончается точкой — она же перед расширением def _person(form: Form, role: str) -> str: key = f"{form.member.code}|{form.period.key}|{role}" if stable_hash(key + "|sex", 2): return f"{FEMALE_SURNAMES[stable_hash(key + '|s', 10)]} {FEMALE_NAMES[stable_hash(key + '|n', 5)]}" return f"{SURNAMES[stable_hash(key + '|s', 10)]} {MALE_NAMES[stable_hash(key + '|n', 5)]}" def _requisites(form: Form) -> dict[str, object]: m = form.member activity, okved = ACTIVITY.get(m.org.kind, ACTIVITY["услуги"]) legal, okopf = LEGAL_FORM.get(m.org.form, LEGAL_FORM["АО"]) h = stable_hash(m.inn, 10**6) address = f"{100000 + stable_hash(m.inn + 'z', 899999)}, {CITIES[h % 8]}, ул. {STREETS[(h // 8) % 8]}, дом № {1 + h % 97}" end = form.period.end return { "title": f"за {form.period.label_1c}", "org": m.name, "okpo": f"{stable_hash(m.inn + 'okpo', 10**8):08d}", "inn": m.inn, "activity": activity, "okved": okved, "legal_form": legal, "okopf": okopf, "okfs": "16", "address": address, "head": _person(form, "head"), "accountant": _person(form, "accountant"), "date": (end.day, end.month, end.year), } # fmt: skip def _tables(ws) -> list[tuple[int, list[int], int]]: """Таблицы формы по заголовку «Коды»: (колонка кодов, колонки значений, строка заголовка).""" found = [] for row in ws.iter_rows(): for cell in row: if cell.value == "Коды": cols = [c.column for c in ws[cell.row + 1] if c.value is not None and c.column > cell.column] found.append((cell.column, cols, cell.row)) return found def write(form: Form, path: Path) -> None: wb = openpyxl.load_workbook(TEMPLATES / TEMPLATE_FILES[form.kind]) ws = wb.active cells = HEADER_CELLS[form.kind] req = _requisites(form) for key, coord in cells.items(): if coord is None: continue if key == "date": for c, v in zip(coord, req["date"], strict=True): ws[c].value = v else: ws[coord].value = req[key] for code_col, value_cols, head in _tables(ws): if len(value_cols) != len(form.columns): raise ValueError(f"в шаблоне {len(value_cols)} колонок значений, в форме {len(form.columns)}") for col, title in zip(value_cols, form.columns, strict=True): ws.cell(head, col).value = title for r in range(head + 2, ws.max_row + 1): code = ws.cell(r, code_col).value if not (isinstance(code, str) and code.isdigit() and len(code) == 4): continue for i, col in enumerate(value_cols): value = int(form.values[code][i]) cell = ws.cell(r, col) cell.value = value cell.number_format = ZERO_FORMAT if value == 0 else NUMBER_FORMAT xlsx.save_deterministic(wb, path)