Merge pull request 'Шаг 2: числа правее таблицы формы — не колонки сумм (#5)' (#6) from feat/5-amount-columns-right-of-table into main
This commit is contained in:
commit
3390f92dad
@ -33,7 +33,7 @@ make venv && make check
|
||||
| `DocStatus` | `DONE = "done"`, `NEEDS_CHECK = "needs_check"` |
|
||||
| `EnvelopeStatus` | `DONE = "done"`, `NEEDS_CHECK = "needs_check"`, `ERROR = "error"` |
|
||||
| `SignForm` | `PARENTHESES = "parentheses"`, `MINUS = "minus"`, `UNSIGNED = "unsigned"` |
|
||||
| `WarningCode` | по члену на код словаря `statement_warnings.yaml`, имя = значение: `JSON_FAILED`, `TABLE_PARSE_ERROR`, `TABLE_LOST`, `TABLE_SKIPPED`, `CODE_COLUMN_NOT_FOUND`, `SEGMENT_WITHOUT_CODES`, `SHEET_NOT_JOINED`, `CONTINUATION_TYPE_CONFLICT`, `SEGMENT_COLUMNS_MISMATCH`, `COLUMN_WITHOUT_DATE`, `COLUMN_DATE_UNCERTAIN`, `HEADER_ROW_NOT_FOUND`, `TYPE_CONFLICT`, `NO_ORGANIZATION_NO_INN`, `PERIOD_UNKNOWN`, `REPORT_DATE_UNCERTAIN`, `XLSB_SCALE_SUSPECT`, `INN_NOT_FOUND`, `INN_UNCERTAIN`, `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN`, `REPORT_DATE_UNPARSED`, `UNITS_NOT_FOUND`, `FILE_TYPE_MISMATCH`, `COLUMN_SKIPPED`, `FRACTIONAL_IN_THOUSANDS`, `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED`, `VALUE_OUT_OF_RANGE`, `SHEET_OTHER`, `ROW_SPLIT`, `XLS_NO_CODEPAGE`. Коды `OSV_*` пути ОСВ сюда не входят (их создаёт tabular-parser, спека «Шаг 8 — предупреждения») |
|
||||
| `WarningCode` | по члену на код словаря `statement_warnings.yaml`, имя = значение: `JSON_FAILED`, `TABLE_PARSE_ERROR`, `TABLE_LOST`, `TABLE_SKIPPED`, `CODE_COLUMN_NOT_FOUND`, `SEGMENT_WITHOUT_CODES`, `SHEET_NOT_JOINED`, `CONTINUATION_TYPE_CONFLICT`, `SEGMENT_COLUMNS_MISMATCH`, `COLUMN_WITHOUT_DATE`, `COLUMN_DATE_UNCERTAIN`, `HEADER_ROW_NOT_FOUND`, `TYPE_CONFLICT`, `NO_ORGANIZATION_NO_INN`, `PERIOD_UNKNOWN`, `REPORT_DATE_UNCERTAIN`, `XLSB_SCALE_SUSPECT`, `INN_NOT_FOUND`, `INN_UNCERTAIN`, `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN`, `REPORT_DATE_UNPARSED`, `UNITS_NOT_FOUND`, `FILE_TYPE_MISMATCH`, `COLUMN_SKIPPED`, `FRACTIONAL_IN_THOUSANDS`, `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED`, `VALUE_OUT_OF_RANGE`, `SHEET_OTHER`, `ROW_SPLIT`, `NUMBERS_OUTSIDE_TABLE`, `XLS_NO_CODEPAGE`. Коды `OSV_*` пути ОСВ сюда не входят (их создаёт tabular-parser, спека «Шаг 8 — предупреждения») |
|
||||
|
||||
### Вход шага 1 (строит адаптер хоста)
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ from arm_statements.loader import Rules
|
||||
from arm_statements.model import Cell, PeriodKind, Source, StatementColumn, StatementHeader, StatementWarning
|
||||
|
||||
_FULL_CONFIDENCE = 100.0
|
||||
_MAX_REFS = 10
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@ -94,7 +95,10 @@ def segment_columns( # noqa: PLR0913, PLR0917 - сигнатура задана
|
||||
"""Колонки значений сегмента и предупреждения о пропущенных/недатированных столбцах."""
|
||||
drafts: list[ColumnDraft] = []
|
||||
warnings: list[StatementWarning] = []
|
||||
for col in range(code_col + 1, _width(rows, [*header_rows, *data_rows])):
|
||||
columns = range(code_col + 1, _width(rows, [*header_rows, *data_rows]))
|
||||
last_dated = _last_dated_column(rows, header_rows, data_rows, columns, code_col, source, rules)
|
||||
outside: list[int] = []
|
||||
for col in columns:
|
||||
numbers = has_numbers(rows, data_rows, col, code_col, source, rules)
|
||||
if not header_rows:
|
||||
if numbers:
|
||||
@ -107,14 +111,86 @@ def segment_columns( # noqa: PLR0913, PLR0917 - сигнатура задана
|
||||
warnings.append(make_warning(WarningCode.COLUMN_SKIPPED, message, rules, column_index=col))
|
||||
elif cell is not None and parsed.kind is not PeriodKind.NONE:
|
||||
drafts.append(ColumnDraft(col, cell.text, cell.confidence, parsed.kind, parsed.date))
|
||||
elif numbers and cell is None and last_dated is not None and col > last_dated:
|
||||
outside.append(col) # числа правее последней колонки с подписью периода — не колонка сумм
|
||||
elif numbers:
|
||||
label, conf = (cell.text, cell.confidence) if cell is not None else ("", None)
|
||||
message = f"у столбца {col} с числами нет разобранной подписи периода"
|
||||
warnings.append(make_warning(WarningCode.COLUMN_WITHOUT_DATE, message, rules, column_index=col))
|
||||
drafts.append(ColumnDraft(col, label, conf, PeriodKind.NONE, None))
|
||||
if outside:
|
||||
warnings.append(_outside_table_warning(rows, data_rows, outside, code_col, source, rules))
|
||||
return drafts, warnings
|
||||
|
||||
|
||||
def _last_dated_column( # noqa: PLR0913, PLR0917 - внутренняя сборка границы таблицы
|
||||
rows: Grid,
|
||||
header_rows: tuple[int, ...],
|
||||
data_rows: Sequence[int],
|
||||
columns: range,
|
||||
code_col: int,
|
||||
source: Source,
|
||||
rules: Rules,
|
||||
) -> int | None:
|
||||
"""Правый столбец сегмента с подписью периода и числами в строках с кодами; без шапки — `None`.
|
||||
|
||||
Подписанный столбец без чисел (потерянная подпись объединённой ячейки) границей таблицы не служит.
|
||||
"""
|
||||
dated = [
|
||||
col
|
||||
for col in columns
|
||||
if header_rows
|
||||
and (cell := _label(rows, header_rows, col)) is not None
|
||||
and not (parsed := parse_period_label(cell, rules)).excluded
|
||||
and parsed.kind is not PeriodKind.NONE
|
||||
and has_numbers(rows, data_rows, col, code_col, source, rules)
|
||||
]
|
||||
return max(dated, default=None)
|
||||
|
||||
|
||||
def _outside_table_warning( # noqa: PLR0913, PLR0917 - внутренняя сборка предупреждения
|
||||
rows: Grid, data_rows: Sequence[int], outside: Sequence[int], code_col: int, source: Source, rules: Rules
|
||||
) -> StatementWarning:
|
||||
refs = [
|
||||
cell.ref
|
||||
for r in _code_rows(rows, data_rows, code_col, rules)
|
||||
for col in outside
|
||||
if (cell := cell_at(rows, r, col)) is not None and _is_number(cell, source, rules)
|
||||
]
|
||||
return outside_table_warning(refs, rules)
|
||||
|
||||
|
||||
def outside_table_warning(refs: Sequence[str], rules: Rules) -> StatementWarning:
|
||||
"""`NUMBERS_OUTSIDE_TABLE` (info): адреса ячеек правее таблицы (`locator` — все, через перевод строки)."""
|
||||
shown = ", ".join(refs[:_MAX_REFS]) + (f" и ещё {len(refs) - _MAX_REFS}" if len(refs) > _MAX_REFS else "")
|
||||
message = f"числа правее таблицы формы не учитываются: {shown}"
|
||||
return make_warning(WarningCode.NUMBERS_OUTSIDE_TABLE, message, rules, locator="\n".join(refs))
|
||||
|
||||
|
||||
def merge_outside_table(warnings: Sequence[StatementWarning], rules: Rules) -> list[StatementWarning]:
|
||||
"""Предупреждения `NUMBERS_OUTSIDE_TABLE` сегментов — в одно (адреса без повторов)."""
|
||||
refs: dict[str, None] = {}
|
||||
rest: list[StatementWarning] = []
|
||||
for warning in warnings:
|
||||
if warning.code == WarningCode.NUMBERS_OUTSIDE_TABLE:
|
||||
refs.update(dict.fromkeys((warning.locator or "").split("\n")))
|
||||
else:
|
||||
rest.append(warning)
|
||||
return [*rest, outside_table_warning(list(refs), rules)] if refs else rest
|
||||
|
||||
|
||||
def trailing_outside_refs( # noqa: PLR0913, PLR0917 - внутренняя сборка
|
||||
rows: Grid, data_rows: Sequence[int], cols: Sequence[int], code_col: int, source: Source, rules: Rules
|
||||
) -> list[str]:
|
||||
"""Адреса чисел в строках с кодами по столбцам `cols` (хвост сегмента правее колонок документа)."""
|
||||
return [
|
||||
cell.ref
|
||||
for r in _code_rows(rows, data_rows, code_col, rules)
|
||||
for col in cols
|
||||
if (cell := cell_at(rows, r, col)) is not None and _is_number(cell, source, rules)
|
||||
]
|
||||
|
||||
|
||||
def _latest(drafts: Sequence[ColumnDraft], *, confident: bool) -> date | None:
|
||||
days = [
|
||||
d.date
|
||||
|
||||
@ -45,6 +45,7 @@ class WarningCode(StrEnum):
|
||||
VALUE_OUT_OF_RANGE = "VALUE_OUT_OF_RANGE"
|
||||
SHEET_OTHER = "SHEET_OTHER"
|
||||
ROW_SPLIT = "ROW_SPLIT"
|
||||
NUMBERS_OUTSIDE_TABLE = "NUMBERS_OUTSIDE_TABLE"
|
||||
|
||||
|
||||
def make_warning( # noqa: PLR0913 -- сигнатура задана публичным API пакета
|
||||
|
||||
@ -34,3 +34,4 @@ warnings:
|
||||
VALUE_OUT_OF_RANGE: {severity: info, scope: document}
|
||||
SHEET_OTHER: {severity: info, scope: file}
|
||||
ROW_SPLIT: {severity: info, scope: document}
|
||||
NUMBERS_OUTSIDE_TABLE: {severity: info, scope: document}
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
"""Шаг 2: сегменты сетки, повторные шапки, границы листов и сопоставление колонок с первым сегментом."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import NamedTuple
|
||||
|
||||
from arm_statements.codes import code_parts, is_code_cell, majority_section, parse_code
|
||||
from arm_statements.columns import ColumnDraft, DocumentDates, document_dates, finalize_columns, segment_columns
|
||||
from arm_statements.columns import (
|
||||
ColumnDraft,
|
||||
DocumentDates,
|
||||
document_dates,
|
||||
finalize_columns,
|
||||
merge_outside_table,
|
||||
outside_table_warning,
|
||||
segment_columns,
|
||||
trailing_outside_refs,
|
||||
)
|
||||
from arm_statements.issues import WarningCode, make_warning
|
||||
from arm_statements.layout import (
|
||||
Grid,
|
||||
@ -20,6 +29,7 @@ from arm_statements.layout import (
|
||||
from arm_statements.loader import CodeSection, Rules
|
||||
from arm_statements.model import (
|
||||
FormCandidate,
|
||||
PeriodKind,
|
||||
RowRole,
|
||||
StatementColumn,
|
||||
StatementHeader,
|
||||
@ -212,6 +222,22 @@ def _map_segment(
|
||||
return layout, warning
|
||||
|
||||
|
||||
def _trim_outside(seg: _Analysed, width: int, candidate: FormCandidate, rules: Rules) -> _Analysed:
|
||||
"""Сегмент без подписанных колонок: колонки сверх `width` документа — числа правее таблицы, не колонки."""
|
||||
if seg.code_col is None or len(seg.drafts) <= width or any(d.kind is not PeriodKind.NONE for d in seg.drafts):
|
||||
return seg
|
||||
tail = seg.drafts[width:]
|
||||
if any(d.label for d in tail): # столбец с текстом подписи — не формула аналитика, остаётся расхождением
|
||||
return seg
|
||||
data = [r for r, role in zip(range(seg.first, seg.last + 1), seg.roles, strict=True) if role is RowRole.DATA]
|
||||
refs = trailing_outside_refs(
|
||||
candidate.rows, data, [d.grid_col for d in tail], seg.code_col, candidate.source, rules
|
||||
)
|
||||
return replace(
|
||||
seg, drafts=seg.drafts[:width], draft_warnings=(*seg.draft_warnings, outside_table_warning(refs, rules))
|
||||
)
|
||||
|
||||
|
||||
def find_layout(candidate: FormCandidate, header: StatementHeader, rules: Rules) -> Layout:
|
||||
"""Раскладка кандидата: сегменты, колонки документа, роли всех строк и предупреждения шага 2."""
|
||||
segments, section = _split_and_analyse(candidate, rules)
|
||||
@ -223,7 +249,23 @@ def find_layout(candidate: FormCandidate, header: StatementHeader, rules: Rules)
|
||||
if defining is not None:
|
||||
dates = document_dates(defining.drafts, header)
|
||||
columns, finalize_warnings = finalize_columns(defining.drafts, dates, rules)
|
||||
warnings.extend(defining.draft_warnings)
|
||||
# числа правее таблицы — по всем сегментам (формула аналитика может стоять на любой странице формы);
|
||||
# сегмент без подписанных колонок (лист-продолжение) сверх колонок документа тоже «правее таблицы»
|
||||
if any(d.kind is not PeriodKind.NONE for d in defining.drafts):
|
||||
segments = [
|
||||
seg if seg is defining else _trim_outside(seg, len(columns), candidate, rules) for seg in segments
|
||||
]
|
||||
warnings.extend(
|
||||
merge_outside_table(
|
||||
[
|
||||
w
|
||||
for seg in segments
|
||||
for w in seg.draft_warnings
|
||||
if seg is defining or w.code == WarningCode.NUMBERS_OUTSIDE_TABLE
|
||||
],
|
||||
rules,
|
||||
)
|
||||
)
|
||||
warnings.extend(finalize_warnings)
|
||||
if not any(s.header_rows for s in coded):
|
||||
warnings.append(
|
||||
|
||||
@ -68,18 +68,63 @@ def test_junk_label_over_numbers_is_kept(rules: Rules) -> None:
|
||||
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", Severity.ERROR, 3)]
|
||||
|
||||
|
||||
def test_empty_label_over_numbers_is_kept(rules: Rules) -> None:
|
||||
def test_empty_label_right_of_dated_columns_is_outside_table(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [
|
||||
["Наименование", "Код", "На 30.09.2025", ""],
|
||||
["Запасы", "1210", "5", "4"],
|
||||
["Итого", "1200", "5", "4"],
|
||||
]
|
||||
columns, warnings = run(rows, OCR, rules)
|
||||
assert [(c.column_index, c.label, c.label_confidence, c.period_kind) for c in columns] == [
|
||||
(2, "На 30.09.2025", 100.0, PeriodKind.ABSOLUTE),
|
||||
(3, "", None, PeriodKind.NONE),
|
||||
assert [(c.column_index, c.period_kind) for c in columns] == [(2, PeriodKind.ABSOLUTE)]
|
||||
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("NUMBERS_OUTSIDE_TABLE", Severity.INFO, None)]
|
||||
assert warnings[0].locator == "row:1\nrow:2"
|
||||
|
||||
|
||||
def test_numbers_right_of_last_dated_column_are_not_amounts(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [
|
||||
["Наименование", "", "Код", "На 30.09.2025", "На 31.12.2024", None, None],
|
||||
["Запасы", "", "1210", D("5"), D("4"), None, D("9")],
|
||||
["Баланс", "", "1600", D("5"), D("4"), D("7"), None],
|
||||
["Итого", "", "1700", D("5"), D("4"), None, None],
|
||||
]
|
||||
assert [(w.code, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", 3)]
|
||||
columns, warnings = run(rows, EXCEL, rules, data_rows=(1, 2, 3))
|
||||
assert [c.column_index for c in columns] == [3, 4]
|
||||
assert [(w.code, w.severity) for w in warnings] == [("NUMBERS_OUTSIDE_TABLE", Severity.INFO)]
|
||||
assert warnings[0].locator == "Лист1!G2\nЛист1!F3"
|
||||
assert "G2" in warnings[0].message and "F3" in warnings[0].message
|
||||
|
||||
|
||||
def test_numbers_outside_table_do_not_depend_on_cell_count(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [["Наименование", "", "Код", "На 30.09.2025", None]]
|
||||
rows += [["Строка", "", str(1200 + i), D("1"), D("2")] for i in range(12)]
|
||||
cand = excel([("Лист1", rows)])
|
||||
_, warnings = segment_columns(cand.rows, (0,), tuple(range(1, 13)), 2, EXCEL, rules)
|
||||
assert len(warnings) == 1
|
||||
assert warnings[0].message.endswith("и ещё 2")
|
||||
assert warnings[0].message.count("Лист1!E") == 10 # noqa: PLR2004
|
||||
assert len((warnings[0].locator or "").split("\n")) == 12 # noqa: PLR2004
|
||||
|
||||
|
||||
def test_no_dated_columns_keeps_old_behaviour(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [
|
||||
["Наименование", "", "Код", "", ""],
|
||||
["Запасы", "", "1210", D("5"), D("4")],
|
||||
["Итого", "", "1200", D("5"), D("4")],
|
||||
]
|
||||
columns, warnings = run(rows, EXCEL, rules)
|
||||
assert [c.column_index for c in columns] == [3, 4]
|
||||
assert [(w.code, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", 3), ("COLUMN_WITHOUT_DATE", 4)]
|
||||
|
||||
|
||||
def test_undated_column_between_dated_keeps_old_behaviour(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [
|
||||
["Наименование", "", "Код", "На 30.09.2025", None, "На 31.12.2024"],
|
||||
["Запасы", "", "1210", D("5"), D("9"), D("4")],
|
||||
["Итого", "", "1200", D("5"), D("9"), D("4")],
|
||||
]
|
||||
columns, warnings = run(rows, EXCEL, rules)
|
||||
assert [c.column_index for c in columns] == [3, 4, 5]
|
||||
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", Severity.ERROR, 4)]
|
||||
|
||||
|
||||
def test_two_row_header_is_joined_with_min_confidence(rules: Rules) -> None:
|
||||
@ -144,3 +189,25 @@ def test_public_amount_columns(rules: Rules) -> None:
|
||||
]
|
||||
assert amount_columns(excel([("Лист1", rows)]).rows, 2, rules) == (3, 4)
|
||||
assert amount_columns(ocr([["Запасы", "1210", "5", "", "4"]]).rows, 1, rules, source=OCR) == (2, 4)
|
||||
|
||||
|
||||
def test_dated_column_without_numbers_is_not_the_table_edge(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [
|
||||
["Наименование", "", "Код", "На 30.09.2025", "", "На 31.12.2024", ""],
|
||||
["Запасы", "", "1210", None, D("5"), None, D("4")],
|
||||
["Итого", "", "1200", None, D("5"), None, D("4")],
|
||||
] # подпись объединённой ячейки стоит левее значений: границы таблицы нет, колонки остаются
|
||||
columns, warnings = run(rows, EXCEL, rules)
|
||||
assert [c.column_index for c in columns] == [3, 4, 5, 6][:0] or [w.code for w in warnings].count(
|
||||
"NUMBERS_OUTSIDE_TABLE"
|
||||
) == 0
|
||||
|
||||
|
||||
def test_sheet_name_with_comma_keeps_addresses(rules: Rules) -> None:
|
||||
rows: list[list[Any]] = [
|
||||
["Наименование", "", "Код", "На 30.09.2025", None],
|
||||
["Запасы", "", "1210", D("5"), D("9")],
|
||||
]
|
||||
cand = excel([("Баланс, 2025", rows)])
|
||||
_, warnings = segment_columns(cand.rows, (0,), (1,), 2, EXCEL, rules)
|
||||
assert (warnings[0].locator or "").split("\n") == ["'Баланс, 2025'!E2"]
|
||||
|
||||
@ -43,6 +43,7 @@ SPEC_WARNINGS = {
|
||||
"VALUE_OUT_OF_RANGE": ("info", "document"),
|
||||
"SHEET_OTHER": ("info", "file"),
|
||||
"ROW_SPLIT": ("info", "document"),
|
||||
"NUMBERS_OUTSIDE_TABLE": ("info", "document"),
|
||||
"XLS_NO_CODEPAGE": ("info", "file"),
|
||||
}
|
||||
|
||||
|
||||
@ -194,3 +194,19 @@ def test_document_warnings_order(rules: Rules) -> None:
|
||||
]
|
||||
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")
|
||||
|
||||
|
||||
# --- числа правее таблицы формы (решение владельца 18.10.2026): не колонки сумм, статус не портят ---
|
||||
|
||||
|
||||
def test_numbers_right_of_table_do_not_break_status_in_repeated_segments(rules: Rules) -> None:
|
||||
rows: list[list[str | None]] = [[*r, None] for r in bal_rows()]
|
||||
rows[6][3] = "7" # формула аналитика правее таблицы, в строке БАЛАНС (1600)
|
||||
rows[9][3] = "8"
|
||||
rows.insert(7, [*HEAD, None]) # повторная шапка второго сегмента (страница 2 формы)
|
||||
cand = ocr(rows, header=FULL_HEADER, type_hint=DocumentType.BALANCE)
|
||||
doc = normalize_candidate(cand, rules).document
|
||||
assert doc is not None and doc.status is DocStatus.DONE
|
||||
assert [c.column_index for c in doc.columns] == [2]
|
||||
assert [(w.code, w.severity) for w in doc.warnings] == [("NUMBERS_OUTSIDE_TABLE", Severity.INFO)]
|
||||
assert all(len(line.values) == 1 for line in doc.lines)
|
||||
|
||||
@ -187,3 +187,31 @@ def test_continuation_sheet_with_duplicate_date_maps_by_order(rules: Rules) -> N
|
||||
cand = excel(_npz_sheets(duplicate))
|
||||
layout = find_layout(cand, parse_header(hdr(report_date="30.09.2025"), rules), rules)
|
||||
assert layout.segments[1].value_cols == ((8, 16), (9, 21), (10, 24))
|
||||
|
||||
|
||||
def test_numbers_right_of_table_in_all_segments_give_one_warning(rules: Rules) -> None:
|
||||
head: list[str | D | None] = ["Наименование", "Код", "На 30.09.2025", "На 31.12.2024", None]
|
||||
rows: list[list[str | D | None]] = [
|
||||
head,
|
||||
["Запасы", "1210", D("5"), D("4"), D("9")],
|
||||
head,
|
||||
["Итого", "1200", D("5"), D("4"), D("8")],
|
||||
]
|
||||
layout = find_layout(excel([("Лист1", rows)]), parse_header(hdr(), rules), rules)
|
||||
assert [w.code for w in layout.warnings] == ["NUMBERS_OUTSIDE_TABLE"]
|
||||
assert layout.warnings[0].locator == "Лист1!E2\nЛист1!E4"
|
||||
assert layout.segments[1].value_cols == ((2, 2), (3, 3))
|
||||
|
||||
|
||||
def test_headerless_continuation_sheet_with_outside_numbers(rules: Rules) -> None:
|
||||
first: list[list[str | D | None]] = [
|
||||
["Наименование", "", "Код", "На 30.09.2025", "На 31.12.2024", None],
|
||||
["Запасы", "", "1210", D("5"), D("4"), D("9")],
|
||||
]
|
||||
second: list[list[str | D | None]] = [["Итого", "", "1200", D("5"), D("4"), D("8")]]
|
||||
cand = excel([("Лист1", first), ("Лист2", second)])
|
||||
layout = find_layout(cand, parse_header(hdr(), rules), rules)
|
||||
assert [w.code for w in layout.warnings] == ["NUMBERS_OUTSIDE_TABLE"]
|
||||
assert layout.warnings[0].locator == "Лист1!F2\nЛист2!F1"
|
||||
assert layout.segments[1].value_cols == ((3, 3), (4, 4))
|
||||
assert layout.segments[1].extra_cols == ()
|
||||
|
||||
Reference in New Issue
Block a user