test(osv): синтетика s33–s36 — колонка «Показатели», срезы валют, шапка без типа данных (#58)
Сценарии намеренно красные: воспроизводят баги AURORA-1613 и AURORA-1661. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
74ba89c417
commit
af70c4c466
@ -425,6 +425,67 @@ def form_sheet(name: str, *lines: str) -> SheetSpec:
|
|||||||
return sheet
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
MERGED = "<объединено>" # ячейка текста — продолжение объединения строки выше
|
||||||
|
INDICATOR_TITLE = "Показа-\nтели"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class GridRow:
|
||||||
|
"""Строка листа «по сетке»: текст (None — пусто, MERGED — объединение с ячейкой выше), метка колонки «Показатели»,
|
||||||
|
шесть сумм (начало Д/К, обороты Д/К, конец Д/К; строки — `Decimal` в файле) и outline level."""
|
||||||
|
|
||||||
|
text: str | None
|
||||||
|
sums: tuple[str | None, str | None, str | None, str | None, str | None, str | None]
|
||||||
|
label: str | None = None
|
||||||
|
outline: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _grid_osv( # noqa: PLR0913 - лист описывается полностью одним вызовом
|
||||||
|
*,
|
||||||
|
account: str,
|
||||||
|
group_label: str,
|
||||||
|
rows: Sequence[GridRow],
|
||||||
|
data_type_line: str | None,
|
||||||
|
indicator_column: bool,
|
||||||
|
name: str = "Лист_1",
|
||||||
|
) -> SheetSpec:
|
||||||
|
"""ОСВ, заданная строками напрямую (не деревом `Node`): колонка «Показатели», объединённые названия, соседи."""
|
||||||
|
sheet = SheetSpec(name)
|
||||||
|
sheet.put(1, 1, ORG)
|
||||||
|
sheet.put(2, 1, f"Оборотно-сальдовая ведомость по счету {account}")
|
||||||
|
sheet.put(3, 1, "Период: 3 квартал 2025 г.")
|
||||||
|
if data_type_line is not None:
|
||||||
|
sheet.put(4, 1, data_type_line)
|
||||||
|
sheet.put(5, 1, "Параметры:")
|
||||||
|
sheet.put(5, 3, "Валюта БУ, НУ: Руб")
|
||||||
|
label_col, first_sum = 4, 5
|
||||||
|
top = 7
|
||||||
|
sheet.put(top, 1, "Счет")
|
||||||
|
if indicator_column:
|
||||||
|
sheet.put(top, label_col, INDICATOR_TITLE)
|
||||||
|
sheet.merge(top, label_col, top + 1, label_col)
|
||||||
|
for index, key in enumerate(("opening", "turnover", "closing")):
|
||||||
|
col = first_sum + 2 * index
|
||||||
|
sheet.put(top, col, GROUP_TITLES[key])
|
||||||
|
sheet.merge(top, col, top, col + 1)
|
||||||
|
sheet.put(top + 1, col, DEBIT)
|
||||||
|
sheet.put(top + 1, col + 1, CREDIT)
|
||||||
|
sheet.put(top + 1, 1, group_label)
|
||||||
|
for row, item in enumerate(rows, start=top + 2):
|
||||||
|
if item.text == MERGED:
|
||||||
|
sheet.merge(row - 1, 1, row, 1)
|
||||||
|
elif item.text:
|
||||||
|
sheet.put(row, 1, item.text)
|
||||||
|
if item.label is not None:
|
||||||
|
sheet.put(row, label_col, item.label)
|
||||||
|
if item.outline:
|
||||||
|
sheet.outline[row] = item.outline
|
||||||
|
for offset, value in enumerate(item.sums):
|
||||||
|
if value is not None:
|
||||||
|
sheet.put(row, first_sum + offset, D(value))
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
# --- Файлы ---
|
# --- Файлы ---
|
||||||
|
|
||||||
|
|
||||||
@ -612,6 +673,68 @@ def _s16() -> list[SheetSpec]:
|
|||||||
return [build_osv(first, base_tree(), "ОСВ 60"), build_osv(second, receivables_tree(), "ОСВ 62")]
|
return [build_osv(first, base_tree(), "ОСВ 60"), build_osv(second, receivables_tree(), "ОСВ 62")]
|
||||||
|
|
||||||
|
|
||||||
|
def _s33() -> list[SheetSpec]:
|
||||||
|
"""Колонка «Показатели» БУ/НУ (AURORA-1613): счёт 02, шапка без «Выводимые данные», название объединено на строки
|
||||||
|
БУ и НУ, «Итого» по БУ и по НУ. Строки НУ — показатели строки БУ над ними."""
|
||||||
|
rows = [
|
||||||
|
GridRow("02", (None, "300000.00", "20000.00", "30000.00", None, "310000.00"), "БУ"),
|
||||||
|
GridRow(None, (None, "300000.02", "20000.00", "30000.03", None, "310000.05"), "НУ"),
|
||||||
|
GridRow("Печь подовая", (None, "100000.00", "20000.00", "10000.00", None, "90000.00"), "БУ", 1),
|
||||||
|
GridRow(MERGED, (None, "100000.02", "20000.00", "10000.01", None, "90000.03"), "НУ", 1),
|
||||||
|
GridRow("Плита индукционная", (None, "200000.00", None, "20000.00", None, "220000.00"), "БУ", 1),
|
||||||
|
GridRow(None, (None, "200000.00", None, "20000.02", None, "220000.02"), "НУ", 1),
|
||||||
|
GridRow("Итого", (None, "300000.00", "20000.00", "30000.00", None, "310000.00"), "БУ"),
|
||||||
|
GridRow(MERGED, (None, "300000.02", "20000.00", "30000.03", None, "310000.05"), "НУ"),
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
_grid_osv(account="02", group_label="Основные средства", rows=rows, data_type_line=None, indicator_column=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _s34() -> list[SheetSpec]:
|
||||||
|
"""Колонка «Показатели»: «Вал.», «Кол.», «ПР», «ВР» и неизвестная метка «Ед.» — показатели вне сверки родителя."""
|
||||||
|
rows = [
|
||||||
|
GridRow("60", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00"), "БУ"),
|
||||||
|
GridRow("ООО Альфа", ("1000.00", None, "500.00", "300.00", "1200.00", None), "БУ", 1),
|
||||||
|
GridRow(None, ("12.50", None, "6.00", "3.00", "15.50", None), "Вал.", 1),
|
||||||
|
GridRow(None, ("3", None, None, None, "3", None), "Кол.", 1),
|
||||||
|
GridRow("ООО Бета", (None, "400.00", "100.00", None, None, "300.00"), "БУ", 1),
|
||||||
|
GridRow(None, (None, None, "10.00", None, "10.00", None), "ПР", 1),
|
||||||
|
GridRow(None, (None, "5.00", None, None, None, "5.00"), "ВР", 1),
|
||||||
|
GridRow(None, ("1", None, None, None, "1", None), "Ед.", 1),
|
||||||
|
GridRow("Итого", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00"), "БУ"),
|
||||||
|
]
|
||||||
|
line = "Выводимые данные: БУ (данные бухгалтерского учета)"
|
||||||
|
return [_grid_osv(account="60", group_label="Контрагенты", rows=rows, data_type_line=line, indicator_column=True)]
|
||||||
|
|
||||||
|
|
||||||
|
def _s35() -> list[SheetSpec]:
|
||||||
|
"""«Валюта <код>» — строка в рублях, «В валюте» — показатель (AURORA-1661): пары на одном outline-уровне."""
|
||||||
|
rows = [
|
||||||
|
GridRow("60", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00")),
|
||||||
|
GridRow("60.21", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00"), outline=1),
|
||||||
|
GridRow("ООО Альфа", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00"), outline=2),
|
||||||
|
GridRow("Валюта USD", ("9000.00", None, "1000.00", None, "10000.00", None), outline=3),
|
||||||
|
GridRow("В валюте", ("100.00", None, "11.00", None, "111.00", None), outline=3),
|
||||||
|
GridRow("Валюта EUR", (None, "2000.00", None, "500.00", None, "2500.00"), outline=3),
|
||||||
|
GridRow("В валюте", (None, "20.00", None, "5.00", None, "25.00"), outline=3),
|
||||||
|
GridRow("Итого", ("9000.00", "2000.00", "1000.00", "500.00", "10000.00", "2500.00")),
|
||||||
|
]
|
||||||
|
line = "Выводимые данные: БУ (данные бухгалтерского учета)"
|
||||||
|
return [_grid_osv(account="60", group_label="Контрагенты", rows=rows, data_type_line=line, indicator_column=False)]
|
||||||
|
|
||||||
|
|
||||||
|
def _s36() -> list[SheetSpec]:
|
||||||
|
"""Шапка без «Выводимые данные», колонки «Показатели» нет: строки — BU по умолчанию с предупреждением."""
|
||||||
|
rows = [
|
||||||
|
GridRow("60", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00")),
|
||||||
|
GridRow("ООО Альфа", ("1000.00", None, "500.00", "300.00", "1200.00", None), outline=1),
|
||||||
|
GridRow("ООО Бета", (None, "400.00", "100.00", None, None, "300.00"), outline=1),
|
||||||
|
GridRow("Итого", ("1000.00", "400.00", "600.00", "300.00", "1200.00", "300.00")),
|
||||||
|
]
|
||||||
|
return [_grid_osv(account="60", group_label="Контрагенты", rows=rows, data_type_line=None, indicator_column=False)]
|
||||||
|
|
||||||
|
|
||||||
# --- Запись файлов ---
|
# --- Запись файлов ---
|
||||||
|
|
||||||
|
|
||||||
@ -842,6 +965,10 @@ FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSp
|
|||||||
"s30_val_kol_vo_materials.xlsx": (_s30, to_xlsx),
|
"s30_val_kol_vo_materials.xlsx": (_s30, to_xlsx),
|
||||||
"s31_rows_below_total.xlsx": (_s31, to_xlsx),
|
"s31_rows_below_total.xlsx": (_s31, to_xlsx),
|
||||||
"s32_total_with_suffix.xlsx": (_s32, to_xlsx),
|
"s32_total_with_suffix.xlsx": (_s32, to_xlsx),
|
||||||
|
"s33_indicator_column_bu_nu.xlsx": (_s33, to_xlsx),
|
||||||
|
"s34_indicator_column_qty_val_diff.xlsx": (_s34, to_xlsx),
|
||||||
|
"s35_currency_slices.xlsx": (_s35, to_xlsx),
|
||||||
|
"s36_no_data_type_header.xlsx": (_s36, to_xlsx),
|
||||||
}
|
}
|
||||||
FILE_NAMES = tuple(FILES)
|
FILE_NAMES = tuple(FILES)
|
||||||
|
|
||||||
@ -860,7 +987,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…s32 (xlsx, один xlsb и один xls) в указанный каталог.",
|
description="Синтетические ОСВ s01…s36 (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)
|
||||||
|
|||||||
@ -69,6 +69,10 @@ EXPECTED_NAMES = [
|
|||||||
VAL_KOL_VO_FILE,
|
VAL_KOL_VO_FILE,
|
||||||
ROWS_BELOW_TOTAL_FILE,
|
ROWS_BELOW_TOTAL_FILE,
|
||||||
TOTAL_WITH_SUFFIX_FILE,
|
TOTAL_WITH_SUFFIX_FILE,
|
||||||
|
"s33_indicator_column_bu_nu.xlsx",
|
||||||
|
"s34_indicator_column_qty_val_diff.xlsx",
|
||||||
|
"s35_currency_slices.xlsx",
|
||||||
|
"s36_no_data_type_header.xlsx",
|
||||||
]
|
]
|
||||||
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)"
|
||||||
@ -93,7 +97,7 @@ class Expect:
|
|||||||
status: ResultStatus = ResultStatus.DONE
|
status: ResultStatus = ResultStatus.DONE
|
||||||
documents: int = 1
|
documents: int = 1
|
||||||
variant: int = 1
|
variant: int = 1
|
||||||
data_type: str = "BU"
|
data_type: str | None = "BU"
|
||||||
organization: str = ORG
|
organization: str = ORG
|
||||||
# Вариант шапки по документам (если у листов файла он разный); пусто — `variant` у всех
|
# Вариант шапки по документам (если у листов файла он разный); пусто — `variant` у всех
|
||||||
variants: tuple[int, ...] = ()
|
variants: tuple[int, ...] = ()
|
||||||
@ -144,6 +148,10 @@ EXPECTED: dict[str, Expect] = {
|
|||||||
ROWS_BELOW_TOTAL_FILE: Expect("outline", 2025, "Q1", True),
|
ROWS_BELOW_TOTAL_FILE: Expect("outline", 2025, "Q1", True),
|
||||||
# итог подписан «Итого по счету 60»: точного «Итого» нет
|
# итог подписан «Итого по счету 60»: точного «Итого» нет
|
||||||
TOTAL_WITH_SUFFIX_FILE: Expect("outline", 2025, "Q1", True),
|
TOTAL_WITH_SUFFIX_FILE: Expect("outline", 2025, "Q1", True),
|
||||||
|
"s33_indicator_column_bu_nu.xlsx": Expect("outline", 2025, "Q3", False, data_type=None),
|
||||||
|
"s34_indicator_column_qty_val_diff.xlsx": Expect("outline", 2025, "Q3", False),
|
||||||
|
"s35_currency_slices.xlsx": Expect("outline", 2025, "Q3", False),
|
||||||
|
"s36_no_data_type_header.xlsx": Expect("outline", 2025, "Q3", False, data_type=None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -221,7 +229,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…s32 (s13 — xlsb; допустима деградация: нет только его)."""
|
"""В `fixtures/osv/synthetic/` лежат файлы набора s01…s36 (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]
|
||||||
@ -690,3 +698,59 @@ def test_s32_total_with_suffix_is_total(fixtures_dir: Path) -> None:
|
|||||||
|
|
||||||
assert doc.rows[-1].row_value == "Итого по счету 60"
|
assert doc.rows[-1].row_value == "Итого по счету 60"
|
||||||
assert doc.rows[-1].group_type == "TOTAL"
|
assert doc.rows[-1].group_type == "TOTAL"
|
||||||
|
|
||||||
|
|
||||||
|
def test_s33_indicator_column_bu_nu(fixtures_dir: Path) -> None:
|
||||||
|
"""AURORA-1613: строки «НУ» колонки «Показатели» — показатели строки БУ; две «Итого»; проверки сходятся."""
|
||||||
|
outcome = parse_named(fixtures_dir, "s33_indicator_column_bu_nu.xlsx")
|
||||||
|
doc = one_document(outcome)
|
||||||
|
by_row = {row.row_number: row for row in doc.rows}
|
||||||
|
nu = [row for row in doc.rows if row.data_type == "NU"]
|
||||||
|
assert [row.row_value for row in nu] == ["НУ"] * 4
|
||||||
|
assert all(row.group_type in {"OTHER", "TOTAL"} for row in nu)
|
||||||
|
for row in nu:
|
||||||
|
if row.group_type != "TOTAL":
|
||||||
|
parent = by_row[row.parent_row_number]
|
||||||
|
assert parent.data_type == "BU" and parent.row_number == row.row_number - 1
|
||||||
|
assert row.level == parent.level + 1
|
||||||
|
assert [row.data_type for row in doc.rows if row.group_type == "TOTAL"] == ["BU", "NU"]
|
||||||
|
assert {row.data_type for row in doc.rows if row.row_value != "НУ"} == {"BU"}
|
||||||
|
assert doc.header.data_type is None
|
||||||
|
texts = warnings_of(outcome)
|
||||||
|
assert not any("ниже таблицы" in text for text in texts), texts
|
||||||
|
assert not any("приняты как БУ" in text for text in texts), texts
|
||||||
|
assert outcome.status is ResultStatus.DONE
|
||||||
|
|
||||||
|
|
||||||
|
def test_s34_indicator_column_quantity_currency_differences(fixtures_dir: Path) -> None:
|
||||||
|
"""«Кол.» — QUANTITY, «Вал.» — CURRENCY, «ПР»/«ВР»/неизвестная метка — null; одно предупреждение о метке."""
|
||||||
|
outcome = parse_named(fixtures_dir, "s34_indicator_column_qty_val_diff.xlsx")
|
||||||
|
doc = one_document(outcome)
|
||||||
|
types = {row.row_value: row.data_type for row in doc.rows if row.row_value in {"Вал.", "Кол.", "ПР", "ВР", "Ед."}}
|
||||||
|
assert types == {"Вал.": "CURRENCY", "Кол.": "QUANTITY", "ПР": None, "ВР": None, "Ед.": None}
|
||||||
|
unknown = [text for text in warnings_of(outcome) if "«Ед.»" in text]
|
||||||
|
assert len(unknown) == 1, warnings_of(outcome)
|
||||||
|
assert outcome.status is ResultStatus.DONE
|
||||||
|
|
||||||
|
|
||||||
|
def test_s35_v_valyute_is_currency_of_previous_row(fixtures_dir: Path) -> None:
|
||||||
|
"""AURORA-1661: «В валюте» — CURRENCY, родитель — «Валюта …» над ней; «Валюта …» — обычные строки в рублях."""
|
||||||
|
outcome = parse_named(fixtures_dir, "s35_currency_slices.xlsx")
|
||||||
|
doc = one_document(outcome)
|
||||||
|
by_row = {row.row_number: row for row in doc.rows}
|
||||||
|
in_currency = [row for row in doc.rows if row.row_value == "В валюте"]
|
||||||
|
assert [row.data_type for row in in_currency] == ["CURRENCY", "CURRENCY"]
|
||||||
|
assert [by_row[row.parent_row_number].row_value for row in in_currency] == ["Валюта USD", "Валюта EUR"]
|
||||||
|
slices = [row for row in doc.rows if row.row_value.startswith("Валюта ")]
|
||||||
|
assert {(row.group_type, row.data_type) for row in slices} == {("OTHER", "BU")}
|
||||||
|
assert outcome.status is ResultStatus.DONE
|
||||||
|
|
||||||
|
|
||||||
|
def test_s36_no_data_type_in_header_rows_are_bu(fixtures_dir: Path) -> None:
|
||||||
|
"""Шапка без «Выводимые данные»: строки — BU, предупреждение одно, `header.data_type` — null."""
|
||||||
|
outcome = parse_named(fixtures_dir, "s36_no_data_type_header.xlsx")
|
||||||
|
doc = one_document(outcome)
|
||||||
|
assert doc.header.data_type is None
|
||||||
|
assert {row.data_type for row in doc.rows} == {"BU"}
|
||||||
|
assert len([text for text in warnings_of(outcome) if "приняты как БУ" in text]) == 1
|
||||||
|
assert outcome.status is ResultStatus.DONE
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user