110 lines
6.6 KiB
Python
110 lines
6.6 KiB
Python
"""ББ и ОФР в 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)
|