synthgen: словарь без совпадений с реальными именами, усиленный тест, prefix в Member (#38)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-12 23:41:42 +04:00
parent b2a89394c9
commit f215bab9b0
4 changed files with 69 additions and 37 deletions

View File

@ -26,7 +26,7 @@ _SECTORS: dict[str, list[tuple[str, str]]] = {
"торговля": [("Рынок", "m"), ("Склад", "m"), ("Лавка", "f"), ("Ярмарка", "f"), ("Базар", "m"),
("Торг", "m"), ("Магазин", "m"), ("Прилавок", "m"), ("Пассаж", "m"), ("Товар", "m")],
"логистика": [("Путь", "m"), ("Маршрут", "m"), ("Фургон", "m"), ("Трасса", "f"), ("Обоз", "m"),
("Тягач", "m"), ("Рейс", "m"), ("Перевал", "m"), ("Караван", "m"), ("Магистраль", "f")],
("Тягач", "m"), ("Рейс", "m"), ("Перевал", "m"), ("Караван", "m"), ("Эстакада", "f")],
"производство": [("Завод", "m"), ("Цех", "m"), ("Станок", "m"), ("Мельница", "f"), ("Пекарня", "f"),
("Кузница", "f"), ("Фабрика", "f"), ("Горн", "m"), ("Верфь", "f"), ("Мануфактура", "f")],
"услуги": [("Сервис", "m"), ("Бюро", "n"), ("Центр", "m"), ("Консалт", "m"), ("Профи", "m"),
@ -40,7 +40,7 @@ _BANK_WORDS = ["Банк", "Кредитный Банк", "Деловой Бан
_SURNAMES = [
"Авдонина", "Бражников", "Ветрова", "Голубев", "Дроздова", "Ермаков", "Жарова", "Зуев", "Игнатова", "Копылов",
"Лаврова", "Мартынов", "Норина", "Овчаров", "Пименова", "Рябов", "Сорокина", "Тарасов", "Устинова", "Фролов",
"Хромова", "Цветков", "Чернова", "Шаров", "Щукина", "Юдин", "Яковлева", "Баранов", "Волкова", "Гусев",
"Хромова", "Цветаев", "Чернова", "Шаров", "Щукина", "Юдин", "Яковлева", "Баранов", "Волкова", "Гусев",
"Денисова", "Елисеев", "Зайцева", "Исаев", "Калинина", "Леонов", "Маслова", "Назаров", "Орлова", "Панов",
]
_INITIALS = ["Е.И.", "А.П.", "Н.С.", "В.Д.", "О.А.", "М.В.", "И.Н.", "С.К.", "Т.Р.", "Д.Л."]

View File

@ -145,19 +145,19 @@ name;form;inn;kpp;kind
Заревная Арка ООО;ООО;0001001448;004401001;строительство
Лазурный Амбар АО;АО;0001001455;004501001;агро
Лазурный Товар ООО;ООО;0001001462;004601001;торговля
Лазурная Магистраль ООО;ООО;0001001470;004701001;логистика
Лазурная Эстакада ООО;ООО;0001001470;004701001;логистика
Лазурная Мануфактура ООО;ООО;0001001487;004801001;производство
Лазурный Приём ООО;ООО;0001001494;004901001;услуги
Лазурный Мост АО;АО;0001001504;005001001;строительство
Рубиновый Амбар ООО;ООО;0001001511;005101001;агро
Рубиновый Товар ООО;ООО;0001001529;005201001;торговля
Рубиновая Магистраль ПАО;ПАО;0001001536;005301001;логистика
Рубиновая Эстакада ПАО;ПАО;0001001536;005301001;логистика
Рубиновая Мануфактура ООО;ООО;0001001543;005401001;производство
Рубиновый Приём АО;АО;0001001550;005501001;услуги
Рубиновый Мост ООО;ООО;0001001568;005601001;строительство
Лунный Амбар ООО;ООО;0001001575;005701001;агро
Лунный Товар ООО;ООО;0001001582;005801001;торговля
Лунная Магистраль ООО;ООО;0001001590;005901001;логистика
Лунная Эстакада ООО;ООО;0001001590;005901001;логистика
Лунная Мануфактура АО;АО;0001001600;006001001;производство
Лунный Приём ООО;ООО;0001001617;006101001;услуги
Лунный Мост ООО;ООО;0001001624;006201001;строительство
@ -307,19 +307,19 @@ name;form;inn;kpp;kind
Лунная Арка ПАО;ПАО;0001003068;000601001;строительство
Янтарный Амбар ООО;ООО;0001003075;000701001;агро
Янтарный Товар ООО;ООО;0001003082;000801001;торговля
Янтарная Магистраль ООО;ООО;0001003090;000901001;логистика
Янтарная Эстакада ООО;ООО;0001003090;000901001;логистика
Янтарная Мануфактура АО;АО;0001003100;001001001;производство
Янтарный Приём ООО;ООО;0001003117;001101001;услуги
Янтарный Мост ООО;ООО;0001003124;001201001;строительство
Серебряный Амбар ООО;ООО;0001003131;001301001;агро
Серебряный Товар ООО;ООО;0001003149;001401001;торговля
Серебряная Магистраль АО;АО;0001003156;001501001;логистика
Серебряная Эстакада АО;АО;0001003156;001501001;логистика
Серебряная Мануфактура ООО;ООО;0001003163;001601001;производство
Серебряный Приём ООО;ООО;0001003170;001701001;услуги
Серебряный Мост ООО;ООО;0001003188;001801001;строительство
Северный Амбар ООО;ООО;0001003195;001901001;агро
Северный Товар АО;АО;0001003205;002001001;торговля
Северная Магистраль ООО;ООО;0001003212;002101001;логистика
Северная Эстакада ООО;ООО;0001003212;002101001;логистика
Северная Мануфактура ООО;ООО;0001003220;002201001;производство
Северный Приём ПАО;ПАО;0001003237;002301001;услуги
Северный Мост ООО;ООО;0001003244;002401001;строительство
@ -350,7 +350,7 @@ name;form;inn;kpp;kind
Устинова Т.Р. ИП;ИП;000010034915;;услуги
Фролов Д.Л. ИП;ИП;000010035010;;торговля
Хромова Е.И. ИП;ИП;000010035193;;логистика
Цветков А.П. ИП;ИП;000010035267;;агро
Цветаев А.П. ИП;ИП;000010035267;;агро
Чернова Н.С. ИП;ИП;000010035330;;строительство
Шаров В.Д. ИП;ИП;000010035404;;производство
Щукина О.А. ИП;ИП;000010035588;;услуги

1 name form inn kpp kind
145 Заревная Арка ООО ООО 0001001448 004401001 строительство
146 Лазурный Амбар АО АО 0001001455 004501001 агро
147 Лазурный Товар ООО ООО 0001001462 004601001 торговля
148 Лазурная Магистраль ООО Лазурная Эстакада ООО ООО 0001001470 004701001 логистика
149 Лазурная Мануфактура ООО ООО 0001001487 004801001 производство
150 Лазурный Приём ООО ООО 0001001494 004901001 услуги
151 Лазурный Мост АО АО 0001001504 005001001 строительство
152 Рубиновый Амбар ООО ООО 0001001511 005101001 агро
153 Рубиновый Товар ООО ООО 0001001529 005201001 торговля
154 Рубиновая Магистраль ПАО Рубиновая Эстакада ПАО ПАО 0001001536 005301001 логистика
155 Рубиновая Мануфактура ООО ООО 0001001543 005401001 производство
156 Рубиновый Приём АО АО 0001001550 005501001 услуги
157 Рубиновый Мост ООО ООО 0001001568 005601001 строительство
158 Лунный Амбар ООО ООО 0001001575 005701001 агро
159 Лунный Товар ООО ООО 0001001582 005801001 торговля
160 Лунная Магистраль ООО Лунная Эстакада ООО ООО 0001001590 005901001 логистика
161 Лунная Мануфактура АО АО 0001001600 006001001 производство
162 Лунный Приём ООО ООО 0001001617 006101001 услуги
163 Лунный Мост ООО ООО 0001001624 006201001 строительство
307 Лунная Арка ПАО ПАО 0001003068 000601001 строительство
308 Янтарный Амбар ООО ООО 0001003075 000701001 агро
309 Янтарный Товар ООО ООО 0001003082 000801001 торговля
310 Янтарная Магистраль ООО Янтарная Эстакада ООО ООО 0001003090 000901001 логистика
311 Янтарная Мануфактура АО АО 0001003100 001001001 производство
312 Янтарный Приём ООО ООО 0001003117 001101001 услуги
313 Янтарный Мост ООО ООО 0001003124 001201001 строительство
314 Серебряный Амбар ООО ООО 0001003131 001301001 агро
315 Серебряный Товар ООО ООО 0001003149 001401001 торговля
316 Серебряная Магистраль АО Серебряная Эстакада АО АО 0001003156 001501001 логистика
317 Серебряная Мануфактура ООО ООО 0001003163 001601001 производство
318 Серебряный Приём ООО ООО 0001003170 001701001 услуги
319 Серебряный Мост ООО ООО 0001003188 001801001 строительство
320 Северный Амбар ООО ООО 0001003195 001901001 агро
321 Северный Товар АО АО 0001003205 002001001 торговля
322 Северная Магистраль ООО Северная Эстакада ООО ООО 0001003212 002101001 логистика
323 Северная Мануфактура ООО ООО 0001003220 002201001 производство
324 Северный Приём ПАО ПАО 0001003237 002301001 услуги
325 Северный Мост ООО ООО 0001003244 002401001 строительство
350 Устинова Т.Р. ИП ИП 000010034915 услуги
351 Фролов Д.Л. ИП ИП 000010035010 торговля
352 Хромова Е.И. ИП ИП 000010035193 логистика
353 Цветков А.П. ИП Цветаев А.П. ИП ИП 000010035267 агро
354 Чернова Н.С. ИП ИП 000010035330 строительство
355 Шаров В.Д. ИП ИП 000010035404 производство
356 Щукина О.А. ИП ИП 000010035588 услуги

View File

@ -15,6 +15,7 @@ _CENT = Decimal("0.01")
@dataclass(frozen=True)
class Member:
code: str # C01, C02, …
prefix: str # префикс ГК
org: Org # запись словаря, давшая корень имени
name: str # АО "<prefix> <корень>"
name_1c: str # <prefix> <корень> АО — как 1С пишет контрагента
@ -48,9 +49,7 @@ def member_kpp(i: int) -> str:
def variant_spelling(member: Member) -> str:
"""Вариантное написание члена группы в ОСВ без ИНН: корень, префикс, форма без кавычек (решение 17)."""
root = member.org.root
prefix = member.name_1c[: -(len(root) + len(member.org.form) + 2)]
return f"{root} {prefix} {member.org.form}"
return f"{member.org.root} {member.prefix} {member.org.form}"
def build_group(cfg: Config, orgs: list[Org]) -> Group:
@ -80,6 +79,7 @@ def build_group(cfg: Config, orgs: list[Org]) -> Group:
members.append(
Member(
code=f"C{i:02d}",
prefix=cfg.prefix,
org=org,
name=f'{org.form} "{cfg.prefix} {org.root}"',
name_1c=f"{cfg.prefix} {org.root} {org.form}",

View File

@ -7,16 +7,57 @@ import pytest
from synthgen import dictionary, inn
from synthgen.dictionary import build
REAL_ECP = Path("/Users/andrey.epifancev/projects/awp-ca/fixtures/directories/ecp-export-russkiy-biskvit.xlsx")
REAL_OSV_DIR = Path("/Users/andrey.epifancev/projects/awp-ca/fixtures/osv/rb/2024-Q1")
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
@ -39,30 +80,21 @@ def test_dictionary_inn():
assert 100001 <= number <= 109999
def _real_names() -> set[str]:
names: set[str] = set()
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])
for path in sorted(REAL_OSV_DIR.glob("*.xlsx")):
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])
names.discard("")
return names
def test_dictionary_not_real():
if not REAL_ECP.exists() or not REAL_OSV_DIR.is_dir():
pytest.skip("нет реальных фикстур")
real = _real_names()
assert len(real) > 50
clash = [o.name for o in dictionary.load() if _norm(o.name) in 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 == []