Files
contracts/testgen/gen_opus.py
T

648 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔
"""
gen_opus.py — Генератор тестовых файлов по 30 кейсам Опуса.
Блок A: classify (11 кейсов, ~11 файлов)
Блок B: group (9 кейсов, ~19 файлов)
Блок C: compare (10 кейсов, ~20 файлов)
Все документы — полноценные: шапка, преамбула с реквизитами сторон,
2-4 раздела, таблицы услуг (6-10 строк), подписи.
"""
import os, json, sys
sys.path.insert(0, os.path.dirname(__file__))
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
OUT = os.path.join(os.path.dirname(__file__), "opus_cases")
MANIFEST = {}
def ensure_dir(path):
os.makedirs(path, exist_ok=True)
def save(doc, folder, fname, info):
path = os.path.join(OUT, folder, fname)
ensure_dir(os.path.dirname(path))
doc.save(path)
MANIFEST[fname] = info
print(f" ✓ {folder}/{fname}")
def _add_title(doc, text):
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(text)
run.bold = True
run.font.size = Pt(14)
def _add_para(doc, text):
doc.add_paragraph(text)
def _add_table(doc, rows_data, headers=None):
if headers:
rows_data = [headers] + rows_data
table = doc.add_table(rows=len(rows_data), cols=len(rows_data[0]))
table.style = 'Table Grid'
for ri, row in enumerate(rows_data):
for ci, val in enumerate(row):
table.rows[ri].cells[ci].text = str(val)
def _add_signatures(doc):
_add_para(doc, "")
_add_para(doc, "10. ПОДПИСИ СТОРОН")
_add_para(doc, "")
_add_para(doc, " ИСПОЛНИТЕЛЬ ЗАКАЗЧИК")
_add_para(doc, " ____________________ ____________________")
_add_para(doc, " Генеральный директор Генеральный директор")
_add_para(doc, " ____________________ ____________________")
_add_para(doc, " М.П. М.П.")
def _preamble(doc, num, date, party1, party2):
"""Стандартная преамбула договора."""
_add_para(doc, f'ООО "НУБЕС", именуемое в дальнейшем "Исполнитель", в лице')
_add_para(doc, f'Генерального директора Степаненко Василия Сергеевича,')
_add_para(doc, f'действующего на основании Устава, с одной стороны, и')
_add_para(doc, f'{party1}, именуемое в дальнейшем "Заказчик", в лице')
_add_para(doc, f'{party2}, действующего на основании Устава, с другой стороны,')
_add_para(doc, f'заключили настоящий Договор о нижеследующем:')
def _contract_sections(doc):
_add_para(doc, "")
_add_para(doc, "1. ПРЕДМЕТ ДОГОВОРА")
_add_para(doc, "1.1. Исполнитель обязуется оказывать услуги по предоставлению")
_add_para(doc, "вычислительных ресурсов ЦОД, а Заказчик обязуется оплачивать")
_add_para(doc, "оказанные услуги в порядке и на условиях настоящего Договора.")
_add_para(doc, "1.2. Состав и стоимость Услуг определяются в Спецификации")
_add_para(doc, "(Приложение № 1), являющейся неотъемлемой частью Договора.")
_add_para(doc, "1.3. Место оказания услуг: ЦОД по адресу: г. Москва, ул. 8 Марта, д. 16, стр. 1.")
_add_para(doc, "")
_add_para(doc, "2. ПРАВА И ОБЯЗАННОСТИ СТОРОН")
_add_para(doc, "2.1. Исполнитель обязуется: обеспечить круглосуточный доступ;")
_add_para(doc, "поддерживать температурный режим и электропитание; осуществлять охрану.")
_add_para(doc, "2.2. Заказчик обязуется: своевременно оплачивать услуги;")
_add_para(doc, "соблюдать правила техники безопасности при посещении ЦОД.")
_add_para(doc, "")
_add_para(doc, "3. СТОИМОСТЬ УСЛУГ И ПОРЯДОК РАСЧЁТОВ")
_add_para(doc, "3.1. Стоимость услуг определяется Спецификацией (Приложение № 1).")
_add_para(doc, "3.2. Оплата производится ежемесячно до 10 числа месяца,")
_add_para(doc, "следующего за отчётным, на основании счёта и акта.")
_add_para(doc, "3.3. НДС не облагается (УСН).")
_add_para(doc, "")
_add_para(doc, "4. СРОК ДЕЙСТВИЯ ДОГОВОРА")
_add_para(doc, "4.1. Договор вступает в силу с даты подписания")
_add_para(doc, "и действует до 31 декабря текущего года.")
_add_para(doc, "4.2. Автоматическая пролонгация на следующий год при отсутствии")
_add_para(doc, "письменного уведомления о расторжении за 30 дней.")
# --- Базовые спецификации ---
BASE8 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
def _gen_contract_spec(num, date):
"""Сгенерировать спецификацию к договору (8 строк)."""
return _make_spec(num, SPEC8, date)
def _make_contract(num, date, counterparty_full, counterparty_person,
modify_preamble=None, hide_counterparty=False):
"""Полноценный договор."""
doc = Document()
_add_title(doc, "ДОГОВОР")
_add_para(doc, f"на оказание технологических услуг № {num}")
_add_para(doc, "")
_add_para(doc, f"г. Москва {date}")
_add_para(doc, "")
if hide_counterparty:
_add_para(doc, "Настоящий Договор регулирует отношения Сторон по оказанию услуг ЦОД.")
elif modify_preamble:
modify_preamble(doc, num, date)
else:
_preamble(doc, num, date, counterparty_full, counterparty_person)
_contract_sections(doc)
_add_signatures(doc)
return doc
def _make_supplement(parent_num, text, parent_date=None):
"""Допсоглашение."""
doc = Document()
_add_title(doc, "ДОПОЛНИТЕЛЬНОЕ СОГЛАШЕНИЕ")
_add_para(doc, f"к Договору № {parent_num}" + (f" от {parent_date}" if parent_date else ""))
_add_para(doc, "")
_add_para(doc, 'ООО "НУБЕС" (Исполнитель) и Заказчик, именуемые совместно "Стороны",')
_add_para(doc, 'заключили настоящее Дополнительное соглашение о нижеследующем:')
_add_para(doc, "")
_add_para(doc, text)
_add_para(doc, "")
_add_para(doc, "Настоящее Соглашение вступает в силу с даты подписания Сторонами")
_add_para(doc, "и является неотъемлемой частью Договора.")
_add_signatures(doc)
return doc
def _make_spec(parent_num, rows, parent_date=None):
"""Спецификация с таблицей."""
doc = Document()
_add_title(doc, "Приложение № 1")
_add_para(doc, f"к Договору № {parent_num}" + (f" от {parent_date}" if parent_date else ""))
_add_para(doc, "Спецификация и стоимость услуг")
_add_para(doc, "")
_add_table(doc, rows, ["№", "Наименование", "Цена, руб.", "Объем", "Сумма, руб.", "Дата начала"])
_add_signatures(doc)
return doc
# ОБЩАЯ спецификация для всех договоров A/B
SPEC8 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# ═══════════════════════════════════════════════════════════════
# БЛОК A — Classify (11 кейсов)
# ═══════════════════════════════════════════════════════════════
def case1_baseline():
doc = _make_contract("XXX001-03700", "15 марта 2025 г.",
'ЗАО "ТехноПлюс"', 'Генерального директора ФИО')
save(doc, "block_a", "договор-XXX001-03700.docx",
{"case": 1, "type": "contract", "own_number": "XXX001-03700",
"counterparty": 'ЗАО "ТехноПлюс"'})
save(_gen_contract_spec("XXX001-03700", "15.03.2025"), "block_a",
"спецификация-XXX001-03700_v1.docx",
{"case": 1, "type": "specification", "parent_number": "XXX001-03700"})
def case2_cyrillic():
doc = _make_contract("МЭС-123/2024", "10 января 2024 г.",
'АО "МосЭнергоСбыт"', 'Генерального директора Петрова А.В.')
save(doc, "block_a", "договор-МЭС.docx",
{"case": 2, "type": "contract", "own_number": "МЭС-123/2024",
"counterparty": 'АО "МосЭнергоСбыт"'})
save(_gen_contract_spec("МЭС-123/2024", "10.01.2024"), "block_a",
"спецификация-МЭС.docx",
{"case": 2, "type": "specification", "parent_number": "МЭС-123/2024"})
def case3_nonstandard():
doc = Document()
_add_title(doc, "СОГЛАШЕНИЕ об оказании услуг связи")
_add_para(doc, "№ SVC-77 от 01.02.2025")
_add_para(doc, "")
_add_para(doc, 'ООО "НУБЕС" и ЗАО "Клиент-Сервис" заключили настоящее Соглашение.')
_add_signatures(doc)
save(doc, "block_a", "договор-нестандарт.docx",
{"case": 3, "type": "contract", "own_number": "SVC-77",
"counterparty": 'ЗАО "Клиент-Сервис"'})
save(_gen_contract_spec("SVC-77", "01.02.2025"), "block_a",
"спецификация-SVC-77.docx",
{"case": 3, "type": "specification", "parent_number": "SVC-77"})
def case4_addendum():
doc = _make_supplement("XXX003-01300", "Изменить Спецификацию. Утвердить новую редакцию Приложения № 1.", "05.06.2024")
save(doc, "block_a", "допник-XXX003-01300.docx",
{"case": 4, "type": "supplement", "own_number": "1", "parent_number": "XXX003-01300"})
def case5_spec():
"""Кейс 5: Спецификация (генерируется в case1, здесь — заглушка для нумерации)."""
pass
def case6_no_counterparty():
doc = _make_contract("NC-09", "03 марта 2025 г.",
"", "", hide_counterparty=True)
save(doc, "block_a", "договор-без-стороны.docx",
{"case": 6, "type": "contract", "counterparty": None})
save(_gen_contract_spec("NC-09", "03.03.2025"), "block_a",
"спецификация-NC-09.docx",
{"case": 6, "type": "specification", "parent_number": "NC-09"})
def case7_date_text():
doc = _make_contract("DT-15", '«пятнадцатого» марта 2025 года',
'ООО "Ресурс-ЦОД"', 'Генерального директора Сидорова С.С.')
save(doc, "block_a", "договор-дата-прописью.docx",
{"case": 7, "type": "contract", "doc_date": "2025-03-15",
"counterparty": 'ООО "Ресурс-ЦОД"'})
save(_gen_contract_spec("DT-15", "15.03.2025"), "block_a",
"спецификация-DT-15.docx",
{"case": 7, "type": "specification", "parent_number": "DT-15"})
def case8_two_dates():
doc = _make_contract("TD-21", "01 апреля 2025 г.",
'АО "ТехноДата"', 'Генерального директора Козлова Д.Д.')
# добавить дату окончания
_add_para(doc, "4.3. Договор действует до 31.12.2026.")
save(doc, "block_a", "договор-две-даты.docx",
{"case": 8, "type": "contract", "doc_date": "2025-04-01",
"counterparty": 'АО "ТехноДата"'})
save(_gen_contract_spec("TD-21", "01.04.2025"), "block_a",
"спецификация-TD-21.docx",
{"case": 8, "type": "specification", "parent_number": "TD-21"})
def case9_long_preamble():
doc = Document()
_add_title(doc, "ДОГОВОР")
_add_para(doc, "на оказание технологических услуг № LATE-99")
_add_para(doc, "")
lorem = ("Настоящий Договор разработан в соответствии с Гражданским кодексом РФ, "
"Федеральным законом «О связи», Федеральным законом «Об информации», "
"а также иными нормативными правовыми актами Российской Федерации. ") * 8
_add_para(doc, lorem[:1600])
_add_para(doc, "")
_add_para(doc, 'ООО "Поздний Контрагент" и ООО "НУБЕС" заключили Договор.')
_contract_sections(doc)
_add_signatures(doc)
save(doc, "block_a", "договор-длинная-преамбула.docx",
{"case": 9, "type": "contract", "own_number": "LATE-99",
"counterparty": 'ООО "Поздний Контрагент"'})
save(_gen_contract_spec("LATE-99", "15.06.2025"), "block_a",
"спецификация-LATE-99.docx",
{"case": 9, "type": "specification", "parent_number": "LATE-99"})
def case10_noisy():
doc = Document()
_add_title(doc, "ДОГОВОР")
_add_para(doc, "Исх. № 456 от 12.05.2025")
_add_para(doc, "Лицензия № 789 от 10.01.2025")
_add_para(doc, "Счёт № 321 от 15.05.2025")
_add_para(doc, "")
_add_para(doc, 'ООО "НУБЕС" и ЗАО "МультиНомер" заключили настоящий Договор.')
_add_para(doc, "ДОГОВОР № MN-01 от 12.05.2025")
_contract_sections(doc)
_add_signatures(doc)
save(doc, "block_a", "договор-много-номеров.docx",
{"case": 10, "type": "contract", "own_number": "MN-01",
"counterparty": 'ЗАО "МультиНомер"'})
save(_gen_contract_spec("MN-01", "12.05.2025"), "block_a",
"спецификация-MN-01.docx",
{"case": 10, "type": "specification", "parent_number": "MN-01"})
def case11_trash():
doc = Document()
_add_title(doc, "Акт сверки взаимных расчётов")
_add_para(doc, "за 1 квартал 2025 г.")
_add_para(doc, "")
_add_para(doc, "Стороны: ООО «НУБЕС» и ООО «Контрагент»")
_add_para(doc, "Сальдо на 01.01.2025: 150 000,00 руб.")
_add_para(doc, "Оборот за период: 45 000,00 руб.")
_add_para(doc, "Сальдо на 31.03.2025: 195 000,00 руб.")
_add_signatures(doc)
save(doc, "block_a", "акт-сверки.docx",
{"case": 11, "type": "other"})
# ═══════════════════════════════════════════════════════════════
# review case line numbers before running!
print("hello gen_opus")
# ═══════════════════════════════════════════════════════════════
# БЛОК B — Group (9 кейсов, ~19 файлов)
# ═══════════════════════════════════════════════════════════════
def case12_normalize():
"""Кейс 12: Разделители нормализуются"""
d1 = _make_contract("МЭС-123/2024", "10 января 2024 г.",
'АО "МосЭнергоСбыт"', 'Генерального директора Петрова А.В.')
save(d1, "block_b", "договор-МЭС-норм.docx", {"case": 12, "own": "МЭС-123/2024", "counterparty": 'АО "МосЭнергоСбыт"'})
save(_gen_contract_spec("МЭС-123/2024", "10.01.2024"), "block_b",
"спецификация-МЭС-норм.docx", {"case": 12, "type": "specification", "parent_number": "МЭС-123/2024"})
d2 = _make_supplement("МЭС 123/2024", "Изменить стоимость услуг с 01.02.2024.", "10.01.2024")
save(d2, "block_b", "допник-МЭС-норм.docx", {"case": 12, "parent": "МЭС 123/2024"})
def case13_cyrillic_O():
d1 = _make_contract("XXX001-03700", "01 января 2025 г.",
'ЗАО "ТехноПлюс"', 'ФИО')
save(d1, "block_b", "договор-03700.docx", {"case": 13, "own": "XXX001-03700", "counterparty": 'ЗАО "ТехноПлюс"'})
save(_gen_contract_spec("XXX001-03700", "01.01.2025"), "block_b",
"спецификация-03700.docx", {"case": 13, "type": "specification", "parent_number": "XXX001-03700"})
d2 = _make_supplement("XXX001-О3700", "Увеличить количество стоек до 3.") # кириллическая О
save(d2, "block_b", "допник-О3700.docx", {"case": 13, "parent": "XXX001-О3700"})
def case14_latin_C():
d1 = _make_contract("CBC-10", "01 февраля 2025 г.",
'ООО "СиБиСи"', 'Генерального директора')
save(d1, "block_b", "договор-CBC.docx", {"case": 14, "own": "CBC-10", "counterparty": 'ООО "СиБиСи"'})
save(_gen_contract_spec("CBC-10", "01.02.2025"), "block_b",
"спецификация-CBC.docx", {"case": 14, "type": "specification", "parent_number": "CBC-10"})
d2 = _make_supplement("СВС-10", "Изменить тарифы.") # кириллическая С
save(d2, "block_b", "допник-СВС.docx", {"case": 14, "parent": "СВС-10"})
def case15_yo():
d1 = _make_contract("ЁЖ-5", "01 марта 2025 г.",
'ООО "Ёжик-Телеком"', 'Генерального директора')
save(d1, "block_b", "договор-ЁЖ.docx", {"case": 15, "own": "ЁЖ-5", "counterparty": 'ООО "Ёжик-Телеком"'})
save(_gen_contract_spec("ЁЖ-5", "01.03.2025"), "block_b",
"спецификация-ЁЖ.docx", {"case": 15, "type": "specification", "parent_number": "ЁЖ-5"})
d2 = _make_supplement("ЁЖ-5", "Добавить услугу резервного копирования.")
save(d2, "block_b", "допник-ЁЖ.docx", {"case": 15, "parent": "ЁЖ-5"})
def case16_no_parent():
doc = _make_supplement("договора оказания услуг", "Стороны договорились об изменении стоимости услуг.")
save(doc, "block_b", "допник-без-родителя.docx", {"case": 16, "type": "supplement", "parent": None})
def case17_own_match():
d1 = _make_contract("GR-50", "01 апреля 2025 г.",
'ООО "ГринРайт"', 'Генерального директора')
save(d1, "block_b", "договор-GR50.docx", {"case": 17, "own": "GR-50", "counterparty": 'ООО "ГринРайт"'})
save(_gen_contract_spec("GR-50", "01.04.2025"), "block_b",
"спецификация-GR50.docx", {"case": 17, "type": "specification", "parent_number": "GR-50"})
d2 = _make_supplement("GR-50", "Изменить спецификацию с 15.04.2025.") # own=GR-50
save(d2, "block_b", "допник-GR50.docx", {"case": 17, "own": "GR-50"})
def case18_collision():
d1 = _make_contract("AB-12", "01 мая 2025 г.",
'ООО "Альфа-Бета"', 'Генерального директора')
save(d1, "block_b", "договор-AB12.docx", {"case": 18, "own": "AB-12", "counterparty": 'ООО "Альфа-Бета"'})
save(_gen_contract_spec("AB-12", "01.05.2025"), "block_b",
"спецификация-AB12.docx", {"case": 18, "type": "specification", "parent_number": "AB-12"})
d2 = _make_contract("A-B12", "01 мая 2025 г.",
'ООО "А-Бет"', 'Генерального директора')
save(d2, "block_b", "договор-A-B12.docx", {"case": 18, "own": "A-B12", "counterparty": 'ООО "А-Бет"'})
save(_gen_contract_spec("A-B12", "01.05.2025"), "block_b",
"спецификация-A-B12.docx", {"case": 18, "type": "specification", "parent_number": "A-B12"})
d3 = _make_supplement("AB12", "Изменить условия.")
save(d3, "block_b", "допник-AB12.docx", {"case": 18, "parent": "AB12"})
def case19_family():
d1 = _make_contract("FAM-01", "10 января 2025 г.",
'ЗАО "Фэмили-Групп"', 'Генерального директора')
save(d1, "block_b", "договор-FAM.docx", {"case": 19, "date": "2025-01-10", "counterparty": 'ЗАО "Фэмили-Групп"'})
d2 = _make_spec("FAM-01", SPEC8, "01.02.2025")
save(d2, "block_b", "спека-FAM.docx", {"case": 19, "date": "2025-02-01"})
d3 = _make_supplement("FAM-01", "Дополнительное соглашение № 1. Увеличить количество стоек до 3.")
save(d3, "block_b", "допник-1-FAM.docx", {"case": 19, "date": "2025-03-01"})
d4 = _make_supplement("FAM-01", "Дополнительное соглашение № 2. Добавить услугу резервного копирования.")
save(d4, "block_b", "допник-2-FAM.docx", {"case": 19, "date": "2025-05-01"})
def case20_suffix():
doc = _make_supplement("XXX003-01300", "Изменить спецификацию. Утвердить новую редакцию.")
save(doc, "block_b", "допник-1-XXX003-01300_2.docx", {"case": 20, "own": "1", "parent": "XXX003-01300"})
# ═══════════════════════════════════════════════════════════════
# БЛОК C — Compare (10 кейсов, ~20 файлов, 6-10 строк)
# ═══════════════════════════════════════════════════════════════
def _compare_spec_v1(parent_num, rows):
"""Спецификация v1 для compare-кейса."""
return _make_spec(parent_num, rows)
def _compare_spec_v2(parent_num, rows):
"""Изменённая спецификация для compare-кейса (подаётся как допник с таблицей)."""
doc = Document()
_add_title(doc, "Приложение № 1 (новая редакция)")
_add_para(doc, f"к Дополнительному соглашению к Договору № {parent_num}")
_add_para(doc, "Спецификация и стоимость услуг")
_add_para(doc, "")
_add_table(doc, rows, ["№", "Наименование", "Цена, руб.", "Объем", "Сумма, руб.", "Дата начала"])
_add_signatures(doc)
return doc
# ---- Базовая спецификация 8 строк для ВСЕХ compare-кейсов ----
# (одинаковая v1, меняется только v2 по кейсу)
SPEC_V1 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# CASE 21: цена +1 копейка → UPDATE (1 строка)
SPEC21_V2 = [
["1", "Аренда стойко-места 42U", "45 000,01", "2", "90 000,02", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# CASE 22: объём → 0 для услуги 3 (DELETE) и цена меняется для услуги 3 (UPDATE)
SPEC22_V2 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
# ["3", "IP-адрес IPv4 (1 шт.)"] — DELETE
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# CASE 23: переименование строки 5
SPEC23_V2 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 расширенный", "3 000,00", "1", "3 000,00", "01.05.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# CASE 24: полная замена — 3 строки совсем другие
SPEC24_V2 = [
["1", "Выделенный сервер Dell R740xd", "85 000,00", "1", "85 000,00", "01.05.2025"],
["2", "Лицензия VMware vSphere 8", "12 000,00", "2", "24 000,00", "01.05.2025"],
["3", "Резервное копирование 1 ТБ", "4 000,00", "1", "4 000,00", "01.05.2025"],
]
# CASE 25: ADD — +2 строки
SPEC25_V2 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
# ADD:
["9", "Резервное копирование 1 ТБ", "4 000,00", "1", "4 000,00", "01.06.2025"],
["10", "DDoS-защита 10 Гбит/с", "15 000,00", "1", "15 000,00", "01.06.2025"],
]
# CASE 26: DELETE — без пункта 7 и 8
SPEC26_V2 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
# ["7", "Кросс-соединение медное"] — DELETE
# ["8", "Аренда полки 19\""] — DELETE
]
# CASE 27: только объём — 3→8 для услуги 3
SPEC27_V2 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "12", "4 200,00", "01.08.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# CASE 28: UNRESOLVED — услуга-призрак (не совпадает ни с одной v1)
SPEC28_V2 = [
["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
# ADD — услуга-призрак (нет в v1)
["9", "VPN-туннель site-to-site", "3 000,00", "1", "3 000,00", "01.03.2025"],
]
# CASE 29: формат чисел (пробелы)
SPEC29_V2 = [
["1", "Аренда стойко-места 42U", "45 500,00", "2", "91 000,00", "01.01.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
# CASE 30: робастность JSON — та же specs что и 21
SPEC30_V2 = [
["1", "Аренда стойко-места 42U", "51 000,00", "2", "102 000,00", "01.10.2025"],
["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"],
["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"],
["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"],
["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"],
["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"],
["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"],
["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"],
]
def case21_kopeck():
save(_compare_spec_v1("CMP-021", SPEC_V1), "block_c", "спека-цена-v1.docx",
{"case": 21, "version": 1, "parent_number": "CMP-021"})
save(_compare_spec_v2("CMP-021", SPEC21_V2), "block_c", "допник-цена.docx",
{"case": 21, "type": "supplement", "parent_number": "CMP-021"})
def case22_zero_qty():
save(_compare_spec_v1("CMP-022", SPEC_V1), "block_c", "спека-обнуление-v1.docx",
{"case": 22, "version": 1, "parent_number": "CMP-022"})
save(_compare_spec_v2("CMP-022", SPEC22_V2), "block_c", "допник-обнуление.docx",
{"case": 22, "type": "supplement", "parent_number": "CMP-022"})
def case23_rename():
save(_compare_spec_v1("CMP-023", SPEC_V1), "block_c", "спека-переименование-v1.docx",
{"case": 23, "version": 1, "parent_number": "CMP-023"})
save(_compare_spec_v2("CMP-023", SPEC23_V2), "block_c", "допник-переименование.docx",
{"case": 23, "type": "supplement", "parent_number": "CMP-023"})
def case24_full_replace():
save(_compare_spec_v1("CMP-024", SPEC_V1), "block_c", "спека-полная-v1.docx",
{"case": 24, "version": 1, "parent_number": "CMP-024"})
save(_compare_spec_v2("CMP-024", SPEC24_V2), "block_c", "допник-новая-редакция.docx",
{"case": 24, "type": "supplement", "parent_number": "CMP-024"})
def case25_add():
save(_compare_spec_v1("CMP-025", SPEC_V1), "block_c", "спека-добавление-v1.docx",
{"case": 25, "version": 1, "parent_number": "CMP-025"})
save(_compare_spec_v2("CMP-025", SPEC25_V2), "block_c", "допник-добавление.docx",
{"case": 25, "type": "supplement", "parent_number": "CMP-025"})
def case26_delete():
save(_compare_spec_v1("CMP-026", SPEC_V1), "block_c", "спека-удаление-v1.docx",
{"case": 26, "version": 1, "parent_number": "CMP-026"})
save(_compare_spec_v2("CMP-026", SPEC26_V2), "block_c", "допник-удаление.docx",
{"case": 26, "type": "supplement", "parent_number": "CMP-026"})
def case27_qty_only():
save(_compare_spec_v1("CMP-027", SPEC_V1), "block_c", "спека-объём-v1.docx",
{"case": 27, "version": 1, "parent_number": "CMP-027"})
save(_compare_spec_v2("CMP-027", SPEC27_V2), "block_c", "допник-объём.docx",
{"case": 27, "type": "supplement", "parent_number": "CMP-027"})
def case28_unresolved():
save(_compare_spec_v1("CMP-028", SPEC_V1), "block_c", "спека-призрак-v1.docx",
{"case": 28, "version": 1, "parent_number": "CMP-028"})
save(_compare_spec_v2("CMP-028", SPEC28_V2), "block_c", "допник-призрак.docx",
{"case": 28, "type": "supplement", "parent_number": "CMP-028"})
def case29_format():
save(_compare_spec_v1("CMP-029", SPEC_V1), "block_c", "спека-формат-v1.docx",
{"case": 29, "version": 1, "parent_number": "CMP-029"})
save(_compare_spec_v2("CMP-029", SPEC29_V2), "block_c", "допник-формат-чисел.docx",
{"case": 29, "type": "supplement", "parent_number": "CMP-029"})
def case30_robust():
save(_compare_spec_v1("CMP-030", SPEC_V1), "block_c", "спека-роб-v1.docx",
{"case": 30, "version": 1, "parent_number": "CMP-030"})
save(_compare_spec_v2("CMP-030", SPEC30_V2), "block_c", "допник-роб.docx",
{"case": 30, "type": "supplement", "parent_number": "CMP-030"})
# ═══════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════
def main():
print("=" * 50)
print("Генератор кейсов Опуса")
print("=" * 50)
print("\n── Блок A: Classify ──")
case1_baseline(); case2_cyrillic(); case3_nonstandard()
case4_addendum(); case5_spec(); case6_no_counterparty()
case7_date_text(); case8_two_dates(); case9_long_preamble()
case10_noisy(); case11_trash()
print("\n── Блок B: Group ──")
case12_normalize(); case13_cyrillic_O(); case14_latin_C()
case15_yo(); case16_no_parent(); case17_own_match()
case18_collision(); case19_family(); case20_suffix()
print("\n── Блок C: Compare ──")
case21_kopeck(); case22_zero_qty(); case23_rename()
case24_full_replace(); case25_add(); case26_delete()
case27_qty_only(); case28_unresolved(); case29_format()
case30_robust()
manifest_path = os.path.join(OUT, "manifest.json")
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(MANIFEST, f, ensure_ascii=False, indent=2)
print(f"\n{'=' * 50}")
print(f"Всего файлов: {len(MANIFEST)}")
print(f"Вывод: {OUT}/")
print(f"{'=' * 50}")
if __name__ == "__main__":
main()