feat: шаг 6 и конвейер кандидата — тип по кодам, вариант формы, пояснения, предупреждения файла
normalize_candidate / normalize; __init__ экспортирует уже реализованный публичный API. DISCLOSURE: шапка не проверяется, предупреждений шапки нет, статус done (P1-6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
a5d10a6b0e
commit
29198ddc44
@ -1,5 +1,119 @@
|
||||
"""Общий код нормализации форм ББ/ОФР."""
|
||||
"""Общий код нормализации форм ББ/ОФР.
|
||||
|
||||
Публичный API — только имена `__all__` (раздел «Публичный API пакета» плана 1). Имена задач 18–19
|
||||
(`recompute`, `Stored*`, `to_contract`, `warning_to_contract`, `status_input_from_contract`) добавляются ими.
|
||||
"""
|
||||
|
||||
from arm_statements.amounts import parse_amount
|
||||
from arm_statements.codes import parse_code, sheet_joins
|
||||
from arm_statements.columns import amount_columns
|
||||
from arm_statements.dates import parse_date, resolve_relative
|
||||
from arm_statements.header import continuation_type, is_continuation, parse_header, type_hint
|
||||
from arm_statements.issues import WarningCode, make_warning
|
||||
from arm_statements.layout import code_column
|
||||
from arm_statements.loader import Rules, default_rules, load_rules
|
||||
from arm_statements.model import (
|
||||
HEADER_FIELDS,
|
||||
CandidateResult,
|
||||
Cell,
|
||||
CellValue,
|
||||
DateField,
|
||||
DocStatus,
|
||||
DocumentType,
|
||||
Edition,
|
||||
EnvelopeStatus,
|
||||
Evidence,
|
||||
FailedRelation,
|
||||
FormCandidate,
|
||||
FormVariant,
|
||||
HeaderCells,
|
||||
HeaderField,
|
||||
LineStatus,
|
||||
NormalizationResult,
|
||||
PeriodKind,
|
||||
Severity,
|
||||
SignForm,
|
||||
Source,
|
||||
StatementChecks,
|
||||
StatementColumn,
|
||||
StatementDocument,
|
||||
StatementHeader,
|
||||
StatementLine,
|
||||
StatementValue,
|
||||
StatementWarning,
|
||||
)
|
||||
from arm_statements.pipeline import normalize, normalize_candidate
|
||||
from arm_statements.serialize import candidate_from_dict, to_jsonable
|
||||
from arm_statements.status import (
|
||||
StatusInput,
|
||||
StatusLine,
|
||||
StatusValue,
|
||||
document_status,
|
||||
envelope_status,
|
||||
status_input,
|
||||
)
|
||||
from arm_statements.structure import choose_edition
|
||||
from arm_statements.text import fix_lookalikes, normalize_caption, normalize_label
|
||||
from arm_statements.version import statements_version
|
||||
|
||||
__all__ = ["statements_version"]
|
||||
__all__ = [
|
||||
"HEADER_FIELDS",
|
||||
"CandidateResult",
|
||||
"Cell",
|
||||
"CellValue",
|
||||
"DateField",
|
||||
"DocStatus",
|
||||
"DocumentType",
|
||||
"Edition",
|
||||
"EnvelopeStatus",
|
||||
"Evidence",
|
||||
"FailedRelation",
|
||||
"FormCandidate",
|
||||
"FormVariant",
|
||||
"HeaderCells",
|
||||
"HeaderField",
|
||||
"LineStatus",
|
||||
"NormalizationResult",
|
||||
"PeriodKind",
|
||||
"Rules",
|
||||
"Severity",
|
||||
"SignForm",
|
||||
"Source",
|
||||
"StatementChecks",
|
||||
"StatementColumn",
|
||||
"StatementDocument",
|
||||
"StatementHeader",
|
||||
"StatementLine",
|
||||
"StatementValue",
|
||||
"StatementWarning",
|
||||
"StatusInput",
|
||||
"StatusLine",
|
||||
"StatusValue",
|
||||
"WarningCode",
|
||||
"amount_columns",
|
||||
"candidate_from_dict",
|
||||
"choose_edition",
|
||||
"code_column",
|
||||
"continuation_type",
|
||||
"default_rules",
|
||||
"document_status",
|
||||
"envelope_status",
|
||||
"fix_lookalikes",
|
||||
"is_continuation",
|
||||
"load_rules",
|
||||
"make_warning",
|
||||
"normalize",
|
||||
"normalize_candidate",
|
||||
"normalize_caption",
|
||||
"normalize_label",
|
||||
"parse_amount",
|
||||
"parse_code",
|
||||
"parse_date",
|
||||
"parse_header",
|
||||
"resolve_relative",
|
||||
"sheet_joins",
|
||||
"statements_version",
|
||||
"status_input",
|
||||
"to_jsonable",
|
||||
"type_hint",
|
||||
]
|
||||
|
||||
215
arm_statements/pipeline.py
Normal file
215
arm_statements/pipeline.py
Normal file
@ -0,0 +1,215 @@
|
||||
"""Шаг 6 и конвейер кандидата: шаги 2–8 одного кандидата и нормализация набора кандидатов."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import replace
|
||||
|
||||
from arm_statements.checks import checks_from
|
||||
from arm_statements.codes import document_type_of, majority_section
|
||||
from arm_statements.columns import DocumentDates
|
||||
from arm_statements.evaluation import EvalLine, EvalValue
|
||||
from arm_statements.header import parse_header
|
||||
from arm_statements.issues import WarningCode, make_warning
|
||||
from arm_statements.lines import build_lines, units_warnings
|
||||
from arm_statements.loader import Rules, default_rules
|
||||
from arm_statements.model import (
|
||||
CandidateResult,
|
||||
DocStatus,
|
||||
DocumentType,
|
||||
FormCandidate,
|
||||
FormVariant,
|
||||
LineStatus,
|
||||
NormalizationResult,
|
||||
Source,
|
||||
StatementDocument,
|
||||
StatementHeader,
|
||||
StatementLine,
|
||||
StatementWarning,
|
||||
)
|
||||
from arm_statements.review import evaluate, review_header
|
||||
from arm_statements.rows import RawLine, collect_raw_lines
|
||||
from arm_statements.segments import Layout, find_layout
|
||||
from arm_statements.status import document_status, status_input
|
||||
from arm_statements.text import has_digit, is_disguised_digit
|
||||
|
||||
_SHEET_PREFIX = "sheet:"
|
||||
|
||||
|
||||
def _eval_lines(lines: Sequence[StatementLine]) -> list[EvalLine]:
|
||||
"""Строки сервиса в общий вид шагов 6–8: ключ — `ref`, колонка — `str(column_index)`."""
|
||||
return [
|
||||
EvalLine(
|
||||
key=line.ref,
|
||||
order=line.row_index,
|
||||
line_status=line.line_status,
|
||||
line_code=line.line_code,
|
||||
code_confidence=line.line_code_confidence,
|
||||
code_fixed=False,
|
||||
values=tuple(
|
||||
EvalValue(str(v.column_index), v.amount, v.text, v.confidence, v.is_recognized, fixed=False)
|
||||
for v in line.values
|
||||
),
|
||||
)
|
||||
for line in lines
|
||||
]
|
||||
|
||||
|
||||
def _form_name(candidate: FormCandidate) -> str | None:
|
||||
title = candidate.evidence.title
|
||||
if title is None or not title.text.strip():
|
||||
return None
|
||||
return title.text
|
||||
|
||||
|
||||
def _has_content(candidate: FormCandidate, rules: Rules) -> bool:
|
||||
"""Содержимое таблицы: ячейка с цифрой или «цифрой под видом буквы»."""
|
||||
return any(
|
||||
cell is not None and (has_digit(cell.text) or is_disguised_digit(cell.text, rules.lookalikes))
|
||||
for row in candidate.rows
|
||||
for cell in row
|
||||
)
|
||||
|
||||
|
||||
def _lost_table(candidate: FormCandidate, rules: Rules) -> StatementWarning:
|
||||
"""Документа нет (нет кодов и `type_hint`): предупреждение файла по источнику и содержимому."""
|
||||
if candidate.source is Source.EXCEL:
|
||||
sheet = candidate.locator.removeprefix(_SHEET_PREFIX).split(",")[0]
|
||||
message = f"лист «{sheet}» не содержит формы ББ/ОФР"
|
||||
return make_warning(WarningCode.SHEET_OTHER, message, rules, locator=f"{_SHEET_PREFIX}{sheet}")
|
||||
if _has_content(candidate, rules):
|
||||
message = "таблица с данными не распознана как форма: нет столбца кодов и типа формы"
|
||||
return make_warning(WarningCode.TABLE_LOST, message, rules, locator=candidate.locator)
|
||||
return make_warning(WarningCode.TABLE_SKIPPED, "таблица без данных пропущена", rules, locator=candidate.locator)
|
||||
|
||||
|
||||
def _without_lines( # noqa: PLR0913 -- сборка документа без строк: поля заданы схемой
|
||||
candidate: FormCandidate,
|
||||
document_type: DocumentType,
|
||||
header: StatementHeader,
|
||||
warnings: Sequence[StatementWarning],
|
||||
*,
|
||||
dates: DocumentDates | None,
|
||||
rules: Rules,
|
||||
) -> StatementDocument:
|
||||
"""Документ без строк: `DISCLOSURE` (`dates=None` — шапку не проверяем, P1-6) или без столбца кодов (P1-19)."""
|
||||
collected = list(warnings)
|
||||
if dates is not None:
|
||||
header, header_warnings = review_header(header, dates, rules)
|
||||
collected.extend(header_warnings)
|
||||
document = StatementDocument(
|
||||
document_type=document_type,
|
||||
form_variant=None if document_type is DocumentType.DISCLOSURE else FormVariant.FULL,
|
||||
locator=candidate.locator,
|
||||
header=header,
|
||||
columns=(),
|
||||
lines=(),
|
||||
checks=None,
|
||||
status=DocStatus.DONE,
|
||||
warnings=tuple(collected),
|
||||
)
|
||||
return replace(document, status=document_status(status_input(document)))
|
||||
|
||||
|
||||
def _lines_document( # noqa: PLR0913 -- сборка документа со строками: поля заданы схемой
|
||||
candidate: FormCandidate,
|
||||
document_type: DocumentType,
|
||||
header: StatementHeader,
|
||||
layout: Layout,
|
||||
*,
|
||||
raw: Sequence[RawLine],
|
||||
row_warnings: Sequence[StatementWarning],
|
||||
conflict: Sequence[StatementWarning],
|
||||
rules: Rules,
|
||||
) -> StatementDocument:
|
||||
lines, range_warnings = build_lines(raw, candidate.source, rules)
|
||||
units = units_warnings(lines, header, candidate.warnings, rules)
|
||||
column_keys = [str(column.column_index) for column in layout.columns]
|
||||
evaluation = evaluate(_eval_lines(lines), column_keys, document_type, layout.dates.effective, rules, accepted=False)
|
||||
checks = checks_from(evaluation.outcomes, {key: int(key) for key in column_keys})
|
||||
header, header_warnings = review_header(header, dates=layout.dates, rules=rules)
|
||||
reviewed = tuple(
|
||||
replace(
|
||||
line,
|
||||
needs_review=evaluation.line_needs_review.get(line.ref, False),
|
||||
values=tuple(
|
||||
replace(v, needs_review=evaluation.value_needs_review.get((line.ref, str(v.column_index)), False))
|
||||
for v in line.values
|
||||
),
|
||||
)
|
||||
for line in lines
|
||||
)
|
||||
document = StatementDocument(
|
||||
document_type=document_type,
|
||||
form_variant=evaluation.variant,
|
||||
locator=candidate.locator,
|
||||
header=header,
|
||||
columns=layout.columns,
|
||||
lines=reviewed,
|
||||
checks=checks,
|
||||
status=DocStatus.DONE,
|
||||
warnings=(
|
||||
*candidate.warnings,
|
||||
*layout.warnings,
|
||||
*row_warnings,
|
||||
*range_warnings,
|
||||
*units,
|
||||
*conflict,
|
||||
*header_warnings,
|
||||
),
|
||||
)
|
||||
return replace(document, status=document_status(status_input(document)))
|
||||
|
||||
|
||||
def normalize_candidate(candidate: FormCandidate, rules: Rules | None = None) -> CandidateResult:
|
||||
"""Шаги 2–8 кандидата; документа нет — предупреждение файла (`TABLE_LOST`, `TABLE_SKIPPED`, `SHEET_OTHER`)."""
|
||||
rules = rules if rules is not None else default_rules()
|
||||
header = replace(parse_header(candidate.header, rules), form_name=_form_name(candidate))
|
||||
layout = find_layout(candidate, header, rules)
|
||||
hint = candidate.type_hint
|
||||
|
||||
if not layout.code_found:
|
||||
if hint is None:
|
||||
return CandidateResult(None, (_lost_table(candidate, rules),))
|
||||
base = [*candidate.warnings, *layout.warnings]
|
||||
if hint is DocumentType.DISCLOSURE:
|
||||
return CandidateResult(
|
||||
_without_lines(candidate, hint, header, candidate.warnings, dates=None, rules=rules), ()
|
||||
)
|
||||
message = "столбец кодов строк не найден"
|
||||
base.append(make_warning(WarningCode.CODE_COLUMN_NOT_FOUND, message, rules))
|
||||
return CandidateResult(_without_lines(candidate, hint, header, base, dates=layout.dates, rules=rules), ())
|
||||
|
||||
raw, row_warnings = collect_raw_lines(candidate, layout, rules)
|
||||
ok_codes = [item.code for item in raw if item.line_status is LineStatus.OK and item.code is not None]
|
||||
section = majority_section(ok_codes, rules)
|
||||
if section is not None:
|
||||
document_type = document_type_of(section)
|
||||
elif hint is not None:
|
||||
document_type = hint
|
||||
else:
|
||||
return CandidateResult(None, (_lost_table(candidate, rules),))
|
||||
|
||||
conflict: list[StatementWarning] = []
|
||||
if hint is not None and hint is not document_type:
|
||||
message = f"тип по кодам строк ({document_type}) противоречит типу по заголовку ({hint})"
|
||||
conflict.append(make_warning(WarningCode.TYPE_CONFLICT, message, rules))
|
||||
|
||||
if document_type is DocumentType.DISCLOSURE:
|
||||
warnings = [*candidate.warnings, *conflict]
|
||||
return CandidateResult(_without_lines(candidate, document_type, header, warnings, dates=None, rules=rules), ())
|
||||
document = _lines_document(
|
||||
candidate, document_type, header, layout, raw=raw, row_warnings=row_warnings, conflict=conflict, rules=rules
|
||||
)
|
||||
return CandidateResult(document, ())
|
||||
|
||||
|
||||
def normalize(candidates: Sequence[FormCandidate], rules: Rules | None = None) -> NormalizationResult:
|
||||
"""Кандидаты по порядку; исключения не перехватываются (изоляцию кандидатов делает хост)."""
|
||||
rules = rules if rules is not None else default_rules()
|
||||
documents: list[StatementDocument] = []
|
||||
file_warnings: list[StatementWarning] = []
|
||||
for candidate in candidates:
|
||||
result = normalize_candidate(candidate, rules)
|
||||
if result.document is not None:
|
||||
documents.append(result.document)
|
||||
file_warnings.extend(result.file_warnings)
|
||||
return NormalizationResult(tuple(documents), tuple(file_warnings))
|
||||
196
arm_statements/tests/test_pipeline.py
Normal file
196
arm_statements/tests/test_pipeline.py
Normal file
@ -0,0 +1,196 @@
|
||||
from decimal import Decimal as D
|
||||
|
||||
from arm_statements import document_status, normalize, normalize_candidate, status_input
|
||||
from arm_statements.issues import make_warning
|
||||
from arm_statements.loader import Rules
|
||||
from arm_statements.model import DocStatus, DocumentType, FormVariant, Severity
|
||||
from arm_statements.tests.builders import excel, hdr, ocr
|
||||
|
||||
HEAD = ["Наименование показателя", "Код", "На 30.09.2025"]
|
||||
FULL_CONFIDENCE = 100.0
|
||||
FULL_HEADER = hdr(organization_name="ООО «Пример»", inn="7700000000", report_date="30.09.2025", units="тыс. руб.")
|
||||
|
||||
|
||||
def bal_rows() -> list[list[str]]:
|
||||
return [
|
||||
HEAD,
|
||||
["Запасы", "1210", "60"],
|
||||
["Дебиторы", "1230", "40"],
|
||||
["Итого II", "1200", "100"],
|
||||
["НМА", "1110", "50"],
|
||||
["Итого I", "1100", "50"],
|
||||
["БАЛАНС", "1600", "150"],
|
||||
["Капитал", "1310", "150"],
|
||||
["Итого III", "1300", "150"],
|
||||
["БАЛАНС", "1700", "150"],
|
||||
]
|
||||
|
||||
|
||||
def test_clean_ocr_balance_done(rules: Rules) -> None:
|
||||
cand = ocr(bal_rows(), header=FULL_HEADER, type_hint=DocumentType.BALANCE, title="Бухгалтерский баланс")
|
||||
result = normalize_candidate(cand, rules)
|
||||
doc = result.document
|
||||
assert doc is not None and result.file_warnings == ()
|
||||
assert doc.header.form_name == "Бухгалтерский баланс"
|
||||
assert document_status(status_input(doc)) is doc.status
|
||||
assert (doc.document_type, doc.form_variant, doc.status) == (DocumentType.BALANCE, FormVariant.FULL, DocStatus.DONE)
|
||||
assert doc.checks is not None and doc.checks.relations_failed == ()
|
||||
assert doc.header.inn.value == "7700000000"
|
||||
|
||||
|
||||
def test_disclosure_hint_with_balance_codes(rules: Rules) -> None:
|
||||
doc = normalize_candidate(ocr(bal_rows(), header=FULL_HEADER, type_hint=DocumentType.DISCLOSURE), rules).document
|
||||
assert doc is not None and doc.document_type is DocumentType.BALANCE
|
||||
assert [(w.code, w.severity) for w in doc.warnings if w.code == "TYPE_CONFLICT"] == [
|
||||
("TYPE_CONFLICT", Severity.ERROR)
|
||||
]
|
||||
|
||||
|
||||
def test_balance_hint_with_income_codes(rules: Rules) -> None:
|
||||
rows = [
|
||||
HEAD,
|
||||
["Выручка", "2110", "100"],
|
||||
["Себестоимость", "2120", "(60)"],
|
||||
["Валовая", "2100", "40"],
|
||||
["Коммерческие", "2210", "(10)"],
|
||||
["Прибыль от продаж", "2200", "30"],
|
||||
]
|
||||
doc = normalize_candidate(ocr(rows, header=FULL_HEADER, type_hint=DocumentType.BALANCE), rules).document
|
||||
assert doc is not None and doc.document_type is DocumentType.INCOME
|
||||
assert "TYPE_CONFLICT" in [w.code for w in doc.warnings]
|
||||
|
||||
|
||||
def test_excel_other_sheet(rules: Rules) -> None:
|
||||
result = normalize_candidate(excel([("Примечания", [["Пояснительный текст без цифр"]])]), rules)
|
||||
assert result.document is None
|
||||
assert [(w.code, w.severity, w.locator) for w in result.file_warnings] == [
|
||||
("SHEET_OTHER", Severity.INFO, "sheet:Примечания")
|
||||
]
|
||||
|
||||
|
||||
def test_ocr_lost_and_skipped_tables(rules: Rules) -> None:
|
||||
lost = normalize_candidate(ocr([["Итого", "", "15 000"]], locator="file:a.json;table:1"), rules)
|
||||
assert [(w.code, w.locator) for w in lost.file_warnings] == [("TABLE_LOST", "file:a.json;table:1")]
|
||||
skipped = normalize_candidate(ocr([["Руководитель", "Иванов"]]), rules)
|
||||
assert [w.code for w in skipped.file_warnings] == ["TABLE_SKIPPED"] and skipped.document is None
|
||||
|
||||
|
||||
def test_hint_without_code_column(rules: Rules) -> None:
|
||||
doc = normalize_candidate(
|
||||
ocr([["Итого", "15 000"]], type_hint=DocumentType.BALANCE, header=FULL_HEADER), rules
|
||||
).document
|
||||
assert doc is not None and (doc.lines, doc.columns, doc.checks) == ((), (), None)
|
||||
assert doc.form_variant is FormVariant.FULL
|
||||
assert doc.status is DocStatus.NEEDS_CHECK and "CODE_COLUMN_NOT_FOUND" in [w.code for w in doc.warnings]
|
||||
disc = normalize_candidate(ocr([["Текст"]], type_hint=DocumentType.DISCLOSURE), rules).document
|
||||
assert disc is not None and (disc.status, disc.form_variant, disc.lines) == (DocStatus.DONE, None, ())
|
||||
assert disc.header.form_name is None
|
||||
|
||||
|
||||
def test_disclosure_by_codes(rules: Rules) -> None:
|
||||
rows = [HEAD, ["Пояснение", "5101", "1"], ["Пояснение", "5102", "2"], ["Пояснение", "5100", "3"]]
|
||||
doc = normalize_candidate(ocr(rows, header=FULL_HEADER), rules).document
|
||||
assert doc is not None and (doc.document_type, doc.lines, doc.status) == (
|
||||
DocumentType.DISCLOSURE,
|
||||
(),
|
||||
DocStatus.DONE,
|
||||
)
|
||||
|
||||
|
||||
def test_simplified_balance(rules: Rules) -> None:
|
||||
rows = [
|
||||
HEAD,
|
||||
["Матактивы", "1150", "50"],
|
||||
["Запасы", "1210", "50"],
|
||||
["БАЛАНС", "1600", "100"],
|
||||
["Капитал", "1300", "100"],
|
||||
["БАЛАНС", "1700", "100"],
|
||||
]
|
||||
doc = normalize_candidate(ocr(rows, header=FULL_HEADER), rules).document
|
||||
assert doc is not None and doc.form_variant is FormVariant.SIMPLIFIED
|
||||
assert doc.checks is not None and doc.checks.relations_failed == ()
|
||||
|
||||
|
||||
def test_income_without_inn_still_done(rules: Rules) -> None:
|
||||
rows: list[list[str | D]] = [
|
||||
["Наименование", "Код", "За 9 месяцев 2025 г."],
|
||||
["Выручка", "2110", D("100")],
|
||||
["Себестоимость", "2120", D("-60")],
|
||||
["Валовая прибыль", "2100", D("40")],
|
||||
["Прибыль от продаж", "2200", D("40")],
|
||||
["Прибыль до налогообложения", "2300", D("40")],
|
||||
["Чистая прибыль", "2400", D("40")],
|
||||
]
|
||||
head = hdr(organization_name="ООО «Пример»", units="тыс. руб.")
|
||||
doc = normalize_candidate(excel([("ОФР", rows)], header=head), rules).document
|
||||
assert doc is not None and doc.status is DocStatus.DONE
|
||||
assert [(w.code, w.severity) for w in doc.warnings] == [("INN_NOT_FOUND", Severity.INFO)]
|
||||
|
||||
|
||||
def test_normalize_collects(rules: Rules) -> None:
|
||||
result = normalize([ocr(bal_rows(), header=FULL_HEADER), ocr([["Руководитель", "Иванов"]])], rules)
|
||||
assert len(result.documents) == 1 and [w.code for w in result.file_warnings] == ["TABLE_SKIPPED"]
|
||||
|
||||
|
||||
# --- P1-6: у DISCLOSURE предупреждений шапки нет, статус DONE (требование ревью T15.1) ---
|
||||
|
||||
|
||||
def test_disclosure_by_hint_empty_header_has_no_warnings(rules: Rules) -> None:
|
||||
doc = normalize_candidate(ocr([["Текст"]], type_hint=DocumentType.DISCLOSURE), rules).document
|
||||
assert doc is not None
|
||||
assert (doc.warnings, doc.status) == ((), DocStatus.DONE)
|
||||
|
||||
|
||||
def test_disclosure_by_codes_empty_header_has_no_warnings(rules: Rules) -> None:
|
||||
rows = [HEAD, ["Пояснение", "5101", "1"], ["Пояснение", "5102", "2"], ["Пояснение", "5100", "3"]]
|
||||
doc = normalize_candidate(ocr(rows), rules).document
|
||||
assert doc is not None and doc.document_type is DocumentType.DISCLOSURE
|
||||
assert (doc.warnings, doc.status) == ((), DocStatus.DONE)
|
||||
|
||||
|
||||
# --- ROW_SPLIT: предупреждение документа без locator (место — locator документа, спека «Шаг 8 — предупреждения») ---
|
||||
|
||||
|
||||
def test_row_split_has_no_locator(rules: Rules) -> None:
|
||||
rows = bal_rows()
|
||||
rows[1] = ["Запасы и дебиторы", "1210\n1230", "60\n40"]
|
||||
doc = normalize_candidate(ocr(rows, header=FULL_HEADER, type_hint=DocumentType.BALANCE), rules).document
|
||||
assert doc is not None
|
||||
split = [w for w in doc.warnings if w.code == "ROW_SPLIT"]
|
||||
assert len(split) == 1 and split[0].locator is None and split[0].severity is Severity.INFO
|
||||
|
||||
|
||||
# --- строка «в тысячах рублей» над таблицей и уверенность даты отчёта у Excel ---
|
||||
|
||||
|
||||
def test_units_row_above_table_is_not_header_and_excel_date_is_confident(rules: Rules) -> None:
|
||||
rows = [["Бухгалтерский баланс"], ["в тысячах рублей"], *bal_rows()]
|
||||
cand = excel([("Баланс", rows)], header=FULL_HEADER, type_hint=DocumentType.BALANCE)
|
||||
doc = normalize_candidate(cand, rules).document
|
||||
assert doc is not None
|
||||
assert doc.header.report_date.confidence == FULL_CONFIDENCE and doc.header.report_date.value is not None
|
||||
assert [c.column_index for c in doc.columns] == [2]
|
||||
assert (doc.status, [w.code for w in doc.warnings]) == (DocStatus.DONE, [])
|
||||
|
||||
|
||||
# --- порядок предупреждений документа: адаптера, раскладки, строк, единиц, TYPE_CONFLICT, шапки ---
|
||||
|
||||
|
||||
def test_document_warnings_order(rules: Rules) -> None:
|
||||
adapter = make_warning("NUMBER_FORMAT_UNPARSED", "формат", rules)
|
||||
rows = [["Наименование", "Код", "Итого"], *bal_rows()[1:]]
|
||||
rows[1] = ["Запасы и дебиторы", "1210\n1230", "60,5\n40,5"]
|
||||
cand = excel([("Баланс", rows)], header=hdr(units="тыс. руб."), type_hint=DocumentType.INCOME, warnings=[adapter])
|
||||
doc = normalize_candidate(cand, rules).document
|
||||
assert doc is not None
|
||||
codes = [w.code for w in doc.warnings]
|
||||
expected = [
|
||||
"NUMBER_FORMAT_UNPARSED",
|
||||
"COLUMN_WITHOUT_DATE",
|
||||
"ROW_SPLIT",
|
||||
"FRACTIONAL_IN_THOUSANDS",
|
||||
"TYPE_CONFLICT",
|
||||
"NO_ORGANIZATION_NO_INN",
|
||||
]
|
||||
assert [c for c in codes if c in expected] == [c for c in expected if c in codes]
|
||||
assert codes[0] == "NUMBER_FORMAT_UNPARSED" and codes.index("TYPE_CONFLICT") < codes.index("NO_ORGANIZATION_NO_INN")
|
||||
Loading…
x
Reference in New Issue
Block a user