Классификация и проверки по признаку строки-показателя, BU по умолчанию (#58)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
33b32cbad6
commit
8924dd13a6
@ -4,16 +4,17 @@
|
|||||||
|
|
||||||
1. тождество сальдо по каждой строке: `opening_debit − opening_credit + turnover_debit − turnover_credit =
|
1. тождество сальдо по каждой строке: `opening_debit − opening_credit + turnover_debit − turnover_credit =
|
||||||
closing_debit − closing_credit`;
|
closing_debit − closing_credit`;
|
||||||
2. по каждому родителю: суммы родителя = Σ детей того же `data_type`; строки CURRENCY/QUANTITY («Вал.», «Кол.»)
|
2. по каждому родителю: суммы родителя = Σ детей того же `data_type`; строки-показатели (`indicator`: «Вал.», «Кол.»,
|
||||||
в суммирование не входят и сами родителями не проверяются. Обороты сверяются по колонкам. Сальдо (начало, конец) —
|
НУ и прочие метки колонки «Показатели») в суммирование не входят и сами родителями не проверяются.
|
||||||
по колонкам (развёрнутое сальдо) либо, если у родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто
|
Обороты сверяются по колонкам. Сальдо (начало, конец) — по колонкам (развёрнутое сальдо) либо, если у родителя
|
||||||
`дебет − кредит`: 1С показывает счёт свёрнутым сальдо, а субсчета и контрагентов — развёрнутым (так во всех
|
сальдо свёрнуто (не больше одной ненулевой стороны), по нетто `дебет − кредит`: 1С показывает счёт свёрнутым
|
||||||
реальных файлах, где у счёта есть субсчета с разными сторонами сальдо);
|
сальдо, а субсчета и контрагентов — развёрнутым (так во всех реальных файлах, где у счёта есть субсчета с
|
||||||
|
разными сторонами сальдо);
|
||||||
3. строка «Итого» (TOTAL) = корневая строка ACCOUNT (первая строка уровня 0) либо сумма всех строк уровня 0, если их
|
3. строка «Итого» (TOTAL) = корневая строка ACCOUNT (первая строка уровня 0) либо сумма всех строк уровня 0, если их
|
||||||
несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03; OQ-106, OQ-133; сальдо —
|
несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03; OQ-106, OQ-133; сальдо —
|
||||||
по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя в п. 2); корня нет
|
по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя в п. 2); корня нет
|
||||||
(пустая ОСВ, R-13) — «Итого» должна быть пустой или нулевой; «Итого» не найдена (R-20) — сверка невозможна,
|
(пустая ОСВ, R-13) — «Итого» должна быть пустой или нулевой; «Итого» не найдена (R-20) — сверка невозможна,
|
||||||
результат `false`. Строки-показатели (валюта, количество) в сумму уровня 0 не входят: это другие единицы.
|
результат `false`. Строки-показатели (валюта, количество, НУ) в сумму уровня 0 не входят: это другие единицы.
|
||||||
|
|
||||||
Счета без сальдо по аналитике (91, 99; список — `rules/osv_checks.yaml`, OQ-132): у них строки аналитики несут только
|
Счета без сальдо по аналитике (91, 99; список — `rules/osv_checks.yaml`, OQ-132): у них строки аналитики несут только
|
||||||
обороты, сальдо закрыто на субсчёт закрытия (91.09). Для строк такого счёта тождество п. 1 не проверяется; в п. 2
|
обороты, сальдо закрыто на субсчёт закрытия (91.09). Для строк такого счёта тождество п. 1 не проверяется; в п. 2
|
||||||
@ -45,8 +46,6 @@ TOLERANCE = Decimal("0.01")
|
|||||||
# Сколько номеров строк показывать в тексте предупреждения; полный список — в `checks`
|
# Сколько номеров строк показывать в тексте предупреждения; полный список — в `checks`
|
||||||
WARNING_ROWS_SHOWN = 20
|
WARNING_ROWS_SHOWN = 20
|
||||||
_ZERO = Decimal(0)
|
_ZERO = Decimal(0)
|
||||||
# Строки-показатели: суммы в других единицах (валюта, количество), в рублёвые суммы родителя не входят
|
|
||||||
_INDICATOR_DATA_TYPES = frozenset({"CURRENCY", "QUANTITY"})
|
|
||||||
_ROOT_LEVEL = 0
|
_ROOT_LEVEL = 0
|
||||||
# Позиции сумм в `BodyRow.sums` (порядок `classify.SUM_FIELDS`): дебет и кредит каждой группы рядом
|
# Позиции сумм в `BodyRow.sums` (порядок `classify.SUM_FIELDS`): дебет и кредит каждой группы рядом
|
||||||
_OPENING, _TURNOVER, _CLOSING = slice(0, 2), slice(2, 4), slice(4, 6)
|
_OPENING, _TURNOVER, _CLOSING = slice(0, 2), slice(2, 4), slice(4, 6)
|
||||||
@ -68,7 +67,7 @@ def _differs(left: Sequence[Decimal], right: Sequence[Decimal]) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _is_indicator(item: ClassifiedRow) -> bool:
|
def _is_indicator(item: ClassifiedRow) -> bool:
|
||||||
return item.data_type in _INDICATOR_DATA_TYPES
|
return item.indicator
|
||||||
|
|
||||||
|
|
||||||
def _no_analytic_balance(item: ClassifiedRow, exempt: OsvCheckRules | None) -> bool:
|
def _no_analytic_balance(item: ClassifiedRow, exempt: OsvCheckRules | None) -> bool:
|
||||||
@ -114,7 +113,7 @@ def parent_sums(rows: Sequence[ClassifiedRow], exempt: OsvCheckRules | None = No
|
|||||||
или по нетто при свёрнутом сальдо родителя (см. описание модуля).
|
или по нетто при свёрнутом сальдо родителя (см. описание модуля).
|
||||||
|
|
||||||
Дерево — по `parent` (номерам строк), не по текстам: одинаковые контрагенты под разными субсчетами не смешиваются.
|
Дерево — по `parent` (номерам строк), не по текстам: одинаковые контрагенты под разными субсчетами не смешиваются.
|
||||||
Строки CURRENCY/QUANTITY не суммируются и не проверяются как родители; родитель без учитываемых детей не
|
Строки-показатели не суммируются и не проверяются как родители; родитель без учитываемых детей не
|
||||||
проверяется.
|
проверяется.
|
||||||
|
|
||||||
Родитель счёта без сальдо по аналитике (`exempt`, OQ-132) без дочерних субсчетов (ACCOUNT) сверяется только по
|
Родитель счёта без сальдо по аналитике (`exempt`, OQ-132) без дочерних субсчетов (ACCOUNT) сверяется только по
|
||||||
@ -147,8 +146,8 @@ def parent_sums(rows: Sequence[ClassifiedRow], exempt: OsvCheckRules | None = No
|
|||||||
|
|
||||||
|
|
||||||
def _total(rows: Sequence[ClassifiedRow]) -> ClassifiedRow | None:
|
def _total(rows: Sequence[ClassifiedRow]) -> ClassifiedRow | None:
|
||||||
"""Основная «Итого» листа — первая строка TOTAL (вторая итоговая строка идёт за ней и в сверку не входит)."""
|
"""Основная «Итого» листа — первая строка TOTAL без признака показателя (следующие за ней в сверку не входят)."""
|
||||||
return next((item for item in rows if item.group_type == "TOTAL"), None)
|
return next((item for item in rows if item.group_type == "TOTAL" and not item.indicator), None)
|
||||||
|
|
||||||
|
|
||||||
def _root(rows: Sequence[ClassifiedRow]) -> ClassifiedRow | None:
|
def _root(rows: Sequence[ClassifiedRow]) -> ClassifiedRow | None:
|
||||||
|
|||||||
@ -83,6 +83,7 @@ class ClassifiedRow:
|
|||||||
counterparty_inn: str | None
|
counterparty_inn: str | None
|
||||||
data_type: DataType | None
|
data_type: DataType | None
|
||||||
extra: dict[str, str | None] | None
|
extra: dict[str, str | None] | None
|
||||||
|
indicator: bool = False # строка-показатель (§1.3): вне проверок сумм, `data_type` — вид по метке
|
||||||
|
|
||||||
|
|
||||||
# --- Строки таблицы ---
|
# --- Строки таблицы ---
|
||||||
@ -368,6 +369,7 @@ class _Kind:
|
|||||||
data_type: DataType | None
|
data_type: DataType | None
|
||||||
counterparty_name: str | None = None
|
counterparty_name: str | None = None
|
||||||
counterparty_inn: str | None = None
|
counterparty_inn: str | None = None
|
||||||
|
indicator: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -391,31 +393,31 @@ def _schema_group(schema: Sequence[OsvGroup], position: int) -> tuple[GroupType,
|
|||||||
|
|
||||||
def _classify_row(item: BodyRow, inherited: _Branch, context: _Context) -> tuple[_Kind, _Branch]:
|
def _classify_row(item: BodyRow, inherited: _Branch, context: _Context) -> tuple[_Kind, _Branch]:
|
||||||
"""Тип строки (кроме TOTAL) по признаку счёта, словарю строк-показателей и позиции в схеме; контекст для детей."""
|
"""Тип строки (кроме TOTAL) по признаку счёта, словарю строк-показателей и позиции в схеме; контекст для детей."""
|
||||||
# ASSUMPTION(OQ-45): `data_type` шапки (в том числе НУ) — как есть
|
code = inherited.account_code_raw
|
||||||
doc_data_type = context.doc_data_type
|
if item.indicator:
|
||||||
|
# Строка-показатель (§1.3): не уровень группировки, а вид сумм родителя; позицию в схеме не занимает
|
||||||
|
return _Kind("OTHER", None, code, item.label_data_type, indicator=True), inherited
|
||||||
|
# ASSUMPTION(OQ-45): `data_type` шапки (в том числе НУ) — как есть; метка строки («БУ») важнее шапки
|
||||||
|
own = item.label_data_type or context.doc_data_type
|
||||||
account = ACCOUNT_RE.match(item.text) if item.is_account else None
|
account = ACCOUNT_RE.match(item.text) if item.is_account else None
|
||||||
if account is not None:
|
if account is not None:
|
||||||
code = account.group(1)
|
code = account.group(1)
|
||||||
_, name = _schema_group(context.schema, _ACCOUNT_POSITION)
|
_, name = _schema_group(context.schema, _ACCOUNT_POSITION)
|
||||||
return _Kind("ACCOUNT", name, code, doc_data_type), _Branch(code, 0)
|
return _Kind("ACCOUNT", name, code, own), _Branch(code, 0)
|
||||||
code = inherited.account_code_raw
|
|
||||||
data_type = row_data_type(item.text, context.rules, context.currency_codes)
|
|
||||||
if data_type is not None:
|
|
||||||
return _Kind("OTHER", None, code, data_type), inherited
|
|
||||||
if item.row in context.unpaired:
|
if item.row in context.unpaired:
|
||||||
# Строка ИНН без пары (#35): OTHER, позицию в схеме не занимает — её дети классифицируются на её месте
|
# Строка ИНН без пары (#35): OTHER, позицию в схеме не занимает — её дети классифицируются на её месте
|
||||||
return _Kind("OTHER", None, code, doc_data_type), inherited
|
return _Kind("OTHER", None, code, own), inherited
|
||||||
position = inherited.depth + 1
|
position = inherited.depth + 1
|
||||||
group_type, name = _schema_group(context.schema, position)
|
group_type, name = _schema_group(context.schema, position)
|
||||||
branch = _Branch(code, position)
|
branch = _Branch(code, position)
|
||||||
if group_type != "COUNTERPARTY":
|
if group_type != "COUNTERPARTY":
|
||||||
return _Kind(group_type, name, code, doc_data_type), branch
|
return _Kind(group_type, name, code, own), branch
|
||||||
inn_col = context.header.inn_col
|
inn_col = context.header.inn_col
|
||||||
column = None if inn_col is None else column_inn(context.sheet.cell(item.row, inn_col), context.rules)
|
column = None if inn_col is None else column_inn(context.sheet.cell(item.row, inn_col), context.rules)
|
||||||
# ИНН: колонка ИНН, затем строка ИНН рядом (`inn_rows`, #35), затем хвост самого названия (OQ-100)
|
# ИНН: колонка ИНН, затем строка ИНН рядом (`inn_rows`, #35), затем хвост самого названия (OQ-100)
|
||||||
counterparty, in_name = split_inn_suffix(item.text, context.rules)
|
counterparty, in_name = split_inn_suffix(item.text, context.rules)
|
||||||
inn = column or context.inns.get(item.row) or in_name
|
inn = column or context.inns.get(item.row) or in_name
|
||||||
return _Kind(group_type, name, code, doc_data_type, counterparty_name=counterparty, counterparty_inn=inn), branch
|
return _Kind(group_type, name, code, own, counterparty_name=counterparty, counterparty_inn=inn), branch
|
||||||
|
|
||||||
|
|
||||||
def classify_rows( # noqa: PLR0913, PLR0917 - интерфейс плана (Task 8): всё, что нужно классификатору, приходит явно
|
def classify_rows( # noqa: PLR0913, PLR0917 - интерфейс плана (Task 8): всё, что нужно классификатору, приходит явно
|
||||||
@ -436,8 +438,11 @@ def classify_rows( # noqa: PLR0913, PLR0917 - интерфейс плана (Ta
|
|||||||
- «Итого» (`total_row`) и вторые итоговые строки после неё («Итого развернутое») — TOTAL, уровень 0 без родителя;
|
- «Итого» (`total_row`) и вторые итоговые строки после неё («Итого развернутое») — TOTAL, уровень 0 без родителя;
|
||||||
в `rows` остаются (# ASSUMPTION(OQ-01): хранить ли «Итого», решает ядро);
|
в `rows` остаются (# ASSUMPTION(OQ-01): хранить ли «Итого», решает ядро);
|
||||||
- счёт (`ACCOUNT_RE`) — ACCOUNT, `account_code_raw` — код как в файле; дети наследуют код ветки;
|
- счёт (`ACCOUNT_RE`) — ACCOUNT, `account_code_raw` — код как в файле; дети наследуют код ветки;
|
||||||
- строка-показатель («Вал.», «Кол.», код валюты) — `data_type` CURRENCY/QUANTITY, группа OTHER: это не уровень
|
- строка-показатель (`BodyRow.indicator`: метка колонки «Показатели» не «БУ», «Вал.», «Кол.», код валюты, §1.3) —
|
||||||
группировки, а вид сумм родителя; позицию в схеме она не занимает;
|
OTHER, `indicator=True`, `data_type` метки (НУ/CURRENCY/QUANTITY/`None`), `account_code_raw` ветки родителя,
|
||||||
|
без контрагента: это не уровень группировки, а вид сумм родителя; позицию в схеме не занимает; итоговая
|
||||||
|
строка-показатель — TOTAL с тем же признаком;
|
||||||
|
- вид данных строки — метка (`label_data_type`), иначе `doc_data_type` (умолчание БУ подставляет `osv.py`);
|
||||||
- прочая строка — `schema[позиция]`, позиция = число не-счётных предков после последнего счёта + 1; за схемой —
|
- прочая строка — `schema[позиция]`, позиция = число не-счётных предков после последнего счёта + 1; за схемой —
|
||||||
OTHER. COUNTERPARTY получает `counterparty_name` (текст строки) и `counterparty_inn` (колонка ИНН);
|
OTHER. COUNTERPARTY получает `counterparty_name` (текст строки) и `counterparty_inn` (колонка ИНН);
|
||||||
- строка ИНН без пары (`unpaired`) — OTHER, позицию в схеме не занимает; ИНН склеенных строк — `inns`.
|
- строка ИНН без пары (`unpaired`) — OTHER, позицию в схеме не занимает; ИНН склеенных строк — `inns`.
|
||||||
@ -452,7 +457,8 @@ def classify_rows( # noqa: PLR0913, PLR0917 - интерфейс плана (Ta
|
|||||||
if total_row is not None and item.row >= total_row:
|
if total_row is not None and item.row >= total_row:
|
||||||
# Всё от основной «Итого» и ниже — итоговые строки (подвал `collect_body_rows` уже отрезал)
|
# Всё от основной «Итого» и ниже — итоговые строки (подвал `collect_body_rows` уже отрезал)
|
||||||
# ASSUMPTION(OQ-01): строка «Итого» остаётся в rows; хранить ли её, решает ядро
|
# ASSUMPTION(OQ-01): строка «Итого» остаётся в rows; хранить ли её, решает ядро
|
||||||
kind, branch = _Kind("TOTAL", None, None, doc_data_type), _ROOT_BRANCH
|
kind = _Kind("TOTAL", None, None, item.label_data_type or doc_data_type, indicator=item.indicator)
|
||||||
|
branch = _ROOT_BRANCH
|
||||||
level, parent = 0, None
|
level, parent = 0, None
|
||||||
else:
|
else:
|
||||||
parent = levels.parent[item.row]
|
parent = levels.parent[item.row]
|
||||||
@ -472,6 +478,7 @@ def classify_rows( # noqa: PLR0913, PLR0917 - интерфейс плана (Ta
|
|||||||
counterparty_inn=kind.counterparty_inn,
|
counterparty_inn=kind.counterparty_inn,
|
||||||
data_type=kind.data_type,
|
data_type=kind.data_type,
|
||||||
extra=_extra(sheet, item.row, extra_keys),
|
extra=_extra(sheet, item.row, extra_keys),
|
||||||
|
indicator=kind.indicator,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from collections.abc import Iterable, Sequence
|
|||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
|
|
||||||
from src.contract import OsvDocument, ResultWarning
|
from src.contract import OsvDocument, ResultWarning
|
||||||
|
from src.contract.common import DataType
|
||||||
from src.contract.result import DocumentStatus, OsvChecks, OsvGroup, OsvHeader, OsvRow
|
from src.contract.result import DocumentStatus, OsvChecks, OsvGroup, OsvHeader, OsvRow
|
||||||
from src.parser.checks import run_checks
|
from src.parser.checks import run_checks
|
||||||
from src.parser.classify import SUM_FIELDS, ClassifiedRow, classify_rows, collect_body_rows
|
from src.parser.classify import SUM_FIELDS, ClassifiedRow, classify_rows, collect_body_rows
|
||||||
@ -37,6 +38,8 @@ __all__ = ["OsvParser", "SheetResult", "data_rows"]
|
|||||||
LOCATOR_PREFIX = "sheet:"
|
LOCATOR_PREFIX = "sheet:"
|
||||||
INN_COLUMN_KEY = "inn"
|
INN_COLUMN_KEY = "inn"
|
||||||
NO_DATA_ROWS_WARNING = "нет строк данных"
|
NO_DATA_ROWS_WARNING = "нет строк данных"
|
||||||
|
DEFAULT_DATA_TYPE: DataType = "BU"
|
||||||
|
DEFAULT_DATA_TYPE_WARNING = "вид данных в шапке не указан — строки без метки приняты как БУ"
|
||||||
|
|
||||||
|
|
||||||
def _columns(header: TableHeader) -> dict[str, str]:
|
def _columns(header: TableHeader) -> dict[str, str]:
|
||||||
@ -194,13 +197,18 @@ class OsvParser:
|
|||||||
rules=rules,
|
rules=rules,
|
||||||
)
|
)
|
||||||
step = "классификация строк"
|
step = "классификация строк"
|
||||||
|
data_type = doc.data_type or DEFAULT_DATA_TYPE
|
||||||
|
# Вид данных шапки не указан: строки без метки приняты как БУ — предупреждение, если умолчание сработало
|
||||||
|
defaulted = doc.data_type is None and any(
|
||||||
|
not item.indicator and item.label_data_type is None for item in body
|
||||||
|
)
|
||||||
classified = classify_rows(
|
classified = classify_rows(
|
||||||
glue.rows,
|
glue.rows,
|
||||||
glue.levels,
|
glue.levels,
|
||||||
schema.groups,
|
schema.groups,
|
||||||
header,
|
header,
|
||||||
sheet,
|
sheet,
|
||||||
doc.data_type,
|
data_type,
|
||||||
total_row,
|
total_row,
|
||||||
rules,
|
rules,
|
||||||
inns=glue.inns,
|
inns=glue.inns,
|
||||||
@ -215,6 +223,7 @@ class OsvParser:
|
|||||||
notes = [
|
notes = [
|
||||||
hierarchy_note(levels.strategy),
|
hierarchy_note(levels.strategy),
|
||||||
*notes_of(OSV_HEADER, doc.warnings),
|
*notes_of(OSV_HEADER, doc.warnings),
|
||||||
|
*([OsvNote(OSV_HEADER, DEFAULT_DATA_TYPE_WARNING)] if defaulted else []),
|
||||||
*notes_of(OSV_ROWS, body_warnings),
|
*notes_of(OSV_ROWS, body_warnings),
|
||||||
*notes_of(OSV_INN_ROWS, glue.warnings),
|
*notes_of(OSV_INN_ROWS, glue.warnings),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -26,6 +26,7 @@ def row( # noqa: PLR0913 - строка теста описывается по
|
|||||||
data_type: DataType | None = "BU",
|
data_type: DataType | None = "BU",
|
||||||
text: str = "строка",
|
text: str = "строка",
|
||||||
code: str | None = "60",
|
code: str | None = "60",
|
||||||
|
indicator: bool = False,
|
||||||
) -> ClassifiedRow:
|
) -> ClassifiedRow:
|
||||||
body = BodyRow(
|
body = BodyRow(
|
||||||
row=number,
|
row=number,
|
||||||
@ -48,6 +49,7 @@ def row( # noqa: PLR0913 - строка теста описывается по
|
|||||||
counterparty_inn=None,
|
counterparty_inn=None,
|
||||||
data_type=data_type,
|
data_type=data_type,
|
||||||
extra=None,
|
extra=None,
|
||||||
|
indicator=indicator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -198,8 +200,24 @@ def test_currency_and_quantity_children_are_excluded() -> None:
|
|||||||
rows = [
|
rows = [
|
||||||
account(7, ROOT),
|
account(7, ROOT),
|
||||||
row(8, ROOT, level=1, parent=7),
|
row(8, ROOT, level=1, parent=7),
|
||||||
row(9, ("1", None, "2", None, "3", None), level=2, parent=8, group_type="OTHER", data_type="CURRENCY"),
|
row(
|
||||||
row(10, ("7", None, None, None, "7", None), level=2, parent=8, group_type="OTHER", data_type="QUANTITY"),
|
9,
|
||||||
|
("1", None, "2", None, "3", None),
|
||||||
|
level=2,
|
||||||
|
parent=8,
|
||||||
|
group_type="OTHER",
|
||||||
|
data_type="CURRENCY",
|
||||||
|
indicator=True,
|
||||||
|
),
|
||||||
|
row(
|
||||||
|
10,
|
||||||
|
("7", None, None, None, "7", None),
|
||||||
|
level=2,
|
||||||
|
parent=8,
|
||||||
|
group_type="OTHER",
|
||||||
|
data_type="QUANTITY",
|
||||||
|
indicator=True,
|
||||||
|
),
|
||||||
total(11, ROOT),
|
total(11, ROOT),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -299,7 +317,9 @@ def test_total_that_equals_neither_first_root_nor_sum_of_roots_mismatches() -> N
|
|||||||
|
|
||||||
def test_sum_of_roots_ignores_indicator_rows_and_total_itself() -> None:
|
def test_sum_of_roots_ignores_indicator_rows_and_total_itself() -> None:
|
||||||
rows = multi_root_tree()
|
rows = multi_root_tree()
|
||||||
rows.insert(2, row(20, ("5", None, None, None, "5", None), group_type="OTHER", data_type="CURRENCY"))
|
rows.insert(
|
||||||
|
2, row(20, ("5", None, None, None, "5", None), group_type="OTHER", data_type="CURRENCY", indicator=True)
|
||||||
|
)
|
||||||
|
|
||||||
assert total_matches_root(rows) is True
|
assert total_matches_root(rows) is True
|
||||||
|
|
||||||
@ -464,3 +484,41 @@ def test_run_checks_without_violations_on_account_91_gives_clean_status() -> Non
|
|||||||
assert checks.parent_sum_violations == []
|
assert checks.parent_sum_violations == []
|
||||||
assert checks.total_row_matches_root is True
|
assert checks.total_row_matches_root is True
|
||||||
assert warnings == []
|
assert warnings == []
|
||||||
|
|
||||||
|
|
||||||
|
# --- Признак строки-показателя (#58) ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_indicator_nu_child_is_excluded_from_parent_sum() -> None:
|
||||||
|
"""Строка-показатель НУ под родителем БУ в сумму детей не входит, хотя её data_type не CURRENCY/QUANTITY."""
|
||||||
|
rows = [
|
||||||
|
account(7, ("10", None, None, None, "10", None)),
|
||||||
|
row(8, ("10", None, None, None, "10", None), level=1, parent=7),
|
||||||
|
row(
|
||||||
|
9, ("5", None, None, None, "5", None), level=1, parent=7, group_type="OTHER", data_type="NU", indicator=True
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert parent_sums(rows) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_of_nu_rows_without_indicator_is_checked_as_usual() -> None:
|
||||||
|
"""«Выводимые данные: НУ» (s12): все строки НУ без признака — проверки идут как обычно."""
|
||||||
|
rows = [
|
||||||
|
row(7, ("10", None, None, None, "10", None), group_type="ACCOUNT", data_type="NU"),
|
||||||
|
row(8, ("4", None, None, None, "4", None), level=1, parent=7, data_type="NU"),
|
||||||
|
row(9, ("5", None, None, None, "5", None), level=1, parent=7, data_type="NU"),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert parent_sums(rows) == [7]
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_skips_leading_indicator_total() -> None:
|
||||||
|
"""Первая итоговая строка без признака — основная «Итого», даже если перед ней стоит итог-показатель."""
|
||||||
|
rows = [
|
||||||
|
*tree(),
|
||||||
|
row(12, ("1", None, None, None, "1", None), group_type="TOTAL", data_type="NU", indicator=True),
|
||||||
|
]
|
||||||
|
rows.insert(0, row(2, ("9", None, None, None, "9", None), group_type="TOTAL", data_type="NU", indicator=True))
|
||||||
|
|
||||||
|
assert total_matches_root(rows) is True
|
||||||
|
|||||||
@ -1074,3 +1074,40 @@ def test_indicator_column_rows_are_labelled_indicators(rules: Rules) -> None:
|
|||||||
assert [by_row[row].label_data_type for row in (4, 5, 6, 7, 8)] == ["BU", "BU", "NU", "BU", "NU"]
|
assert [by_row[row].label_data_type for row in (4, 5, 6, 7, 8)] == ["BU", "BU", "NU", "BU", "NU"]
|
||||||
assert by_row[5].text == "ООО Поставщик"
|
assert by_row[5].text == "ООО Поставщик"
|
||||||
assert not by_row[6].is_account
|
assert not by_row[6].is_account
|
||||||
|
|
||||||
|
|
||||||
|
def test_indicator_rows_and_totals_are_classified_by_flag(rules: Rules) -> None:
|
||||||
|
parsed = parse(_indicator_sheet(), rules, None)
|
||||||
|
by_row = {item.body.row: item for item in parsed.rows}
|
||||||
|
|
||||||
|
nu = by_row[6]
|
||||||
|
assert (nu.group_type, nu.data_type, nu.indicator) == ("OTHER", "NU", True)
|
||||||
|
assert (nu.account_code_raw, nu.counterparty_name, nu.counterparty_inn) == ("60", None, None)
|
||||||
|
nu_total = by_row[8]
|
||||||
|
assert (nu_total.group_type, nu_total.data_type, nu_total.indicator) == ("TOTAL", "NU", True)
|
||||||
|
assert by_row[7].indicator is False
|
||||||
|
assert by_row[7].data_type == "BU"
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_data_type_wins_over_document_data_type(rules: Rules) -> None:
|
||||||
|
parsed = parse(_indicator_sheet(), rules, "NU")
|
||||||
|
|
||||||
|
assert {item.body.row: item.data_type for item in parsed.rows if not item.indicator} == {
|
||||||
|
4: "BU",
|
||||||
|
5: "BU",
|
||||||
|
7: "BU",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_row_without_label_takes_document_data_type(rules: Rules) -> None:
|
||||||
|
"""Строка без метки получает вид данных шапки; умолчание БУ подставляет `osv.py` до вызова классификатора."""
|
||||||
|
sheet = osv(r(0, "60", S100), r(1, "ООО Поставщик", S100))
|
||||||
|
|
||||||
|
assert {item.body.text: item.data_type for item in parse(sheet, rules, "BU").rows} == {
|
||||||
|
"60": "BU",
|
||||||
|
"ООО Поставщик": "BU",
|
||||||
|
}
|
||||||
|
assert {item.body.text: item.data_type for item in parse(sheet, rules, "NU").rows} == {
|
||||||
|
"60": "NU",
|
||||||
|
"ООО Поставщик": "NU",
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user