test: синтетика s40–s44 — плоская форма ОСВ 1С (#59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ecb23475db
commit
e625409818
@ -3,7 +3,8 @@
|
|||||||
Файлы 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 — восемь
|
||||||
раскладок строки ИНН, s28 — ИНН с неверной контрольной суммой (#35), s29…s32 — xls без CODEPAGE, «Вал»/«Кол-во»,
|
раскладок строки ИНН, s28 — ИНН с неверной контрольной суммой (#35), s29…s32 — xls без CODEPAGE, «Вал»/«Кол-во»,
|
||||||
строки ниже «Итого» и «Итого по счету» вместо точного «Итого» (#47); описание — `osv/synthetic/README.md`
|
строки ниже «Итого» и «Итого по счету» вместо точного «Итого» (#47), s40…s44 — плоская форма ОСВ 1С (#59; s33…s39 заняты
|
||||||
|
другим треком); описание — `osv/synthetic/README.md`
|
||||||
в репозитории fixtures. Генератор самодостаточен (openpyxl + `xlsb_writer` + `xls_writer`) и
|
в репозитории fixtures. Генератор самодостаточен (openpyxl + `xlsb_writer` + `xls_writer`) и
|
||||||
детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок записи фиксирован.
|
детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок записи фиксирован.
|
||||||
|
|
||||||
@ -861,6 +862,116 @@ def _s32() -> list[SheetSpec]:
|
|||||||
return [sheet]
|
return [sheet]
|
||||||
|
|
||||||
|
|
||||||
|
FlatRow = tuple[str, tuple[str | None, ...], int]
|
||||||
|
|
||||||
|
|
||||||
|
def _flat_sheet(texts: dict[int, str], group_row: int, rows: Sequence[FlatRow]) -> SheetSpec:
|
||||||
|
"""Плоская форма 1С (#59): колонка A — текст, B…G — суммы «начало / обороты / конец» × «Дебет | Кредит»; заголовок
|
||||||
|
«Субконто» + группы (объединения B:C, D:E, F:G) в `group_row`, подзаголовки под ним, данные сразу ниже. `texts` —
|
||||||
|
строки шапки над таблицей; в `rows` пустая сумма (None) — нет ячейки, отступ — выравнивание ячейки."""
|
||||||
|
sheet = SheetSpec(SHEET)
|
||||||
|
for row, text in texts.items():
|
||||||
|
sheet.put(row, 1, text)
|
||||||
|
sheet.put(group_row, 1, "Субконто")
|
||||||
|
for index, key in enumerate(("opening", "turnover", "closing")):
|
||||||
|
col = 2 + index * PAIR_WIDTH
|
||||||
|
sheet.put(group_row, col, GROUP_TITLES[key])
|
||||||
|
sheet.merge(group_row, col, group_row, col + 1)
|
||||||
|
sheet.put(group_row + 1, col, DEBIT)
|
||||||
|
sheet.put(group_row + 1, col + 1, CREDIT)
|
||||||
|
for offset, (text, sums, indent) in enumerate(rows):
|
||||||
|
row = group_row + 2 + offset
|
||||||
|
sheet.put(row, 1, text, indent=indent)
|
||||||
|
for col, value in enumerate(sums, start=2):
|
||||||
|
if value is not None:
|
||||||
|
sheet.put(row, col, D(value))
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def _s40() -> list[SheetSpec]:
|
||||||
|
"""Плоская форма (#59): счёт в заголовке «по счету: 76.Н.1» и вид аналитики «Контрагенты» строкой ниже, данные без
|
||||||
|
outline и отступов; «Итого развернутое» — сумма строк, «Итого» — свёрнутое сальдо."""
|
||||||
|
texts = {
|
||||||
|
2: "Оборотно-сальдовая ведомость по счету: 76.Н.1",
|
||||||
|
3: "Контрагенты",
|
||||||
|
4: "за 2025 г.",
|
||||||
|
5: 'ООО "Товарное хозяйство"',
|
||||||
|
7: "Единица измерения: рубль (код по ОКЕИ 383)",
|
||||||
|
}
|
||||||
|
rows: list[FlatRow] = [
|
||||||
|
("Август - Агро ООО", ("1000.00", None, "500.00", "300.00", "1200.00", None), 0),
|
||||||
|
("АВИС ООО", (None, "200.00", "100.00", "400.00", None, "500.00"), 0),
|
||||||
|
("Агроаспект ООО", (None, None, "50.00", "50.00", None, None), 0),
|
||||||
|
("Итого развернутое", ("1000.00", "200.00", None, None, "1200.00", "500.00"), 0),
|
||||||
|
("Итого", ("800.00", None, "650.00", "750.00", "700.00", None), 0),
|
||||||
|
]
|
||||||
|
return [_flat_sheet(texts, 9, rows)]
|
||||||
|
|
||||||
|
|
||||||
|
def _s41() -> list[SheetSpec]:
|
||||||
|
"""Плоская форма (#59): вид аналитики «Организации», период «за 2 Квартал 2026 г.» со словами «План счетов …»."""
|
||||||
|
texts = {
|
||||||
|
2: "Оборотно-сальдовая ведомость по счету: 60",
|
||||||
|
3: "Организации",
|
||||||
|
4: "за 2 Квартал 2026 г. План счетов ООО Хреновская Нива",
|
||||||
|
}
|
||||||
|
rows: list[FlatRow] = [
|
||||||
|
("Автодор-Платные Дороги ООО", ("820.21", None, "5000.00", "4991.00", "829.21", None), 0),
|
||||||
|
("АГРОПРОФИ ООО", (None, "2763.79", None, None, None, "2763.79"), 0),
|
||||||
|
("Итого развернутое", ("820.21", "2763.79", None, None, "829.21", "2763.79"), 0),
|
||||||
|
("Итого", (None, "1943.58", "5000.00", "4991.00", None, "1934.58"), 0),
|
||||||
|
]
|
||||||
|
return [_flat_sheet(texts, 5, rows)]
|
||||||
|
|
||||||
|
|
||||||
|
def _s42() -> list[SheetSpec]:
|
||||||
|
"""Плоская форма (#59): под счётом 76 идут субсчета «76.2», «76.5», «76.Н», «76.Н.1»; «Итого развернутое» перед
|
||||||
|
«Итого»."""
|
||||||
|
texts = {
|
||||||
|
2: "Оборотно-сальдовая ведомость по счету: 76",
|
||||||
|
3: "за 2025 г.",
|
||||||
|
4: 'ООО "Товарное хозяйство"',
|
||||||
|
6: "Единица измерения: рубль (код по ОКЕИ 383)",
|
||||||
|
}
|
||||||
|
rows: list[FlatRow] = [
|
||||||
|
("76.2", ("1000.00", None, None, "1000.00", None, None), 0),
|
||||||
|
("76.5", (None, "300.00", "700.00", "600.00", None, "200.00"), 0),
|
||||||
|
("76.Н", (None, None, "5000.00", "5000.00", None, None), 0),
|
||||||
|
("76.Н.1", (None, None, "5000.00", "5000.00", None, None), 0),
|
||||||
|
("Итого развернутое", ("1000.00", "300.00", None, None, None, "200.00"), 0),
|
||||||
|
("Итого", ("700.00", None, "5700.00", "6600.00", None, "200.00"), 0),
|
||||||
|
]
|
||||||
|
return [_flat_sheet(texts, 8, rows)]
|
||||||
|
|
||||||
|
|
||||||
|
def _s43_s44(account: str, root: str, kids: tuple[str, str]) -> list[SheetSpec]:
|
||||||
|
"""Счёт с отступами: корень-субсчёт (без отступа) и два элемента аналитики с отступом 1, без вида аналитики в шапке;
|
||||||
|
«Итого» перед «Итого развернутое»."""
|
||||||
|
texts = {
|
||||||
|
1: 'ООО "Октябрьское"',
|
||||||
|
2: f"Оборотно-сальдовая ведомость по счету {account}",
|
||||||
|
3: "Период: 9 месяцев 2024 г.",
|
||||||
|
}
|
||||||
|
rows: list[FlatRow] = [
|
||||||
|
(root, ("34618000.00", None, "4236500.00", "38854500.00", None, None), 0),
|
||||||
|
(kids[0], ("34618000.00", None, None, "34618000.00", None, None), 1),
|
||||||
|
(kids[1], (None, None, "4236500.00", "4236500.00", None, None), 1),
|
||||||
|
("Итого", ("34618000.00", None, "4236500.00", "38854500.00", None, None), 0),
|
||||||
|
("Итого развернутое", ("34618000.00", None, "4236500.00", "38854500.00", None, None), 0),
|
||||||
|
]
|
||||||
|
return [_flat_sheet(texts, 5, rows)]
|
||||||
|
|
||||||
|
|
||||||
|
def _s43() -> list[SheetSpec]:
|
||||||
|
"""Плоская форма (#59): счёт 60, контрагенты без вида аналитики в шапке (подстроки с отступом)."""
|
||||||
|
return _s43_s44("60", "60.01", ("ИП Знаменских А.И.", 'ООО "ЭСТЕТ ОМСК"'))
|
||||||
|
|
||||||
|
|
||||||
|
def _s44() -> list[SheetSpec]:
|
||||||
|
"""То же, что s43, но счёт 10: номенклатура вместо контрагентов."""
|
||||||
|
return _s43_s44("10", "10.01", ("Мука пшеничная", "Сахар-песок"))
|
||||||
|
|
||||||
|
|
||||||
def _s28() -> list[SheetSpec]:
|
def _s28() -> list[SheetSpec]:
|
||||||
"""Раскладка s20 («над, не в дереве, без сумм»), но ИНН ООО Альфа с опечаткой: контрольная сумма не сходится."""
|
"""Раскладка s20 («над, не в дереве, без сумм»), но ИНН ООО Альфа с опечаткой: контрольная сумма не сходится."""
|
||||||
tree = _inn_layout_tree()
|
tree = _inn_layout_tree()
|
||||||
@ -969,6 +1080,11 @@ FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSp
|
|||||||
"s34_indicator_column_qty_val_diff.xlsx": (_s34, to_xlsx),
|
"s34_indicator_column_qty_val_diff.xlsx": (_s34, to_xlsx),
|
||||||
"s35_currency_slices.xlsx": (_s35, to_xlsx),
|
"s35_currency_slices.xlsx": (_s35, to_xlsx),
|
||||||
"s36_no_data_type_header.xlsx": (_s36, to_xlsx),
|
"s36_no_data_type_header.xlsx": (_s36, to_xlsx),
|
||||||
|
"s40_flat_header_account.xlsx": (_s40, to_xlsx),
|
||||||
|
"s41_flat_chart_of_accounts.xlsx": (_s41, to_xlsx),
|
||||||
|
"s42_subaccounts_totals_reversed.xlsx": (_s42, to_xlsx),
|
||||||
|
"s43_subkonto_unnamed_60.xlsx": (_s43, to_xlsx),
|
||||||
|
"s44_subkonto_unnamed_10.xlsx": (_s44, to_xlsx),
|
||||||
}
|
}
|
||||||
FILE_NAMES = tuple(FILES)
|
FILE_NAMES = tuple(FILES)
|
||||||
|
|
||||||
@ -987,7 +1103,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…s36 (xlsx, один xlsb и один xls) в указанный каталог.",
|
description="Синтетические ОСВ s01…s36, s40…s44 (xlsx, один xlsb и один xls) в указанный каталог.",
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
|||||||
@ -31,7 +31,9 @@ def test_generated_set_is_s01_to_s19_with_one_xlsb(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert names == list(FILE_NAMES)
|
assert names == list(FILE_NAMES)
|
||||||
assert [re.match(r"s(\d\d)_", name) is not None for name in names] == [True] * len(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)]
|
# s33…s36 — строки-показатели (AURORA-1613, #58), плоская форма ОСВ 1С (#59) — s40…s44
|
||||||
|
numbers = [*range(1, 37), *range(40, 45)]
|
||||||
|
assert [name[1:3] for name in names] == [f"{number:02d}" for number in numbers]
|
||||||
assert [name for name in names if name.endswith(".xlsb")] == ["s13_no_outline_indent.xlsb"]
|
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)
|
assert sorted(path.name for path in tmp_path.iterdir()) == sorted(names)
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,15 @@ XLS_NO_CODEPAGE_FILE = "s29_xls_no_codepage.xls"
|
|||||||
VAL_KOL_VO_FILE = "s30_val_kol_vo_materials.xlsx"
|
VAL_KOL_VO_FILE = "s30_val_kol_vo_materials.xlsx"
|
||||||
ROWS_BELOW_TOTAL_FILE = "s31_rows_below_total.xlsx"
|
ROWS_BELOW_TOTAL_FILE = "s31_rows_below_total.xlsx"
|
||||||
TOTAL_WITH_SUFFIX_FILE = "s32_total_with_suffix.xlsx"
|
TOTAL_WITH_SUFFIX_FILE = "s32_total_with_suffix.xlsx"
|
||||||
|
|
||||||
|
# Плоская форма ОСВ 1С (#59): на этапе снимков текущего поведения ожиданий нет — их добавляют задачи 2–5 плана
|
||||||
|
FLAT_FORM_FILES = (
|
||||||
|
"s40_flat_header_account.xlsx",
|
||||||
|
"s41_flat_chart_of_accounts.xlsx",
|
||||||
|
"s42_subaccounts_totals_reversed.xlsx",
|
||||||
|
"s43_subkonto_unnamed_60.xlsx",
|
||||||
|
"s44_subkonto_unnamed_10.xlsx",
|
||||||
|
)
|
||||||
TYPO_ROW = 13 # строка «7701234567» с неверной контрольной суммой в s28
|
TYPO_ROW = 13 # строка «7701234567» с неверной контрольной суммой в s28
|
||||||
EXPECTED_NAMES = [
|
EXPECTED_NAMES = [
|
||||||
"s01_variant2_header.xlsx",
|
"s01_variant2_header.xlsx",
|
||||||
@ -73,6 +82,7 @@ EXPECTED_NAMES = [
|
|||||||
"s34_indicator_column_qty_val_diff.xlsx",
|
"s34_indicator_column_qty_val_diff.xlsx",
|
||||||
"s35_currency_slices.xlsx",
|
"s35_currency_slices.xlsx",
|
||||||
"s36_no_data_type_header.xlsx",
|
"s36_no_data_type_header.xlsx",
|
||||||
|
*FLAT_FORM_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)"
|
||||||
@ -229,7 +239,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…s36 (s13 — xlsb; допустима деградация: нет только его)."""
|
"""В `fixtures/osv/synthetic/` лежат файлы набора s01…s36, s40…s44 (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]
|
||||||
@ -238,11 +248,13 @@ def test_synthetic_set_is_complete(fixtures_dir: Path) -> None:
|
|||||||
|
|
||||||
def test_every_synthetic_file_is_listed(synthetic_file: Path) -> None:
|
def test_every_synthetic_file_is_listed(synthetic_file: Path) -> None:
|
||||||
"""Каждый файл каталога описан в таблице ожиданий (новый файл без ожиданий — ошибка набора)."""
|
"""Каждый файл каталога описан в таблице ожиданий (новый файл без ожиданий — ошибка набора)."""
|
||||||
assert synthetic_file.name in EXPECTED, f"добавьте ожидания для {synthetic_file.name}"
|
assert synthetic_file.name in {*EXPECTED, *FLAT_FORM_FILES}, f"добавьте ожидания для {synthetic_file.name}"
|
||||||
|
|
||||||
|
|
||||||
def test_header_status_and_hierarchy(synthetic_file: Path) -> None:
|
def test_header_status_and_hierarchy(synthetic_file: Path) -> None:
|
||||||
"""Статус, число документов, стратегия иерархии и шапка из таблицы `EXPECTED`; результат валиден по контракту."""
|
"""Статус, число документов, стратегия иерархии и шапка из таблицы `EXPECTED`; результат валиден по контракту."""
|
||||||
|
if synthetic_file.name in FLAT_FORM_FILES:
|
||||||
|
pytest.skip("плоская форма (#59): ожидания по файлу — в сценариях задач 2–5, до них поведение фиксирует golden")
|
||||||
expect = EXPECTED[synthetic_file.name]
|
expect = EXPECTED[synthetic_file.name]
|
||||||
|
|
||||||
outcome = parse_path(synthetic_file)
|
outcome = parse_path(synthetic_file)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user