项目概要

~85%
操作时间节省
1min
优化后每月耗时
4~6h
优化前每月耗时

项目背景

每月需对账期客户进行应收账款账龄分析,手工从 SAP 导出数据后逐一核对逾期/到期情况,并手动编写对账邮件逐一发送,全程 4~6 小时/月。

解决方案

Python 全流程自动化:读取 SAP 导出数据 → 清洗合并 → 账龄分析 → 生成三张报表 → 批量发送对账邮件,同时联动影刀触发相关自动化流程。

读取SAP数据
数据清洗匹配
账龄区间计算
生成三张报表
批量发送邮件

核心难点

部分核销发票合并处理(同一发票参考多行求和);特殊客户账期与系统不一致需按实际账期修正;动态计算各逾期区间(1~6月+超6月);SMTP 批量发邮件3s间隔控制避免触发反垃圾机制。

输出成果

自动生成三张 Excel 表:账期客户主表(含逾期区间+回款透视)、邮件内容表、回款情况表;同步批量发送对账邮件给客户。

技术栈

Python pandas openpyxl smtplib 影刀 RPA

项目文档

版本:v1.0更新:2026-05类型:财务自动化 / RPA

一、背景与目标

账期客户每月月末需出具账龄分析报告,并逐一发送对账邮件确认应收金额。手工操作需 4~6 小时,自动化后压缩至约 10 分钟,节省约 85%

二、脚本说明

脚本功能输出
账期客户管理报告.py数据清洗、账龄计算、生成报表分销账期客户管理报告.xlsx / 邮件表.xlsx
对账邮件.py读取邮件表,批量发送 HTML 格式对账邮件按格式生成对账邮件
main.py按序调用以上两个脚本,同步启动影刀影刀做展示邮件用途

三、输出报表说明

Sheet内容
账期客户客户账龄汇总:未清总计、逾期总计、当月到期、逾期1~6月分区间、超6月、各月回款透视
邮件表按客户明细展示所有未清发票,含到期/逾期标注,用于对账邮件内容
回款情况本年各月收款透视,便于识别长期不回款客户

四、特殊处理逻辑

部分核销:同一发票参考出现多行时,金额合并求和,仅保留最后一行;特殊客户账期与SAP系统不符,脚本按实际账期自动修正到期日;邮件发送间隔可配置(默认3秒),避免触发163邮箱反垃圾限制。

效果展示

以下为应收报告生成与对账邮件发送全流程交互演示,点击「播放」查看完整执行过程。

录屏演示

演示2
生成及批量邮件发送过程录屏

示例文件

以下为项目相关文件截图(注:此文件中的数据均已进行脱敏及适应性修改,与公司业务无直接关联,仅用于演示或测试用途)。

账期客户主表
生成的账期客户管理报告(含逾期区间)
对账邮件
自动发送的对账邮件示例
回款情况
各月回款透视表
SAP数据
SAP导出的行项目原始数据

源码

main.py — 主入口
import subprocess
import sys
import time
import pyautogui

