Контрольный набор синтетики ОСВ других систем (#81)
Генератор: название ведомости, безымянный лист, группы со стороной в ячейке, подпись итога. Позитивы s48–s54 (xfail до словарей и запасного признака), негативы s55–s57. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
29071bdc7b
commit
4d8f242990
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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) == []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user