fix(statements/lines): T10.I1+I2 — порог дробных значений в словаре, тесты границ
Перенесено FRACTIONAL_SHARE в api/rules/statement_headers.yaml как fractional_share_percent (1..100), добавлена валидация в StatementHeaderRules, целочисленное сравнение: fractional * 100 >= numeric * percent. Добавлены тесты границ: ровно 10% (2 из 20), ниже 10% (1 из 12), без падения при нулевых числовых значениях. Проверено RED мутацией: смена >= на > → падение теста граничного условия. Refs #14 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
parent
55b60f58b2
commit
aad6310b97
@ -88,3 +88,6 @@ line_codes:
|
||||
DISCLOSURE: [5000, 5999]
|
||||
min_share: 0.6
|
||||
min_codes: 3
|
||||
|
||||
# Порог для предупреждения о дробных значениях при единицах в тыс. руб. (процент; 1..100)
|
||||
fractional_share_percent: 10
|
||||
|
||||
@ -368,6 +368,7 @@ class StatementHeaderRules(_Model):
|
||||
auditor: list[Pattern]
|
||||
legal_form: list[Pattern]
|
||||
line_codes: _LineCodeRules
|
||||
fractional_share_percent: int
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_required_keys(self) -> "StatementHeaderRules":
|
||||
@ -379,6 +380,10 @@ class StatementHeaderRules(_Model):
|
||||
missing = expected - set(getattr(self, field_name))
|
||||
if missing:
|
||||
raise ValueError(f"{field_name}: нет обязательных ключей {sorted(missing)}")
|
||||
if not 1 <= self.fractional_share_percent <= 100: # noqa: PLR2004
|
||||
raise ValueError(
|
||||
f"fractional_share_percent: должна быть в диапазоне [1, 100], получено {self.fractional_share_percent}"
|
||||
)
|
||||
return self
|
||||
|
||||
def form_type(self, text: str) -> FormType | None:
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
Строка с кодом в колонке кодов → `StatementLine`; строки без кода не отдаются (ASSUMPTION(OQ-36)), но если среди них
|
||||
есть суммы — одно предупреждение. Повтор кода — остаётся первая строка, код в `duplicates`. Числа — `numbers.to_money`
|
||||
(Decimal, скобки → минус, прочерк → null); нечисловой текст — `string_value`, `is_recognized = false`. Единицы не
|
||||
нормализуются: при «тыс.» в единицах и доле значений с дробной частью ≥ `FRACTIONAL_SHARE` — предупреждение.
|
||||
нормализуются: при «тыс.» в единицах и доле значений с дробной частью ≥ порогу (из словаря) — предупреждение.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
@ -14,10 +14,9 @@ from src.parser.numbers import to_money
|
||||
from src.parser.rules import Rules, normalize_text
|
||||
from src.parser.statements.columns import TableLayout, cell_label, line_code
|
||||
|
||||
__all__ = ["FRACTIONAL_SHARE", "LinesResult", "collect_lines"]
|
||||
__all__ = ["LinesResult", "collect_lines"]
|
||||
|
||||
_FULL_CONFIDENCE = 100
|
||||
FRACTIONAL_SHARE = 0.1
|
||||
_THOUSANDS = "тыс"
|
||||
|
||||
|
||||
@ -108,7 +107,7 @@ def collect_lines( # noqa: PLR0913
|
||||
units
|
||||
and _THOUSANDS in normalize_text(units)
|
||||
and result.numeric_values
|
||||
and result.fractional_values / result.numeric_values >= FRACTIONAL_SHARE
|
||||
and result.fractional_values * 100 >= result.numeric_values * rules.statement_headers.fractional_share_percent
|
||||
):
|
||||
result.warnings.append(
|
||||
f"значений с дробной частью при единице тыс. руб.: {result.fractional_values} из {result.numeric_values}"
|
||||
|
||||
@ -13,6 +13,10 @@ _FULL_CONFIDENCE = 100
|
||||
_FRACTIONAL_VALUES_COUNT = 4
|
||||
_NUMERIC_VALUES_COUNT = 9
|
||||
_SKIPPED_WITH_SUMS_COUNT = 1
|
||||
_BOUNDARY_NUMERIC_VALUES = 20
|
||||
_BOUNDARY_FRACTIONAL_VALUES = 2
|
||||
_BELOW_THRESHOLD_NUMERIC_VALUES = 12
|
||||
_BELOW_THRESHOLD_FRACTIONAL_VALUES = 1
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@ -90,3 +94,75 @@ def test_line_without_name_column(rules: Rules) -> None:
|
||||
result = collect_lines(sheet, rules, layout, layout.first_data_row, 4)
|
||||
|
||||
assert [line.line_name for line in result.lines] == ["", "", ""]
|
||||
|
||||
|
||||
def test_fractional_warning_at_boundary_10_percent(rules: Rules) -> None:
|
||||
"""Ровно 10% (2 из 20) → должно быть предупреждение."""
|
||||
# With 2 columns: 10 data rows = 20 numeric values
|
||||
# To get exactly 10%, need 2 fractional values
|
||||
rows = [
|
||||
[None, "Наименование показателя", "Код", "2024", "2023"],
|
||||
[None, "Показатель 1", "1150", 100, 100],
|
||||
[None, "Показатель 2", "1151", 200, 200],
|
||||
[None, "Показатель 3", "1152", 300, 300],
|
||||
[None, "Показатель 4", "1153", 400, 400],
|
||||
[None, "Показатель 5", "1154", Decimal("500.50"), 500], # 1 fractional
|
||||
[None, "Показатель 6", "1155", 600, Decimal("600.50")], # 1 fractional (2 из 20 = 10%)
|
||||
[None, "Показатель 7", "1156", 700, 700],
|
||||
[None, "Показатель 8", "1157", 800, 800],
|
||||
[None, "Показатель 9", "1158", 900, 900],
|
||||
[None, "Показатель 10", "1159", 1000, 1000],
|
||||
]
|
||||
sheet = sheet_from_rows(rows)
|
||||
layout = find_layout(sheet, rules, 1, len(rows))
|
||||
|
||||
result = collect_lines(sheet, rules, layout, layout.first_data_row, len(rows), units="в тыс. рублей")
|
||||
|
||||
# Expect 20 numeric values and 2 fractional (10%)
|
||||
assert result.numeric_values == _BOUNDARY_NUMERIC_VALUES and result.fractional_values == _BOUNDARY_FRACTIONAL_VALUES
|
||||
assert any("дробной частью" in warning for warning in result.warnings)
|
||||
|
||||
|
||||
def test_no_fractional_warning_below_10_percent(rules: Rules) -> None:
|
||||
"""1 из 12 (8.33%) → предупреждения нет."""
|
||||
# With 2 columns: 6 data rows = 12 numeric values
|
||||
# 1 fractional = 8.33% < 10%
|
||||
rows = [
|
||||
[None, "Наименование показателя", "Код", "2024", "2023"],
|
||||
[None, "Показатель 1", "1150", 100, 100],
|
||||
[None, "Показатель 2", "1151", 200, 200],
|
||||
[None, "Показатель 3", "1152", 300, 300],
|
||||
[None, "Показатель 4", "1153", 400, 400],
|
||||
[None, "Показатель 5", "1154", 500, 500],
|
||||
[None, "Показатель 6", "1155", 600, Decimal("600.50")], # 1 из 12 = 8.33%
|
||||
]
|
||||
sheet = sheet_from_rows(rows)
|
||||
layout = find_layout(sheet, rules, 1, len(rows))
|
||||
|
||||
result = collect_lines(sheet, rules, layout, layout.first_data_row, len(rows), units="в тыс. рублей")
|
||||
|
||||
# Expect 12 numeric values and 1 fractional (8.33% < 10%)
|
||||
assert (
|
||||
result.numeric_values == _BELOW_THRESHOLD_NUMERIC_VALUES
|
||||
and result.fractional_values == _BELOW_THRESHOLD_FRACTIONAL_VALUES
|
||||
)
|
||||
assert not any("дробной частью" in warning for warning in result.warnings)
|
||||
|
||||
|
||||
def test_no_crash_with_zero_numeric_values(rules: Rules) -> None:
|
||||
"""Ноль числовых значений — без падения."""
|
||||
# No numeric values (only recognized None and unrecognized text)
|
||||
rows = [
|
||||
[None, "Наименование показателя", "Код", "2024", "2023"],
|
||||
[None, "Показатель 1", "1150", None, None], # recognized None
|
||||
[None, "Показатель 2", "1151", "–", "–"], # recognized empty (dash)
|
||||
[None, "Показатель 3", "1152", "н/д", "н/д"], # unrecognized
|
||||
]
|
||||
sheet = sheet_from_rows(rows)
|
||||
layout = find_layout(sheet, rules, 1, len(rows))
|
||||
|
||||
result = collect_lines(sheet, rules, layout, layout.first_data_row, len(rows), units="в тыс. рублей")
|
||||
|
||||
assert result.numeric_values == 0
|
||||
assert result.fractional_values == 0
|
||||
assert not any("дробной частью" in warning for warning in result.warnings)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user