Преамбула «Обороты: …» над шапкой давала ОСВ, разбор листа падал и ронял книгу. Негатив s56 «Преамбула». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1524 lines
77 KiB
Python
1524 lines
77 KiB
Python
"""Генератор синтетических ОСВ (Срез 2a, Task 11): `python -m tests.synthetic.generate <каталог>` из `api/`.
|
||
|
||
Файлы s01…s13 — по одному сценарию спеки `osv-parsing` на файл, s14…s16 — записи словарей, которых нет в реальных
|
||
ОСВ (R-22), s17…s19 — ИНН в тексте названия, ИНН строкой выше и «ИНН/КПП» в одной ячейке (issue #9), s20…s27 — восемь
|
||
раскладок строки ИНН, s28 — ИНН с неверной контрольной суммой (#35), s29…s32 — xls без CODEPAGE, «Вал»/«Кол-во»,
|
||
строки ниже «Итого» и «Итого по счету» вместо точного «Итого» (#47), s40…s44 — плоская форма ОСВ 1С (#59; s33…s39 заняты
|
||
другим треком), s45 — счёт шапки «Счет Поставщика» | «60*» в соседних ячейках, s46 — выгрузка «по контрагентам» с
|
||
блоками субсчетов (#71), s47 — иерархия по отступам: подуровень счёта, строка закрытия, вложенность по суммам (#78);
|
||
s48…s57 — ОСВ других учётных систем (#81): позитивы s48…s54 (названия, группы и стороны, Navision без названия, xls и
|
||
xlsb), негативы s55…s57 (почти-ОСВ, которые обязаны остаться прочими листами или формой);
|
||
описание — `osv/synthetic/README.md` в репозитории fixtures. Генератор самодостаточен
|
||
(openpyxl + `xlsb_writer` + `xls_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги
|
||
и zip постоянны, порядок записи фиксирован.
|
||
|
||
Суммы арифметически согласованы по построению: у листа дерева сальдо на конец вычисляется из начала и оборотов, у
|
||
родителя — сумма детей по колонкам; поэтому все файлы, кроме `s10_broken_sum`, сходятся по §1.4. Позиции колонок и
|
||
строк в файлах разные: парсер не должен на них полагаться.
|
||
"""
|
||
|
||
import argparse
|
||
import datetime
|
||
import io
|
||
import re
|
||
import sys
|
||
import zipfile
|
||
from collections.abc import Callable, Sequence
|
||
from dataclasses import dataclass, field
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
from typing import Literal
|
||
|
||
import openpyxl
|
||
from openpyxl.styles import Alignment, Font
|
||
|
||
from tests.synthetic.xls_writer import write_xls
|
||
from tests.synthetic.xlsb_writer import write_xlsb
|
||
|
||
__all__ = ["FILE_NAMES", "generate_all", "main", "ORG", "SheetSpec", "OsvSpec", "build_osv", "base_tree", "to_xlsx"]
|
||
|
||
FIXED_TIME = datetime.datetime(2026, 1, 1, 0, 0, 0)
|
||
ZIP_DATE_TIME = (2026, 1, 1, 0, 0, 0)
|
||
CREATOR = "arm-ka synthetic"
|
||
SHEET = "Лист_1"
|
||
ORG = 'ООО "Тестовая организация"'
|
||
D = Decimal
|
||
ZERO = D(0)
|
||
INDENT_STEP = " " # ведущие пробелы одного уровня, когда отступ ячейки недоступен (xlsb)
|
||
|
||
Money = D | None
|
||
Pair = tuple[Money, Money]
|
||
Six = tuple[D, D, D, D, D, D]
|
||
Hierarchy = Literal["outline", "indent", "spaces", "none"]
|
||
NumStyle = Literal["num", "text", "text_nbsp", "text_narrow"]
|
||
# Как ИНН контрагента записан в файле: отдельной колонкой, в тексте названия (разные разделители), отдельной строкой
|
||
# над названием, склеенным «ИНН/КПП» в ячейке колонки ИНН
|
||
InnStyle = Literal["column", "comma", "paren_label", "label", "space", "above", "kpp_cell"]
|
||
|
||
|
||
# --- Дерево ведомости ---
|
||
|
||
|
||
@dataclass
|
||
class Node:
|
||
"""Строка ведомости. Лист: свои начало и обороты, конец вычисляется; родитель: сумма детей по колонкам.
|
||
|
||
`indicator` — строка «Вал.»/«Кол.»/«USD» под контрагентом: свои суммы, в сумму родителя не входит.
|
||
"""
|
||
|
||
text: str
|
||
kids: list["Node"] = field(default_factory=list)
|
||
inn: str | None = None
|
||
kpp: str | None = None
|
||
opening: Pair = (None, None)
|
||
turnover: Pair = (None, None)
|
||
indicator: bool = False
|
||
num_style: NumStyle = "num"
|
||
inn_style: InnStyle = "column"
|
||
closing_debit_delta: D = ZERO # порча суммы (s10): к записанному `closing_debit` прибавляется, родитель не меняется
|
||
blank: bool = False # строка без сумм (строка ИНН без сумм, #35)
|
||
twin: "Node | None" = None # строка ИНН-сосед (#35): суммы печатаются как у `twin`, в сумму родителя не входит
|
||
|
||
def children(self) -> list["Node"]:
|
||
return [kid for kid in self.kids if not kid.indicator and kid.twin is None]
|
||
|
||
def six(self) -> Six:
|
||
"""Шесть сумм по колонкам: начало Д/К, обороты Д/К, конец Д/К (без порчи)."""
|
||
children = self.children()
|
||
if children:
|
||
parts = [kid.six() for kid in children]
|
||
return tuple(sum((part[i] for part in parts), ZERO) for i in range(6)) # type: ignore[return-value]
|
||
opening_debit, opening_credit = (value or ZERO for value in self.opening)
|
||
turnover_debit, turnover_credit = (value or ZERO for value in self.turnover)
|
||
net = opening_debit - opening_credit + turnover_debit - turnover_credit
|
||
return (
|
||
opening_debit,
|
||
opening_credit,
|
||
turnover_debit,
|
||
turnover_credit,
|
||
net if net > 0 else ZERO,
|
||
-net if net < 0 else ZERO,
|
||
)
|
||
|
||
|
||
def flatten(node: Node, depth: int = 0) -> list[tuple[Node, int]]:
|
||
"""Строки в порядке обхода в глубину: узел, его глубина."""
|
||
found = [(node, depth)]
|
||
for kid in node.kids:
|
||
found.extend(flatten(kid, depth + 1))
|
||
return found
|
||
|
||
|
||
def leaf(text: str, opening: Pair, turnover: Pair, **kwargs: object) -> Node:
|
||
return Node(text, opening=opening, turnover=turnover, **kwargs) # type: ignore[arg-type]
|
||
|
||
|
||
def money(value: str | None) -> Money:
|
||
return None if value is None else D(value)
|
||
|
||
|
||
def pair(debit: str | None, credit: str | None) -> Pair:
|
||
return money(debit), money(credit)
|
||
|
||
|
||
def alfa(**kwargs: object) -> Node:
|
||
return leaf("ООО Альфа", pair(None, "250000.50"), pair("180000.25", "420000.00"), inn="7701234567", **kwargs)
|
||
|
||
|
||
def beta(**kwargs: object) -> Node:
|
||
return leaf("ООО Бета", pair("15000.00", None), pair("90000.00", "40000.00"), inn="7707654321", **kwargs)
|
||
|
||
|
||
def ivanov() -> Node:
|
||
return leaf("Иванов Иван", pair(None, "3000.00"), pair("3000.00", None)) # физлицо: без ИНН
|
||
|
||
|
||
def gamma(**kwargs: object) -> Node:
|
||
return leaf("ООО Гамма", pair("120000.00", None), pair("30000.00", "100000.00"), inn="5001234567", **kwargs)
|
||
|
||
|
||
def delta(**kwargs: object) -> Node:
|
||
return leaf("ООО Дельта", pair(None, "8000.10"), pair("8000.10", "12000.35"), inn="7812345678", **kwargs)
|
||
|
||
|
||
def sidorov() -> Node:
|
||
return leaf("ИП Сидоров", pair(None, "1500.00"), pair(None, None), inn="500100732259") # ИНН из 12 цифр
|
||
|
||
|
||
KPP = {"ООО Альфа": "770101001", "ООО Бета": "770701001", "ООО Гамма": "500101001", "ООО Дельта": "781201001"}
|
||
|
||
|
||
def base_tree(account: str = "60", *, kpp: bool = False) -> Node:
|
||
"""Счёт 60: два субсчёта, шесть контрагентов (с ИНН из 10 и 12 цифр и физлицо без ИНН)."""
|
||
kids = {"60.01": [alfa(), beta(), ivanov()], "60.02": [gamma(), delta(), sidorov()]}
|
||
if kpp:
|
||
for group in kids.values():
|
||
for node in group:
|
||
node.kpp = KPP.get(node.text)
|
||
return Node(account, [Node(sub, items) for sub, items in kids.items()])
|
||
|
||
|
||
def receivables_tree() -> Node:
|
||
"""Счёт 62 для второго листа s06."""
|
||
first = leaf("ООО Покупатель-1", pair("500000.00", None), pair("320000.00", "410000.00"), inn="7728123456")
|
||
second = leaf("ООО Покупатель-2", pair(None, "20000.00"), pair("75000.00", "55000.00"), inn="7714987654")
|
||
return Node("62", [Node("62.01", [first, second])])
|
||
|
||
|
||
# --- Лист в памяти ---
|
||
|
||
|
||
@dataclass
|
||
class CellSpec:
|
||
value: str | int | D
|
||
bold: bool = False
|
||
indent: int = 0
|
||
|
||
|
||
@dataclass
|
||
class SheetSpec:
|
||
name: str
|
||
cells: dict[tuple[int, int], CellSpec] = field(default_factory=dict)
|
||
merges: list[tuple[int, int, int, int]] = field(default_factory=list)
|
||
outline: dict[int, int] = field(default_factory=dict)
|
||
|
||
def put(self, row: int, col: int, value: str | int | D, *, bold: bool = False, indent: int = 0) -> None:
|
||
self.cells[(row, col)] = CellSpec(value, bold, indent)
|
||
|
||
def merge(self, top: int, left: int, bottom: int, right: int) -> None:
|
||
if (top, left) != (bottom, right):
|
||
self.merges.append((top, left, bottom, right))
|
||
|
||
|
||
# --- Шапка и таблица ---
|
||
|
||
|
||
@dataclass
|
||
class OsvSpec:
|
||
"""Параметры листа ОСВ: шапка, раскладка колонок и способ выражения иерархии."""
|
||
|
||
account: str = "60"
|
||
period: str = "1 квартал 2025 г."
|
||
data_type_line: str = "Выводимые данные: БУ (данные бухгалтерского учета)"
|
||
currency_line: str = "Валюта БУ, НУ: RUB"
|
||
variant: Literal[1, 2] = 1
|
||
inn_col: int | None = 5
|
||
kpp_col: int | None = None
|
||
first_sum_col: int = 6
|
||
opening: bool = True
|
||
collapsed_closing: bool = False
|
||
hierarchy: Hierarchy = "outline"
|
||
bold_accounts: bool = False
|
||
repeat_header_after: int | None = None # после стольких строк данных повторяется заголовок (печать по страницам)
|
||
detail: str = "Контрагенты, Договоры"
|
||
detail_label: str = "Детализация по субсчетам, субконто" # метка перед перечнем субконто (вариант 2)
|
||
org_name: str | None = ORG # None — в шапке нет строки с названием организации
|
||
group_titles: dict[str, str] | None = None # названия групп сумм вместо `GROUP_TITLES`
|
||
inn_header: str = "ИНН" # подпись колонки ИНН («ИНН/КПП» — склеенная)
|
||
labels_below: bool = False # вариант 1: «Счет» и «Контрагенты» — отдельными строками под подзаголовками сумм
|
||
# Название ведомости вместо «Оборотно-сальдовая ведомость по счету … за …» (вариант 1) и его колонка (#81)
|
||
title: str | None = None
|
||
title_col: int = 1
|
||
untitled: bool = False # нет организации, названия и параметров: заголовок таблицы с `1 + top_padding` (#81)
|
||
top_padding: int = 0
|
||
# Группы, подписанные двумя ячейками со стороной («НачалоДт» | «НачалоКт») вместо названия над «Дебет/Кредит»;
|
||
# разделены все группы — строки подзаголовков нет (#81)
|
||
split_groups: dict[str, tuple[str, str]] = field(default_factory=dict)
|
||
total_text: str | None = None # подпись итоговой строки; None — «Итого»
|
||
|
||
|
||
GROUP_TITLES = {
|
||
"opening": "Сальдо на начало периода",
|
||
"turnover": "Обороты за период",
|
||
"closing": "Сальдо на конец периода",
|
||
}
|
||
PAIR_WIDTH = 2 # колонки группы сумм с подзаголовками «Дебет»/«Кредит»
|
||
DEBIT, CREDIT = "Дебет", "Кредит"
|
||
TOTAL_TEXT = "Итого"
|
||
|
||
|
||
def _groups(spec: OsvSpec) -> list[tuple[str, int, int]]:
|
||
"""Группы сумм слева направо: (ключ, первая колонка, ширина)."""
|
||
result: list[tuple[str, int, int]] = []
|
||
col = spec.first_sum_col
|
||
for key in ("opening", "turnover", "closing"):
|
||
if key == "opening" and not spec.opening:
|
||
continue
|
||
width = 1 if key == "closing" and spec.collapsed_closing else PAIR_WIDTH
|
||
result.append((key, col, width))
|
||
col += width
|
||
return result
|
||
|
||
|
||
def _header_rows(sheet: SheetSpec, spec: OsvSpec, top: int, label: str, sub_label: str) -> int:
|
||
"""Блок заголовка таблицы с `top`: строка групп и строка Дебет/Кредит; возвращает номер следующей строки.
|
||
|
||
`labels_below` (только для блока с меткой «Счет»): метки группировок и «ИНН» — отдельными строками под
|
||
Дебет/Кредит (так печатает 1С), в колонках сумм у них пусто.
|
||
"""
|
||
titles = spec.group_titles or GROUP_TITLES
|
||
below = spec.labels_below and bool(label)
|
||
groups = _groups(spec)
|
||
one_row = all(key in spec.split_groups for key, _, _ in groups) # заголовок в одну строку, без «Дебет/Кредит»
|
||
label_row = top + 2 if below else top # строка меток группировок (первая) и колонок ИНН/КПП
|
||
sheet.put(label_row, 1, label)
|
||
if not one_row:
|
||
sheet.put(label_row + 1, 1, sub_label)
|
||
service_row = label_row + 1 if below else (top if one_row else top + 1)
|
||
if spec.inn_col is not None:
|
||
sheet.put(service_row, spec.inn_col, spec.inn_header)
|
||
if spec.kpp_col is not None:
|
||
sheet.put(service_row, spec.kpp_col, "КПП")
|
||
for key, col, width in groups:
|
||
if key in spec.split_groups:
|
||
debit, credit = spec.split_groups[key]
|
||
sheet.put(top, col, debit)
|
||
sheet.put(top, col + 1, credit)
|
||
continue
|
||
sheet.put(top, col, titles[key])
|
||
sheet.merge(top, col, top, col + width - 1)
|
||
if width == PAIR_WIDTH:
|
||
sheet.put(top + 1, col, DEBIT)
|
||
sheet.put(top + 1, col + 1, CREDIT)
|
||
if below:
|
||
return top + 4
|
||
return top + 1 if one_row else top + 2
|
||
|
||
|
||
def _header_v1(sheet: SheetSpec, spec: OsvSpec) -> int:
|
||
if spec.untitled:
|
||
return _header_rows(sheet, spec, 1 + spec.top_padding, "Счет", "Контрагенты")
|
||
if spec.org_name is not None:
|
||
sheet.put(1, 1, spec.org_name)
|
||
title = spec.title or f"Оборотно-сальдовая ведомость по счету {spec.account} за {spec.period}"
|
||
sheet.put(2, spec.title_col, title)
|
||
sheet.put(4, 1, spec.data_type_line)
|
||
sheet.put(7, 1, "Параметры:")
|
||
sheet.put(7, 3, spec.currency_line)
|
||
return _header_rows(sheet, spec, 9, "Счет", "Контрагенты")
|
||
|
||
|
||
def _header_v2(sheet: SheetSpec, spec: OsvSpec) -> int:
|
||
if spec.org_name is not None:
|
||
sheet.put(1, 1, spec.org_name)
|
||
sheet.put(2, 1, f"Оборотно-сальдовая ведомость по счету {spec.account}")
|
||
sheet.put(3, 1, f"Период: {spec.period}")
|
||
sheet.put(4, 1, f"{spec.detail_label}: {spec.detail}")
|
||
sheet.put(5, 1, spec.data_type_line)
|
||
top = 7
|
||
nxt = _header_rows(sheet, spec, top, "Субконто", "")
|
||
sheet.cells.pop((top + 1, 1), None)
|
||
sheet.merge(top, 1, top + 1, 1)
|
||
return nxt
|
||
|
||
|
||
_INN_IN_NAME = {
|
||
"comma": "{name}, {inn}",
|
||
"paren_label": "{name} (ИНН {inn})",
|
||
"label": "{name} ИНН: {inn}",
|
||
"space": "{name} {inn}",
|
||
}
|
||
|
||
|
||
def _name_with_inn(node: Node) -> str:
|
||
"""Название строки; ИНН дописан в текст, если так задан `inn_style` (иначе — только название)."""
|
||
template = _INN_IN_NAME.get(node.inn_style)
|
||
return node.text if template is None or not node.inn else template.format(name=node.text, inn=node.inn)
|
||
|
||
|
||
def _row_cells( # noqa: PLR0913 - строка листа описывается полностью одним вызовом
|
||
sheet: SheetSpec, spec: OsvSpec, row: int, entry: tuple[Node, int], six: Six, *, blank: bool = False
|
||
) -> None:
|
||
"""Одна строка данных (`entry` — узел и его глубина): текст с оформлением иерархии, ИНН/КПП и суммы."""
|
||
node, depth = entry
|
||
mode = spec.hierarchy
|
||
shown = _name_with_inn(node)
|
||
text = INDENT_STEP * depth + shown if mode == "spaces" else shown
|
||
bold = spec.bold_accounts and bool(node.children())
|
||
sheet.put(row, 1, text, bold=bold, indent=depth if mode == "indent" else 0)
|
||
if mode == "outline" and depth:
|
||
sheet.outline[row] = depth
|
||
if spec.inn_col is not None and node.inn and node.inn_style in ("column", "kpp_cell"):
|
||
sheet.put(row, spec.inn_col, f"{node.inn}/{node.kpp}" if node.inn_style == "kpp_cell" else node.inn)
|
||
if spec.kpp_col is not None and node.kpp:
|
||
sheet.put(row, spec.kpp_col, node.kpp)
|
||
if blank:
|
||
return # строка без сумм (ИНН над названием)
|
||
values = list(six)
|
||
values[4] += node.closing_debit_delta
|
||
for key, col, width in _groups(spec):
|
||
index = {"opening": 0, "turnover": 2, "closing": 4}[key]
|
||
debit, credit = values[index], values[index + 1]
|
||
if width == 1: # свёрнутое сальдо: одна колонка, кредитовое — со знаком «минус»
|
||
_put_number(sheet, row, col, debit - credit, node.num_style)
|
||
else:
|
||
_put_number(sheet, row, col, debit, node.num_style)
|
||
_put_number(sheet, row, col + 1, credit, node.num_style)
|
||
|
||
|
||
NBSP = chr(0x00A0) # неразрывный пробел
|
||
NARROW_NBSP = chr(0x202F) # узкий неразрывный пробел
|
||
_SEPARATORS: dict[str, str] = {"text": " ", "text_nbsp": NBSP, "text_narrow": NARROW_NBSP}
|
||
_DASHES: dict[str, str] = {"text": "—", "text_nbsp": "–", "text_narrow": "-"}
|
||
|
||
|
||
def _text_number(value: D, style: str) -> str:
|
||
"""Число как печатает человек: «1 234 567,89», «(1 234,00)» для отрицательных, прочерк для нуля."""
|
||
if value == 0:
|
||
return _DASHES[style]
|
||
integer, fraction = f"{abs(value):.2f}".split(".")
|
||
groups: list[str] = []
|
||
while integer:
|
||
groups.insert(0, integer[-3:])
|
||
integer = integer[:-3]
|
||
body = f"{_SEPARATORS[style].join(groups)},{fraction}"
|
||
return f"({body})" if value < 0 else body
|
||
|
||
|
||
def _put_number(sheet: SheetSpec, row: int, col: int, value: D, style: str) -> None:
|
||
if style == "num":
|
||
if value != 0:
|
||
sheet.put(row, col, value)
|
||
return
|
||
sheet.put(row, col, _text_number(value, style))
|
||
|
||
|
||
def build_osv(spec: OsvSpec, root: Node, name: str = SHEET) -> SheetSpec:
|
||
"""Лист ОСВ по дереву `root` и параметрам `spec`."""
|
||
sheet = SheetSpec(name)
|
||
next_row = _header_v1(sheet, spec) if spec.variant == 1 else _header_v2(sheet, spec)
|
||
for index, (node, depth) in enumerate(flatten(root)):
|
||
if spec.repeat_header_after == index:
|
||
# Печать по страницам: пустая строка и копия блока заголовка
|
||
next_row = _header_rows(sheet, spec, next_row + 1, "Счет", "Контрагенты")
|
||
if node.inn_style == "above" and node.inn:
|
||
# ИНН отдельной строкой над названием, без сумм, на том же уровне оформления
|
||
inn_line = Node(node.inn)
|
||
_row_cells(sheet, spec, next_row, (inn_line, depth), (ZERO,) * 6, blank=True)
|
||
next_row += 1
|
||
_row_cells(sheet, spec, next_row, (node, depth), (node.twin or node).six(), blank=node.blank)
|
||
next_row += 1
|
||
total = SheetSpec("")
|
||
_row_cells(total, spec, 1, (Node(spec.total_text or TOTAL_TEXT), 0), root.six())
|
||
for (_, col), cell in total.cells.items():
|
||
sheet.cells[(next_row, col)] = CellSpec(cell.value, bold=spec.bold_accounts)
|
||
return sheet
|
||
|
||
|
||
# --- Прочие листы ---
|
||
|
||
|
||
def reference_sheet(name: str = "Справка") -> SheetSpec:
|
||
"""Посторонний лист: таблица без признаков ОСВ и форм."""
|
||
sheet = SheetSpec(name)
|
||
sheet.put(1, 1, "Справка о составе группы")
|
||
sheet.put(3, 1, "Организация")
|
||
sheet.put(3, 2, "ИНН")
|
||
sheet.put(3, 3, "Доля, %")
|
||
sheet.put(4, 1, 'ООО "Тестовая организация"')
|
||
sheet.put(4, 2, "7700000001")
|
||
sheet.put(4, 3, 100)
|
||
sheet.put(5, 1, "ООО Дочерняя")
|
||
sheet.put(5, 2, "7700000002")
|
||
sheet.put(5, 3, 51)
|
||
return sheet
|
||
|
||
|
||
def statement_sheet(name: str = "Бухгалтерский баланс") -> SheetSpec:
|
||
"""Лист формы ББ (признаки «Бухгалтерский баланс» и код формы 0710001): ОСВ-парсер его пропускает."""
|
||
sheet = SheetSpec(name)
|
||
sheet.put(1, 1, "Бухгалтерский баланс")
|
||
sheet.put(2, 1, "на 30 сентября 2025 г.")
|
||
sheet.put(3, 1, "Форма по ОКУД 0710001")
|
||
sheet.put(5, 1, "Актив")
|
||
sheet.put(5, 2, "Код")
|
||
sheet.put(5, 3, "На 30.09.2025")
|
||
sheet.put(6, 1, "Основные средства")
|
||
sheet.put(6, 2, "1150")
|
||
sheet.put(6, 3, 1200000)
|
||
sheet.put(7, 1, "Запасы")
|
||
sheet.put(7, 2, "1210")
|
||
sheet.put(7, 3, 340000)
|
||
return sheet
|
||
|
||
|
||
def table_sheet(name: str, rows: Sequence[Sequence[str | int | D | None]], *, start_row: int = 1) -> SheetSpec:
|
||
"""Лист «как есть»: строки ячеек с `start_row`, `None` — пустая ячейка (листы-негативы #81)."""
|
||
sheet = SheetSpec(name)
|
||
for row, values in enumerate(rows, start=start_row):
|
||
for col, value in enumerate(values, start=1):
|
||
if value is not None:
|
||
sheet.put(row, col, value)
|
||
return sheet
|
||
|
||
|
||
def form_sheet(name: str, *lines: str) -> SheetSpec:
|
||
"""Лист формы без таблицы: в шапке только `lines` (так проверяется каждый признак формы по отдельности)."""
|
||
sheet = SheetSpec(name)
|
||
for row, line in enumerate(lines, start=1):
|
||
sheet.put(row, 1, line)
|
||
sheet.put(len(lines) + 2, 1, "Показатель")
|
||
sheet.put(len(lines) + 2, 2, "Код")
|
||
return sheet
|
||
|
||
|
||
MERGED = "<объединено>" # ячейка текста — продолжение объединения строки выше
|
||
INDICATOR_TITLE = "Показа-\nтели"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class GridRow:
|
||
"""Строка листа «по сетке»: текст (None — пусто, MERGED — объединение с ячейкой выше), метка колонки «Показатели»,
|
||
шесть сумм (начало Д/К, обороты Д/К, конец Д/К; строки — `Decimal` в файле) и outline level."""
|
||
|
||
text: str | None
|
||
sums: tuple[str | None, str | None, str | None, str | None, str | None, str | None]
|
||
label: str | None = None
|
||
outline: int = 0
|
||
|
||
|
||
def _grid_osv( # noqa: PLR0913 - лист описывается полностью одним вызовом
|
||
*,
|
||
account: str,
|
||
group_label: str,
|
||
rows: Sequence[GridRow],
|
||
data_type_line: str | None,
|
||
indicator_column: bool,
|
||
name: str = "Лист_1",
|
||
) -> SheetSpec:
|
||
"""ОСВ, заданная строками напрямую (не деревом `Node`): колонка «Показатели», объединённые названия, соседи."""
|
||
sheet = SheetSpec(name)
|
||
sheet.put(1, 1, ORG)
|
||
sheet.put(2, 1, f"Оборотно-сальдовая ведомость по счету {account}")
|
||
sheet.put(3, 1, "Период: 3 квартал 2025 г.")
|
||
if data_type_line is not None:
|
||
sheet.put(4, 1, data_type_line)
|
||
sheet.put(5, 1, "Параметры:")
|
||
sheet.put(5, 3, "Валюта БУ, НУ: Руб")
|
||
label_col, first_sum = 4, 5
|
||
top = 7
|
||
sheet.put(top, 1, "Счет")
|
||
if indicator_column:
|
||
sheet.put(top, label_col, INDICATOR_TITLE)
|
||
sheet.merge(top, label_col, top + 1, label_col)
|
||
for index, key in enumerate(("opening", "turnover", "closing")):
|
||
col = first_sum + 2 * index
|
||
sheet.put(top, col, GROUP_TITLES[key])
|
||
sheet.merge(top, col, top, col + 1)
|
||
sheet.put(top + 1, col, DEBIT)
|
||
sheet.put(top + 1, col + 1, CREDIT)
|
||
sheet.put(top + 1, 1, group_label)
|
||
for row, item in enumerate(rows, start=top + 2):
|
||
if item.text == MERGED:
|
||
sheet.merge(row - 1, 1, row, 1)
|
||
elif item.text:
|
||
sheet.put(row, 1, item.text)
|
||
if item.label is not None:
|
||
sheet.put(row, label_col, item.label)
|
||
if item.outline:
|
||
sheet.outline[row] = item.outline
|
||
for offset, value in enumerate(item.sums):
|
||
if value is not None:
|
||
sheet.put(row, first_sum + offset, D(value))
|
||
return sheet
|
||
|
||
|
||
# --- Файлы ---
|
||
|
||
|
||
def _s01() -> list[SheetSpec]:
|
||
def contract(text: str, opening: Pair, turnover: Pair) -> Node:
|
||
return leaf(text, opening, turnover)
|
||
|
||
first = Node(
|
||
"ООО Альфа",
|
||
[
|
||
contract("Договор поставки № 15 от 10.01.2026", pair(None, "100000.00"), pair("60000.00", "150000.00")),
|
||
contract("Договор № 7 от 01.02.2026", pair(None, "20000.50"), pair("0.50", "30000.00")),
|
||
],
|
||
inn="7701234567",
|
||
)
|
||
second = Node(
|
||
"ООО Бета",
|
||
[contract("Договор подряда № 3 от 15.01.2026", pair("15000.00", None), pair("90000.00", "40000.00"))],
|
||
inn="7707654321",
|
||
)
|
||
third = Node(
|
||
"ООО Гамма",
|
||
[contract("Договор аренды № 2 от 09.01.2026", pair("120000.00", None), pair("30000.00", "100000.00"))],
|
||
inn="5001234567",
|
||
)
|
||
root = Node("60", [Node("60.01", [first, second]), Node("60.02", [third])])
|
||
spec = OsvSpec(
|
||
period="1 квартал 2026 г.",
|
||
data_type_line="Выводимые данные: Сумма",
|
||
variant=2,
|
||
inn_col=5,
|
||
first_sum_col=6,
|
||
)
|
||
return [build_osv(spec, root)]
|
||
|
||
|
||
def _s02() -> list[SheetSpec]:
|
||
spec = OsvSpec(
|
||
period="2 квартал 2025 г.", currency_line="Валюта БУ, НУ: руб.", hierarchy="indent", inn_col=5, first_sum_col=7
|
||
)
|
||
return [build_osv(spec, base_tree())]
|
||
|
||
|
||
def _s03() -> list[SheetSpec]:
|
||
spec = OsvSpec(period="I кв. 2025 г.", hierarchy="none", bold_accounts=True, inn_col=5, first_sum_col=7)
|
||
return [build_osv(spec, base_tree())]
|
||
|
||
|
||
def _s04() -> list[SheetSpec]:
|
||
first = alfa()
|
||
first.kids = [
|
||
leaf("USD", pair(None, "2500.00"), pair("1800.00", "4200.00"), indicator=True),
|
||
leaf("Кол.", pair("10", None), pair("5", "3"), indicator=True),
|
||
]
|
||
second = beta()
|
||
second.kids = [leaf("Вал.", pair("1000.00", None), pair(None, "400.00"), indicator=True)]
|
||
root = Node("60", [Node("60.01", [first, second]), Node("60.02", [gamma()])])
|
||
return [build_osv(OsvSpec(period="3 квартал 2025 г.", inn_col=5, first_sum_col=7), root)]
|
||
|
||
|
||
def _s05() -> list[SheetSpec]:
|
||
first = leaf(
|
||
"ООО Альфа",
|
||
pair("1234567.89", None),
|
||
pair("500.00", "-1234.00"),
|
||
inn="7701234567",
|
||
num_style="text",
|
||
)
|
||
second = beta(num_style="text_nbsp")
|
||
third = gamma(num_style="text_narrow")
|
||
root = Node("60", [Node("60.01", [first, second]), Node("60.02", [third, delta()])])
|
||
return [build_osv(OsvSpec(period="полугодие 2025 г.", inn_col=4, first_sum_col=5), root)]
|
||
|
||
|
||
def _s06() -> list[SheetSpec]:
|
||
first = build_osv(OsvSpec(account="60", inn_col=5, first_sum_col=6), base_tree(), "ОСВ 60")
|
||
second = build_osv(OsvSpec(account="62", inn_col=3, first_sum_col=4), receivables_tree(), "ОСВ 62")
|
||
return [first, reference_sheet(), second]
|
||
|
||
|
||
def _s07() -> list[SheetSpec]:
|
||
# Смешанные знаки у детей (кредитовое у Альфы, дебетовое у Беты) намеренно проверяют правило R-21: свёрнутое сальдо
|
||
# родителя сверяется по нетто, а не по сумме модулей
|
||
first = leaf("ООО Альфа", pair(None, None), pair("100000.00", "350000.50"), inn="7701234567")
|
||
second = leaf("ООО Бета", pair(None, None), pair("60000.00", "10000.00"), inn="7707654321")
|
||
third = leaf("ООО Гамма", pair(None, None), pair("20000.00", "20000.00"), inn="5001234567")
|
||
fourth = leaf("ООО Дельта", pair(None, None), pair("1000.00", "33000.40"), inn="7812345678")
|
||
root = Node("60", [Node("60.01", [first, second]), Node("60.02", [third, fourth])])
|
||
spec = OsvSpec(period="9 месяцев 2025 г.", opening=False, collapsed_closing=True, inn_col=5, first_sum_col=7)
|
||
return [build_osv(spec, root)]
|
||
|
||
|
||
def _s08() -> list[SheetSpec]:
|
||
spec = OsvSpec(period="период с 01.01.2025 по 31.03.2025", inn_col=5, kpp_col=6, first_sum_col=7)
|
||
return [build_osv(spec, base_tree(kpp=True))]
|
||
|
||
|
||
def _s09() -> list[SheetSpec]:
|
||
named = Node("60.01, Расчеты с поставщиками и подрядчиками", [alfa(), beta()])
|
||
advances = Node("60.02, Расчеты по авансам выданным", [gamma()])
|
||
letter = Node("60.АВ, Расчеты по авансам (буквенный субсчёт)", [delta()])
|
||
root = Node("60", [named, advances, letter])
|
||
return [build_osv(OsvSpec(period="4 квартал 2025 г.", inn_col=5, first_sum_col=7), root)]
|
||
|
||
|
||
def _s10() -> list[SheetSpec]:
|
||
tree = base_tree()
|
||
broken = tree.kids[0].kids[1]
|
||
assert broken.text == "ООО Бета"
|
||
broken.closing_debit_delta = D(1)
|
||
return [build_osv(OsvSpec(inn_col=5, first_sum_col=7), tree)]
|
||
|
||
|
||
def _s11() -> list[SheetSpec]:
|
||
spec = OsvSpec(period="12 месяцев 2025 г.", repeat_header_after=4, inn_col=5, first_sum_col=7)
|
||
return [build_osv(spec, base_tree())]
|
||
|
||
|
||
def _s12() -> list[SheetSpec]:
|
||
spec = OsvSpec(
|
||
period="январь - сентябрь 2025 г.",
|
||
data_type_line="Выводимые данные: НУ (данные налогового учета)",
|
||
inn_col=5,
|
||
first_sum_col=7,
|
||
)
|
||
return [statement_sheet(), build_osv(spec, base_tree(), "ОСВ 60")]
|
||
|
||
|
||
def _s13() -> list[SheetSpec]:
|
||
spec = OsvSpec(
|
||
period="2 квартал 2025 г.", currency_line="Валюта БУ, НУ: руб.", hierarchy="spaces", inn_col=5, first_sum_col=7
|
||
)
|
||
return [build_osv(spec, base_tree())]
|
||
|
||
|
||
def _s14() -> list[SheetSpec]:
|
||
"""Три листа форм, опознаваемые каждый одним своим признаком (заголовки и ОКУД словаря форм), и лист ОСВ."""
|
||
return [
|
||
form_sheet("Форма 1", "Форма по ОКУД 0710001"),
|
||
form_sheet("ОФР", "Отчет о финансовых результатах"),
|
||
form_sheet("Форма 2", "Форма по ОКУД 0710002"),
|
||
build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree(), "ОСВ 60"),
|
||
]
|
||
|
||
|
||
def _s15() -> list[SheetSpec]:
|
||
"""Названия групп сумм: «Входящее/Конечное сальдо» на первом листе, «Начальное/Исходящее сальдо» на втором."""
|
||
first = OsvSpec(
|
||
account="60",
|
||
inn_col=5,
|
||
first_sum_col=7,
|
||
group_titles={"opening": "Входящее сальдо", "turnover": "Обороты", "closing": "Конечное сальдо"},
|
||
)
|
||
second = OsvSpec(
|
||
account="62",
|
||
inn_col=3,
|
||
first_sum_col=4,
|
||
group_titles={"opening": "Начальное сальдо", "turnover": "Обороты", "closing": "Исходящее сальдо"},
|
||
)
|
||
return [build_osv(first, base_tree(), "ОСВ 60"), build_osv(second, receivables_tree(), "ОСВ 62")]
|
||
|
||
|
||
def _s16() -> list[SheetSpec]:
|
||
"""Шапка без названия организации; период — только год; валюта с пояснением; метки группировок отдельными
|
||
строками («Счет» среди них); перечень «Детализация: …» без слова «субконто» и группировки вне ключевых."""
|
||
first = OsvSpec(
|
||
account="60",
|
||
period="2024",
|
||
currency_line="Валюта БУ, НУ: RUB (код 643)",
|
||
org_name=None,
|
||
labels_below=True,
|
||
inn_col=5,
|
||
first_sum_col=7,
|
||
)
|
||
second = OsvSpec(
|
||
account="62",
|
||
variant=2,
|
||
period="2024",
|
||
org_name=None,
|
||
detail_label="Детализация по субсчетам",
|
||
detail="Номенклатура, Статьи затрат, Нематериальные активы, Валюта",
|
||
inn_col=None,
|
||
first_sum_col=3,
|
||
)
|
||
return [build_osv(first, base_tree(), "ОСВ 60"), build_osv(second, receivables_tree(), "ОСВ 62")]
|
||
|
||
|
||
def _s33() -> list[SheetSpec]:
|
||
"""Колонка «Показатели» БУ/НУ (AURORA-1613): счёт 02, шапка без «Выводимые данные», название объединено на строки
|
||
БУ и НУ, «Итого» по БУ и по НУ. Строки НУ — показатели строки БУ над ними."""
|
||
rows = [
|
||
GridRow("02", (None, "300000.00", "20000.00", "30000.00", None, "310000.00"), "БУ"),
|
||
GridRow(None, (None, "300000.02", "20000.00", "30000.03", None, "310000.05"), "НУ"),
|
||
GridRow("Печь подовая", (None, "100000.00", "20000.00", "10000.00", None, "90000.00"), "БУ", 1),
|
||
GridRow(MERGED, (None, "100000.02", "20000.00", "10000.01", None, "90000.03"), "НУ", 1),
|
||
GridRow("Плита индукционная", (None, "200000.00", None, "20000.00", None, "220000.00"), "БУ", 1),
|
||
GridRow(None, (None, "200000.00", None, "20000.02", None, "220000.02"), "НУ", 1),
|
||
GridRow("Итого", (None, "300000.00", "20000.00", "30000.00", None, "310000.00"), "БУ"),
|
||
GridRow(MERGED, (None, "300000.02", "20000.00", "30000.03", None, "310000.05"), "НУ"),
|
||
]
|
||
return [
|
||
_grid_osv(account="02", group_label="Основные средства", rows=rows, data_type_line=None, indicator_column=True)
|
||
]
|
||
|
||
|
||
def _s34() -> list[SheetSpec]:
|
||
"""Колонка «Показатели»: «Вал.», «Кол.», «ПР», «ВР» и неизвестная метка «Ед.» — показатели вне сверки родителя."""
|
||
rows = [
|
||
GridRow("60", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00"), "БУ"),
|
||
GridRow("ООО Альфа", ("1000.00", None, "500.00", "300.00", "1200.00", None), "БУ", 1),
|
||
GridRow(None, ("12.50", None, "6.00", "3.00", "15.50", None), "Вал.", 1),
|
||
GridRow(None, ("3", None, None, None, "3", None), "Кол.", 1),
|
||
GridRow("ООО Бета", (None, "400.00", "100.00", None, None, "300.00"), "БУ", 1),
|
||
GridRow(None, (None, None, "10.00", None, "10.00", None), "ПР", 1),
|
||
GridRow(None, (None, "5.00", None, None, None, "5.00"), "ВР", 1),
|
||
GridRow(None, ("1", None, None, None, "1", None), "Ед.", 1),
|
||
GridRow("Итого", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00"), "БУ"),
|
||
]
|
||
line = "Выводимые данные: БУ (данные бухгалтерского учета)"
|
||
return [_grid_osv(account="60", group_label="Контрагенты", rows=rows, data_type_line=line, indicator_column=True)]
|
||
|
||
|
||
def _s35() -> list[SheetSpec]:
|
||
"""«Валюта <код>» — строка в рублях, «В валюте» — показатель (AURORA-1661): пары на одном outline-уровне."""
|
||
rows = [
|
||
GridRow("60", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00")),
|
||
GridRow("60.21", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00"), outline=1),
|
||
GridRow("ООО Альфа", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00"), outline=2),
|
||
GridRow("Валюта USD", ("9000.00", None, "1000.00", None, "10000.00", None), outline=3),
|
||
GridRow("В валюте", ("100.00", None, "11.00", None, "111.00", None), outline=3),
|
||
GridRow("Валюта EUR", (None, "2000.00", None, "500.00", None, "2500.00"), outline=3),
|
||
GridRow("В валюте", (None, "20.00", None, "5.00", None, "25.00"), outline=3),
|
||
GridRow("Итого", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00")),
|
||
]
|
||
line = "Выводимые данные: БУ (данные бухгалтерского учета)"
|
||
return [_grid_osv(account="60", group_label="Контрагенты", rows=rows, data_type_line=line, indicator_column=False)]
|
||
|
||
|
||
def _s36() -> list[SheetSpec]:
|
||
"""Шапка без «Выводимые данные», колонки «Показатели» нет: строки — BU по умолчанию с предупреждением."""
|
||
rows = [
|
||
GridRow("60", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00")),
|
||
GridRow("ООО Альфа", ("1000.00", None, "500.00", "300.00", "1200.00", None), outline=1),
|
||
GridRow("ООО Бета", (None, "400.00", "100.00", None, None, "300.00"), outline=1),
|
||
GridRow("Итого", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00")),
|
||
]
|
||
return [_grid_osv(account="60", group_label="Контрагенты", rows=rows, data_type_line=None, indicator_column=False)]
|
||
|
||
|
||
# --- Запись файлов ---
|
||
|
||
|
||
_MODIFIED_RE = re.compile(rb"(<dcterms:modified[^>]*>)[^<]*(</dcterms:modified>)")
|
||
_CORE_PART = "docProps/core.xml"
|
||
_FIXED_W3CDTF = FIXED_TIME.strftime("%Y-%m-%dT%H:%M:%SZ").encode()
|
||
|
||
|
||
def _s17() -> list[SheetSpec]:
|
||
"""ИНН в тексте названия контрагента (OQ-100): запятая, «(ИНН …)», «ИНН: …», пробел; колонки ИНН нет."""
|
||
first = alfa(inn_style="comma")
|
||
second = beta(inn_style="paren_label")
|
||
third = gamma(inn_style="label")
|
||
fourth = sidorov()
|
||
fourth.inn_style = "space"
|
||
root = Node("60", [Node("60.01", [first, second, ivanov()]), Node("60.02", [third, fourth])])
|
||
return [build_osv(OsvSpec(period="1 квартал 2025 г.", inn_col=None, first_sum_col=5), root)]
|
||
|
||
|
||
def _s18() -> list[SheetSpec]:
|
||
"""ИНН отдельной строкой над названием контрагента (OQ-130): строка без сумм, того же уровня; колонки ИНН нет.
|
||
ИНН с верной контрольной суммой: без подсказки в шапке другой не склеивается (#35)."""
|
||
first = alfa(inn_style="above")
|
||
first.inn = "7701234560"
|
||
third = gamma(inn_style="above")
|
||
third.inn = "5001234561"
|
||
root = Node("60", [Node("60.01", [first, beta(), ivanov()]), Node("60.02", [third])])
|
||
return [build_osv(OsvSpec(period="2 квартал 2025 г.", inn_col=None, first_sum_col=5), root)]
|
||
|
||
|
||
def _s19() -> list[SheetSpec]:
|
||
"""Колонка «ИНН/КПП» со склеенным значением «7701234567/770101001» в одной ячейке (OQ-100)."""
|
||
items = [alfa(inn_style="kpp_cell"), beta(inn_style="kpp_cell"), ivanov()]
|
||
for node in items:
|
||
node.kpp = KPP.get(node.text)
|
||
root = Node("60", [Node("60.01", items), Node("60.02", [gamma(), delta()])])
|
||
spec = OsvSpec(period="3 квартал 2025 г.", inn_col=5, inn_header="ИНН/КПП", first_sum_col=6)
|
||
return [build_osv(spec, root)]
|
||
|
||
|
||
InnLayout = Literal["above_sibling", "above_parent", "below_child", "below_sibling"]
|
||
|
||
|
||
def inn_rows_tree(node: Node, layout: InnLayout, *, sums: bool) -> Node:
|
||
"""То же дерево, где ИНН каждого контрагента выведен отдельной строкой в раскладке `layout` (#35): над / под
|
||
наименованием, в дереве (родитель / ребёнок) или соседом; `sums` — у строки ИНН те же суммы, иначе строка пустая.
|
||
Колонки ИНН нет, в названии ИНН не пишется."""
|
||
kids: list[Node] = []
|
||
for kid in node.kids:
|
||
kid = inn_rows_tree(kid, layout, sums=sums) # noqa: PLW2901 - узел заменяется своей копией со строками ИНН
|
||
if kid.inn is None or kid.indicator:
|
||
kids.append(kid)
|
||
continue
|
||
inn, kid.inn = kid.inn, None
|
||
if layout == "above_sibling":
|
||
kids += [Node(inn, twin=kid, blank=not sums), kid]
|
||
elif layout == "below_sibling":
|
||
kids += [kid, Node(inn, twin=kid, blank=not sums)]
|
||
elif layout == "above_parent":
|
||
kids.append(Node(inn, [kid], blank=not sums))
|
||
else:
|
||
kid.kids = [Node(inn, kid.kids, opening=kid.opening, turnover=kid.turnover, blank=not sums)]
|
||
kids.append(kid)
|
||
node.kids = kids
|
||
return node
|
||
|
||
|
||
def _inn_layout_tree() -> Node:
|
||
"""Счёт 60 для раскладок строк ИНН (#35): ИНН с верной контрольной суммой (10 и 12 цифр), физлицо без ИНН."""
|
||
first = alfa()
|
||
first.inn = "7701234560"
|
||
third = gamma()
|
||
third.inn = "5001234561"
|
||
return Node("60", [Node("60.01", [first, beta(), ivanov()]), Node("60.02", [third, sidorov()])])
|
||
|
||
|
||
def _s29() -> list[SheetSpec]:
|
||
"""xls BIFF5 без записи CODEPAGE, строки в cp1251 (AURORA-1662): отступ — пробелами, outline в xls-писателе нет."""
|
||
return [build_osv(OsvSpec(hierarchy="spaces", inn_col=5, first_sum_col=7), base_tree())]
|
||
|
||
|
||
def _s30() -> list[SheetSpec]:
|
||
"""Счёт 10: материал «Вал» (без точки) — обычная номенклатура, не валютная строка; «Кол-во» — строка количества
|
||
(AURORA-1663). «Вал.» с точкой и код валюты остаются метками (s04)."""
|
||
shaft = leaf("Вал", pair("1500.00", None), pair("300.00", "100.00"))
|
||
shaft.kids = [leaf("Кол-во", pair("10", None), pair("2", "1"), indicator=True)]
|
||
bolt = leaf("Болт М8", pair("200.00", None), pair("50.00", None))
|
||
bolt.kids = [leaf("Кол-во", pair("400", None), pair("100", None), indicator=True)]
|
||
root = Node("10", [Node("10.1", [shaft, bolt])])
|
||
spec = OsvSpec(
|
||
account="10",
|
||
variant=2,
|
||
period="2 квартал 2025 г.",
|
||
detail="Номенклатура",
|
||
detail_label="Детализация по субсчетам, субконто",
|
||
inn_col=None,
|
||
first_sum_col=3,
|
||
)
|
||
return [build_osv(spec, root)]
|
||
|
||
|
||
def _s31() -> list[SheetSpec]:
|
||
"""Рабочий файл аналитика (AURORA-1664): под «Итого» дописаны подпись «Ответственный:» и блок с числами, в том
|
||
числе строка, начинающаяся с «Итого»; таблица кончается на первой «Итого», хвост вне документа."""
|
||
sheet = build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree())
|
||
last = max(row for row, _ in sheet.cells)
|
||
sheet.put(last + 2, 1, "Ответственный:")
|
||
sheet.put(last + 2, 3, "Иванова И. И.")
|
||
sheet.put(last + 4, 1, "ООО Альфа (ГК)")
|
||
sheet.put(last + 4, 7, D("420000.00"))
|
||
sheet.put(last + 4, 8, D("250000.50"))
|
||
sheet.put(last + 5, 1, "Итого по ГК")
|
||
sheet.put(last + 5, 7, D("420000.00"))
|
||
sheet.put(last + 5, 8, D("250000.50"))
|
||
return [sheet]
|
||
|
||
|
||
def _s32() -> list[SheetSpec]:
|
||
"""Итоговая строка с подписью «Итого по счету 60» вместо точного «Итого» (I-1): правило `total_row` (начало текста)
|
||
действует, когда точного «Итого» на листе нет."""
|
||
sheet = build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree())
|
||
last = max(row for row, _ in sheet.cells)
|
||
sheet.cells[(last, 1)] = CellSpec("Итого по счету 60", bold=sheet.cells[(last, 1)].bold)
|
||
return [sheet]
|
||
|
||
|
||
FlatRow = tuple[str, tuple[str | None, ...], int]
|
||
|
||
|
||
def _flat_sheet(texts: dict[int, str], group_row: int, rows: Sequence[FlatRow]) -> SheetSpec:
|
||
"""Плоская форма 1С (#59): колонка A — текст, B…G — суммы «начало / обороты / конец» × «Дебет | Кредит»; заголовок
|
||
«Субконто» + группы (объединения B:C, D:E, F:G) в `group_row`, подзаголовки под ним, данные сразу ниже. `texts` —
|
||
строки шапки над таблицей; в `rows` пустая сумма (None) — нет ячейки, отступ — выравнивание ячейки."""
|
||
sheet = SheetSpec(SHEET)
|
||
for row, text in texts.items():
|
||
sheet.put(row, 1, text)
|
||
sheet.put(group_row, 1, "Субконто")
|
||
for index, key in enumerate(("opening", "turnover", "closing")):
|
||
col = 2 + index * PAIR_WIDTH
|
||
sheet.put(group_row, col, GROUP_TITLES[key])
|
||
sheet.merge(group_row, col, group_row, col + 1)
|
||
sheet.put(group_row + 1, col, DEBIT)
|
||
sheet.put(group_row + 1, col + 1, CREDIT)
|
||
for offset, (text, sums, indent) in enumerate(rows):
|
||
row = group_row + 2 + offset
|
||
sheet.put(row, 1, text, indent=indent)
|
||
for col, value in enumerate(sums, start=2):
|
||
if value is not None:
|
||
sheet.put(row, col, D(value))
|
||
return sheet
|
||
|
||
|
||
def _s40() -> list[SheetSpec]:
|
||
"""Плоская форма (#59): счёт в заголовке «по счету: 76.Н.1» и вид аналитики «Контрагенты» строкой ниже, данные без
|
||
outline и отступов; «Итого развернутое» — сумма строк, «Итого» — свёрнутое сальдо."""
|
||
texts = {
|
||
2: "Оборотно-сальдовая ведомость по счету: 76.Н.1",
|
||
3: "Контрагенты",
|
||
4: "за 2025 г.",
|
||
5: 'ООО "Товарное хозяйство"',
|
||
7: "Единица измерения: рубль (код по ОКЕИ 383)",
|
||
}
|
||
rows: list[FlatRow] = [
|
||
("Август - Агро ООО", ("1000.00", None, "500.00", "300.00", "1200.00", None), 0),
|
||
("АВИС ООО", (None, "200.00", "100.00", "400.00", None, "500.00"), 0),
|
||
("Агроаспект ООО", (None, None, "50.00", "50.00", None, None), 0),
|
||
("Итого развернутое", ("1000.00", "200.00", None, None, "1200.00", "500.00"), 0),
|
||
("Итого", ("800.00", None, "650.00", "750.00", "700.00", None), 0),
|
||
]
|
||
return [_flat_sheet(texts, 9, rows)]
|
||
|
||
|
||
def _s41() -> list[SheetSpec]:
|
||
"""Плоская форма (#59): вид аналитики «Организации», период «за 2 Квартал 2026 г.» со словами «План счетов …»."""
|
||
texts = {
|
||
2: "Оборотно-сальдовая ведомость по счету: 60",
|
||
3: "Организации",
|
||
4: "за 2 Квартал 2026 г. План счетов ООО Хреновская Нива",
|
||
}
|
||
rows: list[FlatRow] = [
|
||
("Автодор-Платные Дороги ООО", ("820.21", None, "5000.00", "4991.00", "829.21", None), 0),
|
||
("АГРОПРОФИ ООО", (None, "2763.79", None, None, None, "2763.79"), 0),
|
||
("Итого развернутое", ("820.21", "2763.79", None, None, "829.21", "2763.79"), 0),
|
||
("Итого", (None, "1943.58", "5000.00", "4991.00", None, "1934.58"), 0),
|
||
]
|
||
return [_flat_sheet(texts, 5, rows)]
|
||
|
||
|
||
def _s42() -> list[SheetSpec]:
|
||
"""Плоская форма (#59): под счётом 76 идут субсчета «76.2», «76.5», «76.Н», «76.Н.1»; «Итого развернутое» перед
|
||
«Итого»."""
|
||
texts = {
|
||
2: "Оборотно-сальдовая ведомость по счету: 76",
|
||
3: "за 2025 г.",
|
||
4: 'ООО "Товарное хозяйство"',
|
||
6: "Единица измерения: рубль (код по ОКЕИ 383)",
|
||
}
|
||
rows: list[FlatRow] = [
|
||
("76.2", ("1000.00", None, None, "1000.00", None, None), 0),
|
||
("76.5", (None, "300.00", "700.00", "600.00", None, "200.00"), 0),
|
||
("76.Н", (None, None, "5000.00", "5000.00", None, None), 0),
|
||
("76.Н.1", (None, None, "5000.00", "5000.00", None, None), 0),
|
||
("Итого развернутое", ("1000.00", "300.00", None, None, None, "200.00"), 0),
|
||
("Итого", ("700.00", None, "5700.00", "6600.00", None, "200.00"), 0),
|
||
]
|
||
return [_flat_sheet(texts, 8, rows)]
|
||
|
||
|
||
def _s43_s44(account: str, root: str, kids: tuple[str, str]) -> list[SheetSpec]:
|
||
"""Счёт с отступами: корень-субсчёт (без отступа) и два элемента аналитики с отступом 1, без вида аналитики в шапке;
|
||
«Итого» перед «Итого развернутое»."""
|
||
texts = {
|
||
1: 'ООО "Октябрьское"',
|
||
2: f"Оборотно-сальдовая ведомость по счету {account}",
|
||
3: "Период: 9 месяцев 2024 г.",
|
||
}
|
||
rows: list[FlatRow] = [
|
||
(root, ("34618000.00", None, "4236500.00", "38854500.00", None, None), 0),
|
||
(kids[0], ("34618000.00", None, None, "34618000.00", None, None), 1),
|
||
(kids[1], (None, None, "4236500.00", "4236500.00", None, None), 1),
|
||
("Итого", ("34618000.00", None, "4236500.00", "38854500.00", None, None), 0),
|
||
("Итого развернутое", ("34618000.00", None, "4236500.00", "38854500.00", None, None), 0),
|
||
]
|
||
return [_flat_sheet(texts, 5, rows)]
|
||
|
||
|
||
def _s43() -> list[SheetSpec]:
|
||
"""Плоская форма (#59): счёт 60, контрагенты без вида аналитики в шапке (подстроки с отступом)."""
|
||
return _s43_s44("60", "60.01", ("ИП Иванов А.А.", 'ООО "ЭСТЕТ ОМСК"'))
|
||
|
||
|
||
def _s44() -> list[SheetSpec]:
|
||
"""То же, что s43, но счёт 10: номенклатура вместо контрагентов."""
|
||
return _s43_s44("10", "10.01", ("Мука пшеничная", "Сахар-песок"))
|
||
|
||
|
||
def _put_by_counterparty_head(sheet: SheetSpec) -> None:
|
||
"""Первые строки выгрузки 1С «по контрагентам»: организация, название, «За период | диапазон» (s45, s46)."""
|
||
sheet.put(1, 1, 'ООО "Тестовая организация"')
|
||
sheet.put(1, 2, "Оборотно-сальдовая ведомость")
|
||
sheet.put(2, 1, "15 июля 2025(18:40)")
|
||
sheet.put(2, 2, "За период")
|
||
sheet.put(2, 3, "01.01.25..30.06.25")
|
||
|
||
|
||
def _s45() -> list[SheetSpec]:
|
||
"""Выгрузка 1С «ОСВ по счёту 60 по контрагентам» (#71, AURORA-1714): счёт в шапке — «Счет Поставщика» | «60*» в
|
||
соседних ячейках («Счет Клиента» без значения), без слов «по счету»; в первой колонке голые числовые коды
|
||
контрагентов."""
|
||
rows: list[FlatRow] = [
|
||
("441", (None, "108000.00", "108000.00", None, None, None), 0),
|
||
("431", (None, "200.00", "200.00", None, None, None), 0),
|
||
("199", (None, "50.00", "50.00", None, None, None), 0),
|
||
("Итого", (None, "108250.00", "108250.00", None, None, None), 0),
|
||
]
|
||
sheet = _flat_sheet({}, 6, rows)
|
||
_put_by_counterparty_head(sheet)
|
||
sheet.put(3, 2, "Счет Поставщика")
|
||
sheet.put(3, 3, "60*")
|
||
sheet.put(4, 2, "Счет Клиента")
|
||
return [sheet]
|
||
|
||
|
||
def _s46() -> list[SheetSpec]:
|
||
"""Выгрузка 1С «ОСВ по счёту 60 по контрагентам» целиком (#71, AURORA-1714): «За период | 01.01.25..30.06.25»,
|
||
«Счет Поставщика | 60*», вид субконто «Поставщики» в колонке суммы первой строки данных; блоки субсчетов 60.111 и
|
||
60.112 — код контрагента с суммами, строка названия без сумм, договоры (в том числе «441»), «ИТОГ по <субсчёт>»,
|
||
в конце «ИТОГ»."""
|
||
table: list[tuple[str, tuple[str | None, ...]]] = [
|
||
("60.111", (None,) * 6),
|
||
("П005482", (None, "100.00", "100.00", None, None, None)),
|
||
("ВЕСТ БИР ООО", (None,) * 6),
|
||
("ДП87825;З-139/21-1", (None, "100.00", "100.00", None, None, None)),
|
||
("П002913", (None, "50.00", "70.00", "20.00", None, None)),
|
||
("М ДИСТРИБЬЮШН", (None,) * 6),
|
||
("ДП60469;З-46/20", (None, "20.00", "30.00", "10.00", None, None)),
|
||
("441", (None, "30.00", "40.00", "10.00", None, None)),
|
||
("ИТОГ по 60.111", (None, "150.00", "170.00", "20.00", None, None)),
|
||
("60.112", (None,) * 6),
|
||
("П003258", (None, "10.00", "10.00", None, None, None)),
|
||
('ООО "РОМАШКА"', (None,) * 6),
|
||
("ДП1;1", (None, "10.00", "10.00", None, None, None)),
|
||
("ИТОГ по 60.112", (None, "10.00", "10.00", None, None, None)),
|
||
("ИТОГ", (None, "160.00", "180.00", "20.00", None, None)),
|
||
]
|
||
sheet = _flat_sheet({}, 6, [])
|
||
_put_by_counterparty_head(sheet)
|
||
sheet.put(3, 2, "Счет Поставщика")
|
||
sheet.put(3, 3, "60*")
|
||
sheet.put(4, 2, "Счет Клиента")
|
||
sheet.put(8, 3, "Поставщики")
|
||
for offset, (text, sums) in enumerate(table):
|
||
row = 9 + offset
|
||
sheet.put(row, 1, text)
|
||
for col, value in enumerate(sums, start=2):
|
||
if value is not None:
|
||
sheet.put(row, col, D(value))
|
||
return [sheet]
|
||
|
||
|
||
def _s47() -> list[SheetSpec]:
|
||
"""Иерархия по отступам там, где отступа не хватает (AURORA-1858): три листа «Субконто» без вида аналитики в шапке.
|
||
Лист «68»: подуровень «68.01.1» на том же отступе, что «68.01». Лист «90»: безымянная строка закрытия сразу за
|
||
счётом (пустая ячейка отступа не хранит). Лист «91»: аналитика на одном отступе, а вложена по суммам
|
||
(100 = 60 + 40; 30 = 20 + 10; 40 ⊃ 40)."""
|
||
head = {1: 'ООО "Октябрьское"', 3: "Период: 9 месяцев 2024 г."}
|
||
|
||
def sheet(account: str, rows: Sequence[FlatRow]) -> SheetSpec:
|
||
spec = _flat_sheet({**head, 2: f"Оборотно-сальдовая ведомость по счету {account}"}, 5, rows)
|
||
spec.name = f"{account} сч."
|
||
return spec
|
||
|
||
tax = (None, None, "2333.00", "2624.00", None, "291.00")
|
||
tax_old = (None, "15687.00", "45332.00", "45332.00", None, "15687.00")
|
||
rows_68: list[FlatRow] = [
|
||
("68.01", tax, 0),
|
||
("68.01.1", tax, 0),
|
||
("Налог (взносы): начислено / уплачено", tax, 1),
|
||
("68.10", tax_old, 0),
|
||
("Налог (взносы): начислено / уплачено", tax_old, 1),
|
||
("Итого", (None, "15687.00", "47665.00", "47956.00", None, "15978.00"), 0),
|
||
]
|
||
income = (None, None, None, "300.00", None, "300.00")
|
||
expense = (None, None, "200.00", None, "200.00", None)
|
||
closing = (None, None, "500.00", "300.00", "200.00", None)
|
||
rows_90: list[FlatRow] = [
|
||
("90.01", income, 0),
|
||
("90.01.1", income, 0),
|
||
("Аренда", (None, None, None, "200.00", None, None), 1),
|
||
("Прочие", (None, None, None, "100.00", None, None), 1),
|
||
("90.07", expense, 0),
|
||
("90.07.1", expense, 0),
|
||
("Расходы по аренде", (None, None, "200.00", None, None, None), 1),
|
||
("90.09", closing, 0),
|
||
("", (None, None, "500.00", "300.00", None, None), 0),
|
||
("Итого", (None, None, "700.00", "600.00", "400.00", "300.00"), 0),
|
||
]
|
||
|
||
def turn(debit: str | None, credit: str | None) -> tuple[str | None, ...]:
|
||
return (None, None, debit, credit, None, None)
|
||
|
||
rows_91: list[FlatRow] = [
|
||
("91.01", (None, None, None, "100.00", None, "100.00"), 0),
|
||
("Проценты к получению", turn(None, "70.00"), 1),
|
||
("Проценты по вкладам", turn(None, "70.00"), 1),
|
||
("Прочие доходы", turn(None, "30.00"), 1),
|
||
("Пени", turn(None, "20.00"), 1),
|
||
("Штрафы", turn(None, "10.00"), 1),
|
||
("91.02", (None, None, "170.00", None, "170.00", None), 0),
|
||
("91.02.1", (None, None, "170.00", None, "170.00", None), 0),
|
||
("Проценты к уплате", turn("100.00", None), 1),
|
||
("Проценты по займам", turn("60.00", None), 1),
|
||
("Проценты по кредитам", turn("40.00", None), 1),
|
||
("Прочие расходы", turn("30.00", None), 1),
|
||
("Затраты на нужды", turn("20.00", None), 1),
|
||
("Штрафы по договорам", turn("10.00", None), 1),
|
||
("Операционные расходы", turn("40.00", None), 1),
|
||
("Услуги банка", turn("40.00", None), 1),
|
||
("91.09", (None, None, None, "70.00", None, "70.00"), 0),
|
||
("", turn(None, "70.00"), 0),
|
||
("Итого", (None, None, "170.00", "170.00", "170.00", "170.00"), 0),
|
||
]
|
||
return [sheet("68", rows_68), sheet("90", rows_90), sheet("91", rows_91)]
|
||
|
||
|
||
def _s28() -> list[SheetSpec]:
|
||
"""Раскладка s20 («над, не в дереве, без сумм»), но ИНН ООО Альфа с опечаткой: контрольная сумма не сходится."""
|
||
tree = _inn_layout_tree()
|
||
tree.kids[0].kids[0].inn = "7701234567"
|
||
root = inn_rows_tree(tree, "above_sibling", sums=False)
|
||
return [build_osv(OsvSpec(period="1 квартал 2025 г.", inn_col=None, first_sum_col=5), root)]
|
||
|
||
|
||
def _inn_layout(layout: InnLayout, *, sums: bool) -> Callable[[], list[SheetSpec]]:
|
||
def build() -> list[SheetSpec]:
|
||
root = inn_rows_tree(_inn_layout_tree(), layout, sums=sums)
|
||
return [build_osv(OsvSpec(period="1 квартал 2025 г.", inn_col=None, first_sum_col=5), root)]
|
||
|
||
return build
|
||
|
||
|
||
# --- ОСВ других учётных систем (#81): контрольный набор. s48–s54 — позитивы, s55–s57 — негативы ---
|
||
|
||
PERIOD_TAIL = "по счету 60 за 1 квартал 2025 г."
|
||
NAVISION_GROUPS = {
|
||
"opening": ("НачалоДт", "НачалоКт"),
|
||
"turnover": ("ОборотДт", "ОборотКт"),
|
||
"closing": ("КонецДт", "КонецКт"),
|
||
}
|
||
|
||
|
||
def _titled(name: str, title: str, *, title_col: int = 1, total_text: str | None = None) -> SheetSpec:
|
||
spec = OsvSpec(inn_col=5, first_sum_col=7, title=title, title_col=title_col, total_text=total_text)
|
||
return build_osv(spec, base_tree(), name)
|
||
|
||
|
||
def _s48() -> list[SheetSpec]:
|
||
"""Названия ведомостей других систем и отчётов 1С (AURORA-1717) и итог «ВСЕГО» (AURORA-1732): по листу на название;
|
||
название — в разных колонках, верхним регистром с переносом строки."""
|
||
return [
|
||
_titled("ОВ", f"Оборотная ведомость {PERIOD_TAIL}", title_col=3),
|
||
_titled("ОСВ сокр", "ОСВ по сч. 60 за 1 квартал 2025 г."),
|
||
_titled("Оборотный баланс", f"Оборотный баланс {PERIOD_TAIL}", title_col=7),
|
||
_titled("Навижн", "Фин. Обороты Листок за 1 квартал 2025 г.", title_col=4),
|
||
_titled("Главная книга", f"Отчет по Главной книге {PERIOD_TAIL}", title_col=3, total_text="ВСЕГО"),
|
||
_titled("Верхний регистр", f"ОБОРОТНАЯ\nВЕДОМОСТЬ {PERIOD_TAIL}"),
|
||
_titled("Вторая колонка", f"Оборотная ведомость по счетам {PERIOD_TAIL}", title_col=2, total_text="Всего "),
|
||
]
|
||
|
||
|
||
def _s49_sheets(hierarchy: Hierarchy = "outline") -> list[SheetSpec]:
|
||
"""«Остаток на начало/конец» над «Дебет/Кредит» и «Оборот по дебету/кредиту» в ячейке группы (КТЗ); преамбула
|
||
«Остаток на начало: …» над таблицей; второй лист — «Остаток по дебету на начало» (сторона внутри названия)."""
|
||
first = build_osv(
|
||
OsvSpec(
|
||
inn_col=5,
|
||
first_sum_col=7,
|
||
hierarchy=hierarchy,
|
||
title=f"Оборотная ведомость {PERIOD_TAIL}",
|
||
group_titles={
|
||
**GROUP_TITLES,
|
||
"opening": "Остаток на начало периода",
|
||
"closing": "Остаток на конец периода",
|
||
},
|
||
split_groups={"turnover": ("Оборот по дебету", "Оборот по кредиту")},
|
||
),
|
||
base_tree(),
|
||
"Остатки",
|
||
)
|
||
first.put(5, 1, "Остаток на начало: 0")
|
||
first.put(5, 3, "Остаток на конец: 0")
|
||
second = build_osv(
|
||
OsvSpec(
|
||
inn_col=None,
|
||
first_sum_col=3,
|
||
hierarchy=hierarchy,
|
||
title=f"Оборотная ведомость {PERIOD_TAIL}",
|
||
split_groups={
|
||
"opening": ("Остаток по дебету на начало", "Остаток по кредиту на начало"),
|
||
"turnover": ("Оборот по дебету", "Оборот по кредиту"),
|
||
"closing": ("Остаток по дебету на конец", "Остаток по кредиту на конец"),
|
||
},
|
||
),
|
||
receivables_tree(),
|
||
"Остатки по сторонам",
|
||
)
|
||
return [first, second]
|
||
|
||
|
||
def _s49() -> list[SheetSpec]:
|
||
return _s49_sheets()
|
||
|
||
|
||
def _s50() -> list[SheetSpec]:
|
||
"""SAP: «Сальдо вх/исх Дт/Кт» (Виола, обороты 1С-вида — «Дт оборот» это AURORA-1722) и сторона, приклеенная к
|
||
названию группы «Сальдо НачальноеДТВВ» / «ОборотыДтВВ» (НМЖК); контрагент «ООО Дтвв-Сервис» — не сторона."""
|
||
sap = OsvSpec(
|
||
inn_col=None,
|
||
first_sum_col=3,
|
||
title=f"Отчет по Главной книге {PERIOD_TAIL}",
|
||
split_groups={
|
||
"opening": ("Сальдо вх Дт", "Сальдо вх Кт"),
|
||
"turnover": ("Оборот Дт", "Оборот Кт"),
|
||
"closing": ("Сальдо исх Дт", "Сальдо исх Кт"),
|
||
},
|
||
)
|
||
glued = OsvSpec(
|
||
inn_col=None,
|
||
first_sum_col=3,
|
||
split_groups={
|
||
"opening": ("Сальдо НачальноеДТВВ", "Сальдо НачальноеКТВВ"),
|
||
"turnover": ("ОборотыДтВВ", "ОборотыКтВВ"),
|
||
"closing": ("Сальдо КонечноеДТВВ", "Сальдо КонечноеКТВВ"),
|
||
},
|
||
)
|
||
tree = base_tree()
|
||
tree.kids[0].kids[0].text = "ООО Дтвв-Сервис"
|
||
return [build_osv(sap, base_tree(), "SAP"), build_osv(glued, tree, "НМЖК")]
|
||
|
||
|
||
def _navision_sheet(name: str = "ОВ", hierarchy: Hierarchy = "outline", top_padding: int = 0) -> SheetSpec:
|
||
spec = OsvSpec(
|
||
untitled=True,
|
||
top_padding=top_padding,
|
||
inn_col=None,
|
||
first_sum_col=2,
|
||
hierarchy=hierarchy,
|
||
split_groups=NAVISION_GROUPS,
|
||
)
|
||
return build_osv(spec, base_tree(), name)
|
||
|
||
|
||
def _s51() -> list[SheetSpec]:
|
||
"""Navision без названия ведомости: таблица с первой строки, «НачалоДт | … | КонецКт» (Айсберри); запасной
|
||
признак."""
|
||
return [_navision_sheet()]
|
||
|
||
|
||
def _s52() -> list[SheetSpec]:
|
||
"""Форма ББ, лист Navision без названия, посторонний лист: форма остаётся формой, «Справка» — прочим."""
|
||
return [statement_sheet(), _navision_sheet(), reference_sheet()]
|
||
|
||
|
||
def _s53() -> list[SheetSpec]:
|
||
"""s49 в xls (BIFF5 без объединений и outline): отступ пробелами."""
|
||
return _s49_sheets("spaces")
|
||
|
||
|
||
def _s54() -> list[SheetSpec]:
|
||
"""s51 в xlsb (без объединений и outline): отступ пробелами."""
|
||
return [_navision_sheet(hierarchy="spaces")]
|
||
|
||
|
||
def _s55() -> list[SheetSpec]:
|
||
"""Негатив: названия, похожие на ведомость, но не ведомость (не с начала ячейки, «ОСВ за квартал по счетам …» в
|
||
перечне документов шаблона банка, «Оборотный капитал»), и отчёт 1С «Обороты счета» с колонками корр. счетов между
|
||
«Оборот Дт» и «Оборот Кт» (R-1) — все листы прочие."""
|
||
return [
|
||
build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree(), "ОСВ 60"),
|
||
table_sheet(
|
||
"Справка",
|
||
[
|
||
["Справка: оборотная ведомость прилагается"],
|
||
[],
|
||
["Показатель", "Сумма"],
|
||
["Выручка", D("100000.00")],
|
||
],
|
||
),
|
||
table_sheet(
|
||
"Входные данные",
|
||
[
|
||
["Перечень документов к заявке"],
|
||
["№", "Документ", "Срок"],
|
||
[1, "Расшифровка дебиторской задолженности", "ежеквартально"],
|
||
[2, "ОСВ за квартал по счетам бухгалтерского учета 02, 58, 60, 62, 66, 67, 76", "ежеквартально"],
|
||
],
|
||
),
|
||
table_sheet(
|
||
"Капитал",
|
||
[
|
||
["Оборотный капитал и его структура"],
|
||
[],
|
||
["Показатель", "На начало", "На конец"],
|
||
["Запасы", D("500.00"), D("700.00")],
|
||
],
|
||
),
|
||
table_sheet(
|
||
"Обороты счета",
|
||
[
|
||
["Обороты счета 60 за 9 месяцев 2025 г."],
|
||
[],
|
||
["Счет", "Начальное сальдо Дт", "Начальное сальдо Кт", "Оборот Дт", "51", "68", "Оборот Кт"]
|
||
+ ["20", "Конечное сальдо Дт", "Конечное сальдо Кт"],
|
||
["60", D("100.00"), None, D("50.00"), D("30.00"), D("20.00"), D("40.00"), D("40.00"), D("110.00")],
|
||
["Итого", D("100.00"), None, D("50.00"), D("30.00"), D("20.00"), D("40.00"), D("40.00"), D("110.00")],
|
||
],
|
||
),
|
||
]
|
||
|
||
|
||
def _s56() -> list[SheetSpec]:
|
||
"""Негатив: безымянные таблицы, почти ОСВ — расшифровка «Остаток на начало | Поступило | Выбыло | Остаток на
|
||
конец», нет группы «начало», свёрнутое начало, полный заголовок ниже 30-й строки — все прочие (D1, R-2, R-3)."""
|
||
return [
|
||
build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree(), "ОСВ 60"),
|
||
table_sheet(
|
||
"Расшифровка",
|
||
[
|
||
["Наименование", "Остаток на начало", "Поступило", "Выбыло", "Остаток на конец"],
|
||
["Материалы", D("100.00"), D("50.00"), D("30.00"), D("120.00")],
|
||
],
|
||
),
|
||
table_sheet(
|
||
"Без начала",
|
||
[
|
||
["Счет", "Оборот Дт", "Оборот Кт", "Сальдо на конец Дт", "Сальдо на конец Кт"],
|
||
["60", D("10.00"), D("5.00"), D("5.00"), None],
|
||
],
|
||
),
|
||
table_sheet(
|
||
"Свёрнутое начало",
|
||
[
|
||
["Счет", "Сальдо на начало", "Оборот Дт", "Оборот Кт", "Сальдо на конец Дт", "Сальдо на конец Кт"],
|
||
["60", D("100.00"), D("10.00"), D("5.00"), D("105.00"), None],
|
||
],
|
||
),
|
||
_navision_sheet("Ниже 30", top_padding=30),
|
||
_preamble_navision_sheet(),
|
||
]
|
||
|
||
|
||
def _preamble_navision_sheet() -> SheetSpec:
|
||
"""Полная шапка Navision, над ней преамбула «Обороты за период: … | Остаток на конец: …»: разбор берёт строку
|
||
преамбулы, значит, и запасной признак смотрит её — лист прочий, книга не падает (ревью #81, Important-1)."""
|
||
sheet = _navision_sheet("Преамбула", top_padding=1)
|
||
sheet.put(1, 1, "Обороты за период: 50")
|
||
sheet.put(1, 2, "Остаток на конец: 150")
|
||
return sheet
|
||
|
||
|
||
def _s57() -> list[SheetSpec]:
|
||
"""Негатив: форма ББ с примечанием «см. оборотную ведомость …» и текстом «Остаток на начало периода …», за ней
|
||
лист слабого продолжения (одна строка с кодом) — форма и продолжение остаются формой."""
|
||
form = statement_sheet()
|
||
form.put(9, 1, "Примечание: см. оборотную ведомость по сч. 60")
|
||
form.put(10, 1, "Остаток на начало периода — по данным учёта")
|
||
continuation = table_sheet("Баланс (2)", [["Капитал и резервы", "1300", D("500000.00")]])
|
||
return [form, continuation]
|
||
|
||
|
||
def _normalized_zip(data: bytes) -> bytes:
|
||
"""Тот же архив с постоянными метками времени: записей zip и `dcterms:modified` (openpyxl ставит текущее время
|
||
при каждом `save`, заданное в свойствах значение перезаписывает)."""
|
||
out = io.BytesIO()
|
||
with zipfile.ZipFile(io.BytesIO(data)) as source, zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as target:
|
||
for info in source.infolist():
|
||
clone = zipfile.ZipInfo(info.filename, date_time=ZIP_DATE_TIME)
|
||
clone.compress_type = zipfile.ZIP_DEFLATED
|
||
clone.external_attr = info.external_attr
|
||
content = source.read(info.filename)
|
||
if info.filename == _CORE_PART:
|
||
content = _MODIFIED_RE.sub(rb"\g<1>" + _FIXED_W3CDTF + rb"\g<2>", content)
|
||
target.writestr(clone, content)
|
||
return out.getvalue()
|
||
|
||
|
||
def to_xlsx(sheets: Sequence[SheetSpec]) -> bytes:
|
||
book = openpyxl.Workbook()
|
||
default = book.active
|
||
assert default is not None
|
||
book.remove(default)
|
||
book.properties.creator = CREATOR
|
||
book.properties.lastModifiedBy = CREATOR
|
||
book.properties.created = FIXED_TIME
|
||
book.properties.modified = FIXED_TIME
|
||
for spec in sheets:
|
||
ws = book.create_sheet(spec.name)
|
||
for (row, col), cell in sorted(spec.cells.items()):
|
||
target = ws.cell(row, col, cell.value)
|
||
if cell.bold:
|
||
target.font = Font(bold=True)
|
||
if cell.indent:
|
||
target.alignment = Alignment(indent=cell.indent)
|
||
for top, left, bottom, right in spec.merges:
|
||
ws.merge_cells(start_row=top, start_column=left, end_row=bottom, end_column=right)
|
||
for row, level in sorted(spec.outline.items()):
|
||
ws.row_dimensions[row].outline_level = level
|
||
ws.column_dimensions["A"].width = 48
|
||
buffer = io.BytesIO()
|
||
book.save(buffer)
|
||
return _normalized_zip(buffer.getvalue())
|
||
|
||
|
||
def to_xls(sheets: Sequence[SheetSpec]) -> bytes:
|
||
"""xls BIFF5 без записи CODEPAGE (строки cp1251), без стилей: отступ — ведущие пробелы (`hierarchy="spaces"`)."""
|
||
return write_xls(
|
||
{spec.name: {key: cell.value for key, cell in spec.cells.items()} for spec in sheets}, codepage=None
|
||
)
|
||
|
||
|
||
def to_xlsb(sheets: Sequence[SheetSpec]) -> bytes:
|
||
"""xlsb без стилей и объединений: отступ уже выражен ведущими пробелами текста (`hierarchy="spaces"`)."""
|
||
return write_xlsb({spec.name: {key: cell.value for key, cell in spec.cells.items()} for spec in sheets})
|
||
|
||
|
||
FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSpec]], bytes]]] = {
|
||
"s01_variant2_header.xlsx": (_s01, to_xlsx),
|
||
"s02_no_outline_indent.xlsx": (_s02, to_xlsx),
|
||
"s03_no_outline_no_indent.xlsx": (_s03, to_xlsx),
|
||
"s04_currency_quantity_rows.xlsx": (_s04, to_xlsx),
|
||
"s05_text_numbers.xlsx": (_s05, to_xlsx),
|
||
"s06_two_osv_and_other_sheet.xlsx": (_s06, to_xlsx),
|
||
"s07_no_opening_collapsed_closing.xlsx": (_s07, to_xlsx),
|
||
"s08_extra_kpp_column.xlsx": (_s08, to_xlsx),
|
||
"s09_subaccount_names.xlsx": (_s09, to_xlsx),
|
||
"s10_broken_sum.xlsx": (_s10, to_xlsx),
|
||
"s11_header_repeat.xlsx": (_s11, to_xlsx),
|
||
"s12_statement_and_osv.xlsx": (_s12, to_xlsx),
|
||
"s13_no_outline_indent.xlsb": (_s13, to_xlsb),
|
||
"s14_statement_forms.xlsx": (_s14, to_xlsx),
|
||
"s15_group_title_synonyms.xlsx": (_s15, to_xlsx),
|
||
"s16_header_synonyms.xlsx": (_s16, to_xlsx),
|
||
"s17_inn_in_name.xlsx": (_s17, to_xlsx),
|
||
"s18_inn_row_above.xlsx": (_s18, to_xlsx),
|
||
"s19_inn_kpp_cell.xlsx": (_s19, to_xlsx),
|
||
"s20_inn_above_sibling_blank.xlsx": (_inn_layout("above_sibling", sums=False), to_xlsx),
|
||
"s21_inn_above_sibling_sums.xlsx": (_inn_layout("above_sibling", sums=True), to_xlsx),
|
||
"s22_inn_above_parent_blank.xlsx": (_inn_layout("above_parent", sums=False), to_xlsx),
|
||
"s23_inn_above_parent_sums.xlsx": (_inn_layout("above_parent", sums=True), to_xlsx),
|
||
"s24_inn_below_child_blank.xlsx": (_inn_layout("below_child", sums=False), to_xlsx),
|
||
"s25_inn_below_child_sums.xlsx": (_inn_layout("below_child", sums=True), to_xlsx),
|
||
"s26_inn_below_sibling_sums.xlsx": (_inn_layout("below_sibling", sums=True), to_xlsx),
|
||
"s27_inn_below_sibling_blank.xlsx": (_inn_layout("below_sibling", sums=False), to_xlsx),
|
||
"s28_inn_bad_checksum.xlsx": (_s28, to_xlsx),
|
||
"s29_xls_no_codepage.xls": (_s29, to_xls),
|
||
"s30_val_kol_vo_materials.xlsx": (_s30, to_xlsx),
|
||
"s31_rows_below_total.xlsx": (_s31, to_xlsx),
|
||
"s32_total_with_suffix.xlsx": (_s32, to_xlsx),
|
||
"s33_indicator_column_bu_nu.xlsx": (_s33, to_xlsx),
|
||
"s34_indicator_column_qty_val_diff.xlsx": (_s34, to_xlsx),
|
||
"s35_currency_slices.xlsx": (_s35, to_xlsx),
|
||
"s36_no_data_type_header.xlsx": (_s36, to_xlsx),
|
||
"s40_flat_header_account.xlsx": (_s40, to_xlsx),
|
||
"s41_flat_chart_of_accounts.xlsx": (_s41, to_xlsx),
|
||
"s42_subaccounts_totals_reversed.xlsx": (_s42, to_xlsx),
|
||
"s43_subkonto_unnamed_60.xlsx": (_s43, to_xlsx),
|
||
"s44_subkonto_unnamed_10.xlsx": (_s44, to_xlsx),
|
||
"s45_account_label_value_cells.xlsx": (_s45, to_xlsx),
|
||
"s46_counterparty_code_blocks.xlsx": (_s46, to_xlsx),
|
||
"s47_indent_hierarchy_code_sums.xlsx": (_s47, to_xlsx),
|
||
"s48_other_system_titles.xlsx": (_s48, to_xlsx),
|
||
"s49_balance_and_turnover_by_side.xlsx": (_s49, to_xlsx),
|
||
"s50_sap_in_out_glued_side.xlsx": (_s50, to_xlsx),
|
||
"s51_untitled_navision.xlsx": (_s51, to_xlsx),
|
||
"s52_mixed_book.xlsx": (_s52, to_xlsx),
|
||
"s53_balance_by_side.xls": (_s53, to_xls),
|
||
"s54_untitled_navision.xlsb": (_s54, to_xlsb),
|
||
"s55_not_osv_titles.xlsx": (_s55, to_xlsx),
|
||
"s56_almost_osv_tables.xlsx": (_s56, to_xlsx),
|
||
"s57_form_mentions_osv.xlsx": (_s57, to_xlsx),
|
||
}
|
||
FILE_NAMES = tuple(FILES)
|
||
|
||
|
||
def generate_all(directory: Path) -> list[Path]:
|
||
"""Записывает файлы набора в `directory` (создаётся); возвращает пути в порядке `FILE_NAMES`."""
|
||
directory.mkdir(parents=True, exist_ok=True)
|
||
written: list[Path] = []
|
||
for name, (build, serialize) in FILES.items():
|
||
path = directory / name
|
||
path.write_bytes(serialize(build()))
|
||
written.append(path)
|
||
return written
|
||
|
||
|
||
def main(argv: Sequence[str] | None = None) -> int:
|
||
parser = argparse.ArgumentParser(
|
||
prog="python -m tests.synthetic.generate",
|
||
description="Синтетические ОСВ s01…s36, s40…s57 (xlsx, xlsb и xls) в указанный каталог.",
|
||
)
|
||
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
|
||
args = parser.parse_args(argv)
|
||
for path in generate_all(args.directory):
|
||
print(path)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|