ОСВ: предупреждение о противоречии раскладки ИНН и подсказки шапки, синтетика s28 с неверной контрольной суммой (#35)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1f96012884
commit
6d97fa2c94
@ -368,6 +368,22 @@ def _without(rows: Sequence[BodyRow], levels: Levels, removed: set[int]) -> tupl
|
|||||||
return kept, Levels(by_row=by_row, parent=parents(kept, by_row), strategy=levels.strategy)
|
return kept, Levels(by_row=by_row, parent=parents(kept, by_row), strategy=levels.strategy)
|
||||||
|
|
||||||
|
|
||||||
|
def _contradicts(row: int, partner: int, hint: InnHint) -> bool:
|
||||||
|
"""Фактическая раскладка противоречит подсказке шапки: ИНН стоит под наименованием (строка пары выше) при
|
||||||
|
подсказке «над» или над ним (строка пары ниже) при подсказке «под». Годится для пар в дереве и для соседей."""
|
||||||
|
actual: Direction = "below" if partner < row else "above"
|
||||||
|
return actual != hint.direction
|
||||||
|
|
||||||
|
|
||||||
|
def _contradiction(row: int, partner: int, inn: str, hint: InnHint) -> str:
|
||||||
|
"""Склейка состоялась, но раскладка не та, что указана в шапке (design, решение 10): подсветить, проверка."""
|
||||||
|
where, expected = ("под", "над") if hint.direction == "above" else ("над", "под")
|
||||||
|
return (
|
||||||
|
f"строка {row}: ИНН {inn} стоит {where} наименованием в строке {partner}, а шапка («{hint.name}») "
|
||||||
|
f"указывает {expected} — проверьте"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _result( # noqa: PLR0913, PLR0917 - сборка результата из всех частей прохода
|
def _result( # noqa: PLR0913, PLR0917 - сборка результата из всех частей прохода
|
||||||
rows: Sequence[BodyRow],
|
rows: Sequence[BodyRow],
|
||||||
levels: Levels,
|
levels: Levels,
|
||||||
@ -402,6 +418,9 @@ def _result( # noqa: PLR0913, PLR0917 - сборка результата из
|
|||||||
continue
|
continue
|
||||||
inns[partner] = inn
|
inns[partner] = inn
|
||||||
warnings.append(f"строка {row}: ИНН {inn}{label} отнесён к контрагенту в строке {partner}")
|
warnings.append(f"строка {row}: ИНН {inn}{label} отнесён к контрагенту в строке {partner}")
|
||||||
|
if hint is not None and _contradicts(row, partner, hint):
|
||||||
|
ok = False
|
||||||
|
warnings.append(_contradiction(row, partner, inn, hint))
|
||||||
elif row in pairs.reason:
|
elif row in pairs.reason:
|
||||||
shown = "" if inn is None else f" {inn}"
|
shown = "" if inn is None else f" {inn}"
|
||||||
warnings.append(f"строка {row}: ИНН{shown}{label} без пары — {pairs.reason[row]}")
|
warnings.append(f"строка {row}: ИНН{shown}{label} без пары — {pairs.reason[row]}")
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Файлы s01…s13 — по одному сценарию спеки `osv-parsing` на файл, s14…s16 — записи словарей, которых нет в реальных
|
Файлы s01…s13 — по одному сценарию спеки `osv-parsing` на файл, s14…s16 — записи словарей, которых нет в реальных
|
||||||
ОСВ (R-22), s17…s19 — ИНН в тексте названия, ИНН строкой выше и «ИНН/КПП» в одной ячейке (issue #9), s20…s27 — восемь
|
ОСВ (R-22), s17…s19 — ИНН в тексте названия, ИНН строкой выше и «ИНН/КПП» в одной ячейке (issue #9), s20…s27 — восемь
|
||||||
раскладок строки ИНН (#35); описание —
|
раскладок строки ИНН, s28 — ИНН с неверной контрольной суммой (#35); описание —
|
||||||
`osv/synthetic/README.md` в репозитории fixtures. Генератор самодостаточен (openpyxl +
|
`osv/synthetic/README.md` в репозитории fixtures. Генератор самодостаточен (openpyxl +
|
||||||
`xlsb_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок
|
`xlsb_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок
|
||||||
записи фиксирован.
|
записи фиксирован.
|
||||||
@ -687,6 +687,14 @@ def _inn_layout_tree() -> Node:
|
|||||||
return Node("60", [Node("60.01", [first, beta(), ivanov()]), Node("60.02", [third, sidorov()])])
|
return Node("60", [Node("60.01", [first, beta(), ivanov()]), Node("60.02", [third, sidorov()])])
|
||||||
|
|
||||||
|
|
||||||
|
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 _inn_layout(layout: InnLayout, *, sums: bool) -> Callable[[], list[SheetSpec]]:
|
||||||
def build() -> list[SheetSpec]:
|
def build() -> list[SheetSpec]:
|
||||||
root = inn_rows_tree(_inn_layout_tree(), layout, sums=sums)
|
root = inn_rows_tree(_inn_layout_tree(), layout, sums=sums)
|
||||||
@ -771,6 +779,7 @@ FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSp
|
|||||||
"s25_inn_below_child_sums.xlsx": (_inn_layout("below_child", sums=True), 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),
|
"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),
|
"s27_inn_below_sibling_blank.xlsx": (_inn_layout("below_sibling", sums=False), to_xlsx),
|
||||||
|
"s28_inn_bad_checksum.xlsx": (_s28, to_xlsx),
|
||||||
}
|
}
|
||||||
FILE_NAMES = tuple(FILES)
|
FILE_NAMES = tuple(FILES)
|
||||||
|
|
||||||
@ -789,7 +798,7 @@ def generate_all(directory: Path) -> list[Path]:
|
|||||||
def main(argv: Sequence[str] | None = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="python -m tests.synthetic.generate",
|
prog="python -m tests.synthetic.generate",
|
||||||
description="Синтетические ОСВ s01…s27 (xlsx и один xlsb) в указанный каталог.",
|
description="Синтетические ОСВ s01…s28 (xlsx и один xlsb) в указанный каталог.",
|
||||||
)
|
)
|
||||||
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
|
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|||||||
@ -36,6 +36,7 @@ INN_LAYOUT_FILES = (
|
|||||||
"s26_inn_below_sibling_sums.xlsx",
|
"s26_inn_below_sibling_sums.xlsx",
|
||||||
"s27_inn_below_sibling_blank.xlsx",
|
"s27_inn_below_sibling_blank.xlsx",
|
||||||
)
|
)
|
||||||
|
INN_BAD_CHECKSUM_FILE = "s28_inn_bad_checksum.xlsx"
|
||||||
EXPECTED_NAMES = [
|
EXPECTED_NAMES = [
|
||||||
"s01_variant2_header.xlsx",
|
"s01_variant2_header.xlsx",
|
||||||
"s02_no_outline_indent.xlsx",
|
"s02_no_outline_indent.xlsx",
|
||||||
@ -57,6 +58,7 @@ EXPECTED_NAMES = [
|
|||||||
"s18_inn_row_above.xlsx",
|
"s18_inn_row_above.xlsx",
|
||||||
"s19_inn_kpp_cell.xlsx",
|
"s19_inn_kpp_cell.xlsx",
|
||||||
*INN_LAYOUT_FILES,
|
*INN_LAYOUT_FILES,
|
||||||
|
INN_BAD_CHECKSUM_FILE,
|
||||||
]
|
]
|
||||||
XLSB_NAME = "s13_no_outline_indent.xlsb"
|
XLSB_NAME = "s13_no_outline_indent.xlsb"
|
||||||
XLSB_MISSING_REASON = f"нет файла {XLSB_NAME} (xlsb не получен; см. README fixtures)"
|
XLSB_MISSING_REASON = f"нет файла {XLSB_NAME} (xlsb не получен; см. README fixtures)"
|
||||||
@ -122,6 +124,8 @@ EXPECTED: dict[str, Expect] = {
|
|||||||
"s19_inn_kpp_cell.xlsx": Expect("outline", 2025, "Q3", False),
|
"s19_inn_kpp_cell.xlsx": Expect("outline", 2025, "Q3", False),
|
||||||
# восемь раскладок строки ИНН (#35)
|
# восемь раскладок строки ИНН (#35)
|
||||||
**{name: Expect("outline", 2025, "Q1", True) for name in INN_LAYOUT_FILES},
|
**{name: Expect("outline", 2025, "Q1", True) for name in INN_LAYOUT_FILES},
|
||||||
|
# ИНН с неверной контрольной суммой: строка остаётся строкой, документ на проверку
|
||||||
|
INN_BAD_CHECKSUM_FILE: Expect("outline", 2025, "Q1", True, status=ResultStatus.NEEDS_CHECK),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -193,7 +197,7 @@ def tree(doc: OsvDocument) -> list[tuple[str, int, int | None, tuple[Decimal | N
|
|||||||
|
|
||||||
|
|
||||||
def test_synthetic_set_is_complete(fixtures_dir: Path) -> None:
|
def test_synthetic_set_is_complete(fixtures_dir: Path) -> None:
|
||||||
"""В `fixtures/osv/synthetic/` лежат файлы набора s01…s27 (s13 — xlsb; допустима деградация: нет только его)."""
|
"""В `fixtures/osv/synthetic/` лежат файлы набора s01…s28 (s13 — xlsb; допустима деградация: нет только его)."""
|
||||||
present = sorted(path.name for path in (fixtures_dir / SYNTHETIC_DIR).glob("s*") if path.is_file())
|
present = sorted(path.name for path in (fixtures_dir / SYNTHETIC_DIR).glob("s*") if path.is_file())
|
||||||
assert present == [name for name in EXPECTED_NAMES if name in present]
|
assert present == [name for name in EXPECTED_NAMES if name in present]
|
||||||
missing = [name for name in EXPECTED_NAMES if name not in present]
|
missing = [name for name in EXPECTED_NAMES if name not in present]
|
||||||
@ -570,6 +574,24 @@ def test_inn_row_layouts_give_inn_to_own_counterparty(fixtures_dir: Path, name:
|
|||||||
assert not [row for row in doc.rows if row.row_value in inns]
|
assert not [row for row in doc.rows if row.row_value in inns]
|
||||||
|
|
||||||
|
|
||||||
|
def test_s28_bad_checksum_inn_row_stays_a_row_and_needs_check(fixtures_dir: Path) -> None:
|
||||||
|
"""Опечатка в ИНН ООО Альфа: «7701234567» остаётся обычной строкой, у Альфы ИНН нет, предупреждение, проверка;
|
||||||
|
остальные контрагенты получают свои ИНН (направление «над» — по блоку 60.02)."""
|
||||||
|
outcome = parse_named(fixtures_dir, INN_BAD_CHECKSUM_FILE)
|
||||||
|
doc = one_document(outcome)
|
||||||
|
|
||||||
|
assert outcome.status is ResultStatus.NEEDS_CHECK
|
||||||
|
counterparties = {
|
||||||
|
row.counterparty_name: row.counterparty_inn for row in doc.rows if row.group_type == "COUNTERPARTY"
|
||||||
|
}
|
||||||
|
assert counterparties.pop("7701234567") is None # обычная строка: классификатор принял её за контрагента
|
||||||
|
assert counterparties == {**INN_LAYOUT_COUNTERPARTIES, ALFA: None}
|
||||||
|
assert any(
|
||||||
|
"похожа на ИНН 7701234567" in warning and "контрольная сумма не сходится" in warning
|
||||||
|
for warning in warnings_of(outcome)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_s19_inn_kpp_cell_gives_inn_only(fixtures_dir: Path) -> None:
|
def test_s19_inn_kpp_cell_gives_inn_only(fixtures_dir: Path) -> None:
|
||||||
"""«ИНН/КПП» в одной ячейке: в `counterparty_inn` — только ИНН; колонка распознана как колонка ИНН."""
|
"""«ИНН/КПП» в одной ячейке: в `counterparty_inn` — только ИНН; колонка распознана как колонка ИНН."""
|
||||||
doc = one_document(parse_named(fixtures_dir, "s19_inn_kpp_cell.xlsx"))
|
doc = one_document(parse_named(fixtures_dir, "s19_inn_kpp_cell.xlsx"))
|
||||||
|
|||||||
@ -475,3 +475,56 @@ def test_inn_without_name_neighbour_in_direction(rules: Rules) -> None:
|
|||||||
|
|
||||||
assert glue.unpaired == frozenset({3})
|
assert glue.unpaired == frozenset({3})
|
||||||
assert has_warning(glue, "строка 3", "нет наименования")
|
assert has_warning(glue, "строка 3", "нет наименования")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Раскладка противоречит подсказке шапки: склеить, но подсветить (H-2, design #10) ---
|
||||||
|
|
||||||
|
|
||||||
|
def conflict_warning(glue: InnGlue, row: int, partner: int, where: str, expected: str) -> bool:
|
||||||
|
return has_warning(
|
||||||
|
glue,
|
||||||
|
f"строка {row}: ИНН",
|
||||||
|
f"стоит {where} наименованием в строке {partner}",
|
||||||
|
f"указывает {expected} — проверьте",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hint_above_inn_child_of_name_is_glued_with_conflict_warning(rules: Rules) -> None:
|
||||||
|
# T2.12: подсказка «над», строка ИНН без детей под родителем-контрагентом — склеена с родителем, но подсвечена
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(2, INN_A, A), hint=ABOVE)
|
||||||
|
|
||||||
|
assert (glue.inns, glue.unpaired, glue.ok) == ({2: INN_A}, frozenset(), False)
|
||||||
|
assert has_warning(glue, f"ИНН {INN_A} («Контрагенты.ИНН») отнесён к контрагенту в строке 2")
|
||||||
|
assert conflict_warning(glue, 3, 2, "под", "над")
|
||||||
|
assert has_warning(glue, "шапка («Контрагенты.ИНН»)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hint_below_inn_parent_of_name_is_glued_with_conflict_warning(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, INN_A, A), r(2, "ООО Альфа", A), hint=BELOW)
|
||||||
|
|
||||||
|
assert (glue.inns, glue.unpaired, glue.ok) == ({3: INN_A}, frozenset(), False)
|
||||||
|
assert conflict_warning(glue, 2, 3, "над", "под")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hint_above_sibling_below_name_by_sums_is_glued_with_conflict_warning(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A, A), r(1, "ООО Бета", B), hint=ABOVE)
|
||||||
|
|
||||||
|
assert (glue.inns, glue.unpaired, glue.ok) == ({2: INN_A}, frozenset(), False)
|
||||||
|
assert conflict_warning(glue, 3, 2, "под", "над")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hint_below_sibling_above_name_by_sums_is_glued_with_conflict_warning(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Бета", B), r(1, INN_A, A), r(1, "ООО Альфа", A), hint=BELOW)
|
||||||
|
|
||||||
|
assert (glue.inns, glue.unpaired, glue.ok) == ({4: INN_A}, frozenset(), False)
|
||||||
|
assert conflict_warning(glue, 3, 4, "над", "под")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hint_agreeing_with_layout_has_no_conflict_warning(rules: Rules) -> None:
|
||||||
|
below = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(2, INN_A, A), hint=BELOW)
|
||||||
|
above = run(rules, r(0, "60", A), r(1, INN_A, A), r(2, "ООО Альфа", A), hint=ABOVE)
|
||||||
|
blank = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A), hint=BELOW)
|
||||||
|
|
||||||
|
for glue in (below, above, blank):
|
||||||
|
assert glue.ok
|
||||||
|
assert not has_warning(glue, "проверьте")
|
||||||
|
|||||||
@ -47,6 +47,14 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
|
|||||||
7. **«Под, в дереве»** — несколько строк ИНН под одним наименованием — `needs_check` (как #32).
|
7. **«Под, в дереве»** — несколько строк ИНН под одним наименованием — `needs_check` (как #32).
|
||||||
8. **Строка ИНН — только ИНН**: 10/12 цифр числом или текстом, «ИНН …», «ИНН/КПП» (`inn_only_row`); счёт строкой ИНН не
|
8. **Строка ИНН — только ИНН**: 10/12 цифр числом или текстом, «ИНН …», «ИНН/КПП» (`inn_only_row`); счёт строкой ИНН не
|
||||||
бывает.
|
бывает.
|
||||||
|
9. **Синтетика для отказа «неверная контрольная сумма»** (решение владельца 13.10.2026, H-1): «синтетика для отказа
|
||||||
|
«неверная контрольная сумма» — да». Файл `s28_inn_bad_checksum.xlsx`: раскладка `s20` («над, не в дереве, без сумм»),
|
||||||
|
у ООО Альфа строка ИНН `7701234567`; ожидание — строка остаётся обычной, ИНН у Альфы нет, предупреждение «похожа на ИНН»,
|
||||||
|
`needs_check`, остальные контрагенты получают свои ИНН (направление — по блоку 60.02).
|
||||||
|
10. **Противоречие раскладки и подсказки шапки** (решение владельца 13.10.2026, H-2): «Противоречие раскладки и подсказки
|
||||||
|
шапки — СКЛЕИВАТЬ, НО ПОДСВЕЧИВАТЬ». Пара найдена, но ИНН стоит не там, где указывает подсказка `<группировка>.ИНН`
|
||||||
|
(в дереве или соседом по суммам) — склейка как обычно, отдельное предупреждение «строка N: ИНН … стоит <под/над>
|
||||||
|
наименованием в строке M, а шапка («…») указывает <над/под> — проверьте», `ok = False`, `needs_check`. Закрывает T2.12.
|
||||||
|
|
||||||
### Алгоритм `inn_rows.py`
|
### Алгоритм `inn_rows.py`
|
||||||
|
|
||||||
@ -83,7 +91,8 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
|
|||||||
от ИНН строки — колонка побеждает, предупреждение, `needs_check`. ИНН в хвосте названия — строка ИНН побеждает
|
от ИНН строки — колонка побеждает, предупреждение, `needs_check`. ИНН в хвосте названия — строка ИНН побеждает
|
||||||
(прежний порядок §1.3).
|
(прежний порядок §1.3).
|
||||||
9. **Склейка**: строка ИНН уходит из строк и `Levels`, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к
|
9. **Склейка**: строка ИНН уходит из строк и `Levels`, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к
|
||||||
контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»).
|
контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»). Если раскладка противоречит подсказке шапки
|
||||||
|
(пара выше/ниже, чем указывает подсказка) — следом отдельное предупреждение о противоречии, `ok = False` (решение 10).
|
||||||
10. **Без пары**: строка остаётся в `rows`, `group_type = OTHER` (в свод не попадает, суммы не задваиваются, как #32),
|
10. **Без пары**: строка остаётся в `rows`, `group_type = OTHER` (в свод не попадает, суммы не задваиваются, как #32),
|
||||||
предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» = `False` → документ `needs_check`.
|
предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» = `False` → документ `needs_check`.
|
||||||
|
|
||||||
@ -143,3 +152,5 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
|
|||||||
| 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает |
|
| 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает |
|
||||||
| 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и `needs_check` вместо склейки по сумме; правка объединения — одна функция `_pair_flat` |
|
| 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и `needs_check` вместо склейки по сумме; правка объединения — одна функция `_pair_flat` |
|
||||||
| 18 | 13.10.2026 | Task 4: параметризованный `test_header_status_and_hierarchy[s18_inn_row_above.xlsx]` (ожидает `done`) и golden s18 не правятся в Task 4 — оба падают только из-за ИНН s18 с неверной контрольной суммой и чинятся вместе с файлом в Task 5–6; в коммите упавшие тесты выведены из прогона (`--deselect`), `test_s18_…` помечен strict xfail | Бриф Task 4 прямо запрещает обновлять golden; ожидание `done` в параметризации относится к тому же файлу | Если Task 5 забудет снять xfail/починить параметризацию, strict xfail упадёт (XPASS) и напомнит |
|
| 18 | 13.10.2026 | Task 4: параметризованный `test_header_status_and_hierarchy[s18_inn_row_above.xlsx]` (ожидает `done`) и golden s18 не правятся в Task 4 — оба падают только из-за ИНН s18 с неверной контрольной суммой и чинятся вместе с файлом в Task 5–6; в коммите упавшие тесты выведены из прогона (`--deselect`), `test_s18_…` помечен strict xfail | Бриф Task 4 прямо запрещает обновлять golden; ожидание `done` в параметризации относится к тому же файлу | Если Task 5 забудет снять xfail/починить параметризацию, strict xfail упадёт (XPASS) и напомнит |
|
||||||
|
| 19 | 13.10.2026 | H-2: направление фактической раскладки определяется по номерам строк пары (строка пары выше строки ИНН — ИНН «под», ниже — «над»), одинаково для пар в дереве и соседей; проверка выполняется только при подсказке и только для склеенных пар с непустым ИНН и без расхождения с колонкой ИНН | Номер строки однозначно отражает порядок на листе при любом пути поиска пары; пары без ИНН, без пары и с колонкой-победителем уже подсвечены своими предупреждениями | Нет: предупреждение лишь добавляется к уже состоявшейся склейке; результат склейки не меняется |
|
||||||
|
| 20 | 13.10.2026 | H-1: в s28 строка «7701234567» после классификации попадает в `rows` как контрагент (обычная строка по правилам классификатора) — тест фиксирует это «как есть» и проверяет её наличие среди контрагентов без ИНН | Спека: строка «классифицируется как обычно»; подгонки парсера нет | Нет: поведение соответствует ожиданию владельца |
|
||||||
|
|||||||
@ -129,6 +129,13 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
|
|||||||
«строка N: ИНН … отнесён к контрагенту в строке M». ИНН колонки ИНН у наименования, отличный от ИНН строки, остаётся,
|
«строка N: ИНН … отнесён к контрагенту в строке M». ИНН колонки ИНН у наименования, отличный от ИНН строки, остаётся,
|
||||||
в `warnings` — расхождение, документ — `needs_check`.
|
в `warnings` — расхождение, документ — `needs_check`.
|
||||||
|
|
||||||
|
**Противоречие подсказке шапки:** если пара найдена, но фактическая раскладка противоречит направлению подсказки
|
||||||
|
`<группировка>.ИНН` (подсказка «над», а строка ИНН — ребёнок наименования или сосед ниже него по суммам; подсказка «под»,
|
||||||
|
а строка ИНН — родитель наименования или сосед выше него по суммам), строка ИНН MUST склеиваться как обычно, а в
|
||||||
|
`warnings` MUST быть отдельное предупреждение «строка N: ИНН … стоит <под/над> наименованием в строке M, а шапка
|
||||||
|
(«<группировка>.ИНН») указывает <над/под> — проверьте»; документ — `needs_check`. Для соседей без сумм направление берётся
|
||||||
|
из подсказки, конфликта нет. Раскладка, совпадающая с подсказкой (в том числе «под, в дереве» в 1С), предупреждения не даёт.
|
||||||
|
|
||||||
**Без пары:** строка ИНН остаётся в `rows` с `group_type = OTHER` (в свод ядра не попадает), в `warnings` — «строка N:
|
**Без пары:** строка ИНН остаётся в `rows` с `group_type = OTHER` (в свод ядра не попадает), в `warnings` — «строка N:
|
||||||
ИНН … без пары — <причина>», документ — `needs_check`. Структура под склейку MUST NOT подгоняться.
|
ИНН … без пары — <причина>», документ — `needs_check`. Структура под склейку MUST NOT подгоняться.
|
||||||
|
|
||||||
@ -156,6 +163,19 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
|
|||||||
- **WHEN** в синтетической ОСВ без подсказки над наименованием строка «7701234567» (контрольная сумма не сходится)
|
- **WHEN** в синтетической ОСВ без подсказки над наименованием строка «7701234567» (контрольная сумма не сходится)
|
||||||
- **THEN** строка не склеивается, в `warnings` — «похожа на ИНН, контрольная сумма не сходится», статус `needs_check`
|
- **THEN** строка не склеивается, в `warnings` — «похожа на ИНН, контрольная сумма не сходится», статус `needs_check`
|
||||||
|
|
||||||
|
#### Scenario: Опечатка в ИНН среди верных (s28)
|
||||||
|
- **WHEN** разбирается синтетическая ОСВ `s28` (раскладка `s20`, но у ООО Альфа строка ИНН «7701234567» с неверной
|
||||||
|
контрольной суммой; шапка без подсказки)
|
||||||
|
- **THEN** строка «7701234567» остаётся обычной строкой, у ООО Альфа `counterparty_inn` нет, в `warnings` — «строка N
|
||||||
|
похожа на ИНН 7701234567, но контрольная сумма не сходится», остальные контрагенты получают свои ИНН (направление «над» —
|
||||||
|
по блоку 60.02), статус `needs_check`
|
||||||
|
|
||||||
|
#### Scenario: Раскладка противоречит подсказке шапки
|
||||||
|
- **WHEN** в шапке `Контрагенты.ИНН` («над»), а строка ИНН без детей стоит под родителем-контрагентом (или наоборот:
|
||||||
|
подсказка «под», строка ИНН — родитель наименования)
|
||||||
|
- **THEN** строка ИНН склеена с этим наименованием, в `warnings` — предупреждение «стоит под (над) наименованием … а шапка
|
||||||
|
… указывает над (под) — проверьте», статус `needs_check`
|
||||||
|
|
||||||
#### Scenario: Ведущий ноль у числовой строки ИНН
|
#### Scenario: Ведущий ноль у числовой строки ИНН
|
||||||
- **WHEN** строка ИНН без подсказки — числовая ячейка `123456788` (9 цифр; `0123456788` проходит контрольную сумму)
|
- **WHEN** строка ИНН без подсказки — числовая ячейка `123456788` (9 цифр; `0123456788` проходит контрольную сумму)
|
||||||
- **THEN** `counterparty_inn = "0123456788"`
|
- **THEN** `counterparty_inn = "0123456788"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user