Контрольный набор синтетики ОСВ других систем (#81)

Генератор: название ведомости, безымянный лист, группы со стороной в ячейке, подпись итога.
Позитивы s48–s54 (xfail до словарей и запасного признака), негативы s55–s57.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 20:32:29 +04:00
parent 29071bdc7b
commit 4d8f242990
3 changed files with 433 additions and 12 deletions

View File

@ -6,6 +6,8 @@
строки ниже «Итого» и «Итого по счету» вместо точного «Итого» (#47), s40…s44 — плоская форма ОСВ 1С (#59; s33…s39 заняты
другим треком), s45 — счёт шапки «Счет Поставщика» | «60*» в соседних ячейках, s46 — выгрузка «по контрагентам» с
блоками субсчетов (#71), s47 — иерархия по отступам: подуровень счёта, строка закрытия, вложенность по суммам (#78);
s48…s57 — ОСВ других учётных систем (#81): позитивы s48…s54 (названия, группы и стороны, Navision без названия, xls и
xlsb), негативы s55…s57 (почти-ОСВ, которые обязаны остаться прочими листами или формой);
описание — `osv/synthetic/README.md` в репозитории fixtures. Генератор самодостаточен
(openpyxl + `xlsb_writer` + `xls_writer`) и детерминирован: повторный запуск даёт те же байты — метки времени книги
и zip постоянны, порядок записи фиксирован.
@ -214,6 +216,15 @@ class OsvSpec:
group_titles: dict[str, str] | None = None # названия групп сумм вместо `GROUP_TITLES`
inn_header: str = "ИНН" # подпись колонки ИНН («ИНН/КПП» — склеенная)
labels_below: bool = False # вариант 1: «Счет» и «Контрагенты» — отдельными строками под подзаголовками сумм
# Название ведомости вместо «Оборотно-сальдовая ведомость по счету … за …» (вариант 1) и его колонка (#81)
title: str | None = None
title_col: int = 1
untitled: bool = False # нет организации, названия и параметров: заголовок таблицы с `1 + top_padding` (#81)
top_padding: int = 0
# Группы, подписанные двумя ячейками со стороной («НачалоДт» | «НачалоКт») вместо названия над «Дебет/Кредит»;
# разделены все группы — строки подзаголовков нет (#81)
split_groups: dict[str, tuple[str, str]] = field(default_factory=dict)
total_text: str | None = None # подпись итоговой строки; None — «Итого»
GROUP_TITLES = {
@ -247,27 +258,40 @@ def _header_rows(sheet: SheetSpec, spec: OsvSpec, top: int, label: str, sub_labe
"""
titles = spec.group_titles or GROUP_TITLES
below = spec.labels_below and bool(label)
groups = _groups(spec)
one_row = all(key in spec.split_groups for key, _, _ in groups) # заголовок в одну строку, без «Дебет/Кредит»
label_row = top + 2 if below else top # строка меток группировок (первая) и колонок ИНН/КПП
sheet.put(label_row, 1, label)
sheet.put(label_row + 1, 1, sub_label)
service_row = label_row + 1 if below else top + 1
if not one_row:
sheet.put(label_row + 1, 1, sub_label)
service_row = label_row + 1 if below else (top if one_row else top + 1)
if spec.inn_col is not None:
sheet.put(service_row, spec.inn_col, spec.inn_header)
if spec.kpp_col is not None:
sheet.put(service_row, spec.kpp_col, "КПП")
for key, col, width in _groups(spec):
for key, col, width in groups:
if key in spec.split_groups:
debit, credit = spec.split_groups[key]
sheet.put(top, col, debit)
sheet.put(top, col + 1, credit)
continue
sheet.put(top, col, titles[key])
sheet.merge(top, col, top, col + width - 1)
if width == PAIR_WIDTH:
sheet.put(top + 1, col, DEBIT)
sheet.put(top + 1, col + 1, CREDIT)
return top + 4 if below else top + 2
if below:
return top + 4
return top + 1 if one_row else top + 2
def _header_v1(sheet: SheetSpec, spec: OsvSpec) -> int:
if spec.untitled:
return _header_rows(sheet, spec, 1 + spec.top_padding, "Счет", "Контрагенты")
if spec.org_name is not None:
sheet.put(1, 1, spec.org_name)
sheet.put(2, 1, f"Оборотно-сальдовая ведомость по счету {spec.account} за {spec.period}")
title = spec.title or f"Оборотно-сальдовая ведомость по счету {spec.account} за {spec.period}"
sheet.put(2, spec.title_col, title)
sheet.put(4, 1, spec.data_type_line)
sheet.put(7, 1, "Параметры:")
sheet.put(7, 3, spec.currency_line)
@ -375,7 +399,7 @@ def build_osv(spec: OsvSpec, root: Node, name: str = SHEET) -> SheetSpec:
_row_cells(sheet, spec, next_row, (node, depth), (node.twin or node).six(), blank=node.blank)
next_row += 1
total = SheetSpec("")
_row_cells(total, spec, 1, (Node(TOTAL_TEXT), 0), root.six())
_row_cells(total, spec, 1, (Node(spec.total_text or TOTAL_TEXT), 0), root.six())
for (_, col), cell in total.cells.items():
sheet.cells[(next_row, col)] = CellSpec(cell.value, bold=spec.bold_accounts)
return sheet
@ -418,6 +442,16 @@ def statement_sheet(name: str = "Бухгалтерский баланс") -> Sh
return sheet
def table_sheet(name: str, rows: Sequence[Sequence[str | int | D | None]], *, start_row: int = 1) -> SheetSpec:
"""Лист «как есть»: строки ячеек с `start_row`, `None` — пустая ячейка (листы-негативы #81)."""
sheet = SheetSpec(name)
for row, values in enumerate(rows, start=start_row):
for col, value in enumerate(values, start=1):
if value is not None:
sheet.put(row, col, value)
return sheet
def form_sheet(name: str, *lines: str) -> SheetSpec:
"""Лист формы без таблицы: в шапке только `lines` (так проверяется каждый признак формы по отдельности)."""
sheet = SheetSpec(name)
@ -1119,6 +1153,215 @@ def _inn_layout(layout: InnLayout, *, sums: bool) -> Callable[[], list[SheetSpec
return build
# --- ОСВ других учётных систем (#81): контрольный набор. s48–s54 — позитивы, s55–s57 — негативы ---
PERIOD_TAIL = "по счету 60 за 1 квартал 2025 г."
NAVISION_GROUPS = {
"opening": ("НачалоДт", "НачалоКт"),
"turnover": ("ОборотДт", "ОборотКт"),
"closing": ("КонецДт", "КонецКт"),
}
def _titled(name: str, title: str, *, title_col: int = 1, total_text: str | None = None) -> SheetSpec:
spec = OsvSpec(inn_col=5, first_sum_col=7, title=title, title_col=title_col, total_text=total_text)
return build_osv(spec, base_tree(), name)
def _s48() -> list[SheetSpec]:
"""Названия ведомостей других систем и отчётов 1С (AURORA-1717) и итог «ВСЕГО» (AURORA-1732): по листу на название;
название — в разных колонках, верхним регистром с переносом строки."""
return [
_titled("ОВ", f"Оборотная ведомость {PERIOD_TAIL}", title_col=3),
_titled("ОСВ сокр", "ОСВ по сч. 60 за 1 квартал 2025 г."),
_titled("Оборотный баланс", f"Оборотный баланс {PERIOD_TAIL}", title_col=7),
_titled("Навижн", "Фин. Обороты Листок за 1 квартал 2025 г.", title_col=4),
_titled("Главная книга", f"Отчет по Главной книге {PERIOD_TAIL}", title_col=3, total_text="ВСЕГО"),
_titled("Верхний регистр", f"ОБОРОТНАЯ\nВЕДОМОСТЬ {PERIOD_TAIL}"),
_titled("Вторая колонка", f"Оборотная ведомость по счетам {PERIOD_TAIL}", title_col=2, total_text="Всего "),
]
def _s49_sheets(hierarchy: Hierarchy = "outline") -> list[SheetSpec]:
"""«Остаток на начало/конец» над «Дебет/Кредит» и «Оборот по дебету/кредиту» в ячейке группы (КТЗ); преамбула
«Остаток на начало: …» над таблицей; второй лист — «Остаток по дебету на начало» (сторона внутри названия)."""
first = build_osv(
OsvSpec(
inn_col=5,
first_sum_col=7,
hierarchy=hierarchy,
title=f"Оборотная ведомость {PERIOD_TAIL}",
group_titles={
**GROUP_TITLES,
"opening": "Остаток на начало периода",
"closing": "Остаток на конец периода",
},
split_groups={"turnover": ("Оборот по дебету", "Оборот по кредиту")},
),
base_tree(),
"Остатки",
)
first.put(5, 1, "Остаток на начало: 0")
first.put(5, 3, "Остаток на конец: 0")
second = build_osv(
OsvSpec(
inn_col=None,
first_sum_col=3,
hierarchy=hierarchy,
title=f"Оборотная ведомость {PERIOD_TAIL}",
split_groups={
"opening": ("Остаток по дебету на начало", "Остаток по кредиту на начало"),
"turnover": ("Оборот по дебету", "Оборот по кредиту"),
"closing": ("Остаток по дебету на конец", "Остаток по кредиту на конец"),
},
),
receivables_tree(),
"Остатки по сторонам",
)
return [first, second]
def _s49() -> list[SheetSpec]:
return _s49_sheets()
def _s50() -> list[SheetSpec]:
"""SAP: «Сальдо вх/исх Дт/Кт» (Виола, обороты 1С-вида — «Дт оборот» это AURORA-1722) и сторона, приклеенная к
названию группы «Сальдо НачальноеДТВВ» / «ОборотыДтВВ» (НМЖК); контрагент «ООО Дтвв-Сервис» — не сторона."""
sap = OsvSpec(
inn_col=None,
first_sum_col=3,
title=f"Отчет по Главной книге {PERIOD_TAIL}",
split_groups={
"opening": ("Сальдо вх Дт", "Сальдо вх Кт"),
"turnover": ("Оборот Дт", "Оборот Кт"),
"closing": ("Сальдо исх Дт", "Сальдо исх Кт"),
},
)
glued = OsvSpec(
inn_col=None,
first_sum_col=3,
split_groups={
"opening": ("Сальдо НачальноеДТВВ", "Сальдо НачальноеКТВВ"),
"turnover": ("ОборотыДтВВ", "ОборотыКтВВ"),
"closing": ("Сальдо КонечноеДТВВ", "Сальдо КонечноеКТВВ"),
},
)
tree = base_tree()
tree.kids[0].kids[0].text = "ООО Дтвв-Сервис"
return [build_osv(sap, base_tree(), "SAP"), build_osv(glued, tree, "НМЖК")]
def _navision_sheet(name: str = "ОВ", hierarchy: Hierarchy = "outline", top_padding: int = 0) -> SheetSpec:
spec = OsvSpec(
untitled=True,
top_padding=top_padding,
inn_col=None,
first_sum_col=2,
hierarchy=hierarchy,
split_groups=NAVISION_GROUPS,
)
return build_osv(spec, base_tree(), name)
def _s51() -> list[SheetSpec]:
"""Navision без названия ведомости: таблица с первой строки, «НачалоДт | … | КонецКт» (Айсберри); запасной
признак."""
return [_navision_sheet()]
def _s52() -> list[SheetSpec]:
"""Форма ББ, лист Navision без названия, посторонний лист: форма остаётся формой, «Справка» — прочим."""
return [statement_sheet(), _navision_sheet(), reference_sheet()]
def _s53() -> list[SheetSpec]:
"""s49 в xls (BIFF5 без объединений и outline): отступ пробелами."""
return _s49_sheets("spaces")
def _s54() -> list[SheetSpec]:
"""s51 в xlsb (без объединений и outline): отступ пробелами."""
return [_navision_sheet(hierarchy="spaces")]
def _s55() -> list[SheetSpec]:
"""Негатив: названия, похожие на ведомость, но не ведомость (не с начала ячейки, «Оборотный капитал»), и отчёт 1С
«Обороты счета» с колонками корр. счетов между «Оборот Дт» и «Оборот Кт» (R-1) — все три листа прочие."""
return [
build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree(), "ОСВ 60"),
table_sheet(
"Справка",
[
["Справка: оборотная ведомость прилагается"],
[],
["Показатель", "Сумма"],
["Выручка", D("100000.00")],
],
),
table_sheet(
"Капитал",
[
["Оборотный капитал и его структура"],
[],
["Показатель", "На начало", "На конец"],
["Запасы", D("500.00"), D("700.00")],
],
),
table_sheet(
"Обороты счета",
[
["Обороты счета 60 за 9 месяцев 2025 г."],
[],
["Счет", "Начальное сальдо Дт", "Начальное сальдо Кт", "Оборот Дт", "51", "68", "Оборот Кт"]
+ ["20", "Конечное сальдо Дт", "Конечное сальдо Кт"],
["60", D("100.00"), None, D("50.00"), D("30.00"), D("20.00"), D("40.00"), D("40.00"), D("110.00")],
["Итого", D("100.00"), None, D("50.00"), D("30.00"), D("20.00"), D("40.00"), D("40.00"), D("110.00")],
],
),
]
def _s56() -> list[SheetSpec]:
"""Негатив: безымянные таблицы, почти ОСВ — расшифровка «Остаток на начало | Поступило | Выбыло | Остаток на
конец», нет группы «начало», свёрнутое начало, полный заголовок ниже 30-й строки — все прочие (D1, R-2, R-3)."""
return [
build_osv(OsvSpec(inn_col=5, first_sum_col=7), base_tree(), "ОСВ 60"),
table_sheet(
"Расшифровка",
[
["Наименование", "Остаток на начало", "Поступило", "Выбыло", "Остаток на конец"],
["Материалы", D("100.00"), D("50.00"), D("30.00"), D("120.00")],
],
),
table_sheet(
"Без начала",
[
["Счет", "Оборот Дт", "Оборот Кт", "Сальдо на конец Дт", "Сальдо на конец Кт"],
["60", D("10.00"), D("5.00"), D("5.00"), None],
],
),
table_sheet(
"Свёрнутое начало",
[
["Счет", "Сальдо на начало", "Оборот Дт", "Оборот Кт", "Сальдо на конец Дт", "Сальдо на конец Кт"],
["60", D("100.00"), D("10.00"), D("5.00"), D("105.00"), None],
],
),
_navision_sheet("Ниже 30", top_padding=30),
]
def _s57() -> list[SheetSpec]:
"""Негатив: форма ББ с примечанием «см. оборотную ведомость …» и текстом «Остаток на начало периода …», за ней
лист слабого продолжения (одна строка с кодом) — форма и продолжение остаются формой."""
form = statement_sheet()
form.put(9, 1, "Примечание: см. оборотную ведомость по сч. 60")
form.put(10, 1, "Остаток на начало периода — по данным учёта")
continuation = table_sheet("Баланс (2)", [["Капитал и резервы", "1300", D("500000.00")]])
return [form, continuation]
def _normalized_zip(data: bytes) -> bytes:
"""Тот же архив с постоянными метками времени: записей zip и `dcterms:modified` (openpyxl ставит текущее время
при каждом `save`, заданное в свойствах значение перезаписывает)."""
@ -1219,6 +1462,16 @@ FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSp
"s45_account_label_value_cells.xlsx": (_s45, to_xlsx),
"s46_counterparty_code_blocks.xlsx": (_s46, to_xlsx),
"s47_indent_hierarchy_code_sums.xlsx": (_s47, to_xlsx),
"s48_other_system_titles.xlsx": (_s48, to_xlsx),
"s49_balance_and_turnover_by_side.xlsx": (_s49, to_xlsx),
"s50_sap_in_out_glued_side.xlsx": (_s50, to_xlsx),
"s51_untitled_navision.xlsx": (_s51, to_xlsx),
"s52_mixed_book.xlsx": (_s52, to_xlsx),
"s53_balance_by_side.xls": (_s53, to_xls),
"s54_untitled_navision.xlsb": (_s54, to_xlsb),
"s55_not_osv_titles.xlsx": (_s55, to_xlsx),
"s56_almost_osv_tables.xlsx": (_s56, to_xlsx),
"s57_form_mentions_osv.xlsx": (_s57, to_xlsx),
}
FILE_NAMES = tuple(FILES)
@ -1237,7 +1490,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, s40…s44 (xlsx, один xlsb и один xls) в указанный каталог.",
description="Синтетические ОСВ s01…s36, s40…s57 (xlsx, xlsb и xls) в указанный каталог.",
)
parser.add_argument("directory", type=Path, help="каталог назначения, например ../../fixtures/osv/synthetic")
args = parser.parse_args(argv)

View File

@ -26,15 +26,19 @@ def test_generation_is_reproducible_byte_for_byte(tmp_path: Path) -> None:
assert left.read_bytes() == right.read_bytes(), left.name
def test_generated_set_is_s01_to_s36_and_s40_to_s47_with_one_xlsb(tmp_path: Path) -> None:
def test_generated_set_is_s01_to_s36_and_s40_to_s57_with_two_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)
# s33…s36 — строки-показатели (AURORA-1613, #58), плоская форма ОСВ 1С (#59) — s40…s44
numbers = [*range(1, 37), *range(40, 48)]
# s33…s36 — строки-показатели (AURORA-1613, #58), плоская форма ОСВ 1С (#59) — s40…s44, ОСВ других систем (#81) —
# s48…s57
numbers = [*range(1, 37), *range(40, 58)]
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",
"s54_untitled_navision.xlsb",
]
assert sorted(path.name for path in tmp_path.iterdir()) == sorted(names)

View File

@ -54,6 +54,25 @@ FLAT_FORM_FILES = (
"s46_counterparty_code_blocks.xlsx",
"s47_indent_hierarchy_code_sums.xlsx",
)
# ОСВ других учётных систем (#81): контрольный набор — позитивы s48–s54 (на базе падают), негативы s55–s57 (вид листов
# и результат не меняются)
S48 = "s48_other_system_titles.xlsx"
S49 = "s49_balance_and_turnover_by_side.xlsx"
S50 = "s50_sap_in_out_glued_side.xlsx"
S51 = "s51_untitled_navision.xlsx"
S52 = "s52_mixed_book.xlsx"
S53 = "s53_balance_by_side.xls"
S54 = "s54_untitled_navision.xlsb"
S55 = "s55_not_osv_titles.xlsx"
S56 = "s56_almost_osv_tables.xlsx"
S57 = "s57_form_mentions_osv.xlsx"
OTHER_SYSTEMS_FILES = (S48, S49, S50, S51, S52, S53, S54, S55, S56, S57)
PENDING_DICTIONARY = "tp#81 Task 2: словари названий, групп и сторон"
PENDING_FALLBACK = "tp#81 Task 3: запасной признак листа по заголовку таблицы"
PENDING_81 = {S48: PENDING_DICTIONARY, S49: PENDING_DICTIONARY, S50: PENDING_DICTIONARY, S53: PENDING_DICTIONARY}
PENDING_81 |= {S51: PENDING_FALLBACK, S52: PENDING_FALLBACK, S54: PENDING_FALLBACK}
S48_SHEETS = ("ОВ", "ОСВ сокр", "Оборотный баланс", "Навижн", "Главная книга", "Верхний регистр", "Вторая колонка")
TYPO_ROW = 13 # строка «7701234567» с неверной контрольной суммой в s28
EXPECTED_NAMES = [
"s01_variant2_header.xlsx",
@ -86,6 +105,7 @@ EXPECTED_NAMES = [
"s35_currency_slices.xlsx",
"s36_no_data_type_header.xlsx",
*FLAT_FORM_FILES,
*OTHER_SYSTEMS_FILES,
]
XLSB_NAME = "s13_no_outline_indent.xlsb"
XLSB_MISSING_REASON = f"нет файла {XLSB_NAME} (xlsb не получен; см. README fixtures)"
@ -194,6 +214,17 @@ EXPECTED: dict[str, Expect] = {
"s47_indent_hierarchy_code_sums.xlsx": Expect(
"indent", 2024, "Q3", True, data_type=None, organization='ООО "Октябрьское"', documents=3
),
# #81: названия других систем, остатки, SAP; безымянный лист — без шапки (организация, период, вид данных пусты)
S48: Expect("outline", 2025, "Q1", True, documents=len(S48_SHEETS)),
S49: Expect("outline", 2025, "Q1", True, documents=2),
S50: Expect("outline", 2025, "Q1", True, documents=2),
S51: Expect("outline", None, None, None, data_type=None, organization=""),
S52: Expect("outline", None, None, None, status=ResultStatus.NEEDS_CHECK, data_type=None, organization=""),
S53: Expect("indent", 2025, "Q1", True, documents=2),
S54: Expect("indent", None, None, None, data_type=None, organization=""),
S55: Expect("outline", 2025, "Q1", True),
S56: Expect("outline", 2025, "Q1", True),
S57: Expect("outline", None, None, None, status=ResultStatus.NEEDS_CHECK, documents=0),
}
@ -283,8 +314,10 @@ def test_every_synthetic_file_is_listed(synthetic_file: Path) -> None:
assert synthetic_file.name in EXPECTED, f"добавьте ожидания для {synthetic_file.name}"
def test_header_status_and_hierarchy(synthetic_file: Path) -> None:
def test_header_status_and_hierarchy(synthetic_file: Path, request: pytest.FixtureRequest) -> None:
"""Статус, число документов, стратегия иерархии и шапка из таблицы `EXPECTED`; результат валиден по контракту."""
if synthetic_file.name in PENDING_81:
request.applymarker(pytest.mark.xfail(strict=True, reason=PENDING_81[synthetic_file.name]))
expect = EXPECTED[synthetic_file.name]
outcome = parse_path(synthetic_file)
@ -810,3 +843,134 @@ def test_s36_no_data_type_in_header_rows_are_bu(fixtures_dir: Path) -> 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
# --- ОСВ других учётных систем (#81): контрольный набор ---
ALFA_SUMS = (None, D("250000.50"), D("180000.25"), D("420000.00"), None, D("490000.25"))
BUYER_SUMS = (D("500000.00"), None, D("320000.00"), D("410000.00"), D("410000.00"), None)
TOTAL_NOT_FOUND = "строка «Итого» не найдена"
def sheet_other(outcome: ParseOutcome) -> list[str]:
"""Листы, пропущенные как прочие (`SHEET_OTHER`), по порядку."""
warnings = outcome.metrics.warnings if outcome.metrics is not None else None
return [w.locator or "" for w in warnings or [] if w.code == "SHEET_OTHER"]
def assert_done_with_sums(doc: OsvDocument, text: str, expected: tuple[Decimal | None, ...]) -> None:
"""Документ сошёлся, и у строки `text` шесть сумм по сторонам: перепутанные Дебет/Кредит здесь не пройдут."""
assert sums(by_text(doc)[text]) == expected
assert doc.status == "done", doc.checks
assert doc.rows[-1].group_type == "TOTAL"
@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY)
def test_s48_other_system_titles_are_osv(fixtures_dir: Path) -> None:
"""Каждое название ведомости — свой лист ОСВ; «ВСЕГО» и «Всего » — итоговая строка (AURORA-1717, 1732)."""
outcome = parse_named(fixtures_dir, S48)
docs = documents(outcome)
assert [doc.locator for doc in docs] == [f"sheet:{name}" for name in S48_SHEETS]
assert sheet_other(outcome) == []
for doc in docs:
assert_done_with_sums(doc, ALFA, ALFA_SUMS)
totals = {doc.locator: doc.rows[-1].row_value.strip() for doc in docs}
assert totals["sheet:Главная книга"] == "ВСЕГО"
assert totals["sheet:Вторая колонка"] == "Всего"
assert not [text for text in warnings_of(outcome) if TOTAL_NOT_FOUND in text]
@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY)
def test_s49_balance_and_turnover_by_side(fixtures_dir: Path) -> None:
"""«Остаток на начало/конец», «Оборот по дебету/кредиту», «Остаток по дебету на начало»; преамбула «Остаток на
начало: 0» над таблицей строкой групп не считается."""
outcome = parse_named(fixtures_dir, S49)
first, second = documents(outcome)
assert (first.locator, second.locator) == ("sheet:Остатки", "sheet:Остатки по сторонам")
assert_done_with_sums(first, ALFA, ALFA_SUMS)
assert_done_with_sums(second, "ООО Покупатель-1", BUYER_SUMS)
@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY)
def test_s50_sap_in_out_and_glued_side(fixtures_dir: Path) -> None:
"""SAP «Сальдо вх/исх Дт/Кт» и сторона, приклеенная к группе («ДТВВ»); «ООО Дтвв-Сервис» — контрагент."""
outcome = parse_named(fixtures_dir, S50)
sap, glued = documents(outcome)
assert (sap.locator, glued.locator) == ("sheet:SAP", "sheet:НМЖК")
assert_done_with_sums(sap, ALFA, ALFA_SUMS)
assert_done_with_sums(glued, "ООО Дтвв-Сервис", ALFA_SUMS)
assert by_text(glued)["ООО Дтвв-Сервис"].group_type == "COUNTERPARTY"
@pytest.mark.xfail(strict=True, reason=PENDING_FALLBACK)
def test_s51_untitled_sheet_is_osv_by_table(fixtures_dir: Path) -> None:
"""Navision без названия ведомости: ОСВ по полному заголовку таблицы (запасной признак)."""
outcome = parse_named(fixtures_dir, S51)
doc = one_document(outcome)
assert doc.locator == "sheet:ОВ"
assert sheet_other(outcome) == []
assert_done_with_sums(doc, ALFA, ALFA_SUMS)
@pytest.mark.xfail(strict=True, reason=PENDING_FALLBACK)
def test_s52_form_stays_form_navision_is_osv_reference_is_other(fixtures_dir: Path) -> None:
"""Форма ББ — свой документ (лист Navision не присоединяется к ней слабым продолжением, R-12), Navision — ОСВ,
«Справка» — прочий лист."""
outcome = parse_named(fixtures_dir, S52)
assert [(d.document_type, d.locator) for d in outcome.documents] == [
("OSV", "sheet:ОВ"),
("BALANCE", "sheet:Бухгалтерский баланс"),
]
assert sheet_other(outcome) == ["sheet:Справка"]
assert_done_with_sums(one_document(outcome), ALFA, ALFA_SUMS)
@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY)
def test_s53_xls_balance_by_side(fixtures_dir: Path) -> None:
"""s49 в xls (без объединений): те же колонки и суммы."""
outcome = parse_named(fixtures_dir, S53)
first, second = documents(outcome)
assert_done_with_sums(first, ALFA, ALFA_SUMS)
assert_done_with_sums(second, "ООО Покупатель-1", BUYER_SUMS)
@pytest.mark.xfail(strict=True, reason=PENDING_FALLBACK)
def test_s54_xlsb_untitled_navision(fixtures_dir: Path) -> None:
"""s51 в xlsb: ОСВ по запасному признаку, те же суммы."""
outcome = parse_named(fixtures_dir, S54)
assert sheet_other(outcome) == []
assert_done_with_sums(one_document(outcome), ALFA, ALFA_SUMS)
def test_neg_s55_titles_like_osv_are_other(fixtures_dir: Path) -> None:
"""Название не с начала ячейки, «Оборотный капитал», 1С «Обороты счета» с корр. счетами между «Оборот Дт» и
«Оборот Кт» (R-1, R-11) — прочие листы; ОСВ одна."""
outcome = parse_named(fixtures_dir, S55)
assert [doc.locator for doc in documents(outcome)] == ["sheet:ОСВ 60"]
assert sheet_other(outcome) == ["sheet:Справка", "sheet:Капитал", "sheet:Обороты счета"]
def test_neg_s56_almost_osv_tables_are_other(fixtures_dir: Path) -> None:
"""Расшифровка с остатками, таблица без «начала», свёрнутое начало, заголовок ниже 30-й строки — прочие листы."""
outcome = parse_named(fixtures_dir, S56)
assert [doc.locator for doc in documents(outcome)] == ["sheet:ОСВ 60"]
assert sheet_other(outcome) == ["sheet:Расшифровка", "sheet:Без начала", "sheet:Свёрнутое начало", "sheet:Ниже 30"]
def test_neg_s57_form_with_osv_words_stays_form(fixtures_dir: Path) -> None:
"""Форма ББ с примечанием «см. оборотную ведомость» и слабым продолжением — форма из двух листов, ОСВ нет."""
outcome = parse_named(fixtures_dir, S57)
assert [(d.document_type, d.locator) for d in outcome.documents] == [
("BALANCE", "sheet:Бухгалтерский баланс,Баланс (2)")
]
assert sheet_other(outcome) == []