105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
import re
|
||
from pathlib import Path
|
||
|
||
import openpyxl
|
||
import pytest
|
||
|
||
from synthgen import dictionary, inn
|
||
from synthgen.dictionary import build
|
||
|
||
FIXTURES = Path("/Users/andrey.epifancev/projects/awp-ca/fixtures")
|
||
REAL_ECP = FIXTURES / "directories/ecp-export-russkiy-biskvit.xlsx"
|
||
REAL_OSV_DIRS = [FIXTURES / "osv/rb", FIXTURES / "osv/schastl"]
|
||
_FORMS = re.compile(r"\b(ООО|АО|ПАО|ЗАО|ОАО|ИП)\b", re.IGNORECASE)
|
||
_PARENS = re.compile(r"\(.*?(\)|$)")
|
||
|
||
|
||
def _norm(text: str) -> str:
|
||
"""Имя без формы, скобочного хвоста, регистра, ё и пунктуации."""
|
||
text = _PARENS.sub(" ", text)
|
||
text = _FORMS.sub(" ", text)
|
||
return re.sub(r"[^0-9a-zа-яё]+", " ", text.lower().replace("ё", "е")).strip()
|
||
|
||
|
||
def _real_names() -> tuple[set[str], int]:
|
||
"""Нормализованные имена из ЕЦП и колонки A первых листов всех ОСВ; второе значение — число прочитанных файлов."""
|
||
names: set[str] = set()
|
||
files = 0
|
||
wb = openpyxl.load_workbook(REAL_ECP, read_only=True)
|
||
for ws in wb.worksheets:
|
||
rows = ws.iter_rows(values_only=True)
|
||
header = next(rows, None)
|
||
if not header or "Организация" not in header:
|
||
continue
|
||
col = list(header).index("Организация")
|
||
names.update(_norm(str(r[col])) for r in rows if r[col])
|
||
files += 1
|
||
for root_dir in REAL_OSV_DIRS:
|
||
for path in sorted(root_dir.rglob("*.xlsx")):
|
||
if path.name.startswith("~$") or path.name.startswith("chart-of-accounts"):
|
||
continue
|
||
wb = openpyxl.load_workbook(path, read_only=True)
|
||
ws = wb.worksheets[0]
|
||
names.update(_norm(str(r[0])) for r in ws.iter_rows(max_col=1, values_only=True) if r and r[0])
|
||
files += 1
|
||
names.discard("")
|
||
return names, files
|
||
|
||
|
||
def _fio_keys(real: set[str]) -> set[str]:
|
||
"""Из «фамилия имя отчество» — «фамилия и о» (как в словаре: `Цветков А.П.`)."""
|
||
keys = set()
|
||
for name in real:
|
||
tokens = name.split()
|
||
if len(tokens) == 3 and all(len(t) > 1 for t in tokens):
|
||
keys.add(f"{tokens[0]} {tokens[1][0]} {tokens[2][0]}")
|
||
elif len(tokens) == 3 and len(tokens[1]) == 1 and len(tokens[2]) == 1:
|
||
keys.add(name)
|
||
return keys
|
||
|
||
|
||
def test_dictionary_size_and_kinds():
|
||
orgs = dictionary.load()
|
||
assert len(orgs) == 400
|
||
assert sum(o.kind == "банк" for o in orgs) == 30
|
||
assert 30 <= sum(o.form == "ИП" for o in orgs) <= 50
|
||
names = [o.name.lower() for o in orgs]
|
||
assert len(set(names)) == len(names)
|
||
|
||
|
||
def test_dictionary_inn():
|
||
for o in dictionary.load():
|
||
assert inn.is_valid(o.inn)
|
||
if o.form == "ИП":
|
||
assert len(o.inn) == 12 and o.kpp == ""
|
||
number = int(o.inn[2:-2])
|
||
else:
|
||
assert len(o.inn) == 10
|
||
assert re.fullmatch(r"\d{9}", o.kpp)
|
||
number = int(o.inn[2:-1])
|
||
assert 100001 <= number <= 109999
|
||
|
||
|
||
def test_dictionary_not_real():
|
||
if not FIXTURES.is_dir():
|
||
pytest.skip("нет каталога fixtures основного checkout")
|
||
real, files = _real_names()
|
||
assert files > 10 and len(real) > 500, "реальные фикстуры прочитаны не полностью"
|
||
fio = _fio_keys(real)
|
||
padded = [f" {n} " for n in real]
|
||
clash = []
|
||
for o in dictionary.load():
|
||
key = _norm(o.name)
|
||
if o.form == "ИП":
|
||
if key in fio or key in real:
|
||
clash.append(o.name)
|
||
elif key in real or any(f" {_norm(o.root)} " in p for p in padded):
|
||
clash.append(o.name)
|
||
assert clash == []
|
||
|
||
|
||
def test_build_is_reproducible(tmp_path):
|
||
out = tmp_path / "companies.csv"
|
||
build.write_csv(out)
|
||
assert out.read_bytes() == dictionary.CSV_PATH.read_bytes()
|