2026-11-13 01:04:15 +04:00

142 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""README прогона (спека §7): журнал генератора — настройки, состав группы, пары ВГО с расхождениями, число контрагентов.
Это не оракул: ожидаемых итогов консолидации здесь нет, только то, что генератор заложил в данные.
"""
from dataclasses import replace
from decimal import Decimal
from typing import TYPE_CHECKING
from synthgen.config import Config, dump_config
from synthgen.counterparties import WEIGHTS, CounterpartyPlan
from synthgen.group import Group, variant_spelling
from synthgen.intragroup import VgoPair
if TYPE_CHECKING:
from synthgen.generate import Manifest
KIND = {"trade": "торговля", "loan": "заём"}
def _money(x: Decimal) -> str:
return f"{x:,.2f}".replace(",", " ")
def _settings(cfg: Config) -> list[str]:
return ["## Настройки прогона", "", "```yaml", dump_config(replace(cfg, out=None)).rstrip(), "```", ""]
def _members(group: Group) -> list[str]:
lines = [
"## Состав группы", "", f"{group.name} (iD ГК {group.gk_id}); все компании входят в ГСК.", "",
"| Код | Название | ИНН | Доля головной | Миноритарий |", "|---|---|---|---|---|",
] # fmt: skip
for m in group.members:
share = "головная" if m.head_share is None else f"{m.head_share * 100:.0f} %"
minority = f"{m.minority.name} (ИНН {m.minority.inn})" if m.minority is not None else "—"
lines.append(f"| {m.code} | {m.name} | {m.inn} | {share} | {minority} |")
return [*lines, ""]
def _no_inn(group: Group) -> list[str]:
lines = ["## Компании без ИНН и вариантное имя", ""]
if not group.no_inn:
return [*lines, "Все ОСВ выгружены с ИНН контрагентов; вариантных имён нет.", ""]
by_code = {m.code: m for m in group.members}
for code in sorted(group.no_inn):
other = by_code[group.variant_name[code]]
lines.append(
f"- {code}: ОСВ без колонки ИНН; контрагент {other.code} ({other.name}) записан в ней вариантным "
f"написанием «{variant_spelling(other)}» (корень, префикс и форма без кавычек). Ядро "
"такое имя автоматически не сопоставит (имя нормализуется с сохранением порядка слов): нужна ручная привязка алиаса."
)
return [*lines, ""]
def _pairs(group: Group, pairs: list[VgoPair]) -> list[str]:
lines = ["## Пары ВГО", ""]
if not pairs:
return [*lines, "Пар ВГО нет: расхождений сторон тоже нет.", ""]
lines.append(f"Пар: {len(pairs)} (торговля: {sum(p.kind == 'trade' for p in pairs)}, займы: {sum(p.kind == 'loan' for p in pairs)}).")
mism = [(p, m) for p in pairs for m in p.mismatches]
if not mism:
return [*lines, "", "Расхождений сторон в этом прогоне нет.", ""]
lines += [
"", "Расхождения: сумма — разница сторон, % — от меньшей стороны сальдо пары на конец периода; порог 5 %.", "",
"| Пара (продавец/займодавец → покупатель/заёмщик) | Вид | Период | Проводка у | Сумма, руб. | % | Порог | Ожидание в ядре |",
"|---|---|---|---|---|---|---|---|",
] # fmt: skip
for p, m in mism:
verdict = "выше порога 5 %" if m.above_threshold else "ниже порога 5 %"
if not m.above_threshold:
expect = "закрывается автоматически (до 5 %)"
elif p.kind == "trade":
expect = "62.01 продавца нераспределена (MULTIPLE_RULES), 60.01 покупателя — правило 6 односторонне"
else:
expect = "58.03 и 66.03/67.03 нераспределены (MULTIPLE_RULES)"
lines.append(
f"| {p.a} → {p.b} | {KIND[p.kind]} | {m.period_key} | {m.side} | {_money(m.amount)} | {m.pct} % | {verdict} | {expect} |"
)
return [*lines, ""]
def _counts(plan: CounterpartyPlan, group: Group) -> list[str]:
lines = [
"## Внешние контрагенты по файлам ОСВ", "",
"| Счёт | Мин | Среднее | Макс |", "|---|---|---|---|",
] # fmt: skip
for acct in WEIGHTS:
counts = [len(plan.by_file[(m.code, acct)]) for m in group.members]
lines.append(f"| {acct} | {min(counts)} | {sum(counts) / len(counts):.1f} | {max(counts)} |")
lines.append("")
if plan.capped:
lines += ["Среднее ограничено словарём: потолок (размер словаря без банков и компаний группы) срезал выборку.", ""]
return lines
NOTES = """\
## Что нужно знать при чтении результатов
Набор не оракул: ожидаемых итогов консолидации здесь нет, README — журнал того, что заложил генератор. Только форма 66н.
Одинаковые настройки и seed дают побайтно одинаковые файлы в пределах одной платформы (веса и масштабы считаются во float,
на другой платформе округление может отличаться).
### Как ядро увидит расхождения на «Таблице корректировок»
Принадлежность строки правилу ядро определяет только по счёту и стороне, не зная, есть ли у пары встречные счета:
- расхождение до 5 % и пары без расхождений закрываются точными или «до 5 %» упаковками — автоматически;
- торговля с расхождением выше 5 %: строка 62.01 продавца (по дебету) входит в правила 6, 7 и 8, поэтому она
нераспределённая (`MULTIPLE_RULES`), а 60.01 покупателя (только правило 6) уходит в правило 6 односторонне (`AUTO`);
на экране «Пары ВГО» (API `elimination-totals`) такая пара подсвечивается;
- заём с расхождением выше 5 %: 58.03 займодавца входит в правила 3 и 4, 66.03 заёмщика — в правила 4 и 8, 67.03 — в 3 и 7;
обе строки нераспределённые (`MULTIPLE_RULES`);
- правило 6 считает сальдо на конец периода (62.01 продавца ↔ 60.01 покупателя); 76 между компаниями группы на конец
периода равно нулю (проценты и дивиденды гасятся в месяце начисления);
- правил элиминации для ОФР в ядре нет; выручка по 90.01 в ОСВ идёт без аналитики по контрагентам, как обычно в 1С.
### Особенности данных
- Счёт 80 у головной компании без аналитики: строка 80 не раскрывается по участникам (парсер даёт информационный
`OSV_HIERARCHY_BY_TRIVIAL`).
- В ББ головной займы дочерним: 67.03 попадают в строку 1170, 66.03 — в 1240, внешние 58.03 — в 1240 (правила 3 и 4).
- Вариантное имя контрагента в ОСВ без ИНН (раздел выше) ядро автоматически не сопоставит: нужна ручная привязка алиаса.
- ОСВ всех девяти счетов набора проходят парсер tabular-parser без ошибок.
- Подпись колонки ОФР «За 3 месяца» (и «6 месяцев», «9 месяцев») сделана по образцу 1С; для первого квартала с реальной
выгрузкой не сверена; строки 2340 и 2350 в ОФР всегда нулевые.
- Внешние контрагенты по счёту 58 (займы 58.03) и 76.05 только по Кт — вопрос к ядру: как оно трактует такие внешние
стороны; не проверялось.
- При `osv_counterparties_mean = 0` в ОСВ 60, 62 и 76 остаётся по одному служебному внешнему контрагенту, чтобы строки
расчётов с компаниями группы и внешние обороты не смешивались на одном субсчёте.
"""
def render(cfg: Config, group: Group, pairs: list[VgoPair], plan: CounterpartyPlan, manifest: "Manifest") -> str:
head = [
f"# Синтетический набор {group.name}", "",
f"Сгенерирован `python -m synthgen` (seed {cfg.seed}). Файлов: {len(manifest.files)}. Данные вымышленные; "
"это журнал генератора, а не оракул.", "",
] # fmt: skip
parts = [head, _settings(cfg), _members(group), _no_inn(group), _pairs(group, pairs), _counts(plan, group), NOTES.splitlines()]
return "\n".join(line for part in parts for line in part).rstrip() + "\n"