feat: типы шага 1 и выхода шагов 2–8, сериализация кандидата в json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
f5cb60a1cb
commit
03a7ae1a59
246
arm_statements/model.py
Normal file
246
arm_statements/model.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
"""Типы входа (кандидат формы) и выхода (нормализованный документ) пакета."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Source(StrEnum):
|
||||||
|
OCR = "ocr"
|
||||||
|
EXCEL = "excel"
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentType(StrEnum):
|
||||||
|
BALANCE = "BALANCE"
|
||||||
|
INCOME = "INCOME"
|
||||||
|
DISCLOSURE = "DISCLOSURE"
|
||||||
|
|
||||||
|
|
||||||
|
class FormVariant(StrEnum):
|
||||||
|
FULL = "full"
|
||||||
|
SIMPLIFIED = "simplified"
|
||||||
|
|
||||||
|
|
||||||
|
class Edition(StrEnum):
|
||||||
|
RAS_66N = "66n"
|
||||||
|
FSBU4 = "fsbu4"
|
||||||
|
|
||||||
|
|
||||||
|
class PeriodKind(StrEnum):
|
||||||
|
ABSOLUTE = "absolute"
|
||||||
|
CURRENT = "relative:current"
|
||||||
|
PREV_YEAR_END = "relative:prev_year_end"
|
||||||
|
PREV_PREV_YEAR_END = "relative:prev_prev_year_end"
|
||||||
|
PREV_PERIOD = "relative:prev_period"
|
||||||
|
NONE = "none"
|
||||||
|
|
||||||
|
|
||||||
|
class LineStatus(StrEnum):
|
||||||
|
OK = "ok"
|
||||||
|
UNREADABLE = "unreadable"
|
||||||
|
MISSING = "missing"
|
||||||
|
DETAIL = "detail"
|
||||||
|
|
||||||
|
|
||||||
|
class Severity(StrEnum):
|
||||||
|
ERROR = "error"
|
||||||
|
INFO = "info"
|
||||||
|
|
||||||
|
|
||||||
|
class DocStatus(StrEnum):
|
||||||
|
DONE = "done"
|
||||||
|
NEEDS_CHECK = "needs_check"
|
||||||
|
|
||||||
|
|
||||||
|
class EnvelopeStatus(StrEnum):
|
||||||
|
DONE = "done"
|
||||||
|
NEEDS_CHECK = "needs_check"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
class SignForm(StrEnum):
|
||||||
|
PARENTHESES = "parentheses"
|
||||||
|
MINUS = "minus"
|
||||||
|
UNSIGNED = "unsigned"
|
||||||
|
|
||||||
|
|
||||||
|
class RowRole(StrEnum):
|
||||||
|
TITLE = "title"
|
||||||
|
HEADER = "header"
|
||||||
|
NUMBERING = "numbering"
|
||||||
|
DATA = "data"
|
||||||
|
|
||||||
|
|
||||||
|
CellValue = Decimal | date | bool | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Cell:
|
||||||
|
text: str
|
||||||
|
value: CellValue
|
||||||
|
confidence: float
|
||||||
|
ref: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Evidence:
|
||||||
|
okud: Cell | None = None
|
||||||
|
title: Cell | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class HeaderCells:
|
||||||
|
organization_name: Cell | None = None
|
||||||
|
inn: Cell | None = None
|
||||||
|
report_date: Cell | None = None
|
||||||
|
units: Cell | None = None
|
||||||
|
okud: Cell | None = None
|
||||||
|
okpo: Cell | None = None
|
||||||
|
okopf: Cell | None = None
|
||||||
|
okfs: Cell | None = None
|
||||||
|
okei: Cell | None = None
|
||||||
|
address: Cell | None = None
|
||||||
|
|
||||||
|
|
||||||
|
HEADER_FIELDS: tuple[str, ...] = (
|
||||||
|
"organization_name",
|
||||||
|
"inn",
|
||||||
|
"report_date",
|
||||||
|
"units",
|
||||||
|
"okud",
|
||||||
|
"okpo",
|
||||||
|
"okopf",
|
||||||
|
"okfs",
|
||||||
|
"okei",
|
||||||
|
"address",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementWarning:
|
||||||
|
code: str
|
||||||
|
severity: Severity
|
||||||
|
message: str
|
||||||
|
line_refs: tuple[str, ...] = ()
|
||||||
|
column_index: int | None = None
|
||||||
|
field: str | None = None
|
||||||
|
locator: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class FormCandidate:
|
||||||
|
source: Source
|
||||||
|
locator: str
|
||||||
|
type_hint: DocumentType | None
|
||||||
|
evidence: Evidence
|
||||||
|
header: HeaderCells
|
||||||
|
rows: tuple[tuple[Cell | None, ...], ...]
|
||||||
|
segment_breaks: tuple[int, ...] = ()
|
||||||
|
warnings: tuple[StatementWarning, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class HeaderField:
|
||||||
|
text: str | None
|
||||||
|
confidence: float | None
|
||||||
|
value: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DateField:
|
||||||
|
text: str | None
|
||||||
|
confidence: float | None
|
||||||
|
value: date | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementHeader:
|
||||||
|
organization_name: HeaderField
|
||||||
|
inn: HeaderField
|
||||||
|
report_date: DateField
|
||||||
|
units: HeaderField
|
||||||
|
okud: HeaderField
|
||||||
|
okpo: HeaderField
|
||||||
|
okopf: HeaderField
|
||||||
|
okfs: HeaderField
|
||||||
|
okei: HeaderField
|
||||||
|
address: HeaderField
|
||||||
|
form_name: str | None = None
|
||||||
|
needs_review_fields: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementColumn:
|
||||||
|
column_index: int
|
||||||
|
label: str
|
||||||
|
label_confidence: float | None
|
||||||
|
period_date: date | None
|
||||||
|
period_kind: PeriodKind
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementValue:
|
||||||
|
column_index: int
|
||||||
|
amount: Decimal | None
|
||||||
|
string_value: str | None
|
||||||
|
text: str | None
|
||||||
|
confidence: float
|
||||||
|
is_recognized: bool
|
||||||
|
needs_review: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementLine:
|
||||||
|
row_index: int
|
||||||
|
ref: str
|
||||||
|
line_status: LineStatus
|
||||||
|
line_code: str | None
|
||||||
|
line_code_text: str | None
|
||||||
|
line_code_confidence: float | None
|
||||||
|
line_name: str | None
|
||||||
|
line_name_confidence: float | None
|
||||||
|
parent_ref: str | None
|
||||||
|
needs_review: bool
|
||||||
|
values: tuple[StatementValue, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class FailedRelation:
|
||||||
|
column_index: int | None
|
||||||
|
column_key: str
|
||||||
|
relation: str
|
||||||
|
codes: tuple[str, ...]
|
||||||
|
expected: Decimal | None
|
||||||
|
actual: Decimal | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementChecks:
|
||||||
|
relations_failed: tuple[FailedRelation, ...]
|
||||||
|
soft_relations_failed: tuple[FailedRelation, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StatementDocument:
|
||||||
|
document_type: DocumentType
|
||||||
|
form_variant: FormVariant | None
|
||||||
|
locator: str
|
||||||
|
header: StatementHeader
|
||||||
|
columns: tuple[StatementColumn, ...]
|
||||||
|
lines: tuple[StatementLine, ...]
|
||||||
|
checks: StatementChecks | None
|
||||||
|
status: DocStatus
|
||||||
|
warnings: tuple[StatementWarning, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CandidateResult:
|
||||||
|
document: StatementDocument | None
|
||||||
|
file_warnings: tuple[StatementWarning, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class NormalizationResult:
|
||||||
|
documents: tuple[StatementDocument, ...]
|
||||||
|
file_warnings: tuple[StatementWarning, ...]
|
||||||
108
arm_statements/serialize.py
Normal file
108
arm_statements/serialize.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""Сериализация в json-совместимые значения и обратно (для golden-тестов и обмена)."""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import fields, is_dataclass
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from arm_statements.model import (
|
||||||
|
Cell,
|
||||||
|
CellValue,
|
||||||
|
DocumentType,
|
||||||
|
Evidence,
|
||||||
|
FormCandidate,
|
||||||
|
HeaderCells,
|
||||||
|
Severity,
|
||||||
|
Source,
|
||||||
|
StatementWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _value_to_jsonable(value: CellValue) -> Any:
|
||||||
|
"""Тег типа значения ячейки; bool проверяется раньше прочего (не подкласс Decimal/date, но явно)."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return {"bool": value}
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
return {"decimal": str(value)}
|
||||||
|
return {"date": value.isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
def _scalar_to_jsonable(obj: object) -> Any:
|
||||||
|
if isinstance(obj, Enum):
|
||||||
|
return obj.value
|
||||||
|
if isinstance(obj, Decimal):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, date):
|
||||||
|
return obj.isoformat()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def to_jsonable(obj: object) -> Any:
|
||||||
|
"""Dataclass пакета → json-совместимое значение."""
|
||||||
|
if isinstance(obj, Cell):
|
||||||
|
return {
|
||||||
|
"text": obj.text,
|
||||||
|
"value": _value_to_jsonable(obj.value),
|
||||||
|
"confidence": obj.confidence,
|
||||||
|
"ref": obj.ref,
|
||||||
|
}
|
||||||
|
if is_dataclass(obj) and not isinstance(obj, type):
|
||||||
|
return {f.name: to_jsonable(getattr(obj, f.name)) for f in fields(obj)}
|
||||||
|
if isinstance(obj, tuple | list):
|
||||||
|
return [to_jsonable(item) for item in obj]
|
||||||
|
return _scalar_to_jsonable(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _value_from_dict(raw: Mapping[str, Any] | None) -> CellValue:
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
if "decimal" in raw:
|
||||||
|
return Decimal(raw["decimal"])
|
||||||
|
if "date" in raw:
|
||||||
|
return date.fromisoformat(raw["date"])
|
||||||
|
if "bool" in raw:
|
||||||
|
return bool(raw["bool"])
|
||||||
|
raise ValueError(f"неизвестный тег значения ячейки: {sorted(raw)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _cell_from_dict(raw: Mapping[str, Any] | None) -> Cell | None:
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
return Cell(
|
||||||
|
text=raw["text"],
|
||||||
|
value=_value_from_dict(raw["value"]),
|
||||||
|
confidence=float(raw["confidence"]),
|
||||||
|
ref=raw["ref"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _warning_from_dict(raw: Mapping[str, Any]) -> StatementWarning:
|
||||||
|
return StatementWarning(
|
||||||
|
code=raw["code"],
|
||||||
|
severity=Severity(raw["severity"]),
|
||||||
|
message=raw["message"],
|
||||||
|
line_refs=tuple(raw.get("line_refs", ())),
|
||||||
|
column_index=raw.get("column_index"),
|
||||||
|
field=raw.get("field"),
|
||||||
|
locator=raw.get("locator"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def candidate_from_dict(data: Mapping[str, Any]) -> FormCandidate:
|
||||||
|
"""Обратная к `to_jsonable` для кандидата."""
|
||||||
|
type_hint = data["type_hint"]
|
||||||
|
evidence = data["evidence"]
|
||||||
|
return FormCandidate(
|
||||||
|
source=Source(data["source"]),
|
||||||
|
locator=data["locator"],
|
||||||
|
type_hint=None if type_hint is None else DocumentType(type_hint),
|
||||||
|
evidence=Evidence(okud=_cell_from_dict(evidence["okud"]), title=_cell_from_dict(evidence["title"])),
|
||||||
|
header=HeaderCells(**{name: _cell_from_dict(raw) for name, raw in data["header"].items()}),
|
||||||
|
rows=tuple(tuple(_cell_from_dict(raw) for raw in row) for row in data["rows"]),
|
||||||
|
segment_breaks=tuple(data.get("segment_breaks", ())),
|
||||||
|
warnings=tuple(_warning_from_dict(raw) for raw in data.get("warnings", ())),
|
||||||
|
)
|
||||||
144
arm_statements/tests/builders.py
Normal file
144
arm_statements/tests/builders.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
"""Сборщики тестовых кандидатов (только для тестов пакета)."""
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from arm_statements.model import (
|
||||||
|
HEADER_FIELDS,
|
||||||
|
Cell,
|
||||||
|
DocumentType,
|
||||||
|
Evidence,
|
||||||
|
FormCandidate,
|
||||||
|
HeaderCells,
|
||||||
|
Source,
|
||||||
|
StatementWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
OcrInput = str | tuple[str, float] | None
|
||||||
|
ExcelInput = str | Decimal | None
|
||||||
|
PlaceInput = str | Decimal | tuple[str, float]
|
||||||
|
|
||||||
|
_FULL_CONFIDENCE = 100.0
|
||||||
|
_LATIN_LETTERS = 26
|
||||||
|
|
||||||
|
|
||||||
|
def _text_conf(item: str | tuple[str, float]) -> tuple[str, float]:
|
||||||
|
if isinstance(item, tuple):
|
||||||
|
return item
|
||||||
|
return item, _FULL_CONFIDENCE
|
||||||
|
|
||||||
|
|
||||||
|
def hdr(**fields: str | tuple[str, float]) -> HeaderCells:
|
||||||
|
"""Шапка из текстов; ref — `hdr:<поле>`."""
|
||||||
|
unknown = set(fields) - set(HEADER_FIELDS)
|
||||||
|
if unknown:
|
||||||
|
raise TypeError(f"неизвестные поля шапки: {sorted(unknown)}")
|
||||||
|
cells: dict[str, Cell] = {}
|
||||||
|
for name, item in fields.items():
|
||||||
|
text, conf = _text_conf(item)
|
||||||
|
cells[name] = Cell(text, None, conf, f"hdr:{name}")
|
||||||
|
return HeaderCells(**cells)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence(okud: str | None, title: str | None) -> Evidence:
|
||||||
|
return Evidence(
|
||||||
|
okud=None if okud is None else Cell(okud, None, _FULL_CONFIDENCE, "evidence:okud"),
|
||||||
|
title=None if title is None else Cell(title, None, _FULL_CONFIDENCE, "evidence:title"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ocr( # noqa: PLR0913 - сигнатура задана планом (Task 2)
|
||||||
|
rows: Sequence[Sequence[OcrInput]],
|
||||||
|
*,
|
||||||
|
type_hint: DocumentType | None = None,
|
||||||
|
header: HeaderCells | None = None,
|
||||||
|
title: str | None = None,
|
||||||
|
okud: str | None = None,
|
||||||
|
locator: str = "file:t.json;table:0",
|
||||||
|
) -> FormCandidate:
|
||||||
|
"""Кандидат OCR: строка r → ячейки с ref `row:<r>`."""
|
||||||
|
grid: list[tuple[Cell | None, ...]] = []
|
||||||
|
for r, row in enumerate(rows):
|
||||||
|
cells: list[Cell | None] = []
|
||||||
|
for item in row:
|
||||||
|
if item is None:
|
||||||
|
cells.append(None)
|
||||||
|
else:
|
||||||
|
text, conf = _text_conf(item)
|
||||||
|
cells.append(Cell(text, None, conf, f"row:{r}"))
|
||||||
|
grid.append(tuple(cells))
|
||||||
|
return FormCandidate(
|
||||||
|
source=Source.OCR,
|
||||||
|
locator=locator,
|
||||||
|
type_hint=type_hint,
|
||||||
|
evidence=_evidence(okud, title),
|
||||||
|
header=header if header is not None else HeaderCells(),
|
||||||
|
rows=tuple(grid),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _column_letters(index: int) -> str:
|
||||||
|
"""0 → A, 25 → Z, 26 → AA."""
|
||||||
|
letters = ""
|
||||||
|
n = index + 1
|
||||||
|
while n > 0:
|
||||||
|
n, rem = divmod(n - 1, _LATIN_LETTERS)
|
||||||
|
letters = chr(ord("A") + rem) + letters
|
||||||
|
return letters
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet_prefix(name: str) -> str:
|
||||||
|
"""Имя листа в ссылке по правилу Excel: кавычки, если не только буквы/цифры/_ или начинается с цифры."""
|
||||||
|
plain = all(ch.isalpha() or ch.isdigit() or ch == "_" for ch in name) and not name[:1].isdigit()
|
||||||
|
if plain:
|
||||||
|
return name
|
||||||
|
return "'" + name.replace("'", "''") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
def _thousands(value: Decimal) -> str:
|
||||||
|
return f"{value:,}".replace(",", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def excel( # noqa: PLR0913 - сигнатура задана планом (Task 2)
|
||||||
|
sheets: Sequence[tuple[str, Sequence[Sequence[ExcelInput]]]],
|
||||||
|
*,
|
||||||
|
type_hint: DocumentType | None = None,
|
||||||
|
header: HeaderCells | None = None,
|
||||||
|
title: str | None = None,
|
||||||
|
okud: str | None = None,
|
||||||
|
warnings: Sequence[StatementWarning] = (),
|
||||||
|
) -> FormCandidate:
|
||||||
|
"""Кандидат Excel: листы подряд в одной сетке, ref `<лист>!<A1>`."""
|
||||||
|
grid: list[tuple[Cell | None, ...]] = []
|
||||||
|
breaks: list[int] = []
|
||||||
|
for sheet_no, (name, sheet_rows) in enumerate(sheets):
|
||||||
|
if sheet_no > 0:
|
||||||
|
breaks.append(len(grid))
|
||||||
|
prefix = _sheet_prefix(name)
|
||||||
|
for r, row in enumerate(sheet_rows):
|
||||||
|
cells: list[Cell | None] = []
|
||||||
|
for c, item in enumerate(row):
|
||||||
|
ref = f"{prefix}!{_column_letters(c)}{r + 1}"
|
||||||
|
if item is None:
|
||||||
|
cells.append(None)
|
||||||
|
elif isinstance(item, Decimal):
|
||||||
|
cells.append(Cell(_thousands(item), item, _FULL_CONFIDENCE, ref))
|
||||||
|
else:
|
||||||
|
cells.append(Cell(item, None, _FULL_CONFIDENCE, ref))
|
||||||
|
grid.append(tuple(cells))
|
||||||
|
return FormCandidate(
|
||||||
|
source=Source.EXCEL,
|
||||||
|
locator="sheet:" + ",".join(name for name, _ in sheets),
|
||||||
|
type_hint=type_hint,
|
||||||
|
evidence=_evidence(okud, title),
|
||||||
|
header=header if header is not None else HeaderCells(),
|
||||||
|
rows=tuple(grid),
|
||||||
|
segment_breaks=tuple(breaks),
|
||||||
|
warnings=tuple(warnings),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def place(cells: Mapping[int, PlaceInput], width: int | None = None) -> list[PlaceInput | None]:
|
||||||
|
"""Строка с ячейками в заданных столбцах, остальные None."""
|
||||||
|
size = width if width is not None else (max(cells) + 1 if cells else 0)
|
||||||
|
return [cells.get(i) for i in range(size)]
|
||||||
37
arm_statements/tests/test_model.py
Normal file
37
arm_statements/tests/test_model.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import json
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal as D
|
||||||
|
|
||||||
|
from arm_statements.model import Cell, DocumentType, Evidence, FormCandidate, HeaderCells, Source
|
||||||
|
from arm_statements.serialize import candidate_from_dict, to_jsonable
|
||||||
|
from arm_statements.tests.builders import excel, ocr
|
||||||
|
|
||||||
|
|
||||||
|
def test_candidate_roundtrip_through_json() -> None:
|
||||||
|
cand = FormCandidate(
|
||||||
|
source=Source.EXCEL,
|
||||||
|
locator="sheet:Баланс",
|
||||||
|
type_hint=DocumentType.BALANCE,
|
||||||
|
evidence=Evidence(okud=Cell("0710001", None, 100.0, "Баланс!F3"), title=None),
|
||||||
|
header=HeaderCells(report_date=Cell("30.09.2025", date(2025, 9, 30), 100.0, "Баланс!C5")),
|
||||||
|
rows=((Cell("106 311", D("106311"), 100.0, "Баланс!D27"), None, Cell("да", True, 100.0, "Баланс!F27")),),
|
||||||
|
)
|
||||||
|
data = json.loads(json.dumps(to_jsonable(cand), ensure_ascii=False))
|
||||||
|
assert data["rows"][0][0]["value"] == {"decimal": "106311"}
|
||||||
|
assert data["header"]["report_date"]["value"] == {"date": "2025-09-30"}
|
||||||
|
assert candidate_from_dict(data) == cand
|
||||||
|
|
||||||
|
|
||||||
|
def test_excel_builder_refs_follow_excel_quoting() -> None:
|
||||||
|
cand = excel([("Баланс", [["", "", "", "1110"]]), ("Бухгалтерский баланс. Лист 1", [["", "", "1150"]])])
|
||||||
|
assert cand.rows[0][3] is not None and cand.rows[0][3].ref == "Баланс!D1"
|
||||||
|
assert cand.rows[1][2] is not None and cand.rows[1][2].ref == "'Бухгалтерский баланс. Лист 1'!C1"
|
||||||
|
assert cand.segment_breaks == (1,)
|
||||||
|
assert cand.locator == "sheet:Баланс,Бухгалтерский баланс. Лист 1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ocr_builder_cells() -> None:
|
||||||
|
cand = ocr([["Выручка", ("[190 :", 25.0), None]])
|
||||||
|
cell = cand.rows[0][1]
|
||||||
|
assert cell == Cell("[190 :", None, 25.0, "row:0")
|
||||||
|
assert cand.rows[0][2] is None and cand.source is Source.OCR
|
||||||
Reference in New Issue
Block a user