# 第一步:生成报告
print("===== 开始运行:账期客户管理报告.py =====")
result1 = subprocess.run([sys.executable, r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\账期客户管理报告.py"])
if result1.returncode != 0:
    print("报告生成失败,中止")
    input("按回车键关闭...")
    exit()

# 第二步:发送对账邮件 + 同时打开影刀(并行)
print("\n===== 开始运行:对账邮件.py + 影刀 =====")

# 影刀后台启动
subprocess.Popen(r"F:\影刀\ShadowBot.exe")
# 第二步:等影刀加载完,发快捷键
print("\n===== 发送快捷键给影刀 =====")
time.sleep(10)
pyautogui.hotkey('ctrl', 'shift', 'g')

# 对账邮件同步运行(等它跑完)
result2 = subprocess.run([sys.executable, r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\对账邮件.py"])
if result2.returncode != 0:
    print("对账邮件发送失败")



print("\n全部完成")
input("按回车键关闭...")
账期客户管理报告.py — 数据处理与报表生成
import pandas as pd
from pandas.tseries.offsets import DateOffset
from datetime import datetime
from dateutil.relativedelta import relativedelta

# ====================================基础配置=============================================
# 47家客户SAP明细信息
path = r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\47家行项目.xlsx"
# 用于输出最后文件
path2 = r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\分销账期客户管理报告.xlsx"
# 47家客户1-当前月份信息 用于统计每月收款
path3 = r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\47家全部.xlsx"
# 客户综合信息表用于匹配客户号 客户名称账期等信息
path4 = r'C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\客户明细表.xlsx'
# 用于匹配客户号和账期信息
details = pd.read_excel(path4, sheet_name='账期客户')
# 47家行项目基础信息清洗 提取修正为需要格式
result=pd.read_excel(path,sheet_name='Sheet1')

# 说明:分销分销账期客户管理报告.xlsx 最终sheet表明细:
# 账期客户sheet:用于账期客户每月逾期到期及每月收款汇总
# 邮件表sheet:用于统计应收金额明细 对分销商账期客户发送对账邮件
# 回款情况sheet:用于查询逐笔账期回款
# 配送商sheet:用于标注特殊业务客户

# ==============================================================================================


# =============================================构建邮件表基础表==================================

# -----------------------------------------------空发票号填充处理--------------------------------
# 业务逻辑:给客户发的邮件表里不能有负数 发票参考重复的为部分核销 即一张发票核销了一部分还有剩余 客户欠款为剩余部分而不是整张发票
# 筛选 科目为预收应收科目明细
condition=result['总账科目'].astype(str).str.contains('11220101|11220102')
result=result.loc[condition,:]
# 找出“发票参考”号出现不止一次的所有记录 原始数据如下:
# 发票参考    发票号      本币金额    凭证号
# 9001                  -345     200000
# 9001    3770....      896      180000
# 查找发票参考为重复的列
duplicate_mask = result.duplicated(subset=['发票参考'], keep=False)
# 对这些列新增一个原发票金额列 设置格式方便后续加到文本里
result.loc[duplicate_mask, '原发票金额'] = '原发票金额' + result.loc[duplicate_mask, '本币金额'].astype(str)
#科目相同 + 发票参考相同 的行,归为一组  只保留最后一行   把这一组重复发票的 金额全部加起来 然后填回到刚才保留的那一行里
# 这个不能直接赋值给文本列 因为有特殊客户需要带原来的文本
result= result.groupby(["科目","发票参考"], as_index=False).last().assign(本币金额=result.groupby(["科目","发票参考"])["本币金额"].sum().values)
# 业务逻辑:给客户发的邮件表里不能有负数 发票参考重复的为部分核销 即一张发票核销了一部分还有剩余 客户欠款为剩余部分而不是整张发票
# 处理后效果:
# 发票参考    发票号      本币金额    凭证号
# 9001    3770....      551      180000
# ------------------------------------------特殊客户文本处理----------------------------------------------
# 业务逻辑:特定客户文本列内容需要带送达方 而且也要带原始发票金额
# 特殊客户
result["文本"] = result["文本"].fillna("")
has_keyword = result["文本"].str.contains("国飞侠有限公司|烟舞台删有限公司", regex=True, case=False)
# 将除了特定客户外的客户 文本列的内容清空 文本是空 → pandas 会写成 NaN
result.loc[~has_keyword, "文本"] = ""
# 将原发票金额列缺失值填充 原发票金额没值 → pandas 也会写成 NaN
result["原发票金额"] = result["原发票金额"].fillna("")
# 不处理缺失值文本拼接会出问题
result["文本"] = result["文本"].astype(str)+' '+result["原发票金额"].astype(str)
# 47家导出来的原表不带客户号 从客户明细里匹配上客户号
result = pd.merge(
    result,
    details[['客户号','客户']],
    left_on='科目',
    right_on='客户号',
    how='left'
)
# 按照科目进行排序
result = result.sort_values(by='科目', ascending=False)
# -------------------------------------------特殊账期客户到期日处理----------------------------------------------
# 业务逻辑: 特殊客户实际账期与系统账期不一致 计算应收金额要按实际账期计算
# 确保日期列是 datetime 类型(否则无法进行日期运算)
result["净收付到期日"] = pd.to_datetime(result["净收付到期日"])
# 定位特殊客户1,对其日期列加5个月
result.loc[result["客户"] == "临亿飞有限公司", "净收付到期日"] += DateOffset(months=5)
# 定位特殊客户2,将270天账期的加3个月 60天账期的加1个月
result.loc[
    (result["客户"] == "山东李白集团有限公司") &
    (result["付款条件"] == "Z270"),
    "净收付到期日"
] += DateOffset(months=3)
result.loc[
    (result["客户"] == "山东李白集团有限公司") &
    (result["付款条件"] == "Z060"),
    "净收付到期日"
] += DateOffset(months=1)
# 修正日期格式
result["净收付到期日"] = pd.to_datetime(result["净收付到期日"], format="%Y/%m/%d")
# ---------------------- --------根据当前日期计算到期逾期金额 -------------------------------------------------------
# 业务逻辑:根据当前日期计算到期及逾期金额 并进行标注
current_date = datetime.now()  # 获取当前系统日期
current_year = current_date.year  # 当前年份
current_month = current_date.month  # 当前月份
# 条件:日期的年份 == 当前年份 且 日期的月份 == 当前月份
# 当前月份数据筛选  当月到期金额
is_current_month = (result["净收付到期日"].dt.year == current_year) & (result["净收付到期日"].dt.month == current_month)
# 新增备注列
result["备注"] = ""
# 将筛选出来的符合的数据标注上当月到期
result.loc[is_current_month, "备注"] = "到期"
# 逾期金额 除当月到期的 日期小于当月的数据
is_past = (  # 排除无效日期
    ~is_current_month &  # 排除当月
    (
        (result["净收付到期日"].dt.year < current_year) |  # 年份更早
        (result["净收付到期日"].dt.year == current_year) & (result["净收付到期日"].dt.month < current_month)  # 同年更早月份
    )
)
# 将筛选出来的数据标注上逾期
result.loc[is_past, "备注"] = "逾期"
# 按照客户备注进行排序
result= result.sort_values(by=['客户', '备注'], ascending=[True, False])
# 修改表头
new_columns=['客户号','客户','年度/月份',
'付款条件','付款起算日期','净收付到期日',
    'VAT 注册号','本币金额','文本','备注']
result=result[new_columns]
# 处理一下日期格式
result["付款起算日期"] = result["付款起算日期"].dt.strftime("%Y/%m/%d")
result["净收付到期日"] = result["净收付到期日"].dt.strftime("%Y/%m/%d")
# ====================================================邮件表结束=====================================================

# ==================================================账期客户主表处理====================================================
# 数据预处理
result["净收付到期日"] = pd.to_datetime(result["净收付到期日"])  # 转换为日期类型
current_date = datetime.now()  # 当前日期(基准日)
current_month_start = datetime(current_date.year, current_date.month, 1)  # 当月1日
# 定义各逾期月份的日期范围
# 逾期n个月 = 到期日在「当前月-n个月的1日」到「当前月-n个月的月末」之间
ranges = {
    "逾期1个月": (
        current_month_start - relativedelta(months=1),  # 前1个月1日
        current_month_start - relativedelta(days=1)  # 前1个月末
    ),
    "逾期2个月": (
        current_month_start - relativedelta(months=2),  # 前2个月1日
        current_month_start - relativedelta(months=1, days=1)  # 前2个月末
    ),
    "逾期3个月": (
        current_month_start - relativedelta(months=3),
        current_month_start - relativedelta(months=2, days=1)
    ),
    "逾期4个月": (
        current_month_start - relativedelta(months=4),
        current_month_start - relativedelta(months=3, days=1)
    ),
    "逾期5个月": (
        current_month_start - relativedelta(months=5),
        current_month_start - relativedelta(months=4, days=1)
    ),
    "逾期6个月": (
        current_month_start - relativedelta(months=6),
        current_month_start - relativedelta(months=5, days=1)
    ),
    "超过6个月": (
        pd.Timestamp.min,  # 最小日期(无限早)
        current_month_start - relativedelta(months=6, days=1)  # 前6个月末之前
    )
}

# 3. 一次性统计所有列(原有3列 + 7个逾期月份列)
result2 = result.groupby("客户").agg(
    # 原有三列
    逾期总计=("本币金额", lambda x: x[result.loc[x.index, "备注"] == "逾期"].sum()),
    当月到期=("本币金额", lambda x: x[result.loc[x.index, "备注"] == "到期"].sum()),
    未清项目总计=("本币金额", "sum"),

    # 新增:各逾期月份区间的金额合计(仅统计备注=逾期的数据)
    **{
        col: ("本币金额", lambda x, start=start, end=end: x[
            (result.loc[x.index, "备注"] == "逾期") &  # 仅逾期
            (result.loc[x.index, "净收付到期日"] >= start) &  # 在区间内
            (result.loc[x.index, "净收付到期日"] <= end)
            ].sum())
        for col, (start, end) in ranges.items()
    }
).reset_index()

# 根据客户匹配明细表的客户号和付款条件
result2 = pd.merge(
    result2,
    details[['客户号','客户','付款条件']],
    on='客户',
    how='left'
)
# 重新定义行标签顺序
column_order = [
    '客户号', '客户', '付款条件',  # 前3列
    '未清项目总计', '逾期总计', '当月到期',  # 中间3列
    '逾期1个月', '逾期2个月', '逾期3个月', '逾期4个月', '逾期5个月', '逾期6个月',  # 逾期1-6个月
    '超过6个月'  # 最后一列
]
result2 = result2[column_order]
# ====================================================================================================================


# ==============================================回款情况表处理=========================================================
# 业务逻辑:回款情况表为了统计显示客户本年每月回款情况 便于及时发现超过3个月不回款客户 对连续不回款客户进行预警
# 读取47家全部表数据
receivables = pd.read_excel(path3, sheet_name='Sheet1')
# 筛选数据 将收款凭证进行筛选
conditions = receivables['特别总帐标志'].astype(str).str.contains("A") & receivables['凭证编号'].astype(str).str.contains('16000|18000')
# 将筛选出来的数据复制到新的表
sreceivables = receivables.loc[conditions,:].copy()
# 重命名科目列为客户号
sreceivables.rename(
    columns={"科目": "客户号"},
    inplace=True
)
# 确保数据类型一致
sreceivables['客户号'] = sreceivables['客户号'].astype(int).astype(str)
details['客户号'] = details['客户号'].astype(int).astype(str)
# 处理缺失值
sreceivables["文本"] = sreceivables["文本"].fillna("")
# 筛选出来的金额包括现款金额 本表是计算账期客户收款情况需要将试剂款删除
contains_reagent = sreceivables["文本"].str.contains("试剂款", regex=False, case=False)
sreceivables.drop(sreceivables[contains_reagent].index, inplace=True)
# 4. 重置索引 把试剂款行删除后 不重置索引后续合并透视表会出问题
sreceivables.reset_index(drop=True, inplace=True)
# 对收款明细表进行透视 与账期客户主表联动追加每月回款明细
# 创建透视表
pivot_table = pd.pivot_table(
    sreceivables,
    values='本币金额',
    index=['客户号'],
    columns='年度/月份',
    aggfunc='sum',
    fill_value=0,
)
# 重置索引
receivable_finally = pivot_table.reset_index()
# 客户号格式处理
result2['客户号'] = result2['客户号'].astype(int).astype(str)
# 将透视出来的表与汇总表进行匹配 匹配上每个月回款情况
result2 = pd.merge(
    result2,
    receivable_finally,
    on='客户号',
    how='left'
)
# 按照逾期总计金额进项排序 逾期金额大的在前面
result2 = result2.sort_values(by=['逾期总计'], ascending=[False])
# ===============================================================================================================
# 特殊客户只做标记处理
# result3 =pd.to_excel(path2, sheet_name='配送商')
# 写入文件
with pd.ExcelWriter(path2, engine="openpyxl") as writer:
    result2.to_excel(writer, index=False, sheet_name="账期客户")
    result.to_excel(writer, index=False, sheet_name="邮件表")
    sreceivables.to_excel(writer, index=False, sheet_name="回款情况")
    # result3 .to_excel(writer, index=False, sheet_name="配送商")
path5 = r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\邮件表.xlsx"
with pd.ExcelWriter(path5, engine="openpyxl", mode='a', if_sheet_exists='replace') as writer:
    result.to_excel(writer, index=False, sheet_name="邮件内容")
print("已为您生成账期客户管理报告")
# =================================================================================================================
对账邮件.py — 批量发送对账邮件
import pandas as pd
import datetime
import smtplib
import time
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from typing import Dict


def get_dynamic_dates() -> Dict[str, str]:
    """获取动态日期:上月月底、当月、当月最后一天(新增当月最后一天用于到期款判断)"""
    today = datetime.datetime.now()
    current_month = today.strftime("%Y.%m")
    last_day_of_last_month = (today.replace(day=1) - datetime.timedelta(days=1)).strftime("%Y.%m.%d")

    # 新增:计算当月最后一天(无论今日几号,取当月月末日期)
    first_day_of_next_month = (today.replace(day=1) + datetime.timedelta(days=32)).replace(day=1)
    last_day_of_current_month = (first_day_of_next_month - datetime.timedelta(days=1)).strftime("%Y.%m.%d")

    return {
        "last_month_end": last_day_of_last_month,
        "current_month": current_month,
        "current_month_end": last_day_of_current_month  # 新增字段:当月最后一天
    }


def read_excel_data(excel_path: str) -> Dict:
    """读取Excel中的“邮箱地址”和“邮件内容”表"""
    try:
        email_df = pd.read_excel(excel_path, sheet_name="邮箱地址")
        debt_df = pd.read_excel(excel_path, sheet_name="邮件内容")

        # 数据预处理:清洗客户名称、删除关键列空值
        email_df = email_df.dropna(subset=["客户", "收件人"]).reset_index(drop=True)
        email_df["客户_clean"] = email_df["客户"].str.strip().str.lower()

        debt_df = debt_df.dropna(subset=["客户", "本币金额"]).reset_index(drop=True)
        debt_df["客户_clean"] = debt_df["客户"].str.strip().str.lower()

        # 处理日期格式(确保到期日可用于比较)
        debt_df["净收付到期日"] = pd.to_datetime(debt_df["净收付到期日"], errors="coerce")

        # 检查文本列,无则添加空列
        if "文本" not in debt_df.columns:
            print("警告:邮件内容表中未找到'文本'列,将添加空列")
            debt_df["文本"] = ""

        print(f"成功读取Excel:{len(email_df)}个客户邮箱,{len(debt_df)}条欠款记录")
        print("邮箱表前3个客户:", list(email_df["客户_clean"].head(3)))
        print("欠款表前3个客户:", list(debt_df["客户_clean"].head(3)))

        return {"email_df": email_df, "debt_df": debt_df}
    except Exception as e:
        raise Exception(f"读取Excel失败:{str(e)}")


def process_debt_data(debt_df: pd.DataFrame, current_month_end: str) -> Dict:
    """处理欠款数据:核心修改——current_due改为“当月及历史所有到期款总和”"""
    # 1. 计算每个客户的总应付账款(原有逻辑不变)
    summary_by_customer = debt_df.groupby("客户_clean").agg(
        客户=("客户", "first"),
        总应付账款=("本币金额", "sum")
    ).reset_index()

    # 2. 核心修改:筛选“到期日≤当月最后一天”的所有记录(含历史逾期+当月到期)
    detail_df = debt_df.copy()
    # 将“当月最后一天”转为datetime格式,用于日期比较
    current_month_end_dt = datetime.datetime.strptime(current_month_end, "%Y.%m.%d")
    # 筛选条件:所有到期日不晚于当月月末的记录
    all_due_detail = detail_df[detail_df["净收付到期日"] <= current_month_end_dt].copy()

    # 按客户求和,得到“当月及历史所有到期款”
    current_due_summary = all_due_detail.groupby("客户_clean")["本币金额"].sum().reset_index()
    current_due_summary.columns = ["客户_clean", "当月到期款"]  # 字段名保留,含义已更新
    current_due_summary["当月到期款"] = current_due_summary["当月到期款"].astype(float)

    return {
        "summary_df": summary_by_customer,
        "detail_df": detail_df,
        "current_due_summary": current_due_summary
    }


def generate_email_content(customer_info: Dict, debt_detail: pd.DataFrame, dates: Dict) -> str:
    """生成HTML格式的邮件正文,包含文本列并处理空值"""
    customer_name = customer_info["客户"]
    total_debt = customer_info["总应付账款"]
    current_due = customer_info.get("当月到期款", 0.0)

    # 生成欠款明细HTML表格
    detail_table = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; text-align: center;'>"
    # 表格表头
    headers = ["客户", "年度/月份", "付款条件", "付款起算日期", "净收付到期日",
               "VAT注册号", "文本", "本币金额", "备注"]
    detail_table += "<tr style='background-color: #f0f0f0;'>"
    for header in headers:
        detail_table += f"<th>{header}</th>"
    detail_table += "</tr>"

    # 表格内容(处理空值和日期格式)
    for _, row in debt_detail.iterrows():
        detail_table += "<tr>"
        # 处理日期空值
        due_date = row["净收付到期日"].strftime("%Y-%m-%d") if pd.notna(row["净收付到期日"]) else ""
        # 处理文本、备注空值
        text_content = row["文本"] if pd.notna(row["文本"]) else ""
        remark = row["备注"] if pd.notna(row["备注"]) else ""

        row_data = [
            row["客户"],
            row["年度/月份"],
            row["付款条件"],
            row["付款起算日期"],
            due_date,
            row.get("VAT 注册号", ""),
            text_content,
            f"{row['本币金额']:.2f}",
            remark
        ]
        for data in row_data:
            detail_table += f"<td>{data}</td>"
        detail_table += "</tr>"

    # 添加总计行
    detail_table += "<tr style='background-color: #e6f3ff; font-weight: bold;'>"
    detail_table += f"<td colspan='7'>总计</td>"
    detail_table += f"<td>{total_debt:.2f}</td>"
    detail_table += f"<td></td>"
    detail_table += "</tr>"
    detail_table += "</table>"

    # 拼接邮件正文
    email_body = f"""
    <p>尊敬的{customer_name}:</p>
    <p>您好!</p>
    <p>截至{dates['last_month_end']}日,{customer_name}在我司账面应付账款共计:<strong>{total_debt:.2f}元</strong>,{dates['current_month']}到期应付款(含历史逾期):<strong>{current_due:.2f}元</strong>,明细如下:</p>
    <br>
    {detail_table}
    <br>
    <p>请贵司协助安排付款事宜,如有疑问,请及时与我司联系。感谢您的配合!</p>
    <p>顺颂商祺!</p>
    <p>青岛XX公司<br>{datetime.datetime.now().strftime("%Y年%m月%d日")}</p>
    """
    return email_body


def send_batch_emails(excel_path: str, smtp_config: Dict, send_interval: int = 40):
    """批量发送邮件主函数"""
    # 1. 初始化动态日期(含当月最后一天)和Excel数据
    dates = get_dynamic_dates()
    excel_data = read_excel_data(excel_path)
    email_df = excel_data["email_df"]
    debt_df = excel_data["debt_df"]

    # 2. 处理欠款数据:传入“当月最后一天”(核心修改点)
    debt_processed = process_debt_data(debt_df, dates["current_month_end"])  # 原参数为dates["current_month"]
    summary_df = debt_processed["summary_df"]
    detail_df = debt_processed["detail_df"]
    current_due_summary = debt_processed["current_due_summary"]

    # 3. 关联客户邮箱与欠款数据
    debt_summary = pd.merge(
        summary_df[["客户_clean", "客户", "总应付账款"]],
        current_due_summary,
        on="客户_clean",
        how="left"
    ).fillna({"当月到期款": 0.0})

    customer_all = pd.merge(
        debt_summary,
        email_df[["客户_clean", "收件人", "抄送", "客户"]],
        on="客户_clean",
        how="left",
        suffixes=('', '_email')
    ).dropna(subset=["收件人"]).reset_index(drop=True)

    # 调试信息
    print(f"\n匹配到 {len(customer_all)} 个客户-邮箱对应关系")
    if len(customer_all) > 0:
        print("匹配到的客户列表:", list(customer_all["客户"].unique()))
        print(f"将以{send_interval}秒间隔发送邮件...")

    # 无匹配数据时提示
    if len(customer_all) == 0:
        print("\n未匹配到数据,可能原因:")
        print("1. 邮箱表客户:", set(email_df["客户_clean"]))
        print("2. 欠款表客户:", set(summary_df["客户_clean"]))
        print("请检查两个表中的客户名称是否一致")
        return

    # 4. 配置SMTP服务器
    try:
        server = smtplib.SMTP_SSL(smtp_config["server"], smtp_config["port"])
        server.login(smtp_config["sender_email"], smtp_config["sender_auth_code"])
        print(f"\n成功连接SMTP服务器:{smtp_config['server']}")
    except Exception as e:
        raise Exception(f"SMTP服务器连接失败:{str(e)}")

    # 5. 遍历客户发送邮件(含间隔控制)
    total = len(customer_all)
    for i, (_, customer) in enumerate(customer_all.iterrows(), 1):
        try:
            customer_name = customer["客户"]
            customer_clean = customer["客户_clean"]
            receiver = customer["收件人"]
            cc = customer["抄送"] if pd.notna(customer["抄送"]) else ""

            # 提取该客户的欠款明细
            customer_detail = detail_df[detail_df["客户_clean"] == customer_clean].copy()
            if len(customer_detail) == 0:
                print(f"客户{customer_name}无欠款明细,跳过")
                continue

            # 生成邮件主题和内容
            email_subject = f"{dates['current_month']} {customer_name}欠款核对提醒(截至{dates['last_month_end']}日)"
            email_body = generate_email_content(
                customer_info={
                    "客户": customer_name,
                    "总应付账款": customer["总应付账款"],
                    "当月到期款": customer["当月到期款"]
                },
                debt_detail=customer_detail,
                dates=dates
            )

            # 构建邮件对象
            msg = MIMEMultipart()
            msg["From"] = formataddr(("青岛XX公司", smtp_config["sender_email"]))
            msg["To"] = receiver
            if cc:
                msg["Cc"] = cc
            msg["Subject"] = email_subject
            msg.attach(MIMEText(email_body, "html", "utf-8"))

            # 收集所有收件人(含抄送)
            all_receivers = receiver.split(",")
            if cc:
                all_receivers.extend(cc.split(","))
            all_receivers = [r.strip() for r in all_receivers if r.strip()]

            # 发送邮件
            server.sendmail(smtp_config["sender_email"], all_receivers, msg.as_string())
            print(f"✅ 成功发送({i}/{total}):{customer_name}(收件人:{receiver})")

        except Exception as e:
            print(f"❌ 发送失败({i}/{total}):{customer['客户']},原因:{str(e)}")

        # 非最后一封邮件,等待指定间隔
        if i < total:
            print(f"等待{send_interval}秒后发送下一封...")
            time.sleep(send_interval)

    # 关闭SMTP连接
    server.quit()
    print("\n所有客户邮件处理完毕!")


if __name__ == "__main__":
    # 配置信息 - 请根据实际情况修改

    EXCEL_PATH = r"C:\Users\Yuyu\Desktop\work\2.应收账期管理报告\邮件表.xlsx"  # Excel文件路径

    SMTP_CONFIG = {
        "server": "smtp.163.com",  # 个人163 SMTP
        "port": 465,
        "sender_email": "邮箱账号",
        "sender_auth_code": "授权码"
    }

    # 发送间隔(秒)- 避免触发邮箱反垃圾机制
    SEND_INTERVAL = 3

    # 启动批量发送
    try:
        send_batch_emails(EXCEL_PATH, SMTP_CONFIG, SEND_INTERVAL)
    except Exception as e:
        print(f"程序异常终止:{str(e)}")