Плоская форма ОСВ 1С: счёт из шапки, «Итого развернутое», плоский список, контрагенты расчётных счетов (#59) #63
@ -3,7 +3,8 @@
|
||||
Файлы s01…s13 — по одному сценарию спеки `osv-parsing` на файл, s14…s16 — записи словарей, которых нет в реальных
|
||||
ОСВ (R-22), s17…s19 — ИНН в тексте названия, ИНН строкой выше и «ИНН/КПП» в одной ячейке (issue #9), s20…s27 — восемь
|
||||
раскладок строки ИНН, 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`) и
|
||||
детерминирован: повторный запуск даёт те же байты — метки времени книги и zip постоянны, порядок записи фиксирован.
|
||||
|
||||
@ -861,6 +862,116 @@ def _s32() -> list[SheetSpec]:
|
||||
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]:
|
||||
"""Раскладка s20 («над, не в дереве, без сумм»), но ИНН ООО Альфа с опечаткой: контрольная сумма не сходится."""
|
||||
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),
|
||||
"s35_currency_slices.xlsx": (_s35, 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)
|
||||
|
||||
@ -987,7 +1103,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…s36 (xlsx, один xlsb и один xls) в указанный каталог.",
|
||||
description="Синтетические ОСВ s01…s36, s40…s44 (xlsx, один xlsb и один xls) в указанный каталог.",
|
||||
)
|
||||
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
|
||||
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 [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 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"
|
||||
ROWS_BELOW_TOTAL_FILE = "s31_rows_below_total.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
|
||||
EXPECTED_NAMES = [
|
||||
"s01_variant2_header.xlsx",
|
||||
@ -73,6 +82,7 @@ EXPECTED_NAMES = [
|
||||
"s34_indicator_column_qty_val_diff.xlsx",
|
||||
"s35_currency_slices.xlsx",
|
||||
"s36_no_data_type_header.xlsx",
|
||||
*FLAT_FORM_FILES,
|
||||
]
|
||||
XLSB_NAME = "s13_no_outline_indent.xlsb"
|
||||
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:
|
||||
"""В `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())
|
||||
assert present == [name for name in EXPECTED_NAMES if name 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:
|
||||
"""Каждый файл каталога описан в таблице ожиданий (новый файл без ожиданий — ошибка набора)."""
|
||||
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:
|
||||
"""Статус, число документов, стратегия иерархии и шапка из таблицы `EXPECTED`; результат валиден по контракту."""
|
||||
if synthetic_file.name in FLAT_FORM_FILES:
|
||||
pytest.skip("плоская форма (#59): ожидания по файлу — в сценариях задач 2–5, до них поведение фиксирует golden")
|
||||
expect = EXPECTED[synthetic_file.name]
|
||||
|
||||
outcome = parse_path(synthetic_file)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user