Files
ss-tools/scripts/generate_financial_arrears_data.py
busya 7947700188 fix(translate): add EXPLORE logging for LLM JSON parse failures
Preview + executor now log the raw LLM response (first 1000 chars)
when JSON parsing fails, enabling debugging of malformed LLM output.

Previously ValueErrors from JSON parse failures were silently returned
as 400 without any diagnostic data in logs.
2026-05-15 09:15:36 +03:00

408 lines
23 KiB
Python
Executable File
Raw Permalink 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.

#!/usr/bin/env python3
"""
Generate test data for dm_view.financial_arrears on ClickHouse (ss-dev).
Inserts 5000 rows with comment_text in Russian (80%), Chinese (10%), English (10%),
200-1000 chars each, using financial/judicial terminology related to RUSAL overdue debt.
Usage:
cd backend && source .venv/bin/activate
python ../scripts/generate_financial_arrears_data.py
"""
import os
import random
import json
from datetime import date, datetime, timedelta
BACKEND_DIR = os.path.join(os.path.dirname(__file__), "..", "backend")
os.chdir(BACKEND_DIR)
from src.core.config_manager import ConfigManager
from src.plugins.translate.superset_executor import SupersetSqlLabExecutor
# ── Seed for reproducibility ────────────────────────────────
RANDOM_SEED = 42
random.seed(RANDOM_SEED)
# ── Counterparties (контрагенты РУСАЛа) ─────────────────────
COUNTERPARTIES = [
'АО "РУСАЛ Красноярск"',
'АО "РУСАЛ Братск"',
'АО "РУСАЛ Саяногорск"',
'АО "РУСАЛ Новокузнецк"',
'ООО "РУСАЛ Торговый Дом"',
'ООО "РУСАЛ Логистика"',
'АО "РУСАЛ Урал"',
'ООО "Инженерно-технологический центр РУСАЛ"',
'ООО "РУСАЛ Глобал Менеджмент"',
'АО "Боксит Тимана"',
'ООО "РУС-Инжиниринг"',
'ООО "Торговый дом Алюминий"',
'АО "Сибирско-Уральская алюминиевая компания"',
'ООО "Алюминиевые технологии"',
'ООО "Металлургический завод"',
'ПАО "Корпорация ВСМПО-АВИСМА"',
'ООО "Транспортная компания"',
'ООО "Энергосбыт"',
'АО "РУСАЛ Ачинск"',
'АО "РУСАЛ Волгоград"',
]
SUPPLY_COUNTRIES = [
"Россия", "Россия", "Россия", "Россия", "Россия",
"Россия", "Россия", "Россия",
"Китай", "Китай",
"Казахстан", "Гвинея", "Австралия", "Бразилия",
"Индия", "Индонезия", "ОАЭ", "Ямайка",
]
# ── Russian financial/judicial comment templates ─────────────
RUSSIAN_COMMENT_PARTS = {
"openers": [
"В отношении задолженности",
"По состоянию на отчётную дату",
"Согласно акту сверки взаимных расчётов",
"В соответствии с решением арбитражного суда",
"На основании исполнительного листа",
"По договору поставки",
"В рамках дела о банкротстве",
"Согласно претензионному порядку",
"По данным бухгалтерского учёта",
"В связи с неисполнением обязательств",
"По результатам инвентаризации дебиторской задолженности",
"Согласно определению Арбитражного суда Красноярского края",
"В соответствии с Федеральным законом № 127-ФЗ",
"На основании решения налогового органа",
"По товарной накладной",
],
"debtors": [
"предприятия РУСАЛ",
"дочернего общества РУСАЛ",
"алюминиевого завода",
"обогатительной фабрики",
"транспортного цеха",
"энергосбытовой компании",
"торгового дома",
"литейного производства",
"глинозёмного комбината",
"рудодобывающего предприятия",
"ремонтно-механического завода",
"строительно-монтажного управления",
],
"amounts": [
"на сумму 15 780 432,50 руб.",
"в размере 42 156 789,00 руб.",
"на общую сумму 8 934 561,23 руб.",
"в сумме 231 567 890,00 руб.",
"на 5 678 912,34 руб.",
"в размере 89 345 678,00 руб.",
"на сумму 1 234 567,89 руб.",
"в объёме 67 890 123,45 руб.",
"на сумму 345 678 901,20 руб.",
"в размере 12 456 789,00 руб.",
"на общую сумму 54 321 098,76 руб.",
"в сумме 987 654 321,00 руб.",
],
"terms": [
"со сроком просрочки свыше 180 календарных дней",
"просрочка исполнения составляет более 120 дней",
"с нарушением срока оплаты свыше 90 дней",
"период просрочки превышает 150 дней",
"с просрочкой платежа более 210 дней",
"нарушение условий договора по срокам оплаты свыше 60 дней",
"срок неисполнения обязательств превышает 270 дней",
"период задержки платежа составляет более 300 дней",
"с просрочкой исполнения денежного обязательства свыше 365 дней",
],
"legal_actions": [
"направлена досудебная претензия",
"подан иск в арбитражный суд",
"вынесено решение о взыскании задолженности",
"возбуждено исполнительное производство",
"наложен арест на расчётные счета",
"подано заявление о признании банкротом",
"заключено мировое соглашение",
"утверждён график реструктуризации долга",
"приостановлено исполнение обязательств",
"введена процедура наблюдения",
"открыто конкурсное производство",
"обжаловано в апелляционной инстанции",
"подана кассационная жалоба",
"выдан исполнительный лист",
"направлено требование о погашении",
"инициирована процедура медиации",
"проведена сверка задолженности",
],
"classifications": [
"Задолженность признана безнадёжной к взысканию",
"Дебиторская задолженность классифицирована как сомнительная",
"Отнесена к категории проблемных активов",
"Признана нереальной к взысканию в связи с ликвидацией должника",
"Классифицирована как просроченная свыше 3 лет",
"Отнесена на формирование резерва по сомнительным долгам",
"Признана маловероятной к взысканию в досудебном порядке",
"Классифицирована как высокорискованная дебиторская задолженность",
"Включена в реестр требований кредиторов",
"Признана текущей задолженностью в процедуре банкротства",
],
"reserve_mentions": [
"Сформирован резерв по сомнительным долгам",
"Резерв начислен в размере 100% от суммы задолженности",
"Оценочный резерв составляет 50% от номинальной суммы",
"Резервирование произведено на основании МСФО (IAS) 39",
"Создан оценочный резерв под обесценение дебиторской задолженности",
"Начислен резерв по сомнительной задолженности согласно РСБУ",
"Формирование резерва приостановлено до решения суда",
"Корректировка резерва произведена в отчётном периоде",
],
"closers": [
"Требуется дополнительный анализ платёжеспособности",
"Проведён анализ финансового состояния должника",
"Рекомендовано списание за счёт чистой прибыли",
"Направлено уведомление о расторжении договора",
"Ожидается решение вышестоящего суда",
"Рассмотрение дела отложено на неопределённый срок",
"Проводится мониторинг финансового состояния контрагента",
"Инициирована внесудебная процедура урегулирования",
"Подготовлено заключение о целесообразности взыскания",
"Рекомендовано обратиться в службу судебных приставов",
"Требуется продление срока исковой давности",
"Осуществляется контроль за ходом исполнительного производства",
],
}
# ── Chinese financial/judicial terms ─────────────────────────
CHINESE_COMMENT_EXAMPLES = [
"关于俄罗斯铝业集团子公司的逾期应收账款金额为45,678,901.23卢布逾期天数已超过180天。根据仲裁法院判决已启动强制执行程序冻结了债务人的银行账户。"
"同时正在准备破产申请材料。此笔债务已被分类为可疑应收账款并计提了50%的减值准备。正在进行债务人偿付能力的全面分析。",
"根据相互结算对账报告截至报告日未履行的供货合同义务金额为12,345,678.90卢布。已发出预审索赔通知要求债务人在30日内清偿债务。"
"若逾期未能偿还,将向阿尔比特拉日法院提起诉讼,并申请财产保全措施。本笔应收账款被列为高风险类别。",
"依据联邦破产法第127号的规定已向仲裁法院提交债务人破产申请。债务总额为234,567,890.00卢布逾期超过270天。法院已启动监督程序"
"并任命了临时管理人。债权人会议定于下月召开届时将审议债务重组方案。应收账款准备按100%计提。",
"关于俄罗斯铝业联合公司子公司的逾期应付账款金额为89,012,345.67卢布。根据法院判决,已发出执行令状并提交法警执行。初步分析表明,"
"债务人存在明显的偿付能力不足迹象。建议对该笔应收款进行全额减值处理,并寻求额外的担保措施。同时,正在审查合同条款中的违约责任条款。",
"根据库存应收账款盘点结果已识别出逾期超过365天的可疑债务金额为567,890,123.45卢布。债务人为俄罗斯铝业集团的前供应商,目前正在"
"进行清算程序。债权已列入债权人名册。预计回收率不超过5%。建议管理层批准将该笔应收账款核销。",
"在破产案件的框架下已完成对债务人俄罗斯铝业子公司的财务分析。应收账款总额为78,901,234.56卢布。法院已裁定实行外部管理,"
"并暂停了所有单独的债权执行程序。债权人委员会已批准了债务重组计划期限为18个月。目前正在监控重组计划的执行情况。",
"仲裁法院克拉斯诺亚尔斯克边疆区分院已作出判决全额支持我方索赔请求金额为34,567,890.12卢布。债务人提起了上诉,"
"案件已移交至上诉法院审理。同时,已申请采取临时措施,包括冻结债务人的不动产和银行账户。预计下一场听证会在两个月内举行。",
"关于逾期应收账款的追偿已向债务人发出正式追偿要求。债务总额为56,789,012.34卢布逾期超过90天。债务人提出了分期偿还的方案"
"建议在6个月内分6期偿还。我方正在审查该方案并评估是否需要提供额外的担保。同时正在计算逾期利息和违约金。",
]
# ── English financial/judicial terms ─────────────────────────
ENGLISH_COMMENT_EXAMPLES = [
"With respect to the overdue accounts receivable of RUSAL subsidiary in the amount of RUB 67,890,123.45, the delinquency period exceeds 210 days. "
"A statement of claim has been filed with the Arbitration Court of the Krasnoyarsk Territory. The court has granted interim measures, including "
"the seizure of the debtor's bank accounts. An impairment provision of 100% has been recognized in accordance with IFRS 9. The expected credit "
"loss (ECL) model indicates a probability of default exceeding 95% over a 12-month horizon.",
"Pursuant to the reconciliation statement, the outstanding amount of RUB 23,456,789.01 under the supply agreement has been overdue for more than "
"150 days. A pre-trial claim was dispatched on 15 March 2024. The debtor has requested a moratorium on payments citing liquidity constraints. "
"The receivable has been classified as Stage 3 under IFRS 9 (credit-impaired). A restructuring proposal is under evaluation.",
"In accordance with Federal Law No. 127-FZ, a petition for declaring the debtor bankrupt has been submitted to the Arbitration Court. The total "
"claim amount is RUB 890,123,456.78. The court has commenced supervision proceedings. The temporary manager has been appointed and the first "
"creditors' meeting is scheduled for next quarter. As per IAS 36, an impairment test has been performed, resulting in a full write-down.",
"Based on the inventory of accounts receivable, an overdue amount of RUB 12,345,678.90 has been identified as uncollectible due to the debtor's "
"liquidation. The creditor claim has been included in the register of creditors' claims. Management has approved the write-off against retained "
"earnings. Tax implications of this write-off are being assessed in accordance with the Russian Tax Code Article 265.",
"The enforcement proceedings have been initiated based on the writ of execution issued by the Arbitration Court. The bailiff service has imposed "
"a garnishment on the debtor's accounts at multiple banks. The outstanding principal amount is RUB 45,678,901.23, plus accrued interest of "
"RUB 5,432,109.87 calculated at the contractual rate of 0.1% per day of delay.",
]
# ── Helpers ──────────────────────────────────────────────────
def _random_date(start: date, end: date) -> date:
delta = (end - start).days
return start + timedelta(days=random.randint(0, delta))
def _doc_number(idx: int) -> str:
prefixes = ["ДЗ", "ИНВ", "АКТ", "ПРМ", "СЧ", "НКЛ", "ИСК", "РШН"]
prefix = random.choice(prefixes)
return f"{prefix}-{idx:06d}"
def _build_russian_comment() -> str:
parts = [
random.choice(RUSSIAN_COMMENT_PARTS["openers"]),
random.choice(RUSSIAN_COMMENT_PARTS["debtors"]),
random.choice(RUSSIAN_COMMENT_PARTS["amounts"]),
random.choice(RUSSIAN_COMMENT_PARTS["terms"]),
]
n_legal = random.randint(2, 4)
legal = random.sample(RUSSIAN_COMMENT_PARTS["legal_actions"], n_legal)
parts.append(", ".join(legal))
parts.append(random.choice(RUSSIAN_COMMENT_PARTS["classifications"]))
if random.random() < 0.7:
parts.append(random.choice(RUSSIAN_COMMENT_PARTS["reserve_mentions"]))
parts.append(random.choice(RUSSIAN_COMMENT_PARTS["closers"]))
if random.random() < 0.5:
extra = [
f"Дата возникновения задолженности: "
f"{_random_date(date(2022, 1, 1), date(2024, 6, 30)).strftime('%d.%m.%Y')}.",
f"Реквизиты договора: № {random.randint(100, 999)}/{random.randint(2020, 2024)}-АЛ.",
f"Ответственный менеджер: "
f"{random.choice(['Иванов И.И.', 'Петров П.П.', 'Сидоров С.С.', 'Кузнецов А.В.', 'Смирнова Е.А.'])}.",
f"Номер дела в суде: А{random.randint(10, 99)}-{random.randint(10000, 99999)}/"
f"{random.randint(2023, 2024)}.",
f"Статья закона: ст. {random.choice(['309', '310', '330', '395', '450'])} ГК РФ.",
f"Ставка пени: {random.choice(['0.05%', '0.1%', '0.15%', '0.2%'])} за каждый день просрочки.",
f"Дата последнего платежа: "
f"{_random_date(date(2023, 1, 1), date(2024, 6, 30)).strftime('%d.%m.%Y')}.",
f"Сумма основного долга: {random.randint(100000, 50000000):,} руб., "
f"пени: {random.randint(10000, 5000000):,} руб.".replace(",", " "),
]
parts.append(random.choice(extra))
return ". ".join(parts)
def _build_chinese_comment() -> str:
return random.choice(CHINESE_COMMENT_EXAMPLES)
def _build_english_comment() -> str:
return random.choice(ENGLISH_COMMENT_EXAMPLES)
def _ensure_length(text: str, lang_rand: float) -> str:
while len(text) < 200:
if lang_rand < 0.80:
text += ". " + random.choice(RUSSIAN_COMMENT_PARTS["closers"])
elif lang_rand < 0.90:
text += "。债务人的财务状况正在进一步分析中。"
else:
text += " The debtor's financial condition is under further review."
if len(text) > 1000:
text = text[:997] + "..."
return text
def generate_row(idx: int) -> dict:
lang_rand = random.random()
if lang_rand < 0.80:
comment = _build_russian_comment()
elif lang_rand < 0.90:
comment = _build_chinese_comment()
else:
comment = _build_english_comment()
comment = _ensure_length(comment, lang_rand)
return {
"report_date": _random_date(
date(2022, 1, 1), date(2025, 5, 15)
).strftime("%Y-%m-%d"),
"document_number": _doc_number(idx),
"comment_text": comment,
"counterparty": random.choice(COUNTERPARTIES),
"supply_country": random.choice(SUPPLY_COUNTRIES),
}
# ── Main ─────────────────────────────────────────────────────
BATCH_SIZE = 100
TOTAL_ROWS = 5000
def main():
cm = ConfigManager()
executor = SupersetSqlLabExecutor(cm, "ss-dev")
db_id = executor.resolve_database_id(
database_name="ClickHouse Connect (Superset)"
)
print(f"ClickHouse DB ID: {db_id}")
print(f"Generating {TOTAL_ROWS} rows...")
all_rows = [generate_row(i + 1) for i in range(TOTAL_ROWS)]
lengths = [len(r["comment_text"]) for r in all_rows]
print(f"Comment length range: {min(lengths)}{max(lengths)} chars")
ru = sum(1 for r in all_rows if any(
c in r["comment_text"] for c in "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"))
cn = sum(1 for r in all_rows if any(
c in r["comment_text"] for c in "的一是不了人我在有他这"))
en = TOTAL_ROWS - ru - cn
print(f"Language: RU={ru} ({ru/TOTAL_ROWS*100:.0f}%), "
f"CN={cn} ({cn/TOTAL_ROWS*100:.0f}%), EN={en} ({en/TOTAL_ROWS*100:.0f}%)")
total_inserted = 0
for batch_start in range(0, TOTAL_ROWS, BATCH_SIZE):
batch = all_rows[batch_start:batch_start + BATCH_SIZE]
values_lines = []
for row in batch:
report_date = row["report_date"]
doc_num = row["document_number"].replace("'", "''")
comment = row["comment_text"].replace("'", "''")
counterparty = row["counterparty"].replace("'", "''")
country = row["supply_country"].replace("'", "''")
values_lines.append(
f"('{report_date}', '{doc_num}', '{comment}', "
f"'{counterparty}', '{country}')"
)
sql = (
"INSERT INTO dm_view.financial_arrears "
"(`report_date`, `document_number`, `comment_text`, "
"`counterparty`, `supply_country`) VALUES\n"
+ ",\n".join(values_lines)
+ ";"
)
print(
f" Batch [{batch_start + 1}{batch_start + len(batch)}]: "
f"SQL={len(sql):,} chars"
)
try:
result = executor.execute_sql(
sql, database_id=db_id, run_async=False
)
status = result.get("status", "unknown")
if status == "success":
total_inserted += len(batch)
print(f" -> OK. Cumulative: {total_inserted}/{TOTAL_ROWS}")
else:
err = (
result.get("raw_response", {})
.get("query", {})
.get("errorMessage", "unknown")
)
print(f" -> FAIL: {err}")
except Exception as e:
print(f" -> EXCEPTION: {e}")
print(f"\nInserted: {total_inserted}/{TOTAL_ROWS}")
# Verify
sql_check = "SELECT count(*) as cnt FROM dm_view.financial_arrears"
res = executor.execute_sql(sql_check, database_id=db_id, run_async=False)
data = res.get("raw_response", {}).get("data", [])
print(f"Final count: {data[0]['cnt'] if data else '?'}")
# Sample
sql_sample = (
"SELECT * FROM dm_view.financial_arrears "
"ORDER BY report_date DESC LIMIT 3"
)
res = executor.execute_sql(sql_sample, database_id=db_id, run_async=False)
for row in res.get("raw_response", {}).get("data", []):
print(
f" {row['report_date']} {row['document_number']} "
f"{row['counterparty'][:25]:25s} "
f"{row['comment_text'][:70]}..."
)
if __name__ == "__main__":
main()