648 lines
39 KiB
Python
648 lines
39 KiB
Python
# ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔
|
||
"""
|
||
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()
|