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
|
||||
|
||||
|
||||
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")]
|
||||
|
||||
|
||||
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),
|
||||
"s31_rows_below_total.xlsx": (_s31, 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)
|
||||
|
||||
@ -860,7 +987,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…s32 (xlsx, один xlsb и один xls) в указанный каталог.",
|
||||
description="Синтетические ОСВ s01…s36 (xlsx, один xlsb и один xls) в указанный каталог.",
|
||||
)
|
||||
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
@ -69,6 +69,10 @@ EXPECTED_NAMES = [
|
||||
VAL_KOL_VO_FILE,
|
||||
ROWS_BELOW_TOTAL_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_MISSING_REASON = f"нет файла {XLSB_NAME} (xlsb не получен; см. README fixtures)"
|
||||
@ -93,7 +97,7 @@ class Expect:
|
||||
status: ResultStatus = ResultStatus.DONE
|
||||
documents: int = 1
|
||||
variant: int = 1
|
||||
data_type: str = "BU"
|
||||
data_type: str | None = "BU"
|
||||
organization: str = ORG
|
||||
# Вариант шапки по документам (если у листов файла он разный); пусто — `variant` у всех
|
||||
variants: tuple[int, ...] = ()
|
||||
@ -144,6 +148,10 @@ EXPECTED: dict[str, Expect] = {
|
||||
ROWS_BELOW_TOTAL_FILE: Expect("outline", 2025, "Q1", True),
|
||||
# итог подписан «Итого по счету 60»: точного «Итого» нет
|
||||
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:
|
||||
"""В `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())
|
||||
assert present == [name for name in EXPECTED_NAMES if name 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].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