Spaces:
Paused
Paused
Update process_report.py
Browse files- process_report.py +59 -56
process_report.py
CHANGED
|
@@ -13,7 +13,6 @@ import json
|
|
| 13 |
import base64
|
| 14 |
from io import BytesIO
|
| 15 |
from typing import Optional, Tuple, List
|
| 16 |
-
|
| 17 |
from datetime import datetime, date
|
| 18 |
|
| 19 |
import pandas as pd
|
|
@@ -55,6 +54,14 @@ EMAIL_COLS_DEFAULT = [
|
|
| 55 |
"主数量","需求日期","供应商","到货日期","到货主数量","入库日期","入库主数量","目前进度"
|
| 56 |
]
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
# ====== 工具函数 ======
|
| 59 |
def _today() -> date:
|
| 60 |
return datetime.now().date()
|
|
@@ -139,18 +146,30 @@ def aggregate_for_email(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 139 |
grouped = df.groupby(group_keys, dropna=False).agg(final_agg_map).reset_index()
|
| 140 |
grouped["目前进度"] = grouped.apply(_calc_progress_row, axis=1)
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
if "计划到货日期" in grouped.columns:
|
| 143 |
grouped = grouped.drop(columns=["计划到货日期"])
|
| 144 |
|
| 145 |
final_cols = [col for col in EMAIL_COLS_DEFAULT if col in grouped.columns]
|
| 146 |
final = grouped[final_cols]
|
| 147 |
|
| 148 |
-
# --- 唯一的修改在此处 ---
|
| 149 |
-
# 将日期格式化为 yyyy-mm-dd 标准格式
|
| 150 |
date_cols_to_format = ["请购日期", "需求日期", "到货日期", "入库日期"]
|
| 151 |
for col in date_cols_to_format:
|
| 152 |
if col in final.columns:
|
| 153 |
-
final[col] = pd.to_datetime(final[col], errors='coerce').dt.strftime('%Y-%m-%d')
|
| 154 |
|
| 155 |
return final
|
| 156 |
|
|
@@ -199,9 +218,40 @@ def _find_latest_input(input_dir: str) -> Optional[str]:
|
|
| 199 |
files.sort(key=os.path.getmtime, reverse=True)
|
| 200 |
return files[0]
|
| 201 |
|
| 202 |
-
def
|
| 203 |
bio = BytesIO()
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
bio.seek(0)
|
| 206 |
return bio.read()
|
| 207 |
|
|
@@ -211,8 +261,8 @@ def _build_html_body(df: pd.DataFrame, title: str) -> str:
|
|
| 211 |
table {{ border-collapse: collapse; font-size: 13px; }}
|
| 212 |
table, th, td {{ border: 1px solid #ccc; padding: 6px; }}
|
| 213 |
th {{ background:#f6f6f6; }}
|
|
|
|
| 214 |
</style></head><body><h3>{title}</h3>{table_html}
|
| 215 |
-
<p style="color:#666;">备注:此邮件由自动化系统生成。</p>
|
| 216 |
</body></html>"""
|
| 217 |
return html
|
| 218 |
|
|
@@ -244,12 +294,10 @@ def run_once(file_path: Optional[str] = None) -> dict:
|
|
| 244 |
raw = read_system_export(file_path)
|
| 245 |
final = aggregate_for_email(raw)
|
| 246 |
|
| 247 |
-
# --- 使用带样式的Excel生成函数 ---
|
| 248 |
-
attach = _df_to_styled_excel_bytes(final) # 假设这个函数存在
|
| 249 |
-
|
| 250 |
out_name = f"邮件发送的格式_{datetime.now().strftime('%Y%m%d')}.xlsx"
|
| 251 |
out_path = os.path.join(OUTPUT_DIR, out_name)
|
| 252 |
-
|
|
|
|
| 253 |
final.to_excel(out_path, index=False)
|
| 254 |
|
| 255 |
subject = f"采购执行表自动推��� {datetime.now().date()}"
|
|
@@ -262,51 +310,6 @@ def run_once(file_path: Optional[str] = None) -> dict:
|
|
| 262 |
import traceback
|
| 263 |
return {"ok": False, "msg": f"处理文件时发生严重错误: {e}", "traceback": traceback.format_exc()}
|
| 264 |
|
| 265 |
-
def _df_to_styled_excel_bytes(df: pd.DataFrame) -> bytes:
|
| 266 |
-
"""
|
| 267 |
-
生成带样式的Excel文件。
|
| 268 |
-
使用XlsxWriter引擎来添加字体、边框、条件格式等。
|
| 269 |
-
"""
|
| 270 |
-
bio = BytesIO()
|
| 271 |
-
# fillna('') 可以在生成excel时将 <NA> 显示为空字符串
|
| 272 |
-
writer = pd.ExcelWriter(bio, engine='xlsxwriter')
|
| 273 |
-
|
| 274 |
-
sheet_name = '采购执行表'
|
| 275 |
-
df.fillna('').to_excel(writer, sheet_name=sheet_name, index=False)
|
| 276 |
-
|
| 277 |
-
workbook = writer.book
|
| 278 |
-
worksheet = writer.sheets[sheet_name]
|
| 279 |
-
|
| 280 |
-
# 定义格式
|
| 281 |
-
header_format = workbook.add_format({'bold': True, 'font_name': 'Arial', 'font_size': 10, 'border': 1, 'align': 'center', 'valign': 'vcenter'})
|
| 282 |
-
default_format = workbook.add_format({'font_name': 'Arial', 'font_size': 10, 'border': 1})
|
| 283 |
-
overdue_format = workbook.add_format({'font_name': 'Arial', 'font_size': 10, 'border': 1, 'font_color': 'red'})
|
| 284 |
-
|
| 285 |
-
# 应用表头格式
|
| 286 |
-
for col_num, value in enumerate(df.columns.values):
|
| 287 |
-
worksheet.write(0, col_num, value, header_format)
|
| 288 |
-
|
| 289 |
-
# 应用内容格式和条件格式
|
| 290 |
-
worksheet.conditional_format(1, 0, len(df), len(df.columns)-1, {'type': 'no_blanks', 'format': default_format})
|
| 291 |
-
|
| 292 |
-
try:
|
| 293 |
-
progress_col_idx = df.columns.get_loc('目前进度')
|
| 294 |
-
for row_num in range(len(df)):
|
| 295 |
-
status_text = df.iloc[row_num, progress_col_idx]
|
| 296 |
-
if isinstance(status_text, str) and "逾期" in status_text:
|
| 297 |
-
worksheet.set_row(row_num + 1, None, overdue_format)
|
| 298 |
-
except KeyError:
|
| 299 |
-
pass
|
| 300 |
-
|
| 301 |
-
# 自动调整列宽
|
| 302 |
-
for i, col in enumerate(df.columns):
|
| 303 |
-
column_len = max(df[col].astype(str).str.len().max(), len(col)) + 2
|
| 304 |
-
worksheet.set_column(i, i, min(column_len, 40))
|
| 305 |
-
|
| 306 |
-
writer.close()
|
| 307 |
-
bio.seek(0)
|
| 308 |
-
return bio.read()
|
| 309 |
-
|
| 310 |
def main():
|
| 311 |
arg_file = sys.argv[1] if len(sys.argv) > 1 else None
|
| 312 |
result = run_once(arg_file)
|
|
|
|
| 13 |
import base64
|
| 14 |
from io import BytesIO
|
| 15 |
from typing import Optional, Tuple, List
|
|
|
|
| 16 |
from datetime import datetime, date
|
| 17 |
|
| 18 |
import pandas as pd
|
|
|
|
| 54 |
"主数量","需求日期","供应商","到货日期","到货主数量","入库日期","入库主数量","目前进度"
|
| 55 |
]
|
| 56 |
|
| 57 |
+
TEMPLATE_CANDIDATES = [
|
| 58 |
+
"/workspace/邮件发送的格式.xlsx",
|
| 59 |
+
"/workspace/templates/邮件发送的格式.xlsx",
|
| 60 |
+
"/app/邮件发送的格式.xlsx",
|
| 61 |
+
"/app/templates/邮件发送的格式.xlsx",
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
# ====== 工具函数 ======
|
| 66 |
def _today() -> date:
|
| 67 |
return datetime.now().date()
|
|
|
|
| 146 |
grouped = df.groupby(group_keys, dropna=False).agg(final_agg_map).reset_index()
|
| 147 |
grouped["目前进度"] = grouped.apply(_calc_progress_row, axis=1)
|
| 148 |
|
| 149 |
+
def get_sort_key(status_text):
|
| 150 |
+
if "逾期" in status_text and "未到货" in status_text:
|
| 151 |
+
return 1
|
| 152 |
+
if "逾期" in status_text and "部分到货" in status_text:
|
| 153 |
+
return 2
|
| 154 |
+
if "未来7天" in status_text:
|
| 155 |
+
return 3
|
| 156 |
+
if "完全到货" in status_text:
|
| 157 |
+
return 4
|
| 158 |
+
return 5
|
| 159 |
+
|
| 160 |
+
grouped['sort_key'] = grouped['目前进度'].apply(get_sort_key)
|
| 161 |
+
grouped = grouped.sort_values(by='sort_key').drop(columns=['sort_key'])
|
| 162 |
+
|
| 163 |
if "计划到货日期" in grouped.columns:
|
| 164 |
grouped = grouped.drop(columns=["计划到货日期"])
|
| 165 |
|
| 166 |
final_cols = [col for col in EMAIL_COLS_DEFAULT if col in grouped.columns]
|
| 167 |
final = grouped[final_cols]
|
| 168 |
|
|
|
|
|
|
|
| 169 |
date_cols_to_format = ["请购日期", "需求日期", "到货日期", "入库日期"]
|
| 170 |
for col in date_cols_to_format:
|
| 171 |
if col in final.columns:
|
| 172 |
+
final[col] = pd.to_datetime(final[col], errors='coerce').dt.strftime('%Y-%m-%d')
|
| 173 |
|
| 174 |
return final
|
| 175 |
|
|
|
|
| 218 |
files.sort(key=os.path.getmtime, reverse=True)
|
| 219 |
return files[0]
|
| 220 |
|
| 221 |
+
def _df_to_styled_excel_bytes(df: pd.DataFrame) -> bytes:
|
| 222 |
bio = BytesIO()
|
| 223 |
+
writer = pd.ExcelWriter(bio, engine='xlsxwriter')
|
| 224 |
+
|
| 225 |
+
sheet_name = '采购执行表'
|
| 226 |
+
df.fillna('').to_excel(writer, sheet_name=sheet_name, index=False)
|
| 227 |
+
|
| 228 |
+
workbook = writer.book
|
| 229 |
+
worksheet = writer.sheets[sheet_name]
|
| 230 |
+
|
| 231 |
+
header_format = workbook.add_format({'bold': True, 'font_name': 'Arial', 'font_size': 10, 'border': 1, 'align': 'center', 'valign': 'vcenter'})
|
| 232 |
+
default_format = workbook.add_format({'font_name': 'Arial', 'font_size': 10, 'border': 1})
|
| 233 |
+
overdue_format = workbook.add_format({'font_name': 'Arial', 'font_size': 10, 'border': 1, 'font_color': 'red'})
|
| 234 |
+
|
| 235 |
+
for col_num, value in enumerate(df.columns.values):
|
| 236 |
+
worksheet.write(0, col_num, value, header_format)
|
| 237 |
+
|
| 238 |
+
worksheet.conditional_format(1, 0, len(df), len(df.columns)-1, {'type': 'no_blanks', 'format': default_format})
|
| 239 |
+
|
| 240 |
+
try:
|
| 241 |
+
progress_col_idx = df.columns.get_loc('目前进度')
|
| 242 |
+
for row_num in range(len(df)):
|
| 243 |
+
status_text = df.iloc[row_num, progress_col_idx]
|
| 244 |
+
if isinstance(status_text, str) and "逾期" in status_text:
|
| 245 |
+
worksheet.set_row(row_num + 1, None, overdue_format)
|
| 246 |
+
except KeyError:
|
| 247 |
+
pass
|
| 248 |
+
|
| 249 |
+
for i, col in enumerate(df.columns):
|
| 250 |
+
column_len = df[col].astype(str).str.len().max()
|
| 251 |
+
column_len = max(column_len, len(col) * 2)
|
| 252 |
+
worksheet.set_column(i, i, min(column_len, 40))
|
| 253 |
+
|
| 254 |
+
writer.close()
|
| 255 |
bio.seek(0)
|
| 256 |
return bio.read()
|
| 257 |
|
|
|
|
| 261 |
table {{ border-collapse: collapse; font-size: 13px; }}
|
| 262 |
table, th, td {{ border: 1px solid #ccc; padding: 6px; }}
|
| 263 |
th {{ background:#f6f6f6; }}
|
| 264 |
+
tr:nth-child(even) {{background-color: #f2f2f2;}}
|
| 265 |
</style></head><body><h3>{title}</h3>{table_html}
|
|
|
|
| 266 |
</body></html>"""
|
| 267 |
return html
|
| 268 |
|
|
|
|
| 294 |
raw = read_system_export(file_path)
|
| 295 |
final = aggregate_for_email(raw)
|
| 296 |
|
|
|
|
|
|
|
|
|
|
| 297 |
out_name = f"邮件发送的格式_{datetime.now().strftime('%Y%m%d')}.xlsx"
|
| 298 |
out_path = os.path.join(OUTPUT_DIR, out_name)
|
| 299 |
+
|
| 300 |
+
attach = _df_to_styled_excel_bytes(final)
|
| 301 |
final.to_excel(out_path, index=False)
|
| 302 |
|
| 303 |
subject = f"采购执行表自动推��� {datetime.now().date()}"
|
|
|
|
| 310 |
import traceback
|
| 311 |
return {"ok": False, "msg": f"处理文件时发生严重错误: {e}", "traceback": traceback.format_exc()}
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
def main():
|
| 314 |
arg_file = sys.argv[1] if len(sys.argv) > 1 else None
|
| 315 |
result = run_once(arg_file)
|