项目概要
~85%
操作时间节省
1min
优化后每月耗时
4~6h
优化前每月耗时
项目背景
每月需对账期客户进行应收账款账龄分析,手工从 SAP 导出数据后逐一核对逾期/到期情况,并手动编写对账邮件逐一发送,全程 4~6 小时/月。
解决方案
Python 全流程自动化:读取 SAP 导出数据 → 清洗合并 → 账龄分析 → 生成三张报表 → 批量发送对账邮件,同时联动影刀触发相关自动化流程。
读取SAP数据
→
数据清洗匹配
→
账龄区间计算
→
生成三张报表
→
批量发送邮件
核心难点
部分核销发票合并处理(同一发票参考多行求和);特殊客户账期与系统不一致需按实际账期修正;动态计算各逾期区间(1~6月+超6月);SMTP 批量发邮件3s间隔控制避免触发反垃圾机制。
输出成果
自动生成三张 Excel 表:账期客户主表(含逾期区间+回款透视)、邮件内容表、回款情况表;同步批量发送对账邮件给客户。
技术栈
项目文档
一、背景与目标
账期客户每月月末需出具账龄分析报告,并逐一发送对账邮件确认应收金额。手工操作需 4~6 小时,自动化后压缩至约 10 分钟,节省约 85%。
二、脚本说明
| 脚本 | 功能 | 输出 |
|---|---|---|
| 账期客户管理报告.py | 数据清洗、账龄计算、生成报表 | 分销账期客户管理报告.xlsx / 邮件表.xlsx |
| 对账邮件.py | 读取邮件表,批量发送 HTML 格式对账邮件 | 按格式生成对账邮件 |
| main.py | 按序调用以上两个脚本,同步启动影刀 | 影刀做展示邮件用途 |
三、输出报表说明
| Sheet | 内容 |
|---|---|
| 账期客户 | 客户账龄汇总:未清总计、逾期总计、当月到期、逾期1~6月分区间、超6月、各月回款透视 |
| 邮件表 | 按客户明细展示所有未清发票,含到期/逾期标注,用于对账邮件内容 |
| 回款情况 | 本年各月收款透视,便于识别长期不回款客户 |
四、特殊处理逻辑
部分核销:同一发票参考出现多行时,金额合并求和,仅保留最后一行;特殊客户账期与SAP系统不符,脚本按实际账期自动修正到期日;邮件发送间隔可配置(默认3秒),避免触发163邮箱反垃圾限制。
效果展示
以下为应收报告生成与对账邮件发送全流程交互演示,点击「播放」查看完整执行过程。
录屏演示
生成及批量邮件发送过程录屏
示例文件
以下为项目相关文件截图(注:此文件中的数据均已进行脱敏及适应性修改,与公司业务无直接关联,仅用于演示或测试用途)。
生成的账期客户管理报告(含逾期区间)
自动发送的对账邮件示例
各月回款透视表
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)}")