Merge pull request 'ОСВ других учётных систем: названия, запасной признак, группы, «ВСЕГО» (AURORA-1717, 1719, 1732)' (#82) from feat/81-osv-other-systems into main
This commit is contained in:
commit
56af97cdb9
@ -75,7 +75,10 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote
|
||||
обращения к ячейке константами. Значения enum контракта вычисляются из `src.contract` и под линтер не попадают.
|
||||
- Каждая запись словаря должна срабатывать хотя бы на одном реальном или синтетическом файле
|
||||
(`tests/fixtures/test_rules_coverage.py`); мёртвый синоним удаляется, пробел в фикстурах закрывается синтетикой.
|
||||
- Golden-снимки (`../fixtures/golden/expected_osv_parse/`, 136 файлов: 72 реальных + 23 пакета ДКБ (`osv-dkb/**` до 1 МиБ, снимки в `osv-dkb/`) + 41 синтетический) — регрессия, а не
|
||||
- Признак листа ОСВ шире названия ведомости — запасной признак по заголовку таблицы (`detect.has_osv_table`, #81).
|
||||
Его ограничения (три группы, Дт и Кт рядом, первые 30 строк, без свёрнутого сальдо) держат негативы синтетики
|
||||
s55–s57: ослабили ограничение — падает свой негатив. Новое ослабление — только с новым негативом.
|
||||
- Golden-снимки (`../fixtures/golden/expected_osv_parse/`, 149 файлов: 72 реальных + 23 пакета ДКБ (`osv-dkb/**` до 1 МиБ, снимки в `osv-dkb/`) + 54 синтетических) — регрессия, а не
|
||||
оракул; правятся только `make golden-update` с просмотром diff и отдельным коммитом в `fixtures`. Корректность
|
||||
проверяют property-тест и ручная сверка «Итого».
|
||||
- Деньги — `Decimal`, `float` в домене нет; `needs_check` — только по контрольным суммам, структуру под сходимость не
|
||||
|
||||
@ -121,7 +121,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
|
||||
| `grid.py` | модель листа: `Cell`, `Sheet`, `MergedRange`, `column_letter` (буква колонки — только для диагностики) |
|
||||
| `readers/` | чтение книги в `Sheet`: `read_book(data, file_type) -> (листы, предупреждения)`; формат определяется по сигнатуре байтов (xlsx — openpyxl; части архива, отличающиеся от ссылок `[Content_Types].xml`/`*.rels` только регистром (`xl/SharedStrings.xml`), до openpyxl переименовываются по ссылкам, как их находит Excel — AURORA-1707; xls — xlrd с `formatting_info`, xlsb — pyxlsb, без outline level); несовпадение с `file_type` — предупреждение; до библиотеки чтения — лимиты чтения (`_limits.py`, см. «Лимиты») |
|
||||
| `rules.py` | загрузчик словарей `rules/*.yaml` в frozen-модели (pydantic), `normalize_text`, `Pattern`; ошибка словаря — `RuntimeError` с файлом и ключом при старте |
|
||||
| `detect.py` | вид листа (`classify_sheet`: ОСВ / форма ББ-ОФР / прочее), блок заголовка таблицы (`find_table_header`: 1–4 строки, шесть колонок сумм, колонка текста, ИНН, дополнительные колонки, правая граница, повтор заголовка внутри данных) |
|
||||
| `detect.py` | вид листа (`classify_sheet`: ОСВ / форма ББ-ОФР / прочее; ОСВ — по названию ведомости `sheet_markers` или, если лист не форма, по полному заголовку таблицы `has_osv_table`: три группы, у каждой «Дебет» и «Кредит» рядом, в первых 30 строках, #81), блок заголовка таблицы (`find_table_header`: 1–4 строки, шесть колонок сумм, колонка текста, ИНН, дополнительные колонки, правая граница, повтор заголовка внутри данных) |
|
||||
| `header.py` | шапка документа (организация, счёт, период, тип данных, валюта) и схема группировок `group_schema` (варианты шапки 1 и 2) |
|
||||
| `hierarchy.py` | каскад стратегий уровней строк: `outline` → `indent` → `account_pattern` → `header_account` → `sums`; `trivial` — меньше двух строк данных; `header_account` — плоский список под счётом шапки (код счёта в шапке, строк-счетов в теле нет; плоская форма 1С, #59); выбранная стратегия пишется в `warnings[]` документа листа как `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>` (`info`) |
|
||||
| `numbers.py` | суммы в `Decimal` с двумя знаками (`to_money`), ИНН из текста ячейки (`inn_digits`); `float` в домене нет |
|
||||
|
||||
@ -7,29 +7,54 @@
|
||||
# ведомость», «д т», `\w` вместо диапазона;
|
||||
# - новый синоним = новая строка в нужном списке, без правки кода.
|
||||
|
||||
# Лист содержит ОСВ: любая ячейка (первые строки листа) матчит один из паттернов
|
||||
# Лист содержит ОСВ: любая ячейка (первые строки листа) матчит один из паттернов. Названия других учётных систем и
|
||||
# отчётов 1С (AURORA-1717) — только с начала ячейки: «… см. оборотную ведомость» в примечании формы листом ОСВ не делает
|
||||
sheet_markers:
|
||||
- 'оборотно\s*сальдовая\s+ведомость'
|
||||
- '^оборотн\w*\s+ведомост' # «Оборотная ведомость по счетам …» (выгрузки КТЗ)
|
||||
# «ОСВ по сч. 41 за 3 кв.», «ОСВ по ДЗ по сч. 62.01» (КТЗ); не «ОСВ за квартал по счетам …» в перечне документов
|
||||
- '^осв\s+по\s+(\w+\s+по\s+)?сч\b'
|
||||
- '^оборотн\w*\s+баланс' # «Оборотный баланс» (КТЗ)
|
||||
- '^фин\.?\s*обороты' # «Фин. Обороты Листок» (Navision, Айсберри)
|
||||
- '^отчет\s+по\s+главной\s+книге' # SAP, ООО «Виола»
|
||||
|
||||
# Три группы сумм в заголовке таблицы. «Оборотно сальдовая» (название ведомости) не должно матчить turnover:
|
||||
# отсюда границы слова `\bоборот(ы)?\b`.
|
||||
# отсюда границы слова `\bоборот(ы)?\b`. Сторона может стоять в ячейке группы после названия — в том числе вплотную
|
||||
# («НачалоДт», «ОборотыДтВВ»): паттерн группы тогда заканчивается перед стороной (опережающая проверка), остаток ячейки
|
||||
# разбирают `debit`/`credit` (AURORA-1719)
|
||||
groups:
|
||||
opening:
|
||||
- 'сальдо\s+на\s+начало'
|
||||
- '\bнач(альн\w*)?\.?\s*сальдо'
|
||||
- 'входящ\w*\s+сальдо'
|
||||
- 'остат\w*\s+на\s+начало' # «Остаток на начало (периода)» (КТЗ)
|
||||
- 'остат\w*(?=\s+по\s+(дебету|кредиту)\s+на\s+начало)' # «Остаток по дебету на начало» (КТЗ, ЧАЗ)
|
||||
- 'сальдо\s+начальное' # «Сальдо НачальноеДТВВ» (SAP, НМЖК)
|
||||
- 'сальдо\s+вх\b' # «Сальдо вх Дт» (SAP, Виола)
|
||||
- '^начало(?=\s*(дт|кт)\b)' # «НачалоДт» (Navision без названия ведомости, Айсберри)
|
||||
turnover:
|
||||
- '\bоборот(ы)?\b'
|
||||
- '^оборот(ы)?(?=\s*(дт|кт))' # «ОборотДт» (Navision), «ОборотыДтВВ» (SAP, НМЖК)
|
||||
closing:
|
||||
- 'сальдо\s+на\s+конец'
|
||||
- '\bкон(ечн\w*)?\.?\s*сальдо'
|
||||
- 'исходящ\w*\s+сальдо'
|
||||
- 'остат\w*\s+на\s+конец' # «Остаток на конец (периода)» (КТЗ)
|
||||
- 'остат\w*(?=\s+по\s+(дебету|кредиту)\s+на\s+конец)' # «Остаток по кредиту на конец» (КТЗ, ЧАЗ)
|
||||
- 'сальдо\s+конечное' # «Сальдо КонечноеКТВВ» (SAP, НМЖК)
|
||||
- 'сальдо\s+исх\b' # «Сальдо исх Кт» (SAP, Виола)
|
||||
- '^конец(?=\s*(дт|кт)\b)' # «КонецКт» (Navision без названия ведомости, Айсберри)
|
||||
|
||||
# Подзаголовки сумм под группой («Дебет»/«Кредит», «Дт»/«Кт», «Д-т»/«К-т»): ячейка начинается с синонима
|
||||
# Подзаголовки сумм под группой («Дебет»/«Кредит», «Дт»/«Кт», «Д-т»/«К-т») или сторона в остатке ячейки группы:
|
||||
# ячейка (остаток) начинается с синонима
|
||||
debit:
|
||||
- '^(дебет|дт|д т)\b'
|
||||
- '^по\s+дебету\b' # «Оборот по дебету», «Остаток по дебету на начало» (КТЗ)
|
||||
- '^дт\s*вв\b' # «Сальдо НачальноеДТВВ» (SAP, НМЖК)
|
||||
credit:
|
||||
- '^(кредит|кт|к т)\b'
|
||||
- '^по\s+кредиту\b' # «Оборот по кредиту» (КТЗ)
|
||||
- '^кт\s*вв\b' # «Сальдо НачальноеКТВВ» (SAP, НМЖК)
|
||||
|
||||
# Служебные колонки таблицы по тексту заголовка. Код знает колонки ИНН и «Показатели»; прочие колонки таблицы (КПП, код,
|
||||
# количество, валюта и т. п.) попадают в `rows[].extra` под текстом своего заголовка и словаря не требуют.
|
||||
@ -52,6 +77,8 @@ total_row_exact:
|
||||
- '^итого$'
|
||||
# выгрузка 1С «по контрагентам»: итог листа — «ИТОГ» (без «О»); «ИТОГ по 60.111» — итог субсчёта (subaccount_sums)
|
||||
- '^итог$'
|
||||
# «ВСЕГО» — итог листа ведомостей ГК КТЗ по счёту 009 (AURORA-1732)
|
||||
- '^всего$'
|
||||
|
||||
# Итог субсчёта в выгрузке 1С «по контрагентам»: «ИТОГ по 60.111» — суммы строки-счёта с тем же кодом (группа account);
|
||||
# отдельной строкой в результат не попадает (05-domain-rules.md §1.3, AURORA-1714)
|
||||
|
||||
@ -35,7 +35,7 @@ from src.contract import (
|
||||
)
|
||||
from src.parser import ParseOutcome
|
||||
from src.parser.deadline import Deadline
|
||||
from src.parser.detect import is_osv_sheet
|
||||
from src.parser.detect import has_osv_table, is_osv_sheet
|
||||
from src.parser.errors import InvalidDocumentError, ParserError, UnsupportedFormatError
|
||||
from src.parser.osv import OsvParser, data_rows
|
||||
from src.parser.osv_warnings import OSV_XLSB_NO_OUTLINE, OsvNote
|
||||
@ -143,7 +143,13 @@ class BookParser:
|
||||
if read.no_codepage:
|
||||
warnings.append(_file_warning(make_warning(WarningCode.XLS_NO_CODEPAGE, NO_CODEPAGE_WARNING, rules)))
|
||||
sheets = read.sheets
|
||||
plan = plan_sheets(sheets, lambda sheet: is_osv_sheet(sheet, self._rules), rules, deadline)
|
||||
plan = plan_sheets(
|
||||
sheets,
|
||||
lambda sheet: is_osv_sheet(sheet, self._rules),
|
||||
rules,
|
||||
deadline,
|
||||
osv_by_table=lambda sheet: has_osv_table(sheet, self._rules),
|
||||
)
|
||||
osv_names = {sheet.name for sheet in plan.osv}
|
||||
other_names = {sheet.name for sheet in plan.other}
|
||||
reader_notes = _reader_notes(read)
|
||||
|
||||
@ -30,6 +30,7 @@ __all__ = [
|
||||
"classify_sheet",
|
||||
"find_table_header",
|
||||
"first_data_row",
|
||||
"has_osv_table",
|
||||
"inn_from_cell",
|
||||
"is_header_repeat",
|
||||
"is_osv_sheet",
|
||||
@ -166,13 +167,39 @@ def is_osv_sheet(sheet: Sheet, rules: Rules) -> bool:
|
||||
|
||||
def classify_sheet(sheet: Sheet, rules: Rules, statement_rules: StatementRules | None = None) -> SheetKind:
|
||||
"""ОСВ — если в первых строках есть признак ведомости; иначе форма ББ/ОФР, её продолжение или прочее — по словарям
|
||||
пакета `arm_statements` (`statements.forms`; `None` — словари пакета по умолчанию)."""
|
||||
пакета `arm_statements` (`statements.forms`; `None` — словари пакета по умолчанию); «прочее» с полным заголовком
|
||||
таблицы ОСВ — ОСВ (`has_osv_table`)."""
|
||||
if is_osv_sheet(sheet, rules):
|
||||
return SheetKind.OSV
|
||||
# цикл импорта: forms → cells → number_format → numbers → detect (INN_LENGTHS, inn_from_cell)
|
||||
from src.parser.statements.forms import classify_statement_sheet # noqa: PLC0415
|
||||
|
||||
return _STATEMENT_KINDS[classify_statement_sheet(sheet, statement_rules or default_rules())]
|
||||
kind = _STATEMENT_KINDS[classify_statement_sheet(sheet, statement_rules or default_rules())]
|
||||
if kind is SheetKind.OTHER and has_osv_table(sheet, rules):
|
||||
return SheetKind.OSV
|
||||
return kind
|
||||
|
||||
|
||||
def has_osv_table(sheet: Sheet, rules: Rules) -> bool:
|
||||
"""Запасной признак листа ОСВ без названия ведомости (#81, design D1): в первых `SHEET_MARKER_ROWS` строках есть
|
||||
строка со всеми тремя группами сумм, и у каждой группы — колонки «Дебет» и «Кредит» (свёрнутое сальдо не годится,
|
||||
R-3), между которыми в строках заголовка нет других подписей (R-11: у отчёта 1С «Обороты счета» между «Оборот Дт» и
|
||||
«Оборот Кт» — колонки корр. счетов, это не ОСВ). Проверяется та строка групп, которую возьмёт `find_table_header`
|
||||
(первая с «оборотами» и «концом»), — иначе преамбула «Обороты: …» над шапкой уронила бы разбор листа; колонки
|
||||
ищутся тем же разбором, что у обычного заголовка (R-6)."""
|
||||
for row in range(1, min(sheet.n_rows, SHEET_MARKER_ROWS) + 1):
|
||||
cells = _group_cells(sheet, row, rules)
|
||||
groups = {cell.group for cell in cells}
|
||||
if not REQUIRED_GROUPS <= groups:
|
||||
continue
|
||||
if groups != set(GROUP_ORDER):
|
||||
return False
|
||||
try:
|
||||
columns = [_columns_of_group(sheet, row, cells, group, rules) for group in GROUP_ORDER]
|
||||
except InvalidFormatError:
|
||||
return False
|
||||
return all(_sides_side_by_side(sheet, row, group_columns) for group_columns in columns)
|
||||
return False
|
||||
|
||||
|
||||
# --- Заголовок таблицы ---
|
||||
@ -311,6 +338,18 @@ def _check_sides(sheet: Sheet, group: SumGroup, debit: _SumCell | None, credit:
|
||||
return _GroupColumns(debit, credit, None)
|
||||
|
||||
|
||||
def _sides_side_by_side(sheet: Sheet, group_row: int, columns: _GroupColumns) -> bool:
|
||||
"""У группы найдены «Дебет» и «Кредит», и между их колонками в строках заголовка (от строки групп до нижней строки
|
||||
подзаголовков) нет других непустых ячеек."""
|
||||
if columns.debit is None or columns.credit is None:
|
||||
return False
|
||||
left, right = sorted((columns.debit, columns.credit), key=lambda cell: cell.col)
|
||||
bottom = max(left.bottom, right.bottom)
|
||||
return not any(
|
||||
left.right < col < right.col for row in range(group_row, bottom + 1) for col, _ in _texts(sheet, row)
|
||||
)
|
||||
|
||||
|
||||
def _is_label_row(sheet: Sheet, row: int, sums: range, rules: Rules) -> bool:
|
||||
"""Строка-метка блока заголовка: левее сумм — текст группировки, «ИНН» или «Субконто», в колонках сумм — пусто.
|
||||
|
||||
|
||||
@ -54,10 +54,16 @@ class SheetPlan:
|
||||
|
||||
|
||||
def plan_sheets(
|
||||
sheets: Sequence[Sheet], is_osv: Callable[[Sheet], bool], rules: StatementRules, deadline: Deadline
|
||||
sheets: Sequence[Sheet],
|
||||
is_osv: Callable[[Sheet], bool],
|
||||
rules: StatementRules,
|
||||
deadline: Deadline,
|
||||
osv_by_table: Callable[[Sheet], bool] | None = None,
|
||||
) -> SheetPlan:
|
||||
"""Листы по порядку книги: ОСВ; форма; продолжение (в том числе слабое — лист из 1–2 кодов сразу после листа
|
||||
формы или продолжения); прочее. Лист ОСВ или «прочее» прерывает «сразу после»."""
|
||||
"""Листы по порядку книги: ОСВ; форма; продолжение; ОСВ по заголовку таблицы (`osv_by_table`, запасной признак
|
||||
#81: только для листа, который не форма и не продолжение по признакам пакета, — раньше слабого продолжения, R-12);
|
||||
слабое продолжение — лист из 1–2 кодов сразу после листа формы или продолжения; прочее. Лист ОСВ или «прочее»
|
||||
прерывает «сразу после»."""
|
||||
osv: list[Sheet] = []
|
||||
statements: list[tuple[Sheet, SheetKind]] = []
|
||||
other: list[Sheet] = []
|
||||
@ -75,6 +81,9 @@ def plan_sheets(
|
||||
last_type = blocks[-1].type_hint if blocks else None
|
||||
elif kind == "continuation":
|
||||
statements.append((sheet, SheetKind.STATEMENT_CONTINUATION))
|
||||
elif osv_by_table is not None and osv_by_table(sheet):
|
||||
osv.append(sheet)
|
||||
last_type = None
|
||||
elif last_type is not None and is_weak_continuation(sheet, rules, last_type):
|
||||
statements.append((sheet, SheetKind.STATEMENT_CONTINUATION))
|
||||
else:
|
||||
|
||||
@ -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,235 @@ 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(
|
||||
"Входные данные",
|
||||
[
|
||||
["Перечень документов к заявке"],
|
||||
["№", "Документ", "Срок"],
|
||||
[1, "Расшифровка дебиторской задолженности", "ежеквартально"],
|
||||
[2, "ОСВ за квартал по счетам бухгалтерского учета 02, 58, 60, 62, 66, 67, 76", "ежеквартально"],
|
||||
],
|
||||
),
|
||||
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),
|
||||
_preamble_navision_sheet(),
|
||||
]
|
||||
|
||||
|
||||
def _preamble_navision_sheet() -> SheetSpec:
|
||||
"""Полная шапка Navision, над ней преамбула «Обороты за период: … | Остаток на конец: …»: разбор берёт строку
|
||||
преамбулы, значит, и запасной признак смотрит её — лист прочий, книга не падает (ревью #81, Important-1)."""
|
||||
sheet = _navision_sheet("Преамбула", top_padding=1)
|
||||
sheet.put(1, 1, "Обороты за период: 50")
|
||||
sheet.put(1, 2, "Остаток на конец: 150")
|
||||
return sheet
|
||||
|
||||
|
||||
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 +1482,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 +1510,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,21 @@ 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)
|
||||
S48_SHEETS = ("ОВ", "ОСВ сокр", "Оборотный баланс", "Навижн", "Главная книга", "Верхний регистр", "Вторая колонка")
|
||||
TYPO_ROW = 13 # строка «7701234567» с неверной контрольной суммой в s28
|
||||
EXPECTED_NAMES = [
|
||||
"s01_variant2_header.xlsx",
|
||||
@ -86,6 +101,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 +210,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),
|
||||
}
|
||||
|
||||
|
||||
@ -810,3 +837,138 @@ 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"
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# «Входные данные» (перечень документов шаблона банка, R-13) пакет форм относит к расшифровке — как реальный лист
|
||||
# «Входные данные_ИП» на базе; главное — не ОСВ и книга не падает
|
||||
assert [(d.document_type, d.locator) for d in outcome.documents] == [
|
||||
("OSV", "sheet:ОСВ 60"),
|
||||
("DISCLOSURE", "sheet:Входные данные"),
|
||||
]
|
||||
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",
|
||||
"sheet:Преамбула",
|
||||
]
|
||||
|
||||
|
||||
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) == []
|
||||
|
||||
@ -146,6 +146,61 @@ def test_plan_sheets_weak_continuation_only_after_form(rules: StatementRules) ->
|
||||
assert [s.name for s in plan.other] == ["Примечания", "2400"]
|
||||
|
||||
|
||||
class _CountingTable:
|
||||
"""`osv_by_table` для `plan_sheets`: лист с именем из `osv` — ОСВ по таблице; запоминает, для каких листов звали."""
|
||||
|
||||
def __init__(self, *osv: str) -> None:
|
||||
self.osv = set(osv)
|
||||
self.asked: list[str] = []
|
||||
|
||||
def __call__(self, sheet: Sheet) -> bool:
|
||||
self.asked.append(sheet.name)
|
||||
return sheet.name in self.osv
|
||||
|
||||
|
||||
def _no_osv(sheet: Sheet) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def test_plan_sheets_table_fallback_only_for_other(rules: StatementRules) -> None:
|
||||
"""#81, D1: запасной признак не спрашивается у листа формы; лист с таблицей ОСВ — ОСВ, «Справка» — прочее."""
|
||||
form = _form("Бухгалтерский баланс", BALANCE_CODES, "ББ")
|
||||
table = sheet_from_rows([["НачалоДт"]], name="ОВ")
|
||||
reference = sheet_from_rows([["Справка"], ["Аренда", 100]], name="Справка")
|
||||
by_table = _CountingTable("ОВ")
|
||||
|
||||
plan = plan_sheets([form, table, reference], _no_osv, rules, NO_DEADLINE, osv_by_table=by_table)
|
||||
|
||||
assert plan.osv == (table,)
|
||||
assert [(s.name, k) for s, k in plan.statements] == [("ББ", STATEMENT)]
|
||||
assert plan.other == (reference,)
|
||||
assert by_table.asked == ["ОВ", "Справка"]
|
||||
|
||||
|
||||
def test_plan_sheets_table_fallback_before_weak_continuation(rules: StatementRules) -> None:
|
||||
"""R-12: лист с таблицей ОСВ после формы — ОСВ, а не слабое продолжение; без таблицы — продолжение, как было
|
||||
(Review Focus 4)."""
|
||||
ofr = _form("Отчет о финансовых результатах", INCOME_CODES, "ОФР")
|
||||
weak = sheet_from_rows([["Чистая прибыль", "2400", 10, 8]], name="2400")
|
||||
|
||||
plan = plan_sheets([ofr, weak], _no_osv, rules, NO_DEADLINE, osv_by_table=_CountingTable())
|
||||
assert [(s.name, k) for s, k in plan.statements] == [("ОФР", STATEMENT), ("2400", CONTINUATION)]
|
||||
assert plan.osv == ()
|
||||
|
||||
plan = plan_sheets([ofr, weak], _no_osv, rules, NO_DEADLINE, osv_by_table=_CountingTable("2400"))
|
||||
assert [(s.name, k) for s, k in plan.statements] == [("ОФР", STATEMENT)]
|
||||
assert plan.osv == (weak,)
|
||||
|
||||
|
||||
def test_plan_sheets_without_table_fallback_is_unchanged(rules: StatementRules) -> None:
|
||||
"""Без `osv_by_table` — прежнее поведение: «прочее» остаётся прочим."""
|
||||
reference = sheet_from_rows([["Справка"], ["Аренда", 100]], name="Справка")
|
||||
|
||||
plan = plan_sheets([reference], _no_osv, rules, NO_DEADLINE)
|
||||
|
||||
assert (plan.osv, plan.statements, plan.other) == ((), (), (reference,))
|
||||
|
||||
|
||||
def test_is_osv_sheet_by_marker() -> None:
|
||||
rules = load_rules()
|
||||
osv = sheet_from_rows([["Оборотно-сальдовая ведомость по счету 60"]])
|
||||
|
||||
@ -17,6 +17,7 @@ from src.parser.detect import (
|
||||
classify_sheet,
|
||||
find_table_header,
|
||||
first_data_row,
|
||||
has_osv_table,
|
||||
inn_from_cell,
|
||||
is_header_repeat,
|
||||
)
|
||||
@ -110,6 +111,86 @@ def test_osv_marker_far_below_the_head_is_not_searched(rules: Rules) -> None:
|
||||
assert classify_sheet(sheet, rules) is SheetKind.OTHER
|
||||
|
||||
|
||||
# --- Запасной признак листа ОСВ по заголовку таблицы (#81, design D1, R-2, R-3, R-11) ---
|
||||
|
||||
NAVISION_HEADER = ["ФинСчет", "ИсточникНазвание", "НачалоДт", "НачалоКт", "ОборотДт", "ОборотКт", "КонецДт", "КонецКт"]
|
||||
NAVISION_ROW = at({1: "60", 3: 10, 5: 5, 7: 15})
|
||||
|
||||
|
||||
def test_has_osv_table_navision_split_sides(rules: Rules) -> None:
|
||||
"""Navision без названия: все три группы, у каждой Дт и Кт в ячейках группы — ОСВ."""
|
||||
assert has_osv_table(sheet_from_rows([NAVISION_HEADER, NAVISION_ROW]), rules) is True
|
||||
|
||||
|
||||
def test_has_osv_table_with_subheaders(rules: Rules) -> None:
|
||||
"""Обычный заголовок: группы над «Дебет | Кредит»."""
|
||||
sheet = sheet_from_rows(
|
||||
[
|
||||
["Счет", OPENING, None, TURNOVER, None, CLOSING],
|
||||
[None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
|
||||
at({1: "60", 2: 10, 7: 10}),
|
||||
]
|
||||
)
|
||||
|
||||
assert has_osv_table(sheet, rules) is True
|
||||
|
||||
|
||||
def test_has_osv_table_requires_opening(rules: Rules) -> None:
|
||||
header = ["Счет", "ОборотДт", "ОборотКт", "КонецДт", "КонецКт"]
|
||||
|
||||
assert has_osv_table(sheet_from_rows([header, at({1: "60", 2: 5})]), rules) is False
|
||||
|
||||
|
||||
def test_has_osv_table_rejects_collapsed(rules: Rules) -> None:
|
||||
header = ["Счет", "Сальдо на начало", "Оборот Дт", "Оборот Кт", "Сальдо на конец Дт", "Сальдо на конец Кт"]
|
||||
|
||||
assert has_osv_table(sheet_from_rows([header, at({1: "60", 2: 100, 3: 10})]), rules) is False
|
||||
|
||||
|
||||
def test_has_osv_table_rejects_disclosure(rules: Rules) -> None:
|
||||
"""Расшифровка с остатками: «Поступило/Выбыло» — не обороты (Review Focus 1)."""
|
||||
header = ["Наименование", "Остаток на начало", "Поступило", "Выбыло", "Остаток на конец"]
|
||||
|
||||
assert has_osv_table(sheet_from_rows([header, ["Материалы", 100, 50, 30, 120]]), rules) is False
|
||||
|
||||
|
||||
def test_has_osv_table_rejects_columns_between_sides(rules: Rules) -> None:
|
||||
"""1С «Обороты счета»: между «Оборот Дт» и «Оборот Кт» — колонки корр. счетов (R-1, R-11)."""
|
||||
header = ["Счет", "Начальное сальдо Дт", "Начальное сальдо Кт", "Оборот Дт", "51", "68", "Оборот Кт"]
|
||||
header += ["Конечное сальдо Дт", "Конечное сальдо Кт"]
|
||||
|
||||
assert has_osv_table(sheet_from_rows([header, at({1: "60", 2: 100, 4: 50})]), rules) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("blank_rows", "expected"), [(29, True), (30, False)])
|
||||
def test_has_osv_table_only_first_rows(rules: Rules, blank_rows: int, expected: bool) -> None:
|
||||
"""Строка групп ищется в первых `SHEET_MARKER_ROWS` (30) строках (R-2): 30-я — да, 31-я — нет."""
|
||||
sheet = sheet_from_rows([*([[]] * blank_rows), NAVISION_HEADER, NAVISION_ROW])
|
||||
|
||||
assert has_osv_table(sheet, rules) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"preamble",
|
||||
[
|
||||
["Обороты за период: 50", "Остаток на конец: 150"],
|
||||
["Остаток на начало: 100", "Обороты: 50", "Остаток на конец: 150"],
|
||||
],
|
||||
)
|
||||
def test_has_osv_table_checks_the_row_find_table_header_takes(rules: Rules, preamble: list[object]) -> None:
|
||||
"""Финальное ревью, Important-1: преамбула с «оборотами» над полной шапкой — строка, которую возьмёт разбор
|
||||
(`find_table_header`); она не годится, значит, лист не ОСВ (иначе разбор листа упадёт и уронит книгу)."""
|
||||
sheet = sheet_from_rows([preamble, NAVISION_HEADER, NAVISION_ROW])
|
||||
|
||||
with pytest.raises(InvalidFormatError):
|
||||
find_table_header(sheet, rules)
|
||||
assert has_osv_table(sheet, rules) is False
|
||||
|
||||
|
||||
def test_classify_sheet_falls_back_to_table(rules: Rules) -> None:
|
||||
assert classify_sheet(sheet_from_rows([NAVISION_HEADER, NAVISION_ROW]), rules) is SheetKind.OSV
|
||||
|
||||
|
||||
# --- find_table_header: варианты заголовка ---
|
||||
|
||||
|
||||
@ -282,6 +363,25 @@ def test_one_row_header_with_sides_in_group_cells(rules: Rules) -> None:
|
||||
assert [first_data_row(sheet, header)] == [3]
|
||||
|
||||
|
||||
def test_side_without_word_boundary_only_in_group_cell(rules: Rules) -> None:
|
||||
"""#81, SAP НМЖК: сторона приклеена к названию группы («Сальдо НачальноеДТВВ», «ОборотыДтВВ»); «Дтвв» в тексте
|
||||
контрагента ниже — не сторона и колонок не сдвигает."""
|
||||
sheet = sheet_from_rows(
|
||||
[
|
||||
[TITLE],
|
||||
["Счет", "Сальдо НачальноеДТВВ", "Сальдо НачальноеКТВВ", "ОборотыДтВВ", "ОборотыКтВВ"]
|
||||
+ ["Сальдо КонечноеДТВВ", "Сальдо КонечноеКТВВ"],
|
||||
[L(0), *at({1: "60", 2: 10, 7: 10})],
|
||||
[L(1), *at({1: "ООО Дтвв-Сервис", 2: 10, 7: 10})],
|
||||
]
|
||||
)
|
||||
|
||||
header = find_table_header(sheet, rules)
|
||||
|
||||
assert header.columns == ColumnMap(2, 3, 4, 5, 6, 7, frozenset())
|
||||
assert (header.first_row, header.last_row) == (2, 2)
|
||||
|
||||
|
||||
def test_unmerged_group_titles_take_subheaders_up_to_the_next_group(rules: Rules) -> None:
|
||||
sheet = sheet_from_rows(
|
||||
[
|
||||
|
||||
@ -330,10 +330,16 @@ def test_sheet_title_is_not_a_turnover_header(rules: Rules) -> None:
|
||||
|
||||
def test_initial_balance_synonym_is_not_in_the_default_dictionary(rules: Rules) -> None:
|
||||
# «Нач. остаток» добавляет сценарий «новый синоним без релиза кода» (R-8): в штатном словаре его быть не должно
|
||||
for text in ("Нач. остаток", "Начальный остаток", "Остаток на начало периода"):
|
||||
for text in ("Нач. остаток", "Начальный остаток"):
|
||||
assert not any(p.search(text) for p in rules.headers.groups["opening"]), text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", ["остаток на начало периода", "остаток по дебету на начало", "сальдо вх дт"])
|
||||
def test_other_systems_opening_synonyms(rules: Rules, text: str) -> None:
|
||||
"""#81, AURORA-1719: написания группы «начало» выгрузок КТЗ и SAP (текст — нормализованный)."""
|
||||
assert any(p.search(text) for p in rules.headers.groups["opening"]), text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", ["Дебет", "Дт", "Д-т", "дебет, руб."])
|
||||
def test_debit_synonyms(rules: Rules, text: str) -> None:
|
||||
assert any(p.search(text) for p in rules.headers.debit)
|
||||
|
||||
292
docs/superpowers/plans/2026-10-28-osv-other-systems.md
Normal file
292
docs/superpowers/plans/2026-10-28-osv-other-systems.md
Normal file
@ -0,0 +1,292 @@
|
||||
# ОСВ других учётных систем — план реализации
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** листы ОСВ SAP, Navision, выгрузок КТЗ и отчётов 1С с другим названием распознаются как ОСВ: признаки листа по
|
||||
названию, запасной признак по полному заголовку таблицы, написания групп и сторон, итог «ВСЕГО».
|
||||
|
||||
**Architecture:** новые написания — только записи `api/rules/osv_headers.yaml` + синтетика на каждую (R-22). Код меняется
|
||||
в одном месте: запасной признак `has_osv_table` в `detect.py`, который `plan_sheets` вызывает для листа, попавшего бы в
|
||||
«прочее». Регрессия — прогон банка первички до и после, сравнение по документам.
|
||||
|
||||
**Tech Stack:** Python 3.11, pytest, ruff, mypy; openpyxl (генератор синтетики); OpenSpec.
|
||||
|
||||
**Spec:** `tabular-parser/openspec/changes/osv-other-systems/` (proposal.md, design.md, specs/osv-parsing/spec.md).
|
||||
Правило — `docs/context/05-domain-rules.md` §1.1–§1.2 (меняется в Task 5). Issue RSHB/arm-tabular-parser#81. Jira
|
||||
AURORA-1717, AURORA-1719, AURORA-1732. Ветка `feat/81-osv-other-systems` в трёх worktree: `.wt/osv-dict/tabular-parser`,
|
||||
`.wt/osv-dict/fixtures`, `.wt/osv-dict/docs` (корень `W = /Users/andrey.epifancev/projects/awp-ca/.wt/osv-dict`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Никаких `if` под файл, организацию, систему-источник или имя листа: новое написание = запись YAML + синтетика (CLAUDE.md
|
||||
tabular-parser, «Правила расширения»).
|
||||
- Каждая новая запись словаря срабатывает хотя бы на одной синтетике (`tests/fixtures/test_rules_coverage.py`, R-22).
|
||||
- Паттерны пишутся по нормализованному тексту: нижний регистр, «ё» → «е», дефисы/тире/переносы/nbsp → пробел, пробелы
|
||||
схлопнуты (шапка `osv_headers.yaml`).
|
||||
- Новые `sheet_markers` — с якорем `^` (R-5); якорь снимается для названия, только если файлы списка 1717 без этого не
|
||||
находятся (записать в design.md, раздел решений).
|
||||
- Запасной признак: только лист, который `plan_sheets` иначе отнёс бы к «прочее»; строка групп в первых
|
||||
`SHEET_MARKER_ROWS` (30) строках; все три группы; у каждой — и Дебет, и Кредит; свёрнутое сальдо не годится (D1, R-2,
|
||||
R-3, R-6).
|
||||
- Пакет `api/arm_statements/` не трогать. Контракт не меняется.
|
||||
- Код — только Edit/Write, не sed/скрипты через Bash (правило владельца 18.10.2026). cwd оболочки держать в
|
||||
`W/tabular-parser`; fixtures и docs править по абсолютным путям, git — `git -C <абсолютный путь>`.
|
||||
- Файлы банка первички (`/Users/andrey.epifancev/projects/awp-ca/doc-bank/`) — только чтение, в git не кладутся.
|
||||
- Golden правится только `make golden-update` с просмотром diff, отдельным коммитом в fixtures.
|
||||
- Комментарии, сообщения коммитов — по-русски; коммиты заканчиваются строкой
|
||||
`Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Расшифровка ББ, которую пакет не узнал как форму («Остаток на начало | Поступило | Выбыло | Остаток на конец»),
|
||||
не становится ОСВ: в ней нет оборотов → `has_osv_table` ложь (тест в Task 3).
|
||||
2. Преамбула шапки «Остаток на начало: … | Остаток на конец: …» над таблицей не принимается за строку групп: в ней нет
|
||||
«оборотов» (тест в Task 2 на синтетике s49 — преамбула выше таблицы).
|
||||
3. Сторона без границы слова («ДТВВ») не ловится в ячейках, где нет группы: «Дт» внутри текста контрагента
|
||||
(«ООО Дтвв-Сервис») не даёт стороны (тест в Task 2).
|
||||
4. Лист «прочее» после листа формы, который сейчас идёт как слабое продолжение (`is_weak_continuation`), остаётся
|
||||
продолжением — запасной признак проверяется только в ветке «прочее» (тест в Task 3).
|
||||
5. Книга с формами и без единого листа ОСВ: на листах форм `has_osv_table` не вызывается (тест в Task 3 — счётчик
|
||||
вызовов на книге s14).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Базовая линия: банк первички, образцы, контрольный набор синтетики
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/andrey.epifancev/projects/awp-ca/admin/osv-other-systems-regress/regress.py`
|
||||
- Create: `/Users/andrey.epifancev/projects/awp-ca/admin/osv-other-systems-regress/headers.md`
|
||||
- Create (вывод): `.../osv-other-systems-regress/base.csv`, `control-base.md`
|
||||
- Modify: `api/tests/synthetic/generate.py` (`OsvSpec`, `_header_v1`, `_header_rows`, `build_osv`, `table_sheet`,
|
||||
`_s48…_s57`, `FILE_NAMES`, docstring модуля), `api/tests/synthetic/test_synthetic_files.py`
|
||||
- Create (генерация): `W/fixtures/osv/synthetic/s48…s57`; Modify: `W/fixtures/osv/synthetic/README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `regress.py run <out.csv>` и `regress.py diff <base.csv> <new.csv>`; `headers.md` — точные тексты шапок и
|
||||
заголовков реальных образцов для синтетики Task 2–3.
|
||||
|
||||
- [ ] **Step 1: venv.** `cd W/tabular-parser && make venv` → `api/.venv/bin/python` есть; `make check` зелёный на
|
||||
исходной ветке (golden и покрытие читают `../fixtures` — это worktree fixtures на той же базе `f1f7f83`).
|
||||
|
||||
- [ ] **Step 2: `regress.py run`.** Обходит `doc-bank/index/index.csv`, строки с `route == "парсер"`; разбирает файл
|
||||
в процессе тем же путём, что `src.parser.cli` (`get_parser(Settings())`, тип файла — по расширению), запуск из
|
||||
`W/tabular-parser/api` с `ENV_FILE=.env.test` и `PYTHONPATH=.`. Пишет CSV, по строке на документ (на файл без
|
||||
документов — одна строка): `sha256, name, locator, kind (osv|BALANCE|INCOME|…|none), status, error_code,
|
||||
sheet_other_count`. Исключение парсера — строка с `error_code = EXCEPTION:<класс>`, прогон не прерывается.
|
||||
|
||||
- [ ] **Step 3: `regress.py diff`.** Сопоставляет по `(sha256, locator)`; печатает таблицу переходов `(kind, status) →
|
||||
(kind, status)` с числом файлов и список ухудшений по D3: `done → needs_check|error`, `needs_check → error`, любой
|
||||
`kind` формы → `osv`, `none/other → osv`. Переходы в `osv` помечает «проверить вручную» с именем файла.
|
||||
|
||||
- [ ] **Step 4: базовый прогон.** `regress.py run base.csv` на текущем HEAD ветки (код = main `a8c9605`). Ожидаемо по
|
||||
индексу: ~1681 файл; среди ошибок — `INVALID_DOCUMENT` у файлов из списка 1717.
|
||||
|
||||
- [ ] **Step 5: образцы заголовков.** Из `doc-bank` найти по `index.csv` (`parse_bug` содержит `AURORA-1717`/`1719`)
|
||||
по одному файлу на каждое написание из таблиц AURORA-1717 и 1719 (+ «ВСЕГО» из 1732) и выписать в `headers.md`
|
||||
дословно: строку названия, строку(и) групп и подзаголовков (ячейка за ячейкой), подпись итога. Особо: как у Navision
|
||||
подписан оборот («ОборотДт»? «Оборот Дт»?), как у SAP НМЖК — «Оборот» со стороной.
|
||||
|
||||
- [ ] **Step 6: генератор.** В `api/tests/synthetic/generate.py` поля `OsvSpec` (см. Interfaces ниже) и их поддержка в
|
||||
`_header_v1`/`_header_rows`/`build_osv`; существующие s01–s47 побайтно не меняются (`test_generate.py` и повторная
|
||||
генерация в `W/fixtures/osv/synthetic` — `git -C W/fixtures status` чистый по старым файлам).
|
||||
|
||||
- [ ] **Step 7: контрольный набор синтетики** — файлы и тесты по таблице ниже, тексты подписей — из `headers.md`, где
|
||||
есть реальный образец. Набор пишется до правок словаря и кода и должен уметь опровергнуть успех: позитивы проверяют
|
||||
не только «лист — ОСВ», но и точные суммы по сторонам (перепутанные Дебет/Кредит ломают тождество строки и суммы);
|
||||
негативы проверяют, что вид листа и результат не изменились.
|
||||
|
||||
Позитивы (на базе падают, после Task 2–3 проходят):
|
||||
|
||||
| Файл | Что в нём | Тест и проверки |
|
||||
|---|---|---|
|
||||
| `s48_other_system_titles.xlsx` | листы `build_osv` с `title`: «Оборотная ведомость по счету 60 за 1 квартал 2025 г.», «ОСВ по сч. 60 …», «Оборотный баланс …», «Фин. Обороты Листок» (+ строка «Период: …»), «Отчет по Главной книге …»; ещё два листа — название во второй колонке и «ОБОРОТНАЯ\nВЕДОМОСТЬ по счету 60» (верхний регистр, перенос); у одного листа `total_text="ВСЕГО"`, у другого `"Всего "` | `test_s48_other_system_titles_are_osv`: 7 `osvDocument` по листам, `SHEET_OTHER` нет; у листов «ВСЕГО» итог — `TOTAL`, предупреждения «строка «Итого» не найдена» нет; суммы `base_tree()` |
|
||||
| `s49_balance_and_turnover_by_side.xlsx` | «Остаток на начало периода» / «Остаток на конец периода» с «Дебет/Кредит» под ними, `split_groups={"turnover": ("Оборот по дебету", "Оборот по кредиту")}`; над таблицей преамбула «Остаток на начало: 0 \| Остаток на конец: 0» (Review Focus 2) | шесть колонок, суммы дерева, статус `done` |
|
||||
| `s50_sap_in_out_glued_side.xlsx` | лист SAP «Сальдо вх Дт \| Сальдо вх Кт \| Оборот Дт \| Оборот Кт \| Сальдо исх Дт \| Сальдо исх Кт»; лист НМЖК «Сальдо НачальноеДТВВ \| … \| Сальдо КонечноеКТВВ»; контрагент «ООО Дтвв-Сервис» (Review Focus 3) | оба листа — шесть колонок с верными сторонами, суммы дерева, «ООО Дтвв-Сервис» — строка контрагента, `done` |
|
||||
| `s51_untitled_navision.xlsx` | `untitled=True`, три `split_groups` «НачалоДт/НачалоКт», «Оборот…», «КонецДт/КонецКт» (по `headers.md`), первая колонка «ФинСчет» | один `osvDocument`, `SHEET_OTHER` нет, суммы дерева; статус — фактический (без шапки ожидаемо `needs_check` по периоду/организации), зафиксировать в `EXPECTED` с комментарием |
|
||||
| `s52_mixed_book.xlsx` | `statement_sheet()` (форма ББ) → лист как в s51 → `reference_sheet()` | документ формы тот же, что у книги без Navision-листа; Navision — `osvDocument`; «Справка» — `SHEET_OTHER` |
|
||||
| `s53_balance_by_side.xls`, `s54_untitled_navision.xlsb` | s49 через `to_xls`, s51 через `to_xlsb` | те же проверки, что у s49 / s51 (читатели xls и xlsb) |
|
||||
|
||||
Негативы (проходят и на базе, и после — иначе успех опровергнут):
|
||||
|
||||
| Файл | Что в нём | Тест и проверки |
|
||||
|---|---|---|
|
||||
| `s55_not_osv_titles.xlsx` | обычная ОСВ 1С (чтобы книга не была ошибкой) + листы с таблицей без групп ОСВ: «Справка: оборотная ведомость прилагается», «Оборотный капитал и его структура», отчёт 1С «Обороты счета 60» с колонками корр. счетов внутри «Оборот Дт/Кт» (R-1) | 1 `osvDocument`; три листа — `SHEET_OTHER` |
|
||||
| `s56_almost_osv_tables.xlsx` | обычная ОСВ 1С + безымянные листы: (а) «Наименование \| Остаток на начало \| Поступило \| Выбыло \| Остаток на конец»; (б) «Оборот Дт \| Оборот Кт \| Сальдо на конец Дт \| Кт» без начала; (в) свёрнутое начало «Сальдо на начало» одной колонкой + обороты и конец с Дт/Кт; (г) полный заголовок Navision с 31-й строки | 1 `osvDocument`; (а)–(г) — `SHEET_OTHER` (D1, R-2, R-3, Review Focus 1) |
|
||||
| `s57_form_mentions_osv.xlsx` | форма ББ (`statement_sheet()`) с ячейкой примечания «см. оборотную ведомость по сч. 60» и строкой «Остаток на начало периода» в тексте пояснения; следом лист слабого продолжения формы (1–2 кода, как в существующих тестах `is_weak_continuation`) | документы формы те же, что на базе (вид, `locator`, число строк); `osvDocument` нет; продолжение присоединено (Review Focus 4) |
|
||||
|
||||
`EXPECTED`, список файлов, `FILE_NAMES` — для всех. Негативам — отдельный тест-маркер в имени (`test_neg_…`), чтобы
|
||||
их прогон читался отдельно.
|
||||
|
||||
- [ ] **Step 8: прогон набора на базе.** Сгенерировать в `W/fixtures/osv/synthetic`, `make golden-update` (снимки
|
||||
всех s48–s57; у негативов это регрессия базового поведения, снимки позитивов фиксируют ошибку базы и обновятся в
|
||||
Task 2–3), затем
|
||||
`.venv/bin/pytest tests/synthetic -k "s48 or s49 or s50 or s51 or s52 or s53 or s54 or s55 or s56 or s57" -rA`.
|
||||
Ожидаемо: все `test_neg_…` — PASS, все позитивы — FAIL. Иначе стоп: позитив, проходящий на базе, ничего не
|
||||
доказывает (переписать), негатив, падающий на базе, неверно описывает текущее поведение (разобрать). Итог — таблица
|
||||
«тест → результат на базе» в `admin/osv-other-systems-regress/control-base.md`.
|
||||
|
||||
- [ ] **Step 9: коммиты.** tabular-parser: генератор и тесты контрольного набора — «Контрольный набор синтетики ОСВ
|
||||
других систем (#81)» (позитивы падают — допустимо в ветке до Task 3: пометить их `pytest.mark.xfail(strict=True,
|
||||
reason="tp#81: до Task 2–3")`, снимать `xfail` по мере реализации). fixtures: файлы s48–s57 и README — «Синтетика
|
||||
s48–s57 под tp#81»; снимки негативов — отдельным коммитом.
|
||||
|
||||
**Interfaces (генератор, Step 6):**
|
||||
- Поля `OsvSpec`:
|
||||
- `title: str | None = None` — строка названия вместо «Оборотно-сальдовая ведомость по счету {account} за {period}»
|
||||
(`None` — как сейчас);
|
||||
- `title_col: int = 1` — колонка строки названия;
|
||||
- `untitled: bool = False` — без строк организации, названия, параметров: заголовок таблицы с первой строки;
|
||||
- `top_padding: int = 0` — пустых строк над заголовком таблицы при `untitled` (для s56 (г): 30);
|
||||
- `split_groups: dict[str, tuple[str, str]] = {}` — группы, у которых вместо слитого названия + «Дебет/Кредит» две
|
||||
ячейки в строке групп со своими текстами (сторона в ячейке группы); если разделены все три группы, строки
|
||||
подзаголовков нет;
|
||||
- `total_text: str = TOTAL_TEXT`.
|
||||
- Листы-негативы без дерева ОСВ — функцией по образцу `reference_sheet`/`_flat_sheet`: `table_sheet(name: str,
|
||||
rows: Sequence[Sequence[str | D | None]], *, start_row: int = 1) -> SheetSpec`.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Словари (1717 названия, 1719 группы и стороны, 1732 «ВСЕГО»)
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/rules/osv_headers.yaml` (`sheet_markers`, `groups.opening`, `groups.closing`, `groups.turnover` — если
|
||||
нужно для «ОборотДт», `debit`, `credit`, `total_row_exact`)
|
||||
- Modify: `api/tests/synthetic/test_synthetic_files.py` (снять `xfail` у s48–s50, s53)
|
||||
- Modify: `api/tests/unit/parser/test_detect.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: контрольный набор и `headers.md` из Task 1.
|
||||
|
||||
- [ ] **Step 1: падающий unit-тест.** `test_detect.py::test_side_without_word_boundary_only_in_group_cell` — сетка
|
||||
`sheet_from_rows`: заголовок «Сальдо НачальноеДТВВ | …», ниже строка с текстом «Дтвв» в колонке сумм →
|
||||
`find_table_header` берёт колонки только из строки групп. Снять `xfail` у тестов s48, s49, s50, s53.
|
||||
|
||||
- [ ] **Step 2: тесты падают.** `.venv/bin/pytest tests/synthetic tests/unit/parser/test_detect.py -k "s48 or s49 or
|
||||
s50 or s53 or word_boundary"` — FAIL (`INVALID_DOCUMENT`/`INVALID_FORMAT`).
|
||||
|
||||
- [ ] **Step 3: записи словаря.** Ориентир (уточнить по `headers.md`; ловушки: `\w*` жадно съест приклеенную сторону —
|
||||
«сальдо\s+начальн\w*» оставит пустой остаток вместо «дтвв»; `\bоборот(ы)?\b` не матчит «оборотдт»):
|
||||
- `sheet_markers`: `^оборотн\w*\s+ведомост`, `^осв\b`, `^оборотн\w*\s+баланс`, `^фин\.?\s*обороты`,
|
||||
`^отчет\s+по\s+главной\s+книге`;
|
||||
- `groups.opening`: `остат\w*\s+на\s+начало`, `сальдо\s+начальное`, `сальдо\s+вх\b`, `^начало(?=\s*(дт|кт))`;
|
||||
- `groups.closing`: `остат\w*\s+на\s+конец`, `сальдо\s+конечное`, `сальдо\s+исх\b`, `^конец(?=\s*(дт|кт))`;
|
||||
- `groups.turnover` (если `headers.md` подтверждает «ОборотДт» без пробела): `^оборот(?=\s*(дт|кт))`;
|
||||
- `debit`: `^по\s+дебету\b`, `^дт\s*вв\b`; `credit`: `^по\s+кредиту\b`, `^кт\s*вв\b`;
|
||||
- `total_row_exact`: `^всего$` (комментарий: ГК КТЗ, сч. 009, AURORA-1732).
|
||||
Каждой записи — комментарий с источником (AURORA-…, система). Если сторону без границы слова нельзя выразить
|
||||
записью (остаток ячейки не начинается со стороны), правка `_side`/`_group_cells` общая, без упоминания системы
|
||||
(design D2), и тест на неё.
|
||||
|
||||
- [ ] **Step 4: тесты зелёные.** Тот же `pytest` → PASS; все `test_neg_…` контрольного набора → PASS (негатив упал —
|
||||
запись словаря слишком широкая: сузить, не ослаблять тест). `make golden-update`: меняются снимки s48–s50, s53; снимки
|
||||
негативов s55–s57 и всех старых файлов не изменились (изменились — стоп, разобрать). `make check` → PASS (покрытие
|
||||
словарей видит новые записи).
|
||||
|
||||
- [ ] **Step 5: коммиты.** tabular-parser: `rules/osv_headers.yaml`, `tests/synthetic/test_synthetic_files.py`,
|
||||
`tests/unit/parser/test_detect.py` — «ОСВ: названия ведомостей, группы и стороны других систем, «ВСЕГО» (#81)».
|
||||
fixtures (`git -C W/fixtures`): снимки s48–s50, s53 — «Golden синтетики s48–s50, s53 под tp#81».
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Запасной признак листа ОСВ по заголовку таблицы
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/src/parser/detect.py` (новая `has_osv_table`, `classify_sheet`, `__all__`)
|
||||
- Modify: `api/src/parser/statements/adapter.py:56-83` (`plan_sheets`)
|
||||
- Modify: `api/src/parser/book.py:146`
|
||||
- Modify: `api/tests/synthetic/test_synthetic_files.py` (снять `xfail` у s51, s52, s54)
|
||||
- Test: `api/tests/unit/parser/test_detect.py`, `api/tests/unit/parser/statements/test_adapter.py` (тесты `plan_sheets`), `api/tests/unit/parser/test_book_parser.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: контрольный набор из Task 1 (s51, s52, s54 — позитивы; s55–s57 — негативы).
|
||||
- Produces:
|
||||
- `detect.has_osv_table(sheet: Sheet, rules: Rules) -> bool` — в первых `SHEET_MARKER_ROWS` строках есть строка, где
|
||||
`_group_cells` даёт все три группы, и `_columns_of_group` для каждой возвращает `debit` и `credit` (не `collapsed`);
|
||||
`InvalidFormatError` из `_columns_of_group` → `False`. Без дедлайна (≤ 30 строк).
|
||||
- `plan_sheets(sheets, is_osv, rules, deadline, osv_by_table: Callable[[Sheet], bool] | None = None) -> SheetPlan` —
|
||||
в ветке `else` (лист пошёл бы в `other`): если `osv_by_table` задан и истинен — лист в `osv`, `last_type = None`;
|
||||
иначе `other`, как сейчас.
|
||||
- `classify_sheet`: если пакет дал «прочее» и `has_osv_table` — `SheetKind.OSV` (согласованность с `plan_sheets`).
|
||||
- `book.py`: `plan_sheets(..., osv_by_table=lambda sheet: has_osv_table(sheet, self._rules))`.
|
||||
|
||||
- [ ] **Step 1: падающие тесты.**
|
||||
- `test_detect.py`:
|
||||
- `test_has_osv_table_navision_split_sides` — `sheet_from_rows`: первая строка «ФинСчет | НачалоДт | НачалоКт |
|
||||
ОборотДт | ОборотКт | КонецДт | КонецКт» (тексты — из `headers.md`), строки данных → `True`;
|
||||
- `test_has_osv_table_requires_opening` — то же без «НачалоДт/Кт» → `False`;
|
||||
- `test_has_osv_table_rejects_collapsed` — «Сальдо на начало | Обороты (Дебет/Кредит) | Сальдо на конец» с одной
|
||||
колонкой у начала → `False`;
|
||||
- `test_has_osv_table_rejects_disclosure` — «Наименование | Остаток на начало | Поступило | Выбыло | Остаток на
|
||||
конец» → `False` (Review Focus 1);
|
||||
- `test_has_osv_table_only_first_rows` — полный заголовок в строке 31 → `False`;
|
||||
- `test_classify_sheet_falls_back_to_table` — Navision-сетка → `SheetKind.OSV`.
|
||||
- тест `plan_sheets`:
|
||||
- `test_plan_sheets_table_fallback_only_for_other` — лист формы, затем Navision-лист, затем «Справка» → `osv`
|
||||
содержит только Navision-лист, «Справка» в `other`; `osv_by_table` вызван только для двух последних листов
|
||||
(счётчик вызовов, Review Focus 5);
|
||||
- `test_plan_sheets_weak_continuation_not_taken` — лист формы, затем лист из 1–2 кодов (слабое продолжение,
|
||||
образец — существующие тесты `is_weak_continuation`) → остаётся `STATEMENT_CONTINUATION`, `osv_by_table` не
|
||||
вызван (Review Focus 4).
|
||||
- снять `xfail` у тестов s51, s52, s54 контрольного набора.
|
||||
- `test_book_parser.py::test_statement_book_sheets_unchanged_by_fallback` — книга s14 (формы + ОСВ): виды листов те же,
|
||||
что без запасного признака.
|
||||
|
||||
- [ ] **Step 2: тесты падают.** `cd api && .venv/bin/pytest tests/unit/parser tests/synthetic -k "osv_table or
|
||||
plan_sheets or s51 or s52 or s54 or fallback"` → FAIL (`ImportError: has_osv_table`, `INVALID_DOCUMENT`).
|
||||
|
||||
- [ ] **Step 3: реализация** по Interfaces. `has_osv_table` переиспользует `_group_cells` и `_columns_of_group` (R-6),
|
||||
отдельного разбора сторон нет.
|
||||
|
||||
- [ ] **Step 4: тесты зелёные.** Тот же `pytest` → PASS; все `test_neg_…` → PASS; `make golden-update` — в diff
|
||||
меняются только снимки s51, s52, s54, снимки негативов s55–s57 и старых файлов не изменились; `make check` → PASS.
|
||||
|
||||
- [ ] **Step 5: коммиты.** tabular-parser: «ОСВ: запасной признак листа по заголовку таблицы (#81, AURORA-1717)».
|
||||
fixtures: снимки s51, s52, s54 — отдельным коммитом.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Golden, регрессия банка, check-full
|
||||
|
||||
**Files:**
|
||||
- Modify (генерация): `W/fixtures/golden/expected_osv_parse/*.json`
|
||||
- Create (вывод): `/Users/andrey.epifancev/projects/awp-ca/admin/osv-other-systems-regress/new.csv`, `report.md`
|
||||
|
||||
- [ ] **Step 1: golden.** `make golden-check` → код 0; `git -C W/fixtures diff --stat f1f7f83 -- golden/` — только
|
||||
добавленные снимки s48–s57, ни одного изменённого (изменённый — стоп, разобрать с объяснением в design.md).
|
||||
Контрольный набор целиком: `.venv/bin/pytest tests/synthetic -k "s48 or s49 or s50 or s51 or s52 or s53 or s54 or
|
||||
s55 or s56 or s57" -rA` → все PASS, `xfail` не осталось; таблица «тест → база → ветка» дописывается в
|
||||
`control-base.md`.
|
||||
|
||||
- [ ] **Step 2: прогон ветки.** `regress.py run new.csv`; `regress.py diff base.csv new.csv > report.md`.
|
||||
|
||||
- [ ] **Step 3: разбор.** Каждое ухудшение и каждый переход в `osv` из формы/«прочее» — открыть файл, решить: ведомость
|
||||
(ожидаемо: «Оборотный баланс» 1718 и файлы списков 1717/1719) или ложное срабатывание. Ложное — правка записи словаря
|
||||
или якоря, повтор Task 2 Step 4 и этого шага. Не устраняется — стоп, вопрос владельцу с файлом и тем, что видно.
|
||||
В `report.md` — таблица переходов, список ручных проверок с вердиктом, файлы списков 1717/1719, оставшиеся с
|
||||
ошибкой, с причиной (1720–1723, «Обороты счета», безымянный лист, ушедший в форму).
|
||||
|
||||
- [ ] **Step 4: `make check-full`** (поднимает compose для интеграционных тестов) → PASS. Также
|
||||
`.venv/bin/pytest tests/fixtures/test_osv_dkb.py` → PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Правило 05-domain-rules, документация сервиса
|
||||
|
||||
**Files:**
|
||||
- Modify: `W/docs/context/05-domain-rules.md` §1.1 (поиск листа), §1.2 (написания групп и сторон); строка «Итого» —
|
||||
где перечислены итоговые подписи (§1.3 или §1.4)
|
||||
- Modify: `W/tabular-parser/README.md` (раздел «Парсер ОСВ»: признаки листа, запасной признак), `W/tabular-parser/CLAUDE.md`
|
||||
(перечень синтетики s48–s57 в абзаце golden), docstring `generate.py` (s48–s57)
|
||||
- Modify: `W/tabular-parser/openspec/changes/osv-other-systems/design.md` — решения, принятые по ходу (R-9…), и
|
||||
сводка регрессии из `report.md`
|
||||
|
||||
- [ ] **Step 1: §1.1** по design D4: признак — название ведомости по `sheet_markers` (перечислить написания) или,
|
||||
если лист не опознан как форма или продолжение, полный заголовок таблицы ОСВ в первых 30 строках: три группы, у каждой
|
||||
Дебет и Кредит, без свёрнутого сальдо. §1.2 — написания групп и сторон из Task 2 (сторона в ячейке группы, «по
|
||||
дебету», приклеенная «ДТВВ»). «ВСЕГО» — рядом с «ИТОГ».
|
||||
|
||||
- [ ] **Step 2: проверки.** `openspec validate osv-other-systems --strict` → valid; `make check` → PASS (docstring и
|
||||
README не ломают линтеры).
|
||||
|
||||
- [ ] **Step 3: коммиты.** docs (`git -C W/docs`): «05 §1.1–§1.2: ОСВ других учётных систем (tp#81)»; tabular-parser:
|
||||
«Документация: признаки листа ОСВ, синтетика s48–s57 (#81)».
|
||||
2
openspec/changes/osv-other-systems/.openspec.yaml
Normal file
2
openspec/changes/osv-other-systems/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-28
|
||||
147
openspec/changes/osv-other-systems/design.md
Normal file
147
openspec/changes/osv-other-systems/design.md
Normal file
@ -0,0 +1,147 @@
|
||||
## Context
|
||||
|
||||
Классификация листа (`detect.classify_sheet`): ОСВ, если в первых `SHEET_MARKER_ROWS` (30) строках есть ячейка,
|
||||
совпадающая с `sheet_markers`; иначе вид листа определяет пакет `arm_statements` (форма / продолжение / прочее).
|
||||
`sheet_markers` сейчас один: `оборотно\s*сальдовая\s+ведомость`. Заголовок таблицы (`_find_group_row`) — первая строка,
|
||||
где есть группы «обороты» и «сальдо на конец». Сторона ищется в начале текста ячейки (`^(дебет|дт|д т)\b`) — в
|
||||
отдельной ячейке под группой либо в остатке ячейки группы после совпадения (`_group_cells`: «Сальдо на начало Дт»).
|
||||
|
||||
Файлы — банк первичных документов `awp-ca/doc-bank/` (индекс `index/index.csv`, колонки `parse_status`, `parse_error`,
|
||||
`parse_bug`, `sha256`; в git не кладутся). Списки затронутых файлов с ГК, ЮЛ и sha256 — вложения `список_файлов.csv`
|
||||
к AURORA-1717 и AURORA-1719. Образцы во вложениях задач:
|
||||
|
||||
| Задача | Файл | Что показывает |
|
||||
|---|---|---|
|
||||
| 1717, 1719 | «ОСВ по внб счету 009 за 9 мес 2025.xlsx» (Промлит) | «Оборотная ведомость», «Остаток на начало/конец» |
|
||||
| 1717, 1719 | «ОСВ 66 67 01-06 2025.xlsx» (Виола) | «Отчет по Главной книге», SAP «Сальдо вх Дт / Сальдо исх Кт» |
|
||||
| 1717 | «67 9 мес. 25.xlsx» (Айсберри) | Navision «Фин. Обороты Листок», «НачалоДт / КонецКт» |
|
||||
| 1719 | «ОСВсч62_3кв2025.xls» (НМЖК) | SAP «Сальдо НачальноеДТВВ / КонечноеКТВВ» |
|
||||
| 1732 | «ОСВ по внб сч 009 за 3 кв 2025.xlsx» (КТЗ) | итог «ВСЕГО» |
|
||||
|
||||
Эксперимент автора задач на копии словарей (20.10.2026, синонимы 1717 + 1719 без «ДТВВ» и без запасного признака): из
|
||||
129 файлов 1717 — 43 «Готово», 43 «Требует проверки», 43 упираются в группы и иерархию; из 159 файлов 1719 — 53
|
||||
«Готово», 58 «Требует проверки», 48 — другое (AURORA-1720, 1721, 1723). Остальные ОСВ банка статусы не ухудшили.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Листы с перечисленными названиями и листы без названия с полным заголовком таблицы ОСВ распознаются как ОСВ.
|
||||
- Перечисленные написания групп и сторон находятся; заголовок таблицы этих файлов не падает с `INVALID_FORMAT`
|
||||
«нет групп сумм».
|
||||
- «ВСЕГО» — итоговая строка листа.
|
||||
- Ни один документ банка первички и ни одна фикстура не ухудшает результат (см. «Регрессия»).
|
||||
|
||||
**Non-Goals:**
|
||||
- Довести все затронутые файлы до «Готово»: дальше их держат иерархия (AURORA-1721, 1730), одинаковые подписи начала и
|
||||
конца (AURORA-1720), обороты без подзаголовков в варианте НОВО-МАР и SAP со стороной перед группой (AURORA-1722),
|
||||
ведомости ОС/ТМЦ (AURORA-1723, 1731). Эти файлы могут остаться `needs_check` или `INVALID_FORMAT` по другой причине.
|
||||
- Отчёт 1С «Обороты счета» (1 файл, ГК Сейма): внутри «Оборот Дт/Кт» колонки корреспондирующих счетов, это отдельная
|
||||
форма. Синоним `обороты\s+счета` не добавляется.
|
||||
- Правило «расшифровка без кодов ББ не может быть Готово» (вторая половина AURORA-1718) — пакет `arm_statements`,
|
||||
ждёт AURORA-1677.
|
||||
|
||||
**Anti-goals (провал, даже если тесты зелёные):**
|
||||
- Лист формы ББ/ОФР, её продолжения или расшифровки становится ОСВ по запасному признаку или по новому названию
|
||||
ведомости, хотя ведомостью не является. Ведомость, которую сегодня ложно забирает конвейер форм (10 файлов «Оборотный
|
||||
баланс», AURORA-1718), становится ОСВ по названию — это цель, а не ухудшение (признак по названию проверяется раньше
|
||||
форм, как сейчас).
|
||||
- `if` под файл, организацию, систему-источник или имя листа вместо синонима в YAML.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Запасной признак листа — последним шагом (решение владельца 28.10.2026)
|
||||
|
||||
Порядок классификации: признак ОСВ по названию (`sheet_markers`) → форма / продолжение / прочее (пакет). Только если
|
||||
пакет вернул «прочее», лист проверяется запасным признаком — раньше слабого продолжения формы (R-12): в первых `SHEET_MARKER_ROWS` строках есть строка групп, где
|
||||
найдены **все три** группы (начало, обороты, конец), и у каждой группы найдены **обе** стороны — Дебет и Кредит
|
||||
(в ячейках под группой или в самой ячейке группы), и между ними в строках заголовка нет других подписей (R-11).
|
||||
Свёрнутое сальдо (одна колонка в группе) запасной признак не даёт.
|
||||
Нашёлся — лист `OSV`, дальше обычный конвейер ОСВ; нет — `SHEET_OTHER`, как сейчас.
|
||||
|
||||
Почему так: расшифровки ББ с колонками «Остаток на начало / … / Остаток на конец» забирает классификатор форм раньше,
|
||||
до запасного признака они не доходят; поиск заголовка выполняется только на листах «прочее», а не на каждом листе книги
|
||||
с формами. Требование трёх групп и обеих сторон строже обычного заголовка ОСВ («начало» там необязательно), поэтому
|
||||
ложное срабатывание требует почти полной таблицы ОСВ.
|
||||
|
||||
### D2. Новые записи словарей
|
||||
|
||||
Ориентир — рекомендации из AURORA-1717/1719/1732; окончательные паттерны подбирает исполнитель по образцам, каждая
|
||||
запись — с синтетикой, где она срабатывает (R-22). Паттерны пишутся по нормализованному тексту (нижний регистр, «ё» →
|
||||
«е», дефисы и переносы → пробел).
|
||||
|
||||
| Ключ | Написания |
|
||||
|---|---|
|
||||
| `sheet_markers` | «Оборотная ведомость …», «ОСВ по сч. …» (с начала ячейки), «Оборотный баланс», «Фин. Обороты Листок», «Отчет по Главной книге» |
|
||||
| `groups.opening` | «Остаток на начало (периода)», «Начальное сальдо» (уже есть `\bнач(альн\w*)?\.?\s*сальдо`), «Сальдо начальное…», «Сальдо вх …», «НачалоДт / НачалоКт» |
|
||||
| `groups.closing` | симметрично: «Остаток на конец», «Конечное сальдо», «Сальдо конечное…», «Сальдо исх …», «КонецДт / КонецКт» |
|
||||
| `debit` / `credit` | «по дебету» / «по кредиту» (в остатке ячейки группы «Оборот по дебету»); сторона, приклеенная к названию без границы слова («…ДТВВ», «…КТВВ», «НачалоДт») |
|
||||
| `total_row_exact` | «ВСЕГО» |
|
||||
|
||||
Если сторону без границы слова нельзя выразить записью словаря при текущем `_side` (поиск с начала остатка ячейки),
|
||||
допустима правка поиска стороны в `detect.py`, общая для всех написаний, без упоминания конкретной системы.
|
||||
|
||||
### D3. Регрессия по банку первички
|
||||
|
||||
До изменений и после — прогон парсера (CLI, `ENV_FILE=.env.test`) по всем файлам банка с `route = парсер` (xls, xlsx,
|
||||
xlsb) на main `a8c9605` и на ветке. Сравнение по каждому документу: вид (ОСВ / форма / прочее), статус, код ошибки.
|
||||
Ухудшение — `done` → `needs_check`/ошибка, `needs_check` → ошибка, любой переход форма/расшифровка → ОСВ через запасной
|
||||
признак, переход форма/расшифровка → ОСВ по названию ведомости или «прочее» → ОСВ, если лист не ведомость (каждый такой
|
||||
переход по названию просматривается вручную: ожидаемы «Оборотный баланс» и другие ведомости из списков 1717/1719).
|
||||
Каждое ухудшение разбирается до PR: либо исправляется, либо выносится владельцу с образцом.
|
||||
|
||||
Банк — `awp-ca/doc-bank/` (в worktree — симлинк `.wt/osv-dict/doc-bank`), только чтение. Базовый прогон на main
|
||||
`a8c9605` снимается до первой правки кода. Скрипт прогона и сравнения и его результаты — `awp-ca/admin/osv-other-systems-regress/`
|
||||
(не репозиторий; в PR — только итоговая таблица).
|
||||
|
||||
Приёмка: сценарии спеки на синтетике, зелёный `make check-full`, нет ухудшений D3. Число файлов банка, перешедших в
|
||||
«Готово» / «Требует проверки», — отчёт, а не порог. Реальные безымянные листы, которые конвейер форм относит к форме или
|
||||
продолжению (до запасного признака они не доходят), остаются как есть и перечисляются в PR как остаток. Итоговая
|
||||
таблица переходов статусов (было → стало, число файлов) — в описание PR и комментарий к AURORA-1717/1719. Плюс обычные
|
||||
`make check-full` (golden, property, покрытие словарей) и `tests/fixtures/test_osv_dkb.py`.
|
||||
|
||||
### D4. Правило в 05-domain-rules.md
|
||||
|
||||
§1.1: признак листа — название ведомости по словарю `sheet_markers` (примеры написаний) или, если лист не опознан как
|
||||
форма ББ/ОФР или её продолжение, полный заголовок таблицы ОСВ в первых 30 строках (D1). §1.2: варианты написания групп
|
||||
и сторон дополняются написаниями из D2. PR в arm-ka-docs из ветки `feat/81-osv-other-systems` worktree
|
||||
`.wt/osv-dict/docs`, ссылка на tabular-parser#81.
|
||||
|
||||
## Итог регрессии (28.10.2026)
|
||||
|
||||
Банк первички, 2152 файла (`admin/osv-other-systems-regress/report.md` workspace): 60 файлов из ошибки → ОСВ «Готово»,
|
||||
70 → «Требует проверки», 40 узнаны как ОСВ, но стоят на 1720–1723; ни один лист формы или прочий лист книги с формами
|
||||
не стал ОСВ; ухудшения — только R-14. Файлы списков 1717/1719 (175): 60 «Готово», 60 «Требует проверки», 50 ошибка
|
||||
формата, 4 ложных «Готово» безымянных ведомостей 009, которые забирает конвейер форм (D1), 1 — неполный заголовок.
|
||||
|
||||
Контрольный набор синтетики s48–s57: позитивы на базе падали, негативы проходили; на ветке проходят все. Мутации
|
||||
запасного признака (без R-11, без лимита 30 строк, со свёрнутым сальдо, с двумя группами) и снятие якоря `^` у названий
|
||||
ловит каждая свой негатив (s55, s56, s57).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [`^осв\b` и «оборотн… ведомост» в тексте формы] → проверка ОСВ идёт первой; регрессия D3 ловит листы форм, ставшие
|
||||
ОСВ; паттерн «ОСВ» — только с начала ячейки.
|
||||
- [Расшифровка, не узнанная пакетом, проходит запасной признак] → D1 требует три группы и обе стороны; регрессия D3.
|
||||
- [Новые синонимы групп матчат преамбулу шапки («Остаток на начало: …») как строку групп] → в такой строке нет
|
||||
«оборотов», `_find_group_row` её пропускает (R-12); синтетика с преамбулой.
|
||||
- [Сторона без границы слова («дт» внутри слова)] → искать её только в остатке ячейки группы после совпадения группы, не
|
||||
в произвольных ячейках.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|
|
||||
| 2026-10-28 | R-1. Отчёт 1С «Обороты счета» вне объёма, синоним не добавляется | автор AURORA-1717 проверил: с синонимом файл даёт «Требует проверки» с договорами в контрагентах — нужна отдельная форма | 1 файл остаётся `INVALID_DOCUMENT`; отдельная задача |
|
||||
| 2026-10-28 | R-2. Запасной признак ищет строку групп только в первых `SHEET_MARKER_ROWS` (30) строках | у безымянных листов таблица начинается сразу; ограничение снижает время и ложные срабатывания на длинных листах «прочее» | безымянный лист с преамбулой длиннее 30 строк не распознаётся; поднимется регрессией D3 |
|
||||
| 2026-10-28 | R-3. Запасной признак не принимает свёрнутое сальдо (одна колонка в группе) | строгость против ложных ОСВ (D1) | ОСВ без названия со свёрнутым сальдо не распознаётся |
|
||||
| 2026-10-28 | R-5. Новые `sheet_markers` якорятся к началу ячейки (`^…`); якорь снимается для конкретного названия, только если по спискам 1717 файлы с ним иначе не находятся | «Оборотная ведомость», «Оборотный баланс» могут встретиться в примечаниях к ББ; признак по названию проверяется раньше форм | часть ведомостей с текстом перед названием не распознаётся; видно в регрессии D3 |
|
||||
| 2026-10-28 | R-6. Запасной признак проверяет ту же строку групп, что возьмёт `find_table_header` (первую с «оборотами» и «концом» в первых 30 строках), и колонки ищет тем же `_columns_of_group`; строгие условия — только к этой строке (финальное ревью, I-1: иначе преамбула «Обороты: …» над шапкой давала ОСВ, разбор падал и ронял книгу; негатив s56 «Преамбула») | запасной признак и конвейер не должны расходиться в том, что считать заголовком | безымянная ОСВ с преамбулой «Обороты: …» над шапкой не распознаётся |
|
||||
| 2026-10-28 | R-7. Порядок PR: `fixtures` (golden синтетики, отдельный коммит `make golden-update`) → tabular-parser → docs | golden-тесты tabular-parser читают `../fixtures` | — |
|
||||
| 2026-10-28 | R-8. Приёмка — синтетика + `make check-full` + нет ухудшений D3; число исправленных файлов банка — отчёт, не порог | эксперимент автора задач — ориентир, а не обязательство; остаток держат 1720–1723 | — |
|
||||
| 2026-10-28 | R-9. Написание «Остаток по дебету/кредиту на начало/конец» (6 файлов КТЗ/ЧАЗ) включено в словарь: группа «остаток» с опережающей проверкой, сторона — из остатка ячейки | та же семья «Остаток на начало/конец» (1719), решается записью словаря | лишняя запись, удаляется |
|
||||
| 2026-10-28 | R-10. В синтетике s50 у SAP-листа обороты «Оборот Дт / Оборот Кт», а не реальное «Дт оборот» | сторона перед группой — AURORA-1722, вне объёма (R-4) | файлы Виолы остаются с ошибкой до 1722 |
|
||||
| 2026-10-28 | R-11. Запасной признак требует, чтобы между колонками «Дебет» и «Кредит» каждой группы в строках заголовка не было других подписей | иначе 1С «Обороты счета» (Дт/Кт оборота разделены колонками корр. счетов) проходит как ОСВ вопреки R-1; держит негатив s55 | безымянная ОСВ с колонкой между Дт и Кт не распознаётся |
|
||||
| 2026-10-28 | R-12. Запасной признак проверяется раньше слабого продолжения формы (лист из 1–2 кодов сразу после формы), но после сильных признаков формы и продолжения | на базе лист Navision после формы ББ пакет присоединял к ней как слабое продолжение (синтетика s52); полная таблица ОСВ — сильнее эвристики «1–2 кода» | лист-продолжение формы с полной таблицей ОСВ отделится от формы (у продолжения формы таких групп нет) |
|
||||
| 2026-10-28 | R-13. Признак «ОСВ …» сужен до `^осв\s+по\s+(\w+\s+по\s+)?сч\b` вместо `^осв\b` | первый прогон банка: строка «ОСВ за квартал по счетам …» перечня документов на листе «Входные данные_ИП» шаблона банка делала лист ОСВ, его разбор падал, и вся книга расшифровок становилась ошибкой (86 файлов); все 20 реальных названий начинаются с «ОСВ по сч.» / «ОСВ по ДЗ по сч.»; держит негатив s55 | «ОСВ» с другим продолжением по названию не узнаётся (остаётся запасной признак) |
|
||||
| 2026-10-28 | R-14. Два файла «Сводная ОСВ … (забаланс)»: ложное «Готово» расшифровки на базе → ошибка иерархии ОСВ на ветке принято, а не исправлено | это цель изменения (ведомость «Оборотный баланс / Оборотная ведомость» перестаёт быть расшифровкой, 1718); дальше файл держит иерархия «002.01: Название» — AURORA-1721 | у двух книг «Ошибка» вместо фальшивого «Готово» до 1721 |
|
||||
| 2026-10-28 | R-4. AURORA-1722 (SAP со стороной перед группой, НОВО-МАР) не входит | в задачах указан порядок: после 1717/1719; причина варианта НОВО-МАР не разобрана | эти файлы остаются с ошибкой до 1722 |
|
||||
42
openspec/changes/osv-other-systems/proposal.md
Normal file
42
openspec/changes/osv-other-systems/proposal.md
Normal file
@ -0,0 +1,42 @@
|
||||
## Why
|
||||
|
||||
Прогон банка первичных документов (AURORA-1706, архивы «ДКБ отчетность» 20.10.2026, 1681 табличная ОСВ) показал, что
|
||||
парсер знает только ведомости 1С с названием «Оборотно-сальдовая ведомость» и написания групп сумм 1С. ОСВ других
|
||||
учётных систем (SAP, Navision, выгрузки КТЗ) и отчёты 1С с другим названием отклоняются целиком:
|
||||
|
||||
- AURORA-1717 — 119 файлов `INVALID_DOCUMENT` «в книге нет листов ОСВ или форм»: «Оборотная ведомость», «ОСВ по сч.»,
|
||||
«Оборотный баланс», «Фин. Обороты Листок» (Navision), «Отчет по Главной книге», листы без названия; ещё 10 файлов
|
||||
«Оборотный баланс» получают ложное «Готово» как расшифровка формы (часть AURORA-1718);
|
||||
- AURORA-1719 — 159 файлов `INVALID_FORMAT` «не найден заголовок таблицы ОСВ»: «Остаток на начало/конец», «Начальное /
|
||||
Конечное сальдо», «Сальдо вх/исх Дт» (SAP), «НачалоДт/КонецКт» (Navision), «Оборот по дебету/кредиту» (группа и
|
||||
сторона в одной ячейке), «Сальдо НачальноеДТВВ» (сторона приклеена к названию);
|
||||
- AURORA-1732 — итог листа «ВСЕГО» (ГК КТЗ, сч. 009) не распознаётся как «Итого» («ИТОГ» и «ИТОГ по …» уже добавлены
|
||||
AURORA-1714).
|
||||
|
||||
Gitea: RSHB/arm-tabular-parser#81.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Признаки листа ОСВ (`sheet_markers`): синонимы названий ведомости других систем и отчётов 1С.
|
||||
- Запасной признак листа ОСВ: лист, который не опознан ни как ОСВ по названию, ни как форма ББ/ОФР или её продолжение,
|
||||
считается ОСВ, если в его первых строках найден полный заголовок таблицы ОСВ (три группы сумм, у каждой — Дебет и
|
||||
Кредит). Проверка выполняется последней, только для листов, которые иначе ушли бы в «прочее» (`SHEET_OTHER`).
|
||||
- Синонимы групп сумм (`groups.opening`, `groups.closing`) и сторон (`debit`, `credit`), включая сторону в одной
|
||||
ячейке с группой («Оборот по дебету», «НачалоДт») и приклеенную к названию группы («НачальноеДТВВ»).
|
||||
- Итоговая строка «ВСЕГО» (`total_row_exact`).
|
||||
- Синтетика на каждое новое написание (правило покрытия словарей R-22).
|
||||
- `05-domain-rules.md` §1.1–§1.2 — правило поиска листа и написания групп (отдельный PR в arm-ka-docs, та же ветка
|
||||
`feat/81-osv-other-systems`).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `osv-parsing`: признак листа ОСВ — название ведомости или (запасной путь) полный заголовок таблицы; написания групп и
|
||||
сторон других учётных систем.
|
||||
|
||||
## Impact
|
||||
|
||||
- `api/rules/osv_headers.yaml`, `api/src/parser/detect.py` (классификация листа, поиск стороны), синтетика
|
||||
`tests/synthetic/generate.py`, golden-снимки синтетики (отдельный коммит в `fixtures`).
|
||||
- Контракт результата не меняется. arm-core не меняется.
|
||||
- Пакет `arm_statements` не меняется (не ждёт AURORA-1677).
|
||||
91
openspec/changes/osv-other-systems/specs/osv-parsing/spec.md
Normal file
91
openspec/changes/osv-other-systems/specs/osv-parsing/spec.md
Normal file
@ -0,0 +1,91 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Листы ОСВ находятся по содержимому
|
||||
Парсер SHALL считать листом ОСВ любой лист, где ячейка в первых 30 строках листа (`SHEET_MARKER_ROWS`: шапка ведомости
|
||||
всегда над таблицей) матчит признак ОСВ из `rules/osv_headers.yaml` (без учёта регистра, «ё/е», переносов строк и
|
||||
дефисов). Признаки ОСВ — названия ведомости разных учётных систем («Оборотно-сальдовая ведомость», «Оборотная
|
||||
ведомость», «ОСВ по сч.», «Оборотный баланс», «Фин. Обороты», «Отчет по Главной книге»). Лист с признаками формы ББ/ОФР
|
||||
или её продолжения уходит в конвейер форм (спека `statement-parsing`). Лист, не опознанный ни как ОСВ по признаку, ни
|
||||
как форма или её продолжение по признакам форм, SHALL считаться листом ОСВ по запасному признаку, если в первых
|
||||
`SHEET_MARKER_ROWS` строках есть строка групп сумм со всеми тремя группами («начало», «обороты», «конец»), у каждой
|
||||
группы найдены обе стороны — «Дебет» и «Кредит», и между колонками «Дебет» и «Кредит» группы в строках заголовка нет
|
||||
других подписей; свёрнутое сальдо (одна колонка в группе) запасного признака не даёт. Запасной признак MUST
|
||||
проверяться только после классификации форм и MUST NOT отнимать у конвейера форм лист формы, её продолжения или
|
||||
расшифровки; слабое продолжение (лист из 1–2 кодов сразу после формы) признаком формы не считается — лист с полной
|
||||
таблицей ОСВ после формы — ОСВ. Прочие листы SHALL пропускаться с предупреждением файла `SHEET_OTHER` (`severity = info`,
|
||||
`locator = sheet:<имя>`) в `metrics.warnings`. Книга без единого листа ОСВ и без форм → `status = error`,
|
||||
`error.code = INVALID_DOCUMENT` («в книге нет листов ОСВ или форм»). Несколько листов ОСВ → несколько `documents` с
|
||||
`locator = sheet:<имя>`. Имя листа MUST NOT использоваться для поиска.
|
||||
|
||||
#### Scenario: Один лист — одна ОСВ (все реальные фикстуры)
|
||||
- **WHEN** разбирается любой файл из `fixtures/osv/**`
|
||||
- **THEN** `documents` содержит ровно один `osvDocument` с `locator = "sheet:Лист_1"`, статус `done`
|
||||
|
||||
#### Scenario: Два листа ОСВ и посторонний лист
|
||||
- **WHEN** разбирается синтетический файл с листами «ОСВ 60», «Справка», «ОСВ 62»
|
||||
- **THEN** `documents` содержит два документа в порядке листов, в `metrics.warnings` есть `SHEET_OTHER` с `locator = "sheet:Справка"`
|
||||
|
||||
#### Scenario: Нет ОСВ
|
||||
- **WHEN** разбирается книга с одним листом произвольной таблицы
|
||||
- **THEN** `status = error`, `error.code = INVALID_DOCUMENT`, сообщение называет число просмотренных листов
|
||||
|
||||
#### Scenario: Название ведомости другой учётной системы
|
||||
- **WHEN** разбирается синтетическая ОСВ, у которой в шапке вместо «Оборотно-сальдовая ведомость» стоит одно из
|
||||
названий «Оборотная ведомость по счету 009», «ОСВ по сч. 41 за 3 кв.», «Оборотный баланс», «Фин. Обороты Листок»,
|
||||
«Отчет по Главной книге»
|
||||
- **THEN** лист разобран как ОСВ (`osvDocument`), `SHEET_OTHER` для него нет
|
||||
|
||||
#### Scenario: Лист без названия ведомости — по заголовку таблицы
|
||||
- **WHEN** разбирается синтетический лист без названия ведомости, таблица начинается в первых строках с групп
|
||||
«НачалоДт | НачалоКт | ОборотДт | ОборотКт | КонецДт | КонецКт» (Navision)
|
||||
- **THEN** лист разобран как ОСВ по запасному признаку
|
||||
|
||||
#### Scenario: Расшифровка формы не становится ОСВ
|
||||
- **WHEN** разбирается книга с формой ББ и листом расшифровки с колонками «Остаток на начало | Поступило | Выбыло |
|
||||
Остаток на конец»
|
||||
- **THEN** вид листа расшифровки тот же, что без запасного признака (форма или продолжение конвейера форм либо
|
||||
`SHEET_OTHER`), документа ОСВ из него нет
|
||||
|
||||
#### Scenario: Лист ОСВ без названия после формы — не слабое продолжение
|
||||
- **WHEN** разбирается книга: форма ББ, за ней лист Navision без названия ведомости с полным заголовком таблицы, за ним
|
||||
посторонний лист
|
||||
- **THEN** форма — свой документ, лист Navision — `osvDocument`, посторонний лист — `SHEET_OTHER`
|
||||
|
||||
#### Scenario: Отчёт «Обороты счета» не ОСВ
|
||||
- **WHEN** лист без названия ведомости содержит все три группы со сторонами, но между «Оборот Дт» и «Оборот Кт» стоят
|
||||
колонки корреспондирующих счетов
|
||||
- **THEN** лист пропускается с `SHEET_OTHER`
|
||||
|
||||
#### Scenario: Неполная таблица не даёт запасного признака
|
||||
- **WHEN** лист без названия ведомости содержит группы «обороты» и «конец», но без группы «начало» или со свёрнутым
|
||||
сальдо (одна колонка в группе)
|
||||
- **THEN** лист пропускается с `SHEET_OTHER`
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Написания групп и сторон других учётных систем
|
||||
Словари `groups`, `debit`, `credit` SHALL знать написания групп и сторон выгрузок SAP, Navision и других систем: «Остаток
|
||||
на начало/конец (периода)», «Начальное/Конечное сальдо», «Сальдо начальное/конечное», «Сальдо вх/исх», «НачалоДт/Кт»,
|
||||
«КонецДт/Кт». Сторона SHALL находиться и в отдельной ячейке под группой, и в самой ячейке группы после её названия — в
|
||||
том числе в форме «по дебету»/«по кредиту» («Оборот по дебету») и приклеенной к названию без пробела («Сальдо
|
||||
НачальноеДТВВ», «НачалоДт»). Сторона без границы слова SHALL искаться только в остатке ячейки группы после совпадения
|
||||
группы. Итоговая строка «ВСЕГО» SHALL распознаваться как итог листа наравне с «Итого» и «ИТОГ».
|
||||
|
||||
#### Scenario: Остаток и оборот по дебету
|
||||
- **WHEN** разбирается синтетическая ОСВ с группами «Остаток на начало периода | Оборот по дебету | Оборот по кредиту |
|
||||
Остаток на конец периода» и подзаголовками «Дебет | Кредит» под остатками
|
||||
- **THEN** найдены все шесть колонок сумм; обороты — из ячеек «Оборот по дебету» и «Оборот по кредиту»
|
||||
|
||||
#### Scenario: Сторона приклеена к названию
|
||||
- **WHEN** разбирается синтетическая ОСВ с заголовком «Сальдо НачальноеДТВВ | Сальдо НачальноеКТВВ | Оборот ДТ | Оборот
|
||||
КТ | Сальдо КонечноеДТВВ | Сальдо КонечноеКТВВ»
|
||||
- **THEN** найдены все шесть колонок сумм с верными сторонами
|
||||
|
||||
#### Scenario: SAP «Сальдо вх/исх»
|
||||
- **WHEN** разбирается синтетическая ОСВ с группами «Сальдо вх Дт | Сальдо вх Кт | Оборот Дт | Оборот Кт | Сальдо исх Дт
|
||||
| Сальдо исх Кт»
|
||||
- **THEN** найдены все шесть колонок сумм
|
||||
|
||||
#### Scenario: Итог «ВСЕГО»
|
||||
- **WHEN** разбирается синтетическая ОСВ, итоговая строка которой подписана «ВСЕГО»
|
||||
- **THEN** строка — `TOTAL`, предупреждения «строка «Итого» не найдена» нет
|
||||
Loading…
x
Reference in New Issue
Block a user