From f215bab9b0fdeeb2b176db5bcc75df8216b9f22a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 12 Nov 2026 23:41:42 +0400 Subject: [PATCH] =?UTF-8?q?synthgen:=20=D1=81=D0=BB=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=20=D0=B1=D0=B5=D0=B7=20=D1=81=D0=BE=D0=B2=D0=BF?= =?UTF-8?q?=D0=B0=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=81=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=D0=BC=D0=B8,=20=D1=83=D1=81=D0=B8=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=82=D0=B5=D1=81=D1=82,=20prefix?= =?UTF-8?q?=20=D0=B2=20Member=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- synthgen/dictionary/build.py | 4 +- synthgen/dictionary/companies.csv | 14 +++--- synthgen/group.py | 6 +-- synthgen/tests/test_dictionary.py | 82 +++++++++++++++++++++---------- 4 files changed, 69 insertions(+), 37 deletions(-) diff --git a/synthgen/dictionary/build.py b/synthgen/dictionary/build.py index 9b2fd70..989dcc9 100644 --- a/synthgen/dictionary/build.py +++ b/synthgen/dictionary/build.py @@ -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 = ["Е.И.", "А.П.", "Н.С.", "В.Д.", "О.А.", "М.В.", "И.Н.", "С.К.", "Т.Р.", "Д.Л."] diff --git a/synthgen/dictionary/companies.csv b/synthgen/dictionary/companies.csv index 3c373a8..84c9fc4 100644 --- a/synthgen/dictionary/companies.csv +++ b/synthgen/dictionary/companies.csv @@ -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;;услуги diff --git a/synthgen/group.py b/synthgen/group.py index bdfce99..19481f2 100644 --- a/synthgen/group.py +++ b/synthgen/group.py @@ -15,6 +15,7 @@ _CENT = Decimal("0.01") @dataclass(frozen=True) class Member: code: str # C01, C02, … + prefix: str # префикс ГК org: Org # запись словаря, давшая корень имени name: str # АО " <корень>" name_1c: str # <корень> АО — как 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}", diff --git a/synthgen/tests/test_dictionary.py b/synthgen/tests/test_dictionary.py index 853a120..ab644f3 100644 --- a/synthgen/tests/test_dictionary.py +++ b/synthgen/tests/test_dictionary.py @@ -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 == []