pylib_ai_arm_statements 0.1.5: разрез по смене формы в конвейере форм (#87)
- Пин pylib_ai_arm_statements==0.1.5 (0.1.4 — группа альтернатив 2400, AURORA-1864; 0.1.5 — split_by_form и редкий столбец, AURORA-1734/1735). - StatementPipeline режет кандидата split_by_form перед normalize_candidate: ББ и ОФР одним листом без заголовка ОФР — два документа (§11, спека statement-normalization «Шаг 2 — разрез кандидата по смене формы»). Тест на такой лист. - Golden трёх ОФР — fixtures #36 (уходит ложное 2400). - CLAUDE.md: версия библиотеки — по пину, конвейер с split_by_form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
be6513a04b
commit
387e92e014
@ -14,7 +14,7 @@ Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам (
|
|||||||
новый формат = новая фикстура + синоним, не новый `if`.
|
новый формат = новая фикстура + синоним, не новый `if`.
|
||||||
|
|
||||||
## Правила репозитория
|
## Правила репозитория
|
||||||
- `arm_statements` — библиотека `pylib_ai_arm_statements==0.1.1`; установка и обновление — `make install`
|
- `arm_statements` — библиотека `pylib_ai_arm_statements` (версия — пин в `api/requirements.txt`); установка и обновление — `make install`
|
||||||
(`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`); код возврата 20 при расхождении. Словари
|
(`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`); код возврата 20 при расхождении. Словари
|
||||||
форм — в пакете; `api/rules/statement_*.yaml` удалены планом 4 (#54).
|
форм — в пакете; `api/rules/statement_*.yaml` удалены планом 4 (#54).
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ property, покрытие словарей); без каталога `../fixtur
|
|||||||
**Модули адаптера** (`api/src/parser/statements/`): `forms.py` (блоки форм и вид листа по словарям пакета), `cells.py`
|
**Модули адаптера** (`api/src/parser/statements/`): `forms.py` (блоки форм и вид листа по словарям пакета), `cells.py`
|
||||||
(ячейка Excel -> `arm_statements.Cell`), `header_cells.py` (шапка по меткам словаря), `adapter.py` (`plan_sheets`,
|
(ячейка Excel -> `arm_statements.Cell`), `header_cells.py` (шапка по меткам словаря), `adapter.py` (`plan_sheets`,
|
||||||
`build_candidates`: листы -> кандидаты для пакета, продолжение присоединяется), `pipeline.py` (`StatementPipeline`:
|
`build_candidates`: листы -> кандидаты для пакета, продолжение присоединяется), `pipeline.py` (`StatementPipeline`:
|
||||||
кандидаты -> `normalize_candidate` -> `to_contract`), `number_format.py` (числовой формат ячейки, #42).
|
кандидаты -> `split_by_form` (разрез по смене формы, AURORA-1734) -> `normalize_candidate` -> `to_contract`), `number_format.py` (числовой формат ячейки, #42).
|
||||||
`book.py` (`BookParser`) склеивает ОСВ и формы; `get_parser(settings)` создаёт его с `default_rules()` пакета.
|
`book.py` (`BookParser`) склеивает ОСВ и формы; `get_parser(settings)` создаёт его с `default_rules()` пакета.
|
||||||
|
|
||||||
**Контракт 2.0:** `CONTRACT_VERSION = "2.0"`; у документов `status` и `warnings`; `checks` обязателен у BALANCE/INCOME со
|
**Контракт 2.0:** `CONTRACT_VERSION = "2.0"`; у документов `status` и `warnings`; `checks` обязателен у BALANCE/INCOME со
|
||||||
|
|||||||
@ -43,7 +43,7 @@ Pygments==2.20.0
|
|||||||
PyNaCl==1.5.0
|
PyNaCl==1.5.0
|
||||||
python-dotenv==1.2.2
|
python-dotenv==1.2.2
|
||||||
python-multipart==0.0.32
|
python-multipart==0.0.32
|
||||||
pylib_ai_arm_statements==0.1.3
|
pylib_ai_arm_statements==0.1.5
|
||||||
pyxlsb==1.0.10
|
pyxlsb==1.0.10
|
||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
rich==15.0.0
|
rich==15.0.0
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
"""Конвейер форм книги: кандидаты адаптера → шаги 2–8 пакета `arm_statements` → документы контракта 2.0.
|
"""Конвейер форм книги: кандидаты адаптера → шаги 2–8 пакета `arm_statements` → документы контракта 2.0.
|
||||||
|
|
||||||
Своего перевода в контракт здесь нет: документ формы — `to_contract` пакета, предупреждение файла —
|
Перед шагами 2–8 кандидат режется по смене формы (`split_by_form` пакета, AURORA-1734): ББ и ОФР одним листом без
|
||||||
`warning_to_contract(..., file_level=True)`. Изоляцию кандидатов друг от друга не делаем: исключение пакета — ошибка
|
своего заголовка — два документа. Своего перевода в контракт здесь нет: документ формы — `to_contract` пакета,
|
||||||
книги (`RECOGNITION_FAILED` в `BookParser`).
|
предупреждение файла — `warning_to_contract(..., file_level=True)`. Изоляцию кандидатов друг от друга не делаем:
|
||||||
|
исключение пакета — ошибка книги (`RECOGNITION_FAILED` в `BookParser`).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from arm_statements import Rules as StatementRules
|
from arm_statements import Rules as StatementRules
|
||||||
from arm_statements import StatementWarning, normalize_candidate, to_contract, warning_to_contract
|
from arm_statements import StatementWarning, normalize_candidate, split_by_form, to_contract, warning_to_contract
|
||||||
|
|
||||||
from src.contract import FileWarning, StatementDocument
|
from src.contract import FileWarning, StatementDocument
|
||||||
from src.parser.deadline import Deadline
|
from src.parser.deadline import Deadline
|
||||||
@ -37,16 +38,17 @@ class StatementPipeline:
|
|||||||
def parse(self, sheets: Sequence[tuple[Sheet, SheetKind]], deadline: Deadline, *, xlsb: bool) -> StatementsOutcome:
|
def parse(self, sheets: Sequence[tuple[Sheet, SheetKind]], deadline: Deadline, *, xlsb: bool) -> StatementsOutcome:
|
||||||
documents: list[StatementDocument] = []
|
documents: list[StatementDocument] = []
|
||||||
file_warnings: list[FileWarning] = []
|
file_warnings: list[FileWarning] = []
|
||||||
for candidate in build_candidates(sheets, self._rules, deadline, xlsb=xlsb):
|
for source in build_candidates(sheets, self._rules, deadline, xlsb=xlsb):
|
||||||
deadline.check(f"форма «{candidate.locator}»")
|
for candidate in split_by_form(source, self._rules):
|
||||||
result = normalize_candidate(candidate, self._rules)
|
deadline.check(f"форма «{candidate.locator}»")
|
||||||
if result.document is not None:
|
result = normalize_candidate(candidate, self._rules)
|
||||||
documents.append(StatementDocument.model_validate(to_contract(result.document)))
|
if result.document is not None:
|
||||||
else:
|
documents.append(StatementDocument.model_validate(to_contract(result.document)))
|
||||||
# Документа нет: предупреждения адаптера (формат xlsb, числовой формат, склейка) пакет не переносит —
|
else:
|
||||||
# они остаются предупреждениями файла со своим locator
|
# Документа нет: предупреждения адаптера (формат xlsb, числовой формат, склейка) пакет не
|
||||||
file_warnings.extend(_file_warning(w) for w in candidate.warnings)
|
# переносит — они остаются предупреждениями файла со своим locator
|
||||||
file_warnings.extend(_file_warning(w) for w in result.file_warnings)
|
file_warnings.extend(_file_warning(w) for w in candidate.warnings)
|
||||||
|
file_warnings.extend(_file_warning(w) for w in result.file_warnings)
|
||||||
return StatementsOutcome(tuple(documents), tuple(file_warnings))
|
return StatementsOutcome(tuple(documents), tuple(file_warnings))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -31,9 +31,9 @@ def statement_rules() -> StatementRules:
|
|||||||
return default_rules()
|
return default_rules()
|
||||||
|
|
||||||
|
|
||||||
def _balance(name: str = "Баланс") -> Sheet:
|
def _balance_rows() -> list[list[object]]:
|
||||||
"""Шапка из восьми строк, подпись столбцов в 9-й, первая строка с кодом — 10-я (код в столбце C)."""
|
"""Шапка из восьми строк, подпись столбцов в 9-й, первая строка с кодом — 10-я (код в столбце C)."""
|
||||||
rows = [
|
return [
|
||||||
at({1: "Бухгалтерский баланс"}),
|
at({1: "Бухгалтерский баланс"}),
|
||||||
at({1: "на 31 марта 2025 г."}),
|
at({1: "на 31 марта 2025 г."}),
|
||||||
at({1: "Организация", 2: "ООО Ромашка"}),
|
at({1: "Организация", 2: "ООО Ромашка"}),
|
||||||
@ -45,7 +45,10 @@ def _balance(name: str = "Баланс") -> Sheet:
|
|||||||
at({1: "Наименование показателя", 3: "Код", 4: "На 31 марта 2025 г."}),
|
at({1: "Наименование показателя", 3: "Код", 4: "На 31 марта 2025 г."}),
|
||||||
*[at({1: title, 3: code, 4: amount}) for title, code, amount in BALANCE_LINES],
|
*[at({1: title, 3: code, 4: amount}) for title, code, amount in BALANCE_LINES],
|
||||||
]
|
]
|
||||||
return sheet_from_rows(rows, name=name)
|
|
||||||
|
|
||||||
|
def _balance(name: str = "Баланс") -> Sheet:
|
||||||
|
return sheet_from_rows(_balance_rows(), name=name)
|
||||||
|
|
||||||
|
|
||||||
def test_document_converts_to_contract(statement_rules: StatementRules) -> None:
|
def test_document_converts_to_contract(statement_rules: StatementRules) -> None:
|
||||||
@ -83,3 +86,34 @@ def test_xlsb_flag_adds_document_warning(statement_rules: StatementRules) -> Non
|
|||||||
outcome = StatementPipeline(statement_rules).parse([(_balance(), SheetKind.STATEMENT)], NO_DEADLINE, xlsb=True)
|
outcome = StatementPipeline(statement_rules).parse([(_balance(), SheetKind.STATEMENT)], NO_DEADLINE, xlsb=True)
|
||||||
|
|
||||||
assert "XLSB_NO_FORMATS" in {w.code for w in outcome.documents[0].warnings}
|
assert "XLSB_NO_FORMATS" in {w.code for w in outcome.documents[0].warnings}
|
||||||
|
|
||||||
|
|
||||||
|
INCOME_LINES = (
|
||||||
|
("Выручка", 2110, 300),
|
||||||
|
("Себестоимость продаж", 2120, -200),
|
||||||
|
("Валовая прибыль (убыток)", 2100, 100),
|
||||||
|
("Прибыль (убыток) от продаж", 2200, 100),
|
||||||
|
("Чистая прибыль (убыток)", 2400, 100),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_and_income_on_one_sheet_split_into_two_documents(statement_rules: StatementRules) -> None:
|
||||||
|
"""ББ и ОФР одним листом без заголовка ОФР: шапка таблицы ОФР режет кандидата (`split_by_form`, AURORA-1734)."""
|
||||||
|
rows = [
|
||||||
|
*_balance_rows(),
|
||||||
|
at({1: "Наименование показателя", 3: "Код", 4: "За январь - март 2025 г."}),
|
||||||
|
*[at({1: title, 3: code, 4: amount}) for title, code, amount in INCOME_LINES],
|
||||||
|
]
|
||||||
|
|
||||||
|
outcome = StatementPipeline(statement_rules).parse(
|
||||||
|
[(sheet_from_rows(rows, name="Баланс"), SheetKind.STATEMENT)], NO_DEADLINE, xlsb=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [(d.document_type, d.locator) for d in outcome.documents] == [
|
||||||
|
("BALANCE", "sheet:Баланс"),
|
||||||
|
("INCOME", "sheet:Баланс;form:1"),
|
||||||
|
]
|
||||||
|
balance, income = outcome.documents
|
||||||
|
assert {line.line_code for line in balance.lines} == {str(code) for _, code, _ in BALANCE_LINES}
|
||||||
|
assert {line.line_code for line in income.lines} == {str(code) for _, code, _ in INCOME_LINES}
|
||||||
|
assert [str(c.period_date) for c in income.columns] == ["2025-03-31"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user