Files
ERP/ai procurement/app/texts.py
T

433 lines
19 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.
"""Persian message templates (the ERP's user-facing layer is Persian)."""
import html
from datetime import date
STATUS_FA = {
"offered": "🔵 در انتظار بررسی",
"accepted": "🟢 پذیرفته‌شده",
"arrived": "✅ رسیده",
"rejected": "🔴 ردشده",
"expired": "⚪️ منقضی",
}
MISSING_MARKS = {True: "✅", False: "❌"}
WARNING_MARK = "⚠️"
def esc(value) -> str:
return html.escape(str(value if value is not None else "—"))
def g2j(gy: int, gm: int, gd: int) -> tuple[int, int, int]:
g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
gy2 = gy if gm > 2 else gy - 1
days = 355666 + (365 * gy) + ((gy2 + 3) // 4) - ((gy2 + 99) // 100) + ((gy2 + 399) // 400) + gd + g_d_m[gm - 1]
jy = -1595 + (33 * (days // 12053))
days %= 12053
jy += 4 * (days // 1461)
days %= 1461
if days > 365:
jy += (days - 1) // 365
days = (days - 1) % 365
if days < 186:
jm = 1 + (days // 31)
jd = 1 + (days % 31)
else:
jm = 7 + ((days - 186) // 30)
jd = 1 + ((days - 186) % 30)
return jy, jm, jd
def fa_num(val, use_fa_digits: bool = True) -> str:
"""Format integer/float with thousands separator and Persian digits."""
if val is None or val == "":
return "—"
try:
num = float(val)
if num.is_integer():
s = f"{int(num):,}"
else:
s = f"{num:,.2f}".rstrip("0").rstrip(".")
if use_fa_digits:
trans = str.maketrans("0123456789,", "۰۱۲۳۴۵۶۷۸۹٬")
return s.translate(trans)
return s
except (ValueError, TypeError):
return str(val)
def fa_digits(val) -> str:
"""Convert ASCII digits to Persian digits without thousands separators."""
if not val:
return ""
trans = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
return str(val).translate(trans)
def fa_date(value, use_fa_digits: bool = True) -> str:
if not value:
return "—"
s = str(value)[:10]
try:
parts = [int(p) for p in s.replace("/", "-").split("-")]
if len(parts) == 3 and parts[0] > 1900:
jy, jm, jd = g2j(parts[0], parts[1], parts[2])
res = f"{jy:04d}/{jm:02d}/{jd:02d}"
if use_fa_digits:
trans = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
return res.translate(trans)
return res
except Exception:
pass
if use_fa_digits:
trans = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
return s.translate(trans)
return s
WELCOME = (
"سلام! 👋 من دستیار تدارکات هستم.\n\n"
"می‌توانی با پیام متنی یا <b>صوتی</b> فعالیتی تعریف کنی — مثلاً:\n"
"«سازان نو برای پروژه برج آسمان، ۵۰۰ کیلو میلگرد A3، هر کیلو ۴۵ تومن، پرداخت ۳ فاز»\n\n"
"من هر چه را فهمیدم به‌صورت پیش‌نویس آماده می‌کنم، چیزی که فهمیدم و چیزهای خالی را نشان "
"می‌دهم و فقط با تأیید تو در سیستم ثبت می‌کنم.\n\n"
"دستورها:\n"
"/drafts — پیش‌نویس‌های باز\n"
"/cancel — لغو پیش‌نویس/عملیات جاری\n"
"/whoami — اتصال من\n"
"/unlink — قطع اتصال\n"
"/help — راهنما"
)
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"
"هر ثبت فقط با دکمه ✅ انجام می‌شود. با /cancel هر چیز نیمه‌کاره را لغو کن."
)
LINK_START = (
"برای اتصال حساب Bale به حساب ERP، <b>نام کاربری Casdoor</b> خود را بفرست "
"(یا اگر رمز جدا داری همان نام کاربری سامانه). برای لغو: /cancel"
)
LINK_ASK_PASSWORD = "ممنون. حالا <b>رمز عبور</b> را بفرست (فقط برای یک بار ورود؛ ذخیره نمی‌شود). /cancel برای لغو"
LINK_OK = "✅ اتصال انجام شد: <b>{name}</b> ({role})\nحالا می‌توانی فعالیت‌ها را بفرستی. /help را ببین."
LINK_FAIL = "ورود ناموفق بود: {detail}\nدوباره /start بزن یا نام کاربری را بفرست."
ADMIN_NO_LINK = "حالت ادمین (بدون اتصال) فعال است؛ ثبت‌ها بدون انتساب کاربر انجام می‌شود."
DRAFTS_LIST_HEADER = "📋 پیش‌نویس‌های باز:"
DRAFTS_EMPTY = "پیش‌نویس بازی نداری. یک فعالیت را با متن یا وویس بفرست."
NOTHING_TO_COMMIT = "پیش‌نویس بازی برای ثبت نیست؛ اول یک فعالیت بفرست."
CANCELLED = "لغو شد. ✔️"
UNKNOWN_HINT = (
"متوجه منظورت نشد 🤔\n"
"می‌توانی مثلاً بگویی: «ثبت استعلام از سازان نو برای ۲۰۰ عدد شیر فلکه» یا /help را ببینی."
)
UNLINKED = "ابتدا حساب خود را متصل کن: /start"
AI_NOT_CONFIGURED = (
"پردازش هوشمند تنظیم نشده است (متغیرهای PROCUREMENT_BOT_AI_*). "
"با مدیر سیستم تماس بگیر."
)
STT_FAILED = "نتوانستم پیام صوتی را تبدیل کنم: {detail}"
VOICE_EMPTY = "از پیام صوتی متنی شنیده نشد؛ دوباره بگو یا با متن بفرست."
ERR_GENERIC = "خطا: {detail}"
PICK_QUOTE = "چند استعلام پیدا شد؛ کدام را می‌خواهی؟"
QUOTE_NOT_FOUND = "استعلامی برای «{q}» پیدا نکردم."
ACCEPT_REPORT = (
"✅ پذیرش انجام شد\n"
"استعلام #{id}\n"
"وضعیت: {status}\n"
"اقلام تصمیم‌گیری‌شده: {count}"
)
ARRIVE_REPORT = (
"🚚 ثبت رسید انجام شد\n"
"استعلام #{id} — وضعیت: {status}\n{lines}"
)
ACCEPT_DONE_TS = "شرایط انتخاب‌شده: {label}"
def missing_block(missing: list[dict]) -> str:
"""Checklist of missing vs. captured fields."""
if not missing:
return "همه فیلدها کامل است ✅"
lines = []
for m in missing:
if m.get("ok"):
mark = "✅"
elif m.get("blocking"):
mark = "❌"
else:
mark = "⚠️"
lines.append(f"{mark} {esc(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>"]
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)
if not has_blocking and not has_warnings:
lines.append("✅ همه‌ی اطلاعات کامل است، آماده ثبت.")
elif has_blocking:
lines.append("⛔️ به دلیل وارد نکردن موارد اجباری (❌) امکان ثبت نهایی وجود ندارد.")
else:
lines.append("⚠️ پیش‌نویس ناقص است (موارد اختیاری خالی است)، اما امکان ثبت وجود دارد.")
if status_note:
lines.append(status_note)
lines.append("━━━━━━━━━━━━━━\n")
# 🏢 تأمین‌کننده
lines.append("🏢 <b>تأمین‌کننده</b>")
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} ✅")
if payload.get("supplier_location"):
lines.append(f"📍 {esc(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"]))
if contact_parts:
lines.append(f"📞 {' — '.join(contact_parts)}")
elif sup_name:
lines.append(f"<b>{esc(sup_name)}</b>{official_badge} ✨ <i>(تأمین‌کننده جدید)</i>")
else:
lines.append("— <i>(مشخص نشده)</i>")
cat_name = payload.get("supplier_category_name")
if cat_name:
lines.append(f"\n🏷️ موضوع: {esc(cat_name)}")
# 📦 اقلام
items = payload.get("items") or []
total_amount = 0.0
has_any_price = False
if items:
lines.append(f"\n📦 <b>اقلام ({fa_num(len(items))})</b>")
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>"]
if it.get("brand_name"):
title_parts.append(esc(it["brand_name"]))
if it.get("specs"):
title_parts.append(esc("، ".join(it["specs"])))
if not it.get("material_id"):
title_parts.append("✨ <i>(کالای جدید)</i>")
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)}")
else:
lines.append(" 🏗️ <i>پروژه مشخص نشده</i>")
# Qty, Unit, Price, Total
qty = it.get("quantity")
unit = it.get("unit_name") or ("(واحد کالا)" if it.get("unit_id") else None)
unit_price = it.get("unit_price")
calc_line = " "
if qty is not None:
calc_line += f"{fa_num(qty)} {esc(unit or '')}".strip()
else:
calc_line += "مقدار نامشخص"
if unit_price:
has_any_price = True
calc_line += f" × {fa_num(unit_price)}"
if qty is not None:
try:
line_sum = float(qty) * float(unit_price)
total_amount += line_sum
calc_line += f" = <b>{fa_num(line_sum)} تومان</b>"
except (ValueError, TypeError):
calc_line += " تومان"
else:
calc_line += " تومان"
else:
calc_line += " × — = — تومان"
lines.append(calc_line)
if has_any_price and total_amount > 0:
lines.append(f"\n💰 <b>مجموع مبلغ: {fa_num(total_amount)} تومان</b>")
elif not has_any_price:
lines.append("\n💰 مجموع مبلغ: <i>محاسبه‌نشده (قیمت اقلام وارد نشده)</i>")
else:
lines.append("\n📦 اقلام: <i>هنوز چیزی ثبت نشده ❌</i>")
# 💳 شرایط پرداخت
sheets = payload.get("termsheets") or []
if sheets:
lines.append(f"\n💳 <b>شرایط پرداخت ({fa_num(len(sheets))})</b>")
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))} مرحله")
step_sum = 0.0
for m in milestones:
due = fa_date(m.get("due_date"))
amt = m.get("amount")
amt_str = f"{fa_num(amt)} تومان" if amt else "مبلغ توافقی"
if amt:
try:
step_sum += float(amt)
except (ValueError, TypeError):
pass
note = f" ({esc(m['note'])})" if m.get("note") else ""
lines.append(f" • {due} — {amt_str}{note}")
if total_amount > 0 and step_sum > 0:
diff = abs(total_amount - step_sum)
if diff < 1.0:
lines.append(" ✅ جمع مراحل با مجموع مبلغ برابر است")
else:
lines.append(f" ⚠️ اختلاف جمع مراحل با مجموع فاکتور: {fa_num(diff)} تومان")
if len(items) > 0:
lines.append(f" شامل: هر {fa_num(len(items))} قلم")
else:
disc = ""
if t.get("discount_value"):
disc = f" (تخفیف {fa_num(t['discount_value'])})"
lines.append(f"▫️ {esc(lbl)}{disc}")
elif items:
lines.append("\n💳 شرایط پرداخت: <i>نقدی / توافقی (پیش‌فرض)</i>")
# Metadata
meta_lines = []
if payload.get("quote_date"):
meta_lines.append(f"🗓️ تاریخ: {fa_date(payload['quote_date'])}")
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'])}")
if payload.get("notes"):
meta_lines.append(f"📝 توضیحات: {esc(payload['notes'])}")
if meta_lines:
lines.append("")
lines.extend(meta_lines)
# 📋 وضعیت فیلدها (اگر نقصی وجود داشته باشد)
if has_blocking or has_warnings:
lines.append("\n📋 <b>وضعیت فیلدها:</b>")
lines.append(missing_block(missing))
# Footer
lines.append("\n━━━━━━━━━━━━━━")
if has_blocking:
lines.append("برای تغییر، پیام بفرست (مثلاً «قیمت قلم ۲ شد ۴۶٬۰۰۰»).")
lines.append("با پیام بعدی می‌توانی چیزهای خالی را تکمیل کنی، یا با دکمه‌ها ثبت/لغو کنی.")
elif has_warnings:
lines.append("برای تغییر، پیام بفرست (مثلاً «قیمت قلم ۲ شد ۴۶٬۰۰۰»).")
lines.append("می‌توانی چیزهای خالی را تکمیل کنی، یا با دکمه‌ها ثبت/لغو کنی.")
else:
lines.append("برای تغییر، پیام بفرست (مثلاً «قیمت قلم ۲ شد ۴۶٬۰۰۰»).")
lines.append("با پیام بعدی می‌توانی چیزهای خالی را تکمیل کنی، یا با دکمه‌ها ثبت/لغو کنی.")
return "\n".join(lines)
def supplier_review_text(payload: dict, missing: list[dict]) -> str:
lines = ["🏢 <b>پیش‌نویس تأمین‌کننده جدید</b>", f"نام: <b>{esc(payload.get('name'))}</b>"]
lines.append(f"دسته: {esc(payload.get('category_name'))}")
if payload.get("phone"):
lines.append(f"تلفن: {esc(payload['phone'])}")
if payload.get("location"):
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(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.append(f"واحد اصلی: {esc(payload.get('unit_name'))}")
if payload.get("topic"):
lines.append(f"تاپیک: {esc(payload['topic'])}")
lines.append("\n<b>وضعیت فیلدها:</b>")
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'))}"]
sup = (created.get("supplier") or {}).get("name") or payload.get("supplier_name")
if sup:
lines.append(f"تأمین‌کننده: {esc(sup)}")
items = created.get("line_items") or []
if items:
lines.append(f"اقلام: {len(items)}")
sheets = created.get("termsheets") or []
if sheets:
lines.append(f"شرایط پرداخت: {len(sheets)}")
if payload.get("project_id"):
lines.append("پروژه: پیوست شد")
if remaining:
lines.append("\n<b>خالی مانده‌ها (بعداً می‌توانی در برنامه تکمیل کنی):</b>")
lines += [f"⚠️ {esc(r)}" for r in remaining]
lines.append("\nمرحله بعد: وقتی تصمیم گرفتی، بگو «استعلام را قبول کن» تا پذیرش را با هم انجام دهیم.")
return "\n".join(lines)
def supplier_committed_report(created: dict) -> str:
return (
f"✅ <b>تأمین‌کننده ثبت شد</b> — {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'))}"
def quote_line(quote: dict) -> str:
sup = (quote.get("supplier") or {}).get("name") or quote.get("supplier_id") or "—"
items = quote.get("line_items") or []
item_names = []
for it in items:
mat = (it.get("material") or {}).get("name")
if mat:
qty = it.get("quantity")
item_names.append(f"{mat} ({qty})" if qty else mat)
tail = f" — {'، '.join(item_names[:3])}" + ("…" if len(item_names) > 3 else "") if item_names else ""
return (
f"#{str(quote.get('id'))[:8]} — {esc(sup)} — {STATUS_FA.get(quote.get('status'), quote.get('status'))} — "
f"{fa_date(quote.get('quote_date'))}{tail}"
)
def status_report(kind: str, quotes: list[dict], extra_note: str | None = None) -> str:
titles = {
"pending": "🔵 استعلام‌های در انتظار بررسی",
"accepted": "🟢 پذیرفته‌شده (در انتظار تحویل)",
"arrived": "✅ رسیده",
"expiring": "⏳ به‌زودی منقضی می‌شوند (۷ روز آینده)",
"recent": "🕘 آخرین استعلام‌ها",
}
if not quotes:
return f"{titles.get(kind, 'نتیجه‌ای')}\nموردی نیست."
lines = [titles.get(kind, "استعلام‌ها")]
if extra_note:
lines.append(extra_note)
lines += [f"• {quote_line(q)}" for q in quotes[:10]]
return "\n".join(lines)