外贸催款信分阶段生成引擎·专业版
输入 / 输出
- 输入:
overdue_days(逾期天数)、amount(金额)、customer_name、company_name、
currency(USD/CNY/EUR/GBP/JPY)、invoice_no、is_old_customer。 - 输出:
stage(1-4)、tone、tone_level、amount_words(金额英文大写)、
subject(英文主题)、body(完整英文正文)、next_action(下一步动作)。
使用示例
from payment_reminder import generate_reminder
r = generate_reminder(20, 88000, "GmbH Buyer", "Hubei Zhongti Chemical Co., Ltd.",
currency="EUR", invoice_no="INV-778", is_old_customer=False)
print(r["stage"], r["subject"])
print(r["body"])
引擎代码:payment_reminder.py
# -*- coding: utf-8 -*-
"""
外贸催款信分阶段生成引擎(专业版)
输入:逾期天数、欠款金额、客户名、发函公司名、币种、发票号、是否老客户
输出:分档阶段、语气强度、邮件主题、正文、下一步动作建议
确定性规则:
逾期 1-7 天 -> stage 1 友好提醒(tone 1)
逾期 8-30 天 -> stage 2 正式催收(tone 2)
逾期 31-60 天 -> stage 3 严肃催收(tone 3)
逾期 61 天以上 -> stage 4 最后通牒(tone 4)
"""
from __future__ import annotations
CURRENCY_SYMBOL = {"USD": "USD", "CNY": "CNY", "EUR": "EUR", "GBP": "GBP", "JPY": "JPY"}
_ONES = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine",
"Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen",
"Seventeen", "Eighteen", "Nineteen"]
_TENS = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"]
def amount_to_words(n: int) -> str:
"""把非负整数金额转成英文大写(整数部分,最大到百万)。"""
if n == 0:
return "Zero"
if n < 0:
raise ValueError("amount must be non-negative")
def _under_thousand(x):
s = ""
if x >= 100:
s += _ONES[x // 100] + " Hundred"
x %= 100
if x:
s += " "
if x >= 20:
s += _TENS[x // 10]
if x % 10:
s += "-" + _ONES[x % 10]
elif x > 0:
s += _ONES[x]
return s.strip()
parts = []
if n >= 1_000_000:
parts.append(_under_thousand(n // 1_000_000) + " Million")
n %= 1_000_000
if n >= 1000:
parts.append(_under_thousand(n // 1000) + " Thousand")
n %= 1000
if n:
parts.append(_under_thousand(n))
return " ".join(parts)
def _stage(days: int):
if days <= 7:
return 1, "friendly", 1
if days <= 30:
return 2, "formal", 2
if days <= 60:
return 3, "serious", 3
return 4, "final", 4
def generate_reminder(overdue_days, amount, customer_name, company_name,
currency="USD", invoice_no=None, is_old_customer=True):
"""生成分阶段催款信。返回 dict。"""
# 入参校验
if not isinstance(overdue_days, int) or overdue_days < 0:
raise ValueError("overdue_days 必须是非负整数")
if not isinstance(amount, (int, float)) or amount <= 0:
raise ValueError("amount 必须是大于 0 的数字")
if not customer_name or not str(customer_name).strip():
raise ValueError("customer_name 不能为空")
if not company_name or not str(company_name).strip():
raise ValueError("company_name 不能为空")
currency = (currency or "USD").upper()
if currency not in CURRENCY_SYMBOL:
raise ValueError(f"不支持的币种: {currency}")
stage, tone_key, tone_level = _stage(overdue_days)
int_amount = int(round(amount))
amount_words = amount_to_words(int_amount)
invoice_text = f"(发票号:{invoice_no})" if invoice_no else ""
rel = "长期合作的老客户" if is_old_customer else "客户"
if stage == 1:
subject = f"Payment Reminder: Invoice {invoice_no or ''} Overdue Notice".strip()
body = (
f"Dear {customer_name},\n\n"
f"Hope this email finds you well. As our valued {rel}, we would like to send a friendly reminder "
f"that invoice {invoice_no or '(see attached)'} {invoice_text} for {currency} {amount:,} "
f"(in words: {currency} {amount_words} Only) is currently {overdue_days} day(s) past due. "
f"It may have just slipped your attention.\n\n"
f"Could you please kindly check and arrange payment at your earliest convenience? "
f"If you have already settled it, please disregard this notice and accept our thanks.\n\n"
f"Best regards,\n{company_name}"
)
next_action = "等待付款或对方反馈;3-5 个工作日未回复再电话跟进。"
elif stage == 2:
subject = f"Second Reminder: Invoice {invoice_no or ''} Payment Now {overdue_days} Days Overdue".strip()
body = (
f"Dear {customer_name},\n\n"
f"We are writing to formally remind you that invoice {invoice_no or '(see attached)'} "
f"for {currency} {amount:,} ({amount_words} {currency}) has been outstanding for {overdue_days} days. "
f"Payment is now past the agreed terms.\n\n"
f"Please confirm the payment date in writing within 3 business days and provide the bank slip once made. "
f"If there is any dispute over the goods or documents, let us know promptly so we can resolve it together.\n\n"
f"Best regards,\n{company_name}"
)
next_action = "要求 3 个工作日内书面确认付款日期;记录沟通,准备对账单。"
elif stage == 3:
subject = f"Urgent: Invoice {invoice_no or ''} {overdue_days} Days Overdue - Payment Required Immediately".strip()
body = (
f"Dear {customer_name},\n\n"
f"Despite our previous reminders, invoice {invoice_no or '(see attached)'} for {currency} {amount:,} "
f"({amount_words} {currency}) remains unpaid and is {overdue_days} days overdue. "
f"This is now a serious matter.\n\n"
f"We must ask you to settle the full amount without further delay, or provide a concrete written "
f"payment schedule with dates and amounts. Failure to respond may lead to suspension of further "
f"shipments and credit, and we reserve the right to pursue collection through formal channels.\n\n"
f"Best regards,\n{company_name}"
)
next_action = "暂停后续发货/授信;要求书面还款计划;评估法务介入。"
else:
subject = f"FINAL NOTICE: Invoice {invoice_no or ''} {overdue_days} Days Overdue - Legal Action Pending".strip()
body = (
f"Dear {customer_name},\n\n"
f"This is our FINAL notice. Invoice {invoice_no or '(see attached)'} for {currency} {amount:,} "
f"({amount_words} {currency}) has been overdue for {overdue_days} days and all previous reminders "
f"have gone unanswered.\n\n"
f"Unless full payment is received within 7 days, we will, without further notice, hand this account "
f"to our legal counsel / collection agency and pursue all available remedies, including interest, "
f"collection costs and legal fees, as permitted by the contract and applicable law.\n\n"
f"Best regards,\n{company_name}"
)
next_action = "限 7 日内付清;到期移交法务/催收,准备合同、发票、催函留痕。"
return {
"stage": stage,
"tone": tone_key,
"tone_level": tone_level,
"overdue_days": overdue_days,
"currency": currency,
"amount": amount,
"amount_words": amount_words,
"subject": subject,
"body": body,
"next_action": next_action,
}
边界处理(已覆盖)
- 逾期天数 0 天:按第 1 档友好提醒处理,不报错;
- 临界值 7/8、30/31、60/61 逐档判定正确;
- 逾期天数为负、金额 ≤ 0、客户名/公司名为空、币种不支持:直接抛 ValueError 拦截;
- 金额大写支持 0 到百万级整数。
断言测试结果
- 4 个分档正例(5/20/45/75 天)全部正确;
- 金额大写(12,500、88,000、0、1,000,000)正确;
- 6 组临界天数边界全部正确;
- 5 组非法入参 + 非法币种全部拦截。
- 共 26 项断言全部通过。
批量接入建议
- 从 ERP/财务系统按账龄拉取应收清单,循环调用本引擎批量生成各档催款函;
- 生成结果先由业务员确认客户名与发票号,再通过邮件系统统一发送;
- 每次发送记录回写 CRM,形成催收时间线与证据链。