Синтетика: восемь раскладок строки ИНН 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/`.
|
"""Генератор синтетических ОСВ (Срез 2a, Task 11): `python -m tests.synthetic.generate <каталог>` из `api/`.
|
||||||
|
|
||||||
Файлы s01…s13 — по одному сценарию спеки `osv-parsing` на файл, s14…s16 — записи словарей, которых нет в реальных
|
Файлы 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 +
|
`osv/synthetic/README.md` в репозитории fixtures. Генератор самодостаточен (openpyxl +
|
||||||
`xlsb_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок
|
`xlsb_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок
|
||||||
записи фиксирован.
|
записи фиксирован.
|
||||||
@ -69,9 +70,11 @@ class Node:
|
|||||||
num_style: NumStyle = "num"
|
num_style: NumStyle = "num"
|
||||||
inn_style: InnStyle = "column"
|
inn_style: InnStyle = "column"
|
||||||
closing_debit_delta: D = ZERO # порча суммы (s10): к записанному `closing_debit` прибавляется, родитель не меняется
|
closing_debit_delta: D = ZERO # порча суммы (s10): к записанному `closing_debit` прибавляется, родитель не меняется
|
||||||
|
blank: bool = False # строка без сумм (строка ИНН без сумм, #35)
|
||||||
|
twin: "Node | None" = None # строка ИНН-сосед (#35): суммы печатаются как у `twin`, в сумму родителя не входит
|
||||||
|
|
||||||
def children(self) -> list["Node"]:
|
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:
|
def six(self) -> Six:
|
||||||
"""Шесть сумм по колонкам: начало Д/К, обороты Д/К, конец Д/К (без порчи)."""
|
"""Шесть сумм по колонкам: начало Д/К, обороты Д/К, конец Д/К (без порчи)."""
|
||||||
@ -365,7 +368,7 @@ def build_osv(spec: OsvSpec, root: Node, name: str = SHEET) -> SheetSpec:
|
|||||||
inn_line = Node(node.inn)
|
inn_line = Node(node.inn)
|
||||||
_row_cells(sheet, spec, next_row, (inn_line, depth), (ZERO,) * 6, blank=True)
|
_row_cells(sheet, spec, next_row, (inn_line, depth), (ZERO,) * 6, blank=True)
|
||||||
next_row += 1
|
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
|
next_row += 1
|
||||||
total = SheetSpec("")
|
total = SheetSpec("")
|
||||||
_row_cells(total, spec, 1, (Node(TOTAL_TEXT), 0), root.six())
|
_row_cells(total, spec, 1, (Node(TOTAL_TEXT), 0), root.six())
|
||||||
@ -628,10 +631,13 @@ def _s17() -> list[SheetSpec]:
|
|||||||
|
|
||||||
|
|
||||||
def _s18() -> list[SheetSpec]:
|
def _s18() -> list[SheetSpec]:
|
||||||
"""ИНН отдельной строкой над названием контрагента (OQ-130): строка без сумм, того же уровня; колонки ИНН нет."""
|
"""ИНН отдельной строкой над названием контрагента (OQ-130): строка без сумм, того же уровня; колонки ИНН нет.
|
||||||
root = Node(
|
ИНН с верной контрольной суммой: без подсказки в шапке другой не склеивается (#35)."""
|
||||||
"60", [Node("60.01", [alfa(inn_style="above"), beta(), ivanov()]), Node("60.02", [gamma(inn_style="above")])]
|
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)]
|
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)]
|
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:
|
def _normalized_zip(data: bytes) -> bytes:
|
||||||
"""Тот же архив с постоянными метками времени: записей zip и `dcterms:modified` (openpyxl ставит текущее время
|
"""Тот же архив с постоянными метками времени: записей zip и `dcterms:modified` (openpyxl ставит текущее время
|
||||||
при каждом `save`, заданное в свойствах значение перезаписывает)."""
|
при каждом `save`, заданное в свойствах значение перезаписывает)."""
|
||||||
@ -713,6 +763,14 @@ FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSp
|
|||||||
"s17_inn_in_name.xlsx": (_s17, to_xlsx),
|
"s17_inn_in_name.xlsx": (_s17, to_xlsx),
|
||||||
"s18_inn_row_above.xlsx": (_s18, to_xlsx),
|
"s18_inn_row_above.xlsx": (_s18, to_xlsx),
|
||||||
"s19_inn_kpp_cell.xlsx": (_s19, 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)
|
FILE_NAMES = tuple(FILES)
|
||||||
|
|
||||||
@ -731,7 +789,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…s19 (xlsx и один xlsb) в указанный каталог.",
|
description="Синтетические ОСВ s01…s27 (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)
|
||||||
|
|||||||
@ -26,6 +26,16 @@ def _require_synthetic(synthetic_dir: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
SYNTHETIC_DIR = Path("osv") / "synthetic"
|
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 = [
|
EXPECTED_NAMES = [
|
||||||
"s01_variant2_header.xlsx",
|
"s01_variant2_header.xlsx",
|
||||||
"s02_no_outline_indent.xlsx",
|
"s02_no_outline_indent.xlsx",
|
||||||
@ -46,6 +56,7 @@ EXPECTED_NAMES = [
|
|||||||
"s17_inn_in_name.xlsx",
|
"s17_inn_in_name.xlsx",
|
||||||
"s18_inn_row_above.xlsx",
|
"s18_inn_row_above.xlsx",
|
||||||
"s19_inn_kpp_cell.xlsx",
|
"s19_inn_kpp_cell.xlsx",
|
||||||
|
*INN_LAYOUT_FILES,
|
||||||
]
|
]
|
||||||
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)"
|
||||||
@ -109,6 +120,8 @@ EXPECTED: dict[str, Expect] = {
|
|||||||
"s18_inn_row_above.xlsx": Expect("outline", 2025, "Q2", False),
|
"s18_inn_row_above.xlsx": Expect("outline", 2025, "Q2", False),
|
||||||
# колонка «ИНН/КПП»
|
# колонка «ИНН/КПП»
|
||||||
"s19_inn_kpp_cell.xlsx": Expect("outline", 2025, "Q3", 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:
|
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())
|
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]
|
||||||
@ -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
|
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:
|
def test_s18_inn_row_above_is_glued_and_not_other(fixtures_dir: Path) -> None:
|
||||||
"""OQ-130: строка с одним ИНН над названием не попадает в строки и не уходит в OTHER, ИНН — у контрагента."""
|
"""OQ-130: строка с одним ИНН над названием не попадает в строки и не уходит в OTHER, ИНН — у контрагента."""
|
||||||
outcome = parse_named(fixtures_dir, "s18_inn_row_above.xlsx")
|
outcome = parse_named(fixtures_dir, "s18_inn_row_above.xlsx")
|
||||||
doc = one_document(outcome)
|
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.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 = {
|
counterparties = {
|
||||||
row.counterparty_name: row.counterparty_inn for row in doc.rows if row.group_type == "COUNTERPARTY"
|
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 counterparties == {ALFA: "7701234560", BETA: None, "Иванов Иван": None, GAMMA: "5001234561"}
|
||||||
assert any("ИНН 7701234567" in warning for warning in warnings_of(outcome))
|
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:
|
def test_s19_inn_kpp_cell_gives_inn_only(fixtures_dir: Path) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user