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`.
|
||||
|
||||
## Правила репозитория
|
||||
- `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 при расхождении. Словари
|
||||
форм — в пакете; `api/rules/statement_*.yaml` удалены планом 4 (#54).
|
||||
|
||||
@ -117,7 +117,7 @@ property, покрытие словарей); без каталога `../fixtur
|
||||
**Модули адаптера** (`api/src/parser/statements/`): `forms.py` (блоки форм и вид листа по словарям пакета), `cells.py`
|
||||
(ячейка Excel -> `arm_statements.Cell`), `header_cells.py` (шапка по меткам словаря), `adapter.py` (`plan_sheets`,
|
||||
`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()` пакета.
|
||||
|
||||
**Контракт 2.0:** `CONTRACT_VERSION = "2.0"`; у документов `status` и `warnings`; `checks` обязателен у BALANCE/INCOME со
|
||||
|
||||
@ -43,7 +43,7 @@ Pygments==2.20.0
|
||||
PyNaCl==1.5.0
|
||||
python-dotenv==1.2.2
|
||||
python-multipart==0.0.32
|
||||
pylib_ai_arm_statements==0.1.3
|
||||
pylib_ai_arm_statements==0.1.5
|
||||
pyxlsb==1.0.10
|
||||
PyYAML==6.0.3
|
||||
rich==15.0.0
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
"""Конвейер форм книги: кандидаты адаптера → шаги 2–8 пакета `arm_statements` → документы контракта 2.0.
|
||||
|
||||
Своего перевода в контракт здесь нет: документ формы — `to_contract` пакета, предупреждение файла —
|
||||
`warning_to_contract(..., file_level=True)`. Изоляцию кандидатов друг от друга не делаем: исключение пакета — ошибка
|
||||
книги (`RECOGNITION_FAILED` в `BookParser`).
|
||||
Перед шагами 2–8 кандидат режется по смене формы (`split_by_form` пакета, AURORA-1734): ББ и ОФР одним листом без
|
||||
своего заголовка — два документа. Своего перевода в контракт здесь нет: документ формы — `to_contract` пакета,
|
||||
предупреждение файла — `warning_to_contract(..., file_level=True)`. Изоляцию кандидатов друг от друга не делаем:
|
||||
исключение пакета — ошибка книги (`RECOGNITION_FAILED` в `BookParser`).
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
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.parser.deadline import Deadline
|
||||
@ -37,16 +38,17 @@ class StatementPipeline:
|
||||
def parse(self, sheets: Sequence[tuple[Sheet, SheetKind]], deadline: Deadline, *, xlsb: bool) -> StatementsOutcome:
|
||||
documents: list[StatementDocument] = []
|
||||
file_warnings: list[FileWarning] = []
|
||||
for candidate in build_candidates(sheets, self._rules, deadline, xlsb=xlsb):
|
||||
deadline.check(f"форма «{candidate.locator}»")
|
||||
result = normalize_candidate(candidate, self._rules)
|
||||
if result.document is not None:
|
||||
documents.append(StatementDocument.model_validate(to_contract(result.document)))
|
||||
else:
|
||||
# Документа нет: предупреждения адаптера (формат xlsb, числовой формат, склейка) пакет не переносит —
|
||||
# они остаются предупреждениями файла со своим locator
|
||||
file_warnings.extend(_file_warning(w) for w in candidate.warnings)
|
||||
file_warnings.extend(_file_warning(w) for w in result.file_warnings)
|
||||
for source in build_candidates(sheets, self._rules, deadline, xlsb=xlsb):
|
||||
for candidate in split_by_form(source, self._rules):
|
||||
deadline.check(f"форма «{candidate.locator}»")
|
||||
result = normalize_candidate(candidate, self._rules)
|
||||
if result.document is not None:
|
||||
documents.append(StatementDocument.model_validate(to_contract(result.document)))
|
||||
else:
|
||||
# Документа нет: предупреждения адаптера (формат xlsb, числовой формат, склейка) пакет не
|
||||
# переносит — они остаются предупреждениями файла со своим locator
|
||||
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))
|
||||
|
||||
|
||||
|
||||
@ -31,9 +31,9 @@ def statement_rules() -> StatementRules:
|
||||
return default_rules()
|
||||
|
||||
|
||||
def _balance(name: str = "Баланс") -> Sheet:
|
||||
def _balance_rows() -> list[list[object]]:
|
||||
"""Шапка из восьми строк, подпись столбцов в 9-й, первая строка с кодом — 10-я (код в столбце C)."""
|
||||
rows = [
|
||||
return [
|
||||
at({1: "Бухгалтерский баланс"}),
|
||||
at({1: "на 31 марта 2025 г."}),
|
||||
at({1: "Организация", 2: "ООО Ромашка"}),
|
||||
@ -45,7 +45,10 @@ def _balance(name: str = "Баланс") -> Sheet:
|
||||
at({1: "Наименование показателя", 3: "Код", 4: "На 31 марта 2025 г."}),
|
||||
*[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:
|
||||
@ -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)
|
||||
|
||||
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