number_format: 15 значащих цифр с округлением вверх, неразобранные условия и >4 секций; признак xlsb по содержимому (#42)
Фикс-волна по финальному ревью: I-1, M-1, M-4, M-5, M-6 (T4.1, T4.2); тесты на ;;;, 5 секций, [$-419], \(…\), [=5]0;[=5]0,. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
388276c261
commit
904392ddcb
@ -306,7 +306,7 @@ openpyxl 0,58 с; синтетический лист в 2 млн ячеек (63
|
||||
как в файле и одно предупреждение на документ `формат чисел xlsb не читается: значения как в файле`. Разбор ОСВ
|
||||
формат ячейки не применяет. При «тыс. руб.» и доле значений (по показываемым) с дробной частью не меньше
|
||||
`fractional_share_percent` выдаётся одно предупреждение на документ; само оно на статус не влияет — `needs_check`
|
||||
задают нарушенные соотношения (расхождение до `rounding_tolerance_percent` — округление итогов, OQ-138).
|
||||
задают нарушенные соотношения (расхождение до `rounding_tolerance_percent` — мягкое нарушение, OQ-138).
|
||||
- Страницы одной формы в разных файлах не склеиваются: каждая страница-продолжение без первой страницы остаётся
|
||||
одиноким продолжением и даёт `needs_check`.
|
||||
- Лист с 1–2 кодами форм (меньше `min_codes`) формой не считается, если идёт первым; после листа формы он берётся
|
||||
@ -321,16 +321,16 @@ openpyxl 0,58 с; синтетический лист в 2 млн ячеек (63
|
||||
|---|---|---|
|
||||
| `unagrande-co/2024-FY/Б.Б Лист 1.xls` | BALANCE `needs_check` | только актив: `1600 = 1700` не сходится |
|
||||
| `unagrande-co/2024-FY/Б.Б. Лист 2.xls` | BALANCE `needs_check` | страница 2 без шапки |
|
||||
| `unagrande-co/2024-FY/ОФР. Лист 1.xls` | INCOME `done` | значения по числовому формату (тыс. руб.); итоги округлены до тысяч: мягкие нарушения «в пределах округления» (OQ-138) |
|
||||
| `unagrande-co/2024-FY/ОФР. Лист 1.xls` | INCOME `done` | значения по числовому формату (тыс. руб.); мягко расходится `2400 = 2300 - 2410 + 2420` (в пределах допуска, OQ-138) |
|
||||
| `unagrande-co/2024-FY/ОФР. Лист 2 .xls` | INCOME `needs_check` | страница 2 без шапки |
|
||||
| `unagrande-group/2024-FY/ББ. Лист 1 .xls` | BALANCE `needs_check` | только актив |
|
||||
| `unagrande-group/2024-FY/ББ. Лист 2 (2).xls` | BALANCE `needs_check` | страница 2 без шапки |
|
||||
| `unagrande-group/2024-FY/ОФР. Лист 2.xls` | INCOME `needs_check` | страница 2 без шапки, страницы 1 в пакете нет |
|
||||
| `unagrande-group/2025-Q3/ББ. Лист 1 3кв.25.xls` | BALANCE `needs_check` | только актив |
|
||||
| `unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls` | BALANCE `needs_check` | страница 2 без шапки |
|
||||
| `unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls` | INCOME `done` | значения по числовому формату (тыс. руб.); 2200 — малая разность больших слагаемых, округление (OQ-138) |
|
||||
| `unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls` | INCOME `done` | значения по числовому формату (тыс. руб.); мягко расходятся `2400 = 2300 - 2410 + 2420` и `2410 = 2411 + 2412` (OQ-138) |
|
||||
| `unagrande-group/2025-Q3/ОФР. Лист 2 3кв.25.xls` | INCOME `needs_check` | страница 2 без шапки |
|
||||
| `npz/2025-Q3/Бухгалтерская отчетность … (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx` | BALANCE `done`, INCOME `done` | значения по числовому формату `#,##0,` (тыс. руб.); расхождения итогов — округление (OQ-138); лист с единственной строкой 2400 присоединён (O-9) |
|
||||
| `npz/2025-Q3/Бухгалтерская отчетность … (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx` | BALANCE `done`, INCOME `done` | значения по числовому формату `#,##0,` (тыс. руб.); мягко расходятся детализации 1150, 1250, 1410, 1500, 1510 (ББ) и 2400, 2410 (ОФР), жёстких нет (OQ-138); лист с единственной строкой 2400 присоединён (O-9) |
|
||||
| `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` | BALANCE `done` | все соотношения сходятся |
|
||||
| `mezheninovskaya/2025-Q3/1_Форма2_бух_09.xlsx` | INCOME `done` | все соотношения сходятся |
|
||||
| `templates/ББ до 01.01.2025.xlsx` | BALANCE `needs_check`, DISCLOSURE `done` | шаблон без сумм и шапки; «Примечания» шаблона — пустой DISCLOSURE |
|
||||
|
||||
@ -19,10 +19,9 @@ from src.parser.detect import SheetKind, classify_sheet
|
||||
from src.parser.errors import InvalidDocumentError, ParserError, UnsupportedFormatError
|
||||
from src.parser.grid import Sheet
|
||||
from src.parser.osv import OsvParser, data_rows, sheet_line, unique
|
||||
from src.parser.readers import read_book
|
||||
from src.parser.readers import read_book_detected
|
||||
from src.parser.readers._common import one_line_reason
|
||||
from src.parser.readers._limits import ZIP_UNCOMPRESSED_LIMIT_FACTOR
|
||||
from src.parser.readers.xlsb import OUTLINE_UNAVAILABLE_WARNING
|
||||
from src.parser.rules import Rules
|
||||
from src.parser.statements.detect import is_weak_continuation
|
||||
|
||||
@ -94,10 +93,11 @@ class BookParser:
|
||||
"книга не открывается"
|
||||
)
|
||||
deadline = Deadline(self._timeout_seconds, self._clock)
|
||||
sheets, read_warnings = read_book(
|
||||
read = read_book_detected(
|
||||
data, file_type, max_uncompressed_bytes=ZIP_UNCOMPRESSED_LIMIT_FACTOR * self._max_bytes
|
||||
)
|
||||
warnings.extend(unique(read_warnings))
|
||||
sheets = read.sheets
|
||||
warnings.extend(unique(read.warnings))
|
||||
osv_documents: list[OsvDocument] = []
|
||||
statement_sheets: list[tuple[Sheet, SheetKind]] = []
|
||||
ok = True
|
||||
@ -130,7 +130,7 @@ class BookParser:
|
||||
warnings.extend(sheet_warnings)
|
||||
ok = ok and sheet_ok
|
||||
statement_documents, statement_warnings, statements_ok = self._statements.parse_sheets(
|
||||
statement_sheets, deadline, formats_unreadable=OUTLINE_UNAVAILABLE_WARNING in read_warnings
|
||||
statement_sheets, deadline, formats_unreadable=read.detected == "XLSB"
|
||||
)
|
||||
warnings.extend(statement_warnings)
|
||||
documents: list[OsvDocument | StatementDocument] = [*osv_documents, *statement_documents]
|
||||
|
||||
@ -13,6 +13,7 @@ PK\\x03\\x04 (zip) → XLSX или XLSB (XLSB — если в архиве ес
|
||||
import io
|
||||
import zipfile
|
||||
from collections.abc import Callable
|
||||
from typing import NamedTuple
|
||||
|
||||
from src.contract import FileType
|
||||
from src.core.config import Settings
|
||||
@ -29,7 +30,7 @@ from src.parser.readers.xls import read_xls
|
||||
from src.parser.readers.xlsb import OUTLINE_UNAVAILABLE_WARNING, read_xlsb
|
||||
from src.parser.readers.xlsx import read_xlsx
|
||||
|
||||
__all__ = ["default_uncompressed_limit", "read_book"]
|
||||
__all__ = ["BookRead", "default_uncompressed_limit", "read_book", "read_book_detected"]
|
||||
|
||||
ZIP_SIGNATURE = b"PK\x03\x04"
|
||||
OLE2_SIGNATURE = bytes.fromhex("D0CF11E0A1B11AE1")
|
||||
@ -49,9 +50,23 @@ def default_uncompressed_limit() -> int:
|
||||
return ZIP_UNCOMPRESSED_LIMIT_FACTOR * max_bytes
|
||||
|
||||
|
||||
class BookRead(NamedTuple):
|
||||
"""Результат чтения: листы, предупреждения и формат, определённый по содержимому (а не заявленный `file_type`)."""
|
||||
|
||||
sheets: tuple[Sheet, ...]
|
||||
warnings: list[str]
|
||||
detected: FileType
|
||||
|
||||
|
||||
def read_book(
|
||||
data: bytes, file_type: FileType, *, max_uncompressed_bytes: int | None = None
|
||||
) -> tuple[tuple[Sheet, ...], list[str]]:
|
||||
"""Листы и предупреждения чтения; см. `read_book_detected`."""
|
||||
result = read_book_detected(data, file_type, max_uncompressed_bytes=max_uncompressed_bytes)
|
||||
return result.sheets, result.warnings
|
||||
|
||||
|
||||
def read_book_detected(data: bytes, file_type: FileType, *, max_uncompressed_bytes: int | None = None) -> BookRead:
|
||||
"""Листы книги и предупреждения чтения (несовпадение `file_type`, ограничения xlsb).
|
||||
|
||||
`max_uncompressed_bytes` — лимит распакованного zip (xlsx/xlsb); `OsvParser` передаёт
|
||||
@ -72,7 +87,7 @@ def read_book(
|
||||
check_merged_area(sheets)
|
||||
if detected == "XLSB":
|
||||
warnings.append(OUTLINE_UNAVAILABLE_WARNING)
|
||||
return sheets, warnings
|
||||
return BookRead(sheets, warnings, detected)
|
||||
|
||||
|
||||
def _detect(data: bytes, file_type: FileType, uncompressed_limit: int) -> FileType:
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation
|
||||
from functools import lru_cache
|
||||
from typing import Literal, NamedTuple
|
||||
|
||||
@ -32,6 +32,7 @@ from src.parser.numbers import quantize_money, to_money
|
||||
__all__ = ["FormatResult", "display_amount"]
|
||||
|
||||
_PLACEHOLDERS = "0#?"
|
||||
_MAX_PARTS = 4
|
||||
_MAX_SECTIONS = 3 # четвёртая секция — текст, для чисел не используется
|
||||
_MAX_DECIMALS = 2 # Money = numeric(18,2)
|
||||
_SCALE_STEP = 3 # одна масштабирующая запятая = деление на 1000
|
||||
@ -57,9 +58,15 @@ _NO_VALUE = FormatResult("value", None)
|
||||
_EXCEL_DIGITS = 15 # Excel сравнивает числа с точностью 15 значащих цифр
|
||||
|
||||
|
||||
_EXCEL_CONTEXT = Context(prec=_EXCEL_DIGITS, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _excel_precision(value: Decimal) -> Decimal:
|
||||
"""Число до 15 значащих цифр: -1486533582.3500001 (артефакт float в xls) равно условию `[=-1486533582.35]`."""
|
||||
return Decimal(f"{value:.{_EXCEL_DIGITS}g}")
|
||||
"""Число до 15 значащих цифр, половина от нуля, как в Excel.
|
||||
|
||||
-1486533582.3500001 (артефакт float в xls) равно условию `[=-1486533582.35]`; 1500499.9999999998 — это 1500500.
|
||||
"""
|
||||
return _EXCEL_CONTEXT.plus(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@ -142,7 +149,9 @@ def _parse_section(text: str) -> _Section | None: # noqa: C901, PLR0912 - ли
|
||||
if found is not None:
|
||||
if condition is not None:
|
||||
return None
|
||||
condition = _Condition(found.group(1), Decimal(found.group(2)))
|
||||
condition = _Condition(found.group(1), _excel_precision(Decimal(found.group(2))))
|
||||
elif body and body[0] in "<>=":
|
||||
return None # условие, которое не разобрать (`[=1E-5]`, `[=1,5]`), нельзя молча принять за оформление
|
||||
index = end + 1 # цвет, [$-419] и прочее оформление игнорируются
|
||||
continue
|
||||
if char == "\\":
|
||||
@ -197,8 +206,8 @@ def _build_section(condition: _Condition | None, literal: list[str], kinds: list
|
||||
def _parse_format(fmt: str) -> tuple[_Section, ...] | None:
|
||||
"""Секции формата либо None, если формат не разобран."""
|
||||
parts = _split_sections(fmt)
|
||||
if parts is None:
|
||||
return None
|
||||
if parts is None or len(parts) > _MAX_PARTS:
|
||||
return None # Excel не принимает формат больше чем из четырёх секций
|
||||
sections: list[_Section] = []
|
||||
for part in parts[:_MAX_SECTIONS]:
|
||||
section = _parse_section(part)
|
||||
@ -233,7 +242,7 @@ def _pick_section(sections: tuple[_Section, ...], raw: Decimal) -> _Section | No
|
||||
def _scaled(raw: Decimal, section: _Section) -> FormatResult:
|
||||
digits = min(section.decimals, _MAX_DECIMALS)
|
||||
try:
|
||||
magnitude = abs(raw).scaleb(-_SCALE_STEP * section.scale)
|
||||
magnitude = abs(_excel_precision(raw)).scaleb(-_SCALE_STEP * section.scale)
|
||||
rounded = magnitude.quantize(Decimal(1).scaleb(-digits), rounding=ROUND_HALF_UP)
|
||||
except InvalidOperation: # число длиннее точности контекста: заведомо вне numeric(18,2)
|
||||
return _UNPARSED
|
||||
|
||||
@ -335,7 +335,7 @@ async def test_real_osv_through_worker_gives_done_and_valid_result_file(
|
||||
async def test_real_statements_through_worker_give_done_with_two_documents(
|
||||
core: Core, uploads: Uploads, fixtures_dir: Path
|
||||
) -> None:
|
||||
"""«Реальные формы через воркер»: книга НПЗ → ack → `done`; расхождения итогов — округление (OQ-138, мягкие)."""
|
||||
"""«Реальные формы через воркер»: книга НПЗ → ack → `done`; жёстких расхождений нет, мягкие — по golden."""
|
||||
request = _new_request(key_name="npz_2025_q3.xlsx")
|
||||
request["file_type"] = "XLSX"
|
||||
await uploads.put(request, (fixtures_dir / REAL_NPZ).read_bytes(), XLSX_CONTENT_TYPE)
|
||||
@ -356,9 +356,15 @@ async def test_real_statements_through_worker_give_done_with_two_documents(
|
||||
balance, income = stored["documents"]
|
||||
assert [balance["document_type"], income["document_type"]] == ["BALANCE", "INCOME"]
|
||||
assert balance["checks"]["relations_failed"] == [] and income["checks"]["relations_failed"] == []
|
||||
# расхождения в пределах округления (OQ-138) видны аналитику как мягкие
|
||||
assert "1600 = 1700" in {item["relation"] for item in balance["checks"]["soft_relations_failed"]}
|
||||
assert "2100 = 2110 - 2120" in {item["relation"] for item in income["checks"]["soft_relations_failed"]}
|
||||
# мягкие расхождения (OQ-138) видны аналитику; набор — как в golden НПЗ
|
||||
assert {item["relation"] for item in balance["checks"]["soft_relations_failed"]} >= {
|
||||
"1500 = 1510 + 1520 + 1530 + 1540 + 1550",
|
||||
"1410 = 14101 + 14102 + 14103 + 14104 + 14105",
|
||||
}
|
||||
assert {item["relation"] for item in income["checks"]["soft_relations_failed"]} >= {
|
||||
"2400 = 2300 - 2410 + 2420",
|
||||
"2410 = 2411 + 2412",
|
||||
}
|
||||
# лист с единственной строкой 2400 присоединён (O-9)
|
||||
assert "2400" in {line["line_code"] for line in income["lines"]}
|
||||
await core.assert_quiet(request)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# Причина needs_check — данные файла (страница без шапки, шаблон без сумм), не дефект разбора.
|
||||
# Значения сверены прогоном парсера на файлах (R-13): расхождения с первоначальным планом помечены «по факту».
|
||||
# Значения ББ/ОФР — как их показывает числовой формат ячейки (#42): рубли с копейками из 1С приходят в тысячах;
|
||||
# мягкие нарушения «в пределах округления» — следствие округления итогов до тысяч (OQ-138).
|
||||
# мягкие нарушения (в пределах допуска округления, OQ-138) статус не меняют; набор — по факту разбора, см. golden.
|
||||
files:
|
||||
"unagrande-co/2024-FY/Б.Б Лист 1.xls":
|
||||
organization: 'Акционерное общество "Унагранде Компани"'
|
||||
@ -15,7 +15,7 @@ files:
|
||||
organization: 'Акционерное общество "Унагранде Компани"'
|
||||
inn: "3249004697"
|
||||
documents:
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: []} # значения по формату в тыс. руб.; итоги округлены до тысяч: мягкие нарушения «в пределах округления» (OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: []} # значения по формату в тыс. руб.; мягко расходится 2400 = 2300 - 2410 + 2420 (в пределах допуска, OQ-138)
|
||||
"unagrande-co/2024-FY/ОФР. Лист 2 .xls":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
"unagrande-group/2024-FY/ББ. Лист 1 .xls":
|
||||
@ -36,7 +36,7 @@ files:
|
||||
organization: 'Акционерное общество "Унагранде Групп"'
|
||||
inn: "7728757141"
|
||||
documents:
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: []} # 2200 — малая разность больших слагаемых, округление (OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: []} # мягко расходятся 2400 = 2300 - 2410 + 2420 и 2410 = 2411 + 2412 (в пределах допуска, OQ-138)
|
||||
"unagrande-group/2025-Q3/ОФР. Лист 2 3кв.25.xls":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
"npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx":
|
||||
@ -44,8 +44,8 @@ files:
|
||||
inn: "2315995868"
|
||||
inn_optional_for: [INCOME]
|
||||
documents:
|
||||
- {type: BALANCE, status: done, complete: true, sheets: 5, failed_relations: []} # 1600 = 1700 на 1.94/9.02/2.48 — округление (OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, sheets: 3, failed_relations: []} # округление итогов (OQ-138); лист с единственной строкой 2400 присоединён (O-9)
|
||||
- {type: BALANCE, status: done, complete: true, sheets: 5, failed_relations: []} # мягко расходятся детализации 1150, 1250, 1410, 1500, 1510 (сверка с итогом в самой выгрузке, OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, sheets: 3, failed_relations: []} # мягко расходятся 2400 и 2410 (OQ-138); лист с единственной строкой 2400 присоединён (O-9)
|
||||
"mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx":
|
||||
organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"'
|
||||
inn: "7016006635"
|
||||
|
||||
@ -42,6 +42,11 @@ def shown(raw: str, fmt: str) -> tuple[str, Decimal | None]:
|
||||
("-872206881", '[=-872206880.07]"(872 207)";#,##0,', "-872207.00"),
|
||||
("-646532", '[=-646532]"(647)"', "-647.00"),
|
||||
("12345678", '[=-872206880.07]"(872 207)";#,##0,', "12346.00"),
|
||||
("1500499.9999999998", "#,##0,", "1501.00"),
|
||||
("1500", "[$-419]#,##0,", "2.00"),
|
||||
("1500", "\\(#,##0\\)", "1500.00"),
|
||||
("5", "[=5]0;[=5]0,", "5.00"),
|
||||
("-1500", "[$-419]#,##0,", "-2.00"),
|
||||
],
|
||||
)
|
||||
def test_value(raw: str, fmt: str, expected: str) -> None:
|
||||
@ -59,6 +64,8 @@ def test_value(raw: str, fmt: str, expected: str) -> None:
|
||||
("0", '[=0]"-";General'),
|
||||
("-0.79", '[=-0.79]"-"'),
|
||||
("0", '#,##0,;(#,##0,);"-"'),
|
||||
("0", ";;;"),
|
||||
("7", ";;;"),
|
||||
],
|
||||
)
|
||||
def test_no_value(raw: str, fmt: str) -> None:
|
||||
@ -78,6 +85,10 @@ def test_no_value(raw: str, fmt: str) -> None:
|
||||
("0", '[=0]"н/д"'),
|
||||
("5", '"н/д"'),
|
||||
("5", "0 руб"),
|
||||
("5", "[=1E-5]0"),
|
||||
("5", "[=1,5]0"),
|
||||
("5", "0 ₽"),
|
||||
("5", "0;0;0;@;0"),
|
||||
],
|
||||
)
|
||||
def test_unparsed(raw: str, fmt: str) -> None:
|
||||
@ -125,13 +136,15 @@ def test_huge_value_out_of_range_is_unparsed() -> None:
|
||||
|
||||
|
||||
def test_max_numeric_18_2_range() -> None:
|
||||
assert shown("9999999999999999", "0") == ("value", Decimal("9999999999999999.00"))
|
||||
"""Excel показывает 15 значащих цифр: 16-значное целое округляется до 1E+16, что уже вне numeric(18,2)."""
|
||||
assert shown("999999999999999", "0") == ("value", Decimal("999999999999999.00"))
|
||||
assert shown("9999999999999999", "0") == ("unparsed", None)
|
||||
assert shown("99999999999999999", "0") == ("unparsed", None)
|
||||
|
||||
|
||||
def test_scaled_huge_value_fits() -> None:
|
||||
assert shown("99999999999999999000", "0,") == ("unparsed", None)
|
||||
assert shown("9999999999999999000", "0,") == ("value", Decimal("9999999999999999.00"))
|
||||
assert shown("999999999999999000", "0,") == ("value", Decimal("999999999999999.00"))
|
||||
|
||||
|
||||
def test_condition_compares_with_excel_15_digit_precision() -> None:
|
||||
@ -141,3 +154,19 @@ def test_condition_compares_with_excel_15_digit_precision() -> None:
|
||||
result = display_amount(Decimal("-1486533582.3500001"), fmt)
|
||||
|
||||
assert result == FormatResult("value", Decimal("-1486534"))
|
||||
|
||||
|
||||
def test_value_is_rounded_to_15_digits_half_up_before_scaling() -> None:
|
||||
"""Артефакт float 1500499.9999999998 в Excel — 1500500, после деления на 1000 половина округляется вверх."""
|
||||
assert shown("1500499.9999999998", "#,##0,") == ("value", Decimal("1501"))
|
||||
|
||||
|
||||
def test_condition_operand_is_rounded_to_15_digits() -> None:
|
||||
"""Литерал «-» — «нет значения»; «x» дал бы unparsed по R-5 независимо от условия, поэтому берём прочерк."""
|
||||
result = display_amount(Decimal("0.1234567890123456"), '[=0.1234567890123456]"-"')
|
||||
|
||||
assert result == FormatResult("value", None)
|
||||
|
||||
|
||||
def test_condition_that_does_not_match_the_pattern_is_unparsed() -> None:
|
||||
assert shown("0", '[=1E-5]"x"') == ("unparsed", None)
|
||||
|
||||
@ -33,9 +33,9 @@ from src.parser.errors import ParseTimeoutError
|
||||
from src.parser.grid import Sheet
|
||||
from src.parser.hierarchy import BodyRow
|
||||
from src.parser.osv import OsvParser
|
||||
from src.parser.readers.xlsb import OUTLINE_UNAVAILABLE_WARNING
|
||||
from src.parser.rules import DEFAULT_RULES_DIR, HEADERS_FILE, Rules, load_rules
|
||||
from src.parser.statements.statement import StatementParser
|
||||
from tests.synthetic.xlsb_writer import write_xlsb
|
||||
|
||||
OPENING = "Сальдо на начало периода"
|
||||
TURNOVER = "Обороты за период"
|
||||
@ -769,12 +769,13 @@ def test_statement_sheets_are_reported_when_osv_sheet_fails(rules: Rules) -> Non
|
||||
assert "sheet=ББ форма не разобрана: книга завершилась ошибкой листа ОСВ «ОСВ 60»" in warnings_of(outcome)
|
||||
|
||||
|
||||
def test_xlsb_book_marks_formats_unreadable_for_statements_only(rules: Rules, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""#42, R-7: признак xlsb из read_book доходит до конвейера форм; ОСВ-документ предупреждения не получает."""
|
||||
sheets = (sheet_from_rows(BALANCE_ROWS, "ББ"), sheet_from_rows(mini_osv(), "ОСВ 60"))
|
||||
monkeypatch.setattr("src.parser.book.read_book", lambda *_args, **_kwargs: (sheets, [OUTLINE_UNAVAILABLE_WARNING]))
|
||||
|
||||
outcome = book(rules).parse(b"ignored", "XLSB")
|
||||
def test_xlsb_book_marks_formats_unreadable_for_statements_only(rules: Rules) -> None:
|
||||
"""#42, R-7: xlsb определяется по содержимому книги (без подмены предупреждений); ОСВ предупреждения не получает."""
|
||||
cells = {
|
||||
name: {position: cell.value for position, cell in sheet_from_rows(rows, name).cells.items()}
|
||||
for name, rows in (("ББ", BALANCE_ROWS), ("ОСВ 60", mini_osv()))
|
||||
}
|
||||
outcome = book(rules).parse(write_xlsb(cells), "XLSB")
|
||||
|
||||
xlsb_warnings = [w for w in warnings_of(outcome) if "формат чисел xlsb" in w]
|
||||
assert xlsb_warnings == ["sheet=ББ формат чисел xlsb не читается: значения как в файле"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user