Andrey Epifancev a592c68509 feat(osv): ИНН из текста названия контрагента, ИНН строкой выше, «ИНН/КПП» в ячейке
- OQ-100: ИНН в конце названия («Полиграф, 3528315710», «(ИНН …)», пробел) уходит в counterparty_inn, имя — без ИНН
  и разделителя; колонка ИНН приоритетнее; только для COUNTERPARTY.
- OQ-130: строка без сумм с одним ИНН над строкой с суммами того же уровня склеивается со следующей и не уходит в OTHER.
- «ИНН/КПП» в ячейке колонки ИНН даёт ИНН.
- Правила — osv_headers.yaml (counterparty_inn_suffix, inn_only_row, inn_kpp_cell), не код.
- Синтетика s17–s19 (генератор, тесты); golden — в репозитории fixtures (feat/9-golden).

Stacked on #13 (feat/11-dkb-formats).
Closes #9

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 13:13:44 +04:00

73 lines
3.2 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.

"""Генератор синтетики: детерминированность, состав набора и читаемость xlsb-писателя (не требует каталога fixtures)."""
import re
import time
from decimal import Decimal
from pathlib import Path
from src.parser.readers import read_book
from tests.synthetic.generate import FILE_NAMES, generate_all
from tests.synthetic.xlsb_writer import write_xlsb
# Сколько ждать между двумя генерациями: у записей zip разрешение метки времени — 2 с
ZIP_TIME_RESOLUTION_SECONDS = 2.1
LONG_TEXT_LENGTH = 400
BIG_ROW = 70000
def test_generation_is_reproducible_byte_for_byte(tmp_path: Path) -> None:
"""Повторный запуск (позже, чем разрешение меток времени) даёт те же байты для каждого файла набора."""
first = generate_all(tmp_path / "first")
time.sleep(ZIP_TIME_RESOLUTION_SECONDS)
second = generate_all(tmp_path / "second")
assert [path.name for path in first] == [path.name for path in second]
for left, right in zip(first, second, strict=True):
assert left.read_bytes() == right.read_bytes(), left.name
def test_generated_set_is_s01_to_s19_with_one_xlsb(tmp_path: Path) -> None:
names = [path.name for path in generate_all(tmp_path)]
assert names == list(FILE_NAMES)
assert [re.match(r"s(\d\d)_", name) is not None for name in names] == [True] * len(names)
assert [name[1:3] for name in names] == [f"{number:02d}" for number in range(1, len(names) + 1)]
assert [name for name in names if name.endswith(".xlsb")] == ["s13_no_outline_indent.xlsb"]
assert sorted(path.name for path in tmp_path.iterdir()) == sorted(names)
def test_xlsb_writer_round_trips_through_the_reader() -> None:
"""Тексты (с ведущими пробелами и кириллицей), числа и несколько листов читаются `src.parser.readers`."""
long_text = "щ" * LONG_TEXT_LENGTH # запись длиннее 127 байт: длина кодируется двумя байтами
data = write_xlsb(
{
"Лист_1": {
(1, 1): "Привет",
(2, 3): Decimal("1234567.89"),
(3, 1): " отступ",
(4, 2): long_text,
(BIG_ROW, 4): 7,
},
"Второй": {(1, 1): "Привет"},
}
)
sheets, warnings = read_book(data, "XLSB")
assert [sheet.name for sheet in sheets] == ["Лист_1", "Второй"]
first = sheets[0]
assert first.text_at(1, 1) == "Привет"
assert first.cell(2, 3).value == Decimal("1234567.89")
assert (first.text_at(3, 1), first.cell(3, 1).indent) == ("отступ", 4)
assert first.text_at(4, 2) == long_text
assert first.cell(BIG_ROW, 4).value == Decimal(7)
assert first.n_rows == BIG_ROW
assert sheets[1].text_at(1, 1) == "Привет"
assert warnings == ["xlsb: outline level недоступен"]
def test_xlsb_writer_is_deterministic() -> None:
cells = {"Лист_1": {(1, 1): "а", (2, 2): Decimal("1.50")}}
assert write_xlsb(cells) == write_xlsb(cells)