ОСВ: контрольная сумма ИНН и ИНН строки (#35)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a2744b3e40
commit
1e5f28bd50
@ -17,7 +17,17 @@ from src.parser.detect import INN_LENGTHS, inn_from_cell
|
||||
from src.parser.grid import Cell
|
||||
from src.parser.rules import Rules, source_offset
|
||||
|
||||
__all__ = ["MONEY_Q", "MoneyParse", "column_inn", "inn_digits", "inn_only", "split_inn_suffix", "to_money"]
|
||||
__all__ = [
|
||||
"MONEY_Q",
|
||||
"MoneyParse",
|
||||
"column_inn",
|
||||
"inn_checksum_ok",
|
||||
"inn_digits",
|
||||
"inn_only",
|
||||
"row_inn",
|
||||
"split_inn_suffix",
|
||||
"to_money",
|
||||
]
|
||||
|
||||
MONEY_Q = Decimal("0.01")
|
||||
# ИНН регионов 01–09, записанный числом, теряет ведущий ноль: 9 или 11 цифр дополняются нулём слева (только при
|
||||
@ -46,6 +56,15 @@ _MINUS_SIGN = "−"
|
||||
_SINGLE_DIGIT_RUN = re.compile(r"^\D*([0-9]+(?:[\s\u00a0\u202f][0-9]+)*)\D*$")
|
||||
_DIGIT_GROUP_SEPARATOR = re.compile(r"[\s\u00a0\u202f]")
|
||||
|
||||
# Контрольная сумма ИНН (алгоритм ФНС): весовые коэффициенты контрольных разрядов; остаток от деления на 11, затем на 10
|
||||
_INN10_WEIGHTS = (2, 4, 10, 3, 5, 9, 4, 6, 8)
|
||||
_INN12_WEIGHTS_FIRST = (7, 2, 4, 10, 3, 5, 9, 4, 6, 8)
|
||||
_INN12_WEIGHTS_SECOND = (3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8)
|
||||
_CHECKSUM_MODULUS = 11
|
||||
_CHECK_DIGIT_MODULUS = 10
|
||||
_INN_LEGAL_LENGTH = 10
|
||||
_INN_PERSON_LENGTH = 12
|
||||
|
||||
|
||||
class MoneyParse(NamedTuple):
|
||||
value: Decimal | None
|
||||
@ -177,3 +196,34 @@ def inn_only(text: str, rules: Rules) -> str | None:
|
||||
if match is not None:
|
||||
return match.group("inn")
|
||||
return None
|
||||
|
||||
|
||||
def _check_digit(digits: str, weights: tuple[int, ...]) -> int:
|
||||
total = sum(int(digit) * weight for digit, weight in zip(digits, weights, strict=False))
|
||||
return total % _CHECKSUM_MODULUS % _CHECK_DIGIT_MODULUS
|
||||
|
||||
|
||||
def inn_checksum_ok(inn: str) -> bool:
|
||||
"""Контрольная сумма ИНН по алгоритму ФНС: у 10 цифр — последний разряд, у 12 — два последних (#35).
|
||||
|
||||
Проверяется только у строки ИНН без подсказки в шапке (`inn_rows`): ИНН колонки и хвоста названия — без проверки.
|
||||
"""
|
||||
if not inn.isdecimal(): # isdecimal: int() по каждому символу гарантированно сработает
|
||||
return False
|
||||
if len(inn) == _INN_LEGAL_LENGTH:
|
||||
return _check_digit(inn, _INN10_WEIGHTS) == int(inn[-1])
|
||||
if len(inn) == _INN_PERSON_LENGTH:
|
||||
first_ok = _check_digit(inn, _INN12_WEIGHTS_FIRST) == int(inn[-2])
|
||||
second_ok = _check_digit(inn, _INN12_WEIGHTS_SECOND) == int(inn[-1])
|
||||
return first_ok and second_ok
|
||||
return False
|
||||
|
||||
|
||||
def row_inn(cell: Cell, rules: Rules) -> str | None:
|
||||
"""ИНН строки, где написан только он (строка ИНН, `inn_rows`): число — 10/12 цифр или 9/11 с потерянным ведущим
|
||||
нулём (R-24, #34, как у колонки ИНН); текст — по `inn_only_row` («7701234560», «ИНН 7701234560 / 770101001»)."""
|
||||
if cell.kind == "number":
|
||||
return inn_digits(cell)
|
||||
if cell.kind != "text":
|
||||
return None
|
||||
return inn_only(cell.text, rules)
|
||||
|
||||
@ -7,7 +7,8 @@ import pytest
|
||||
|
||||
from src.parser import detect
|
||||
from src.parser.grid import EMPTY_CELL, Cell
|
||||
from src.parser.numbers import MONEY_Q, MoneyParse, inn_digits, to_money
|
||||
from src.parser.numbers import MONEY_Q, MoneyParse, inn_checksum_ok, inn_digits, row_inn, to_money
|
||||
from src.parser.rules import Rules, load_rules
|
||||
|
||||
MAX_WARNING_LENGTH = 100
|
||||
|
||||
@ -237,3 +238,50 @@ def test_only_extraction_restores_lost_leading_zero() -> None:
|
||||
|
||||
def test_detector_rule_stays_strict_for_prefixed_text() -> None:
|
||||
assert detect.inn_from_cell(text("ИНН 352819549855")) is None
|
||||
|
||||
|
||||
# --- Контрольная сумма ИНН и ИНН строки (#35) ---
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("inn", "ok"),
|
||||
[
|
||||
("7701234560", True),
|
||||
("7707654321", True),
|
||||
("2370012036", True),
|
||||
("0123456788", True),
|
||||
("500100732259", True),
|
||||
("7701234567", False),
|
||||
("5001234567", False),
|
||||
("500100732258", False),
|
||||
("770123456", False),
|
||||
("77012345601", False),
|
||||
("77012345a0", False),
|
||||
],
|
||||
)
|
||||
def test_inn_checksum_ok(inn: str, ok: bool) -> None:
|
||||
assert inn_checksum_ok(inn) is ok
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def rules() -> Rules:
|
||||
return load_rules()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cell", "expected"),
|
||||
[
|
||||
(num(7701234560), "7701234560"),
|
||||
(num(123456788), "0123456788"), # 9 цифр числом — потерянный ведущий ноль (R-24, #34)
|
||||
(num(50010073225), "050010073225"), # 11 цифр числом
|
||||
(text("7701234560"), "7701234560"),
|
||||
(text("ИНН 7701234560"), "7701234560"),
|
||||
(text("ИНН: 7701234560 / 770101001"), "7701234560"),
|
||||
(text("123456788"), None), # текст из 9 цифр нулём не дополняется
|
||||
(text("ООО Альфа 7701234560"), None), # не только ИНН
|
||||
(num(Decimal("7701234560.5")), None),
|
||||
(EMPTY_CELL, None),
|
||||
],
|
||||
)
|
||||
def test_row_inn(rules: Rules, cell: Cell, expected: str | None) -> None:
|
||||
assert row_inn(cell, rules) == expected
|
||||
|
||||
@ -135,3 +135,5 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
|
||||
| 8 | 13.10.2026 | Пустая подпись — кандидат только с подсказкой и только в положении «под, в дереве» (R-1 #32) | Без подсказки пустая строка ничего не значит; другое положение у пустого ИНН в 1С не встречалось | Пустая строка ИНН «над» при подсказке — остаётся наименованием без текста |
|
||||
| 9 | 13.10.2026 | С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — нераспознанный реквизит: `OTHER`, `needs_check` | Сохраняет поведение #32 (тест «нерез.»): в 1С на этом месте стоит строка реквизита | Контрагент с одним договором с теми же суммами в файле с подсказкой «под», но без строки реквизита — `needs_check` вместо `done` |
|
||||
| 10 | 13.10.2026 | Строка ИНН с детьми под наименованием: решает подсказка; без неё — «под» (ИНН родителю, дети — договоры), если родитель по схеме `COUNTERPARTY`, иначе «над» | Иначе строка реквизита с договорами под ней (#32 со схемой «Контрагенты, Контрагенты.ИНН, Договоры» без подсказки) отдала бы ИНН договору; строка ИНН между подразделением и контрагентом — «над» | Файл без подсказки и без схемы, где ИНН под контрагентом и у строки ИНН есть дети, — ИНН уйдёт ребёнку; `needs_check` не ставится |
|
||||
| 11 | 13.10.2026 | Task 1 (план): `__all__` модуля `numbers.py` дополнен `inn_checksum_ok` и `row_inn`; тело `inn_checksum_ok` при 12 цифрах — два булевых локала вместо одного `and`-выражения (ruff format не принял переносы из плана) | Публичные имена модуля экспортируются по соглашению файла; результат проверки не меняется | Нет: тест `test_inn_checksum_ok` покрывает оба разряда |
|
||||
| 12 | 13.10.2026 | `inn_checksum_ok`: проверка «только цифры» через `isdecimal()`, не `isdigit()` | `isdigit()` пропускает «²» и подобные, на которых `int()` падает с ValueError | Нет: вход — строки ИНН из `[0-9]`-выражений, на практике не встречается |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user