fix(bot): strip html tags for Bale, fallback to direct DB catalogs, align validation strictly with DB schema

This commit is contained in:
2026-09-29 01:49:42 +03:30
parent 17458d8697
commit da68fc683f
6 changed files with 217 additions and 99 deletions
+49 -49
View File
@@ -15,7 +15,7 @@ WARNING_MARK = "⚠️"
def esc(value) -> str:
return html.escape(str(value if value is not None else "—"))
return str(value if value is not None and value != "" else "—")
def g2j(gy: int, gm: int, gd: int) -> tuple[int, int, int]:
@@ -87,7 +87,7 @@ def fa_date(value, use_fa_digits: bool = True) -> str:
WELCOME = (
"سلام! 👋 من دستیار تدارکات هستم.\n\n"
"می‌توانی با پیام متنی یا <b>صوتی</b> فعالیتی تعریف کنی — مثلاً:\n"
"می‌توانی با پیام متنی یا صوتی فعالیتی تعریف کنی — مثلاً:\n"
"«سازان نو برای پروژه برج آسمان، ۵۰۰ کیلو میلگرد A3، هر کیلو ۴۵ تومن، پرداخت ۳ فاز»\n\n"
"من هر چه را فهمیدم به‌صورت پیش‌نویس آماده می‌کنم، چیزی که فهمیدم و چیزهای خالی را نشان "
"می‌دهم و فقط با تأیید تو در سیستم ثبت می‌کنم.\n\n"
@@ -100,22 +100,22 @@ WELCOME = (
)
HELP = (
"<b>چه کارهایی می‌توانم بکنم؟</b>\n\n"
"🧾 <b>ثبت استعلام</b> — تأمین‌کننده، پروژه، اقلام (کالا/مقدار/واحد)، قیمت و شرایط پرداخت را از گفتگو درمی‌آورم.\n"
"🏢 <b>افزودن تأمین‌کننده</b> — «تأمین‌کننده جدید فلانی، دسته لوله و اتصالات، تلفن ...»\n"
"📦 <b>افزودن کالا</b> — «کالای جدید میلگرد A3 با واحد کیلوگرم»\n"
"📊 <b>پرس‌وجو</b> — «استعلام‌های در انتظار؟»، «کدام‌ها به‌زودی منقضی می‌شوند؟»\n"
"✔️ <b>پذیرش استعلام</b> — «استعلام سازان نو را قبول کن»\n"
"🚚 <b>اعلام رسیدن</b> — «اقلام سازان نو رسید، ۵۰۰ کیلو»\n\n"
"چه کارهایی می‌توانم بکنم؟\n\n"
"🧾 ثبت استعلام — تأمین‌کننده، پروژه، اقلام (کالا/مقدار/واحد)، قیمت و شرایط پرداخت را از گفتگو درمی‌آورم.\n"
"🏢 افزودن تأمین‌کننده — «تأمین‌کننده جدید فلانی، دسته لوله و اتصالات، تلفن ...»\n"
"📦 افزودن کالا — «کالای جدید میلگرد A3 با واحد کیلوگرم»\n"
"📊 پرس‌وجو — «استعلام‌های در انتظار؟»، «کدام‌ها به‌زودی منقضی می‌شوند؟»\n"
"✔️ پذیرش استعلام — «استعلام سازان نو را قبول کن»\n"
"🚚 اعلام رسیدن — «اقلام سازان نو رسید، ۵۰۰ کیلو»\n\n"
"هر ثبت فقط با دکمه ✅ انجام می‌شود. با /cancel هر چیز نیمه‌کاره را لغو کن."
)
LINK_START = (
"برای اتصال حساب Bale به حساب ERP، <b>نام کاربری Casdoor</b> خود را بفرست "
"برای اتصال حساب Bale به حساب ERP، نام کاربری Casdoor خود را بفرست "
"(یا اگر رمز جدا داری همان نام کاربری سامانه). برای لغو: /cancel"
)
LINK_ASK_PASSWORD = "ممنون. حالا <b>رمز عبور</b> را بفرست (فقط برای یک بار ورود؛ ذخیره نمی‌شود). /cancel برای لغو"
LINK_OK = "✅ اتصال انجام شد: <b>{name}</b> ({role})\nحالا می‌توانی فعالیت‌ها را بفرستی. /help را ببین."
LINK_ASK_PASSWORD = "ممنون. حالا رمز عبور را بفرست (فقط برای یک بار ورود؛ ذخیره نمی‌شود). /cancel برای لغو"
LINK_OK = "✅ اتصال انجام شد: {name} ({role})\nحالا می‌توانی فعالیت‌ها را بفرستی. /help را ببین."
LINK_FAIL = "ورود ناموفق بود: {detail}\nدوباره /start بزن یا نام کاربری را بفرست."
ADMIN_NO_LINK = "حالت ادمین (بدون اتصال) فعال است؛ ثبت‌ها بدون انتساب کاربر انجام می‌شود."
@@ -162,12 +162,12 @@ def missing_block(missing: list[dict]) -> str:
mark = "❌"
else:
mark = "⚠️"
lines.append(f"{mark} {esc(m.get('label'))}")
lines.append(f"{mark} {m.get('label')}")
return "\n".join(lines)
def quote_review_text(payload: dict, missing: list[dict], status_note: str | None = None) -> str:
lines = ["📋 <b>پیش‌نویس استعلام</b>"]
lines = ["📋 پیش‌نویس استعلام"]
has_blocking = any(m.get("blocking") and not m.get("ok") for m in missing)
has_warnings = any(not m.get("blocking") and not m.get("ok") for m in missing)
@@ -185,30 +185,30 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
lines.append("━━━━━━━━━━━━━━\n")
# 🏢 تأمین‌کننده
lines.append("🏢 <b>تأمین‌کننده</b>")
lines.append("🏢 تأمین‌کننده")
sup_name = payload.get("supplier_name")
is_official = payload.get("is_official", True)
official_badge = " (رسمی)" if is_official else " (غیررسمی)"
if payload.get("supplier_id"):
lines.append(f"<b>{esc(sup_name)}</b>{official_badge} ✅")
lines.append(f"{sup_name}{official_badge} ✅")
if payload.get("supplier_location"):
lines.append(f"📍 {esc(payload['supplier_location'])}")
lines.append(f"📍 {payload['supplier_location']}")
contact_parts = []
if payload.get("supplier_phone"):
contact_parts.append(fa_digits(payload["supplier_phone"]))
if payload.get("supplier_contact_person"):
contact_parts.append(esc(payload["supplier_contact_person"]))
contact_parts.append(str(payload["supplier_contact_person"]))
if contact_parts:
lines.append(f"📞 {' — '.join(contact_parts)}")
elif sup_name:
lines.append(f"<b>{esc(sup_name)}</b>{official_badge} ✨ <i>(تأمین‌کننده جدید)</i>")
lines.append(f"{sup_name}{official_badge} ✨ (تأمین‌کننده جدید)")
else:
lines.append("— <i>(مشخص نشده)</i>")
lines.append("— (مشخص نشده)")
cat_name = payload.get("supplier_category_name")
if cat_name:
lines.append(f"\n🏷️ موضوع: {esc(cat_name)}")
lines.append(f"\n🏷️ موضوع: {cat_name}")
# 📦 اقلام
items = payload.get("items") or []
@@ -216,25 +216,25 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
has_any_price = False
if items:
lines.append(f"\n📦 <b>اقلام ({fa_num(len(items))})</b>")
lines.append(f"\n📦 اقلام ({fa_num(len(items))})")
default_proj = payload.get("project_name")
for i, it in enumerate(items, start=1):
name = it.get("material_name") or f"قلم {i}"
title_parts = [f"<b>{esc(name)}</b>"]
title_parts = [str(name)]
if it.get("brand_name"):
title_parts.append(esc(it["brand_name"]))
title_parts.append(str(it["brand_name"]))
if it.get("specs"):
title_parts.append(esc("، ".join(it["specs"])))
title_parts.append("، ".join(str(s) for s in it["specs"]))
if not it.get("material_id"):
title_parts.append("✨ <i>(کالای جدید)</i>")
title_parts.append("✨ (کالای جدید)")
lines.append(f"{fa_num(i)}. {' — '.join(title_parts)}")
# Project
item_proj = it.get("project_name") or default_proj
if item_proj:
lines.append(f" 🏗️ {esc(item_proj)}")
lines.append(f" 🏗️ {item_proj}")
else:
lines.append(" 🏗️ <i>پروژه مشخص نشده</i>")
lines.append(" 🏗️ پروژه مشخص نشده")
# Qty, Unit, Price, Total
qty = it.get("quantity")
@@ -243,7 +243,7 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
calc_line = " "
if qty is not None:
calc_line += f"{fa_num(qty)} {esc(unit or '')}".strip()
calc_line += f"{fa_num(qty)} {unit or ''}".strip()
else:
calc_line += "مقدار نامشخص"
@@ -254,7 +254,7 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
try:
line_sum = float(qty) * float(unit_price)
total_amount += line_sum
calc_line += f" = <b>{fa_num(line_sum)} تومان</b>"
calc_line += f" = {fa_num(line_sum)} تومان"
except (ValueError, TypeError):
calc_line += " تومان"
else:
@@ -265,21 +265,21 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
lines.append(calc_line)
if has_any_price and total_amount > 0:
lines.append(f"\n💰 <b>مجموع مبلغ: {fa_num(total_amount)} تومان</b>")
lines.append(f"\n💰 مجموع مبلغ: {fa_num(total_amount)} تومان")
elif not has_any_price:
lines.append("\n💰 مجموع مبلغ: <i>محاسبه‌نشده (قیمت اقلام وارد نشده)</i>")
lines.append("\n💰 مجموع مبلغ: محاسبه‌نشده (قیمت اقلام وارد نشده)")
else:
lines.append("\n📦 اقلام: <i>هنوز چیزی ثبت نشده ❌</i>")
lines.append("\n📦 اقلام: هنوز چیزی ثبت نشده ❌")
# 💳 شرایط پرداخت
sheets = payload.get("termsheets") or []
if sheets:
lines.append(f"\n💳 <b>شرایط پرداخت ({fa_num(len(sheets))})</b>")
lines.append(f"\n💳 شرایط پرداخت ({fa_num(len(sheets))})")
for s_idx, t in enumerate(sheets, start=1):
lbl = t.get("label") or f"گزینه {s_idx}"
milestones = t.get("milestones") or t.get("schedule_steps") or []
if milestones:
lines.append(f"▫️ {esc(lbl)} — {fa_num(len(milestones))} مرحله")
lines.append(f"▫️ {lbl} — {fa_num(len(milestones))} مرحله")
step_sum = 0.0
for m in milestones:
due = fa_date(m.get("due_date"))
@@ -290,7 +290,7 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
step_sum += float(amt)
except (ValueError, TypeError):
pass
note = f" ({esc(m['note'])})" if m.get("note") else ""
note = f" ({m['note']})" if m.get("note") else ""
lines.append(f" • {due} — {amt_str}{note}")
if total_amount > 0 and step_sum > 0:
@@ -305,9 +305,9 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
disc = ""
if t.get("discount_value"):
disc = f" (تخفیف {fa_num(t['discount_value'])})"
lines.append(f"▫️ {esc(lbl)}{disc}")
lines.append(f"▫️ {lbl}{disc}")
elif items:
lines.append("\n💳 شرایط پرداخت: <i>نقدی / توافقی (پیش‌فرض)</i>")
lines.append("\n💳 شرایط پرداخت: نقدی / توافقی (پیش‌فرض)")
# Metadata
meta_lines = []
@@ -316,9 +316,9 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
if payload.get("expiry_date"):
meta_lines.append(f"⏳ اعتبار: {fa_date(payload['expiry_date'])}")
if payload.get("proforma_file"):
meta_lines.append(f"📎 پیش‌فاکتور: {esc(payload['proforma_file'])}")
meta_lines.append(f"📎 پیش‌فاکتور: {payload['proforma_file']}")
if payload.get("notes"):
meta_lines.append(f"📝 توضیحات: {esc(payload['notes'])}")
meta_lines.append(f"📝 توضیحات: {payload['notes']}")
if meta_lines:
lines.append("")
@@ -326,7 +326,7 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
# 📋 وضعیت فیلدها (اگر نقصی وجود داشته باشد)
if has_blocking or has_warnings:
lines.append("\n📋 <b>وضعیت فیلدها:</b>")
lines.append("\n📋 وضعیت فیلدها:")
lines.append(missing_block(missing))
# Footer
@@ -345,7 +345,7 @@ def quote_review_text(payload: dict, missing: list[dict], status_note: str | Non
def supplier_review_text(payload: dict, missing: list[dict]) -> str:
lines = ["🏢 <b>پیش‌نویس تأمین‌کننده جدید</b>", f"نام: <b>{esc(payload.get('name'))}</b>"]
lines = ["🏢 پیش‌نویس تأمین‌کننده جدید", f"نام: {esc(payload.get('name'))}"]
lines.append(f"دسته: {esc(payload.get('category_name'))}")
if payload.get("phone"):
lines.append(f"تلفن: {esc(payload['phone'])}")
@@ -353,23 +353,23 @@ def supplier_review_text(payload: dict, missing: list[dict]) -> str:
lines.append(f"موقعیت: {esc(payload['location'])}")
if payload.get("contact_person"):
lines.append(f"رابط: {esc(payload['contact_person'])}")
lines.append("\n<b>وضعیت فیلدها:</b>")
lines.append("\nوضعیت فیلدها:")
lines.append(missing_block(missing))
return "\n".join(lines)
def material_review_text(payload: dict, missing: list[dict]) -> str:
lines = ["📦 <b>پیش‌نویس کالای جدید</b>", f"نام: <b>{esc(payload.get('name'))}</b>"]
lines = ["📦 پیش‌نویس کالای جدید", f"نام: {esc(payload.get('name'))}"]
lines.append(f"واحد اصلی: {esc(payload.get('unit_name'))}")
if payload.get("topic"):
lines.append(f"تاپیک: {esc(payload['topic'])}")
lines.append("\n<b>وضعیت فیلدها:</b>")
lines.append("\nوضعیت فیلدها:")
lines.append(missing_block(missing))
return "\n".join(lines)
def quote_committed_report(created: dict, payload: dict, remaining: list[str]) -> str:
lines = [f"✅ <b>ثبت شد</b> — استعلام #{esc(created.get('id'))}"]
lines = [f"✅ ثبت شد — استعلام #{esc(created.get('id'))}"]
sup = (created.get("supplier") or {}).get("name") or payload.get("supplier_name")
if sup:
lines.append(f"تأمین‌کننده: {esc(sup)}")
@@ -382,7 +382,7 @@ def quote_committed_report(created: dict, payload: dict, remaining: list[str]) -
if payload.get("project_id"):
lines.append("پروژه: پیوست شد")
if remaining:
lines.append("\n<b>خالی مانده‌ها (بعداً می‌توانی در برنامه تکمیل کنی):</b>")
lines.append("\nخالی مانده‌ها (بعداً می‌توانی در برنامه تکمیل کنی):")
lines += [f"⚠️ {esc(r)}" for r in remaining]
lines.append("\nمرحله بعد: وقتی تصمیم گرفتی، بگو «استعلام را قبول کن» تا پذیرش را با هم انجام دهیم.")
return "\n".join(lines)
@@ -390,13 +390,13 @@ def quote_committed_report(created: dict, payload: dict, remaining: list[str]) -
def supplier_committed_report(created: dict) -> str:
return (
f"✅ <b>تأمین‌کننده ثبت شد</b> — {esc(created.get('name'))}\n"
f"✅ تأمین‌کننده ثبت شد — {esc(created.get('name'))}\n"
f"دسته: {esc((created.get('category') or {}).get('name'))}"
)
def material_committed_report(created: dict) -> str:
return f"✅ <b>کالا ثبت شد</b> — {esc(created.get('name'))}"
return f"✅ کالا ثبت شد — {esc(created.get('name'))}"
def quote_line(quote: dict) -> str: