"""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 str(value if value is not None and value != "" 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" "می‌توانی با پیام متنی یا صوتی فعالیتی تعریف کنی — مثلاً:\n" "«سازان نو برای پروژه برج آسمان، ۵۰۰ کیلو میلگرد A3، هر کیلو ۴۵ تومن، پرداخت ۳ فاز»\n\n" "من هر چه را فهمیدم به‌صورت پیش‌نویس آماده می‌کنم، چیزی که فهمیدم و چیزهای خالی را نشان " "می‌دهم و فقط با تأیید تو در سیستم ثبت می‌کنم.\n\n" "دستورها:\n" "/drafts — پیش‌نویس‌های باز\n" "/cancel — لغو پیش‌نویس/عملیات جاری\n" "/whoami — اتصال من\n" "/unlink — قطع اتصال\n" "/help — راهنما" ) HELP = ( "چه کارهایی می‌توانم بکنم؟\n\n" "🧾 ثبت استعلام — تأمین‌کننده، پروژه، اقلام (کالا/مقدار/واحد)، قیمت و شرایط پرداخت را از گفتگو درمی‌آورم.\n" "🏢 افزودن تأمین‌کننده — «تأمین‌کننده جدید فلانی، دسته لوله و اتصالات، تلفن ...»\n" "📦 افزودن کالا — «کالای جدید میلگرد A3 با واحد کیلوگرم»\n" "📊 پرس‌وجو — «استعلام‌های در انتظار؟»، «کدام‌ها به‌زودی منقضی می‌شوند؟»\n" "✔️ پذیرش استعلام — «استعلام سازان نو را قبول کن»\n" "🚚 اعلام رسیدن — «اقلام سازان نو رسید، ۵۰۰ کیلو»\n\n" "هر ثبت فقط با دکمه ✅ انجام می‌شود. با /cancel هر چیز نیمه‌کاره را لغو کن." ) LINK_START = ( "برای اتصال حساب Bale به حساب ERP، نام کاربری Casdoor خود را بفرست " "(یا اگر رمز جدا داری همان نام کاربری سامانه). برای لغو: /cancel" ) LINK_ASK_PASSWORD = "ممنون. حالا رمز عبور را بفرست (فقط برای یک بار ورود؛ ذخیره نمی‌شود). /cancel برای لغو" LINK_OK = "✅ اتصال انجام شد: {name} ({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} {m.get('label')}") return "\n".join(lines) def quote_review_text(payload: dict, missing: list[dict], status_note: str | None = None) -> str: 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) 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("🏢 تأمین‌کننده") 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"{sup_name}{official_badge} ✅") if payload.get("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(str(payload["supplier_contact_person"])) if contact_parts: lines.append(f"📞 {' — '.join(contact_parts)}") elif sup_name: lines.append(f"{sup_name}{official_badge} ✨ (تأمین‌کننده جدید)") else: lines.append("— (مشخص نشده)") cat_name = payload.get("supplier_category_name") if cat_name: lines.append(f"\n🏷️ موضوع: {cat_name}") # 📦 اقلام items = payload.get("items") or [] total_amount = 0.0 has_any_price = False if items: 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 = [str(name)] if it.get("brand_name"): title_parts.append(str(it["brand_name"])) if it.get("specs"): title_parts.append("، ".join(str(s) for s in it["specs"])) if not it.get("material_id"): 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" 🏗️ {item_proj}") else: lines.append(" 🏗️ پروژه مشخص نشده") # 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)} {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" = {fa_num(line_sum)} تومان" 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💰 مجموع مبلغ: {fa_num(total_amount)} تومان") elif not has_any_price: lines.append("\n💰 مجموع مبلغ: محاسبه‌نشده (قیمت اقلام وارد نشده)") else: lines.append("\n📦 اقلام: هنوز چیزی ثبت نشده ❌") # 💳 شرایط پرداخت sheets = payload.get("termsheets") or [] if sheets: 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"▫️ {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" ({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"▫️ {lbl}{disc}") elif items: lines.append("\n💳 شرایط پرداخت: نقدی / توافقی (پیش‌فرض)") # 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"📎 پیش‌فاکتور: {payload['proforma_file']}") if payload.get("notes"): meta_lines.append(f"📝 توضیحات: {payload['notes']}") if meta_lines: lines.append("") lines.extend(meta_lines) # 📋 وضعیت فیلدها (اگر نقصی وجود داشته باشد) if has_blocking or has_warnings: lines.append("\n📋 وضعیت فیلدها:") 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 = ["🏢 پیش‌نویس تأمین‌کننده جدید", f"نام: {esc(payload.get('name'))}"] 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وضعیت فیلدها:") lines.append(missing_block(missing)) return "\n".join(lines) def material_review_text(payload: dict, missing: list[dict]) -> str: 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وضعیت فیلدها:") lines.append(missing_block(missing)) return "\n".join(lines) def quote_committed_report(created: dict, payload: dict, remaining: list[str]) -> str: 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)}") 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خالی مانده‌ها (بعداً می‌توانی در برنامه تکمیل کنی):") lines += [f"⚠️ {esc(r)}" for r in remaining] lines.append("\nمرحله بعد: وقتی تصمیم گرفتی، بگو «استعلام را قبول کن» تا پذیرش را با هم انجام دهیم.") return "\n".join(lines) def supplier_committed_report(created: dict) -> str: return ( f"✅ تأمین‌کننده ثبت شد — {esc(created.get('name'))}\n" f"دسته: {esc((created.get('category') or {}).get('name'))}" ) def material_committed_report(created: dict) -> str: return f"✅ کالا ثبت شد — {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)