Синтетика: восемь раскладок строки ИНН s20–s27, s18 с верными ИНН (#35)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
26ed11ad28
commit
1f96012884
@ -1,7 +1,8 @@
|
||||
"""Генератор синтетических ОСВ (Срез 2a, Task 11): `python -m tests.synthetic.generate <каталог>` из `api/`.
|
||||
|
||||
Файлы s01…s13 — по одному сценарию спеки `osv-parsing` на файл, s14…s16 — записи словарей, которых нет в реальных
|
||||
ОСВ (R-22), s17…s19 — ИНН в тексте названия, ИНН строкой выше и «ИНН/КПП» в одной ячейке (issue #9); описание —
|
||||
ОСВ (R-22), s17…s19 — ИНН в тексте названия, ИНН строкой выше и «ИНН/КПП» в одной ячейке (issue #9), s20…s27 — восемь
|
||||
раскладок строки ИНН (#35); описание —
|
||||
`osv/synthetic/README.md` в репозитории fixtures. Генератор самодостаточен (openpyxl +
|
||||
`xlsb_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок
|
||||
записи фиксирован.
|
||||
@ -69,9 +70,11 @@ class Node:
|
||||
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]
|
||||
return [kid for kid in self.kids if not kid.indicator and kid.twin is None]
|
||||
|
||||
def six(self) -> Six:
|
||||
"""Шесть сумм по колонкам: начало Д/К, обороты Д/К, конец Д/К (без порчи)."""
|
||||
@ -365,7 +368,7 @@ def build_osv(spec: OsvSpec, root: Node, name: str = SHEET) -> SheetSpec:
|
||||
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.six())
|
||||
_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(TOTAL_TEXT), 0), root.six())
|
||||
@ -628,10 +631,13 @@ def _s17() -> list[SheetSpec]:
|
||||
|
||||
|
||||
def _s18() -> list[SheetSpec]:
|
||||
"""ИНН отдельной строкой над названием контрагента (OQ-130): строка без сумм, того же уровня; колонки ИНН нет."""
|
||||
root = Node(
|
||||
"60", [Node("60.01", [alfa(inn_style="above"), beta(), ivanov()]), Node("60.02", [gamma(inn_style="above")])]
|
||||
)
|
||||
"""ИНН отдельной строкой над названием контрагента (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)]
|
||||
|
||||
|
||||
@ -645,6 +651,50 @@ def _s19() -> list[SheetSpec]:
|
||||
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 _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
|
||||
|
||||
|
||||
def _normalized_zip(data: bytes) -> bytes:
|
||||
"""Тот же архив с постоянными метками времени: записей zip и `dcterms:modified` (openpyxl ставит текущее время
|
||||
при каждом `save`, заданное в свойствах значение перезаписывает)."""
|
||||
@ -713,6 +763,14 @@ FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSp
|
||||
"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),
|
||||
}
|
||||
FILE_NAMES = tuple(FILES)
|
||||
|
||||
@ -731,7 +789,7 @@ def generate_all(directory: Path) -> list[Path]:
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python -m tests.synthetic.generate",
|
||||
description="Синтетические ОСВ s01…s19 (xlsx и один xlsb) в указанный каталог.",
|
||||
description="Синтетические ОСВ s01…s27 (xlsx и один xlsb) в указанный каталог.",
|
||||
)
|
||||
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
@ -26,6 +26,16 @@ def _require_synthetic(synthetic_dir: Path) -> None:
|
||||
|
||||
|
||||
SYNTHETIC_DIR = Path("osv") / "synthetic"
|
||||
INN_LAYOUT_FILES = (
|
||||
"s20_inn_above_sibling_blank.xlsx",
|
||||
"s21_inn_above_sibling_sums.xlsx",
|
||||
"s22_inn_above_parent_blank.xlsx",
|
||||
"s23_inn_above_parent_sums.xlsx",
|
||||
"s24_inn_below_child_blank.xlsx",
|
||||
"s25_inn_below_child_sums.xlsx",
|
||||
"s26_inn_below_sibling_sums.xlsx",
|
||||
"s27_inn_below_sibling_blank.xlsx",
|
||||
)
|
||||
EXPECTED_NAMES = [
|
||||
"s01_variant2_header.xlsx",
|
||||
"s02_no_outline_indent.xlsx",
|
||||
@ -46,6 +56,7 @@ EXPECTED_NAMES = [
|
||||
"s17_inn_in_name.xlsx",
|
||||
"s18_inn_row_above.xlsx",
|
||||
"s19_inn_kpp_cell.xlsx",
|
||||
*INN_LAYOUT_FILES,
|
||||
]
|
||||
XLSB_NAME = "s13_no_outline_indent.xlsb"
|
||||
XLSB_MISSING_REASON = f"нет файла {XLSB_NAME} (xlsb не получен; см. README fixtures)"
|
||||
@ -109,6 +120,8 @@ EXPECTED: dict[str, Expect] = {
|
||||
"s18_inn_row_above.xlsx": Expect("outline", 2025, "Q2", False),
|
||||
# колонка «ИНН/КПП»
|
||||
"s19_inn_kpp_cell.xlsx": Expect("outline", 2025, "Q3", False),
|
||||
# восемь раскладок строки ИНН (#35)
|
||||
**{name: Expect("outline", 2025, "Q1", True) for name in INN_LAYOUT_FILES},
|
||||
}
|
||||
|
||||
|
||||
@ -180,7 +193,7 @@ def tree(doc: OsvDocument) -> list[tuple[str, int, int | None, tuple[Decimal | N
|
||||
|
||||
|
||||
def test_synthetic_set_is_complete(fixtures_dir: Path) -> None:
|
||||
"""В `fixtures/osv/synthetic/` лежат файлы набора s01…s19 (s13 — xlsb; допустима деградация: нет только его)."""
|
||||
"""В `fixtures/osv/synthetic/` лежат файлы набора s01…s27 (s13 — xlsb; допустима деградация: нет только его)."""
|
||||
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]
|
||||
missing = [name for name in EXPECTED_NAMES if name not in present]
|
||||
@ -518,19 +531,43 @@ def test_s17_inn_in_name_is_extracted_and_name_is_clean(fixtures_dir: Path) -> N
|
||||
assert "inn" not in doc.columns
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="#35: ИНН в s18 без верной контрольной суммы, исправляется в Task 5", strict=True)
|
||||
def test_s18_inn_row_above_is_glued_and_not_other(fixtures_dir: Path) -> None:
|
||||
"""OQ-130: строка с одним ИНН над названием не попадает в строки и не уходит в OTHER, ИНН — у контрагента."""
|
||||
outcome = parse_named(fixtures_dir, "s18_inn_row_above.xlsx")
|
||||
doc = one_document(outcome)
|
||||
|
||||
assert not [row for row in doc.rows if row.group_type == "OTHER"]
|
||||
assert not [row for row in doc.rows if row.row_value in {"7701234567", "5001234567"}]
|
||||
assert not [row for row in doc.rows if row.row_value in {"7701234560", "5001234561"}]
|
||||
counterparties = {
|
||||
row.counterparty_name: row.counterparty_inn for row in doc.rows if row.group_type == "COUNTERPARTY"
|
||||
}
|
||||
assert counterparties == {ALFA: "7701234567", BETA: None, "Иванов Иван": None, GAMMA: "5001234567"}
|
||||
assert any("ИНН 7701234567" in warning for warning in warnings_of(outcome))
|
||||
assert counterparties == {ALFA: "7701234560", BETA: None, "Иванов Иван": None, GAMMA: "5001234561"}
|
||||
assert any("ИНН 7701234560" in warning for warning in warnings_of(outcome))
|
||||
|
||||
|
||||
INN_LAYOUT_COUNTERPARTIES = {
|
||||
ALFA: "7701234560",
|
||||
BETA: "7707654321",
|
||||
"Иванов Иван": None,
|
||||
GAMMA: "5001234561",
|
||||
"ИП Сидоров": "500100732259",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", INN_LAYOUT_FILES)
|
||||
def test_inn_row_layouts_give_inn_to_own_counterparty(fixtures_dir: Path, name: str) -> None:
|
||||
"""#35: в каждой из восьми раскладок строки ИНН у каждого контрагента — ИНН его строки, строк ИНН в `rows` нет."""
|
||||
outcome = parse_named(fixtures_dir, name)
|
||||
doc = one_document(outcome)
|
||||
|
||||
assert outcome.status is ResultStatus.DONE, warnings_of(outcome)
|
||||
counterparties = {
|
||||
row.counterparty_name: row.counterparty_inn for row in doc.rows if row.group_type == "COUNTERPARTY"
|
||||
}
|
||||
assert counterparties == INN_LAYOUT_COUNTERPARTIES
|
||||
assert not [row for row in doc.rows if row.group_type == "OTHER"]
|
||||
inns = {inn for inn in INN_LAYOUT_COUNTERPARTIES.values() if inn}
|
||||
assert not [row for row in doc.rows if row.row_value in inns]
|
||||
|
||||
|
||||
def test_s19_inn_kpp_cell_gives_inn_only(fixtures_dir: Path) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user