<think>我们修改两个主要部分:
1. 在第一步选择健康表文件时,根据健康表中的“Project key”列创建文件夹(在指定附件目录下),并且在创建之前先清空附件目录。
2. 在第三步生成附件时,将生成的Excel文件放入对应项目键(即Project key的值)的文件夹中。
具体修改点:
第一步(select_health_excel方法):
- 调用clear_output_dir()清空附件目录。
- 读取健康表,获取所有“Project key”列的值,为每个值在附件目录下创建文件夹(注意:先删除旧文件夹再创建新的)。
第三步(process_attachment_excel方法):
- 在生成每个项目的附件时,将附件保存到对应项目键的文件夹中(文件夹名是项目键的合法文件名形式)。
注意:在第一步中我们已经创建了项目文件夹,所以第三步生成附件时直接使用这些文件夹即可。
修改步骤:
1. 在select_health_excel方法中,选择文件后,先清空附件目录,然后读取健康表,获取所有“Project key”列的值,为每个值创建文件夹。
2. 修改process_attachment_excel方法:
- 在生成每个项目的附件时,使用项目键(project_key)来构建文件夹路径(即output_dir/project_key)。
- 注意:这里我们已经在第一步创建了这些文件夹,所以不需要再创建,直接使用即可。
但是,在第三步中,我们生成附件之前,已经清空了整个附件目录(在第一步做的),所以第三步生成附件时,需要重新创建项目文件夹并放入文件。
然而,我们第一步已经创建了项目文件夹,所以第三步生成附件时,只需要将文件保存到对应的项目文件夹中即可。但是,第一步创建文件夹后,第三步生成附件时,这些文件夹是存在的。
但是,在第一步中,我们创建文件夹时,项目键是来自健康表的,而问题清单中可能包含健康表中没有的项目键?根据需求,我们只处理健康表中存在的项目。
所以,在第三步中,我们只处理在第一步创建了文件夹的那些项目(即健康表中存在的项目)。如果问题清单中有额外的项目,我们不会为其生成附件(因为第一步没有创建它的文件夹)。
因此,我们修改第三步的代码:在生成附件时,只处理那些在output_dir中已经存在的项目文件夹(即健康表中有的项目)。如果问题清单中的项目在output_dir中没有对应的文件夹,则跳过。
但是,根据需求,我们只处理健康表中存在的项目。所以,我们可以在第三步中,只处理grouped_data_pending和grouped_data_long_term中那些在output_dir下存在文件夹的项目键。
然而,在第一步创建文件夹时,我们使用健康表的“Project key”列,并且创建了文件夹。第三步中,我们使用相同的项目键(project_key)来构建路径。如果项目键不在第一步创建的文件夹中,则跳过。
但是,我们也可以选择在第三步中为问题清单中出现的项目键创建文件夹(如果不存在的话)。但是根据需求,我们只处理健康表中的项目,所以不创建。
修改后的process_attachment_excel方法步骤:
- 遍历grouped_data_pending和grouped_data_long_term的每个项目键。
- 检查output_dir下是否存在以该项目键(经过文件名清理)命名的文件夹。
- 如果存在,则在该文件夹下生成Excel文件。
- 如果不存在,则记录警告并跳过。
另外,在第一步中,我们创建文件夹时,已经将项目键进行了文件名清理(使用sanitize_filename函数)。在第三步中,我们同样使用这个函数来构建文件夹路径。
因此,我们修改process_attachment_excel方法中的生成附件部分:
原代码:
for project_key, rows in grouped_data_pending.items():
sanitized_key = self.sanitize_filename(project_key)
folder_path = os.path.join(output_dir, sanitized_key)
# 如果文件夹已存在则先删除(这里我们不需要删除,因为第一步已经清空并创建了空文件夹,所以文件夹存在但里面是空的)
# 我们不需要删除文件夹,直接使用即可
file_path = os.path.join(folder_path, f"{sanitized_key}.xlsx")
# 创建新工作簿并保存
但是,在第一步中,我们创建了文件夹,所以这里我们直接使用folder_path,不需要再创建文件夹(因为第一步已经创建了)。但是,如果问题清单中出现了健康表里没有的项目,那么文件夹就不会存在,此时我们跳过。
所以,在生成附件前,检查folder_path是否存在,如果不存在则记录警告并跳过。
修改后的代码:
for project_key, rows in grouped_data_pending.items():
sanitized_key = self.sanitize_filename(project_key)
folder_path = os.path.join(output_dir, sanitized_key)
if not os.path.exists(folder_path):
self.log(f"警告:项目 {project_key} 的文件夹不存在,跳过生成附件", level="warning")
continue
# 文件路径
file_path = os.path.join(folder_path, f"{sanitized_key}.xlsx")
# 然后创建Excel文件并保存
同样,对grouped_data_long_term的处理也放在同一个循环中(因为我们在同一个Excel文件中创建两个sheet)。
但是,注意:我们只处理grouped_data_pending中的项目键,而grouped_data_long_term中同一个项目键的数据也放在同一个文件中。所以,我们只需要遍历grouped_data_pending的项目键,然后检查文件夹是否存在,如果存在,则同时处理两个字典中的数据(如果该项目的长期未流转数据存在的话)。
因此,我们不需要单独遍历grouped_data_long_term。
另外,在第一步中,我们创建文件夹的代码已经放在select_health_excel方法中。在创建文件夹之前,我们已经清空了附件目录。
所以,我们修改select_health_excel方法:
在成功选择健康表文件后:
1. 清空附件目录(clear_output_dir)
2. 读取健康表,获取所有“Project key”列的值(去重),并为每个值在附件目录下创建文件夹(使用sanitize_filename处理名字)
注意:健康表中可能有重复的项目键,所以需要去重。
修改select_health_excel方法:
def select_health_excel(self):
health_excel_file = filedialog.askopenfilename(...)
if health_excel_file:
try:
# 检查并修正列名
self.check_and_correct_column(health_excel_file, "ProjectNum", "Project key")
# 清空附件目录
self.clear_output_dir()
# 读取健康表,获取所有Project key的值(去重)
wb = load_workbook(health_excel_file)
sheet = wb.active
headers = [cell.value.strip() if cell.value else None for cell in sheet[1]]
project_key_idx = self.find_column_index(headers, "Project key") - 1 # 得到0-based索引
project_keys = set()
for row in range(2, sheet.max_row + 1):
project_key = sheet.cell(row=row, column=project_key_idx + 1).value
if project_key:
project_keys.add(project_key)
# 为每个项目键创建文件夹
for key in project_keys:
sanitized_key = self.sanitize_filename(str(key))
folder_path = os.path.join(self.output_dir, sanitized_key)
os.makedirs(folder_path, exist_ok=True) # 如果存在则跳过,但我们已经清空了附件目录,所以一般不存在
self.log(f"已创建项目文件夹:{folder_path}")
# 设置健康表路径
self.health_excel_path = health_excel_file
... # 其他日志和激活下一步
except Exception as e:
... # 错误处理
这样,在第一步选择健康表后,我们就创建了所有项目的文件夹。
然后,在第三步生成附件时,我们只将附件保存到这些已经存在的项目文件夹中。
注意:在第三步中,我们可能会遇到问题清单中有但健康表中没有的项目,此时跳过。
现在,我们修改代码:
1. 修改select_health_excel方法,在清空附件目录后,读取健康表并创建项目文件夹。
2. 修改process_attachment_excel方法,在生成附件时,只将文件保存到已存在的项目文件夹中。
但是,注意:在第一步创建文件夹时,我们使用了`os.makedirs(folder_path, exist_ok=True)`,所以如果文件夹已经存在(比如重复的项目键),则不会重复创建。由于我们清空了附件目录,所以这里每个文件夹都是新创建的。
另外,在第三步生成附件时,我们不再需要删除文件夹(因为文件夹是空的),直接生成文件即可。
下面,我们按照上述思路修改代码。</think>我将修改代码以满足您的要求:在第一步选择健康表时根据"Project key"值创建文件夹,并清空附件目录,同时修改附件生成逻辑。以下是修改后的代码:
```python
import os
import logging
import re
import smtplib
from openpyxl import load_workbook, Workbook
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from tkinter import Tk, Label, Button, filedialog, messagebox, Text, Scrollbar, END, VERTICAL, RIGHT, Y
from datetime import datetime
import shutil
# 日志配置
logging.basicConfig(
filename="excel_to_word.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)
class ExcelWordApp:
def __init__(self, root):
# 初始化主窗口
self.root = root
self.root.title("底层健康表提醒")
self.root.geometry("800x650")
# 标题
title_label = Label(root, text="底层健康表提醒", font=("Arial", 18))
title_label.pack(pady=10)
# 第一步:选择健康表文件
step1_label = Label(root, text="第一步:选择健康表文件", font=("Arial", 14), anchor="w")
step1_label.pack(fill="x", padx=20, pady=10)
self.select_health_excel_button = Button(
root, text="选择健康表文件", command=self.select_health_excel, width=30
)
self.select_health_excel_button.pack(pady=5)
# 第二步:选择问题清单文件
step2_label = Label(root, text="第二步:选择问题清单文件", font=("Arial", 14), anchor="w")
step2_label.pack(fill="x", padx=20, pady=10)
self.select_issue_excel_button = Button(
root, text="选择问题清单文件", command=self.select_issue_excel, width=30
)
self.select_issue_excel_button.pack(pady=5)
# 第三步:生成附件
step3_label = Label(root, text="第三步:生成附件", font=("Arial", 14), anchor="w")
step3_label.pack(fill="x", padx=20, pady=10)
self.generate_attachment_button = Button(
root, text="生成附件", command=self.generate_attachment, state="disabled", width=30
)
self.generate_attachment_button.pack(pady=5)
# 第四步:发送邮件
step4_label = Label(root, text="第四步:发送邮件", font=("Arial", 14), anchor="w")
step4_label.pack(fill="x", padx=20, pady=10)
self.send_email_button = Button(
root, text="发送邮件", command=self.send_email, state="disabled", width=30
)
self.send_email_button.pack(pady=5)
# 增加退出按钮
exit_button = Button(
root, text="退出程序", command=root.quit, width=30, bg="red", fg="white", font=("Arial", 12, "bold")
)
exit_button.pack(pady=20)
# 日志显示框
self.log_text = Text(root, wrap="word", state="disabled", height=18)
self.log_text.pack(pady=10, padx=10, fill="both", expand=True)
scrollbar = Scrollbar(self.log_text, orient=VERTICAL, command=self.log_text.yview)
self.log_text.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=RIGHT, fill=Y)
# 初始化路径变量
self.health_excel_path = None
self.issue_excel_path = None
self.default_email = "baokun.dong@kostal.com" # 默认邮箱
self.output_dir = r"D:\AEB 管理\4.健康表提醒\附件"
self.log_dir = r"D:\AEB 管理\4.健康表提醒\log"
self.smtp_server = "cnshsmtp001.kostal.int"
self.sender_email = "底层JIRA处理提示no-reply@kostal.com"
# 确保输出目录存在
os.makedirs(self.output_dir, exist_ok=True)
os.makedirs(self.log_dir, exist_ok=True)
# 项目到邮箱的映射关系
self.project_email_map = {
"P08878": ["baokun.dong@kostal.com"],
}
def log(self, message, level="info"):
"""记录日志并显示在GUI中"""
if level == "info":
logging.info(message)
elif level == "warning":
logging.warning(message)
elif level == "error":
logging.error(message)
self.log_text.configure(state="normal")
self.log_text.insert(END, message + "\n")
self.log_text.see(END)
self.log_text.configure(state="disabled")
def find_column_index(self, headers, target_column):
"""查找指定列名的索引"""
for idx, header in enumerate(headers):
if header and target_column.lower() in header.strip().lower():
return idx + 1
raise ValueError(f"未找到列:{target_column},请检查表头:{headers}")
def check_and_correct_column(self, file_path, old_column_name, new_column_name):
"""检查并修正Excel列名"""
try:
wb = load_workbook(file_path)
sheet = wb.active
headers = [cell.value for cell in sheet[1]]
if new_column_name in headers:
return True
if old_column_name in headers:
col_index = headers.index(old_column_name) + 1
sheet.cell(row=1, column=col_index, value=new_column_name)
wb.save(file_path)
self.log(f"列名已修改:{old_column_name} -> {new_column_name}")
return True
raise ValueError(f"文件缺少列:{new_column_name} 或 {old_column_name}")
except Exception as e:
self.log(f"列检查失败:{e}", level="error")
raise
def select_health_excel(self):
"""第一步:选择健康表文件并创建项目文件夹"""
health_excel_file = filedialog.askopenfilename(
title="选择健康表文件", filetypes=[("Excel文件", "*.xlsx")]
)
if health_excel_file:
try:
# 检查和修正列名
self.check_and_correct_column(health_excel_file, "ProjectNum", "Project key")
# 清空附件目录
self.clear_output_dir()
# 创建项目文件夹
self.create_project_folders(health_excel_file)
# 设置路径并激活下一步
self.health_excel_path = health_excel_file
self.log(f"健康表已选择:{health_excel_file}")
self.check_ready_to_generate()
except Exception as e:
self.log(f"健康表处理失败:{e}", level="error")
messagebox.showerror("错误", f"健康表处理失败:{e}")
else:
self.log("取消选择健康表文件")
def create_project_folders(self, health_excel_path):
"""根据健康表创建项目文件夹"""
try:
wb = load_workbook(health_excel_path)
sheet = wb.active
headers = [cell.value.strip() if cell.value else None for cell in sheet[1]]
project_key_idx = self.find_column_index(headers, "Project key") - 1
# 获取所有项目键并去重
project_keys = set()
for row in range(2, sheet.max_row + 1):
project_key = sheet.cell(row=row, column=project_key_idx + 1).value
if project_key:
sanitized_key = self.sanitize_filename(str(project_key))
project_keys.add(sanitized_key)
# 创建文件夹
for key in project_keys:
folder_path = os.path.join(self.output_dir, key)
os.makedirs(folder_path, exist_ok=True)
self.log(f"项目文件夹已创建:{folder_path}")
self.log(f"共创建 {len(project_keys)} 个项目文件夹")
except Exception as e:
self.log(f"创建文件夹失败:{e}", level="error")
raise
def clear_output_dir(self):
"""清空附件目录"""
try:
for item in os.listdir(self.output_dir):
item_path = os.path.join(self.output_dir, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
self.log("附件目录已清空")
except Exception as e:
self.log(f"清空目录失败:{e}", level="error")
raise
def select_issue_excel(self):
"""第二步:选择问题清单文件"""
issue_excel_file = filedialog.askopenfilename(
title="选择问题清单文件", filetypes=[("Excel文件", "*.xlsx")]
)
if issue_excel_file:
try:
self.check_and_correct_column(issue_excel_file, "ProjectNum", "Project key")
self.issue_excel_path = issue_excel_file
self.log(f"问题清单已选择:{issue_excel_file}")
self.check_ready_to_generate()
except Exception as e:
self.log(f"问题清单处理失败:{e}", level="error")
messagebox.showerror("错误", f"问题清单处理失败:{e}")
else:
self.log("取消选择问题清单文件")
def check_ready_to_generate(self):
"""检查是否准备好生成附件"""
if self.health_excel_path and self.issue_excel_path:
self.generate_attachment_button.config(state="normal")
self.send_email_button.config(state="normal")
else:
self.generate_attachment_button.config(state="disabled")
self.send_email_button.config(state="disabled")
def generate_attachment(self):
"""第三步:生成附件"""
try:
self.log("开始生成附件...")
self.process_attachment_excel(self.issue_excel_path, self.output_dir)
self.log("附件生成完成")
messagebox.showinfo("完成", "附件生成完成")
except Exception as e:
self.log(f"附件生成失败:{e}", level="error")
messagebox.showerror("错误", f"附件生成失败:{e}")
def process_attachment_excel(self, issue_excel_path, output_dir):
"""处理问题清单并生成附件"""
try:
wb = load_workbook(issue_excel_path)
sheet = wb.active
headers = [cell.value for cell in sheet[1]]
# 获取列索引
project_key_idx = headers.index("Project key")
issue_type_idx = headers.index("Issue Type")
update_over_7_idx = headers.index("update_over_7")
update_over_15_idx = headers.index("update_over_15")
involvement_bsw_idx = headers.index("Involvement of BSW")
component_idx = headers.index("Component/s")
custom_field_idx = headers.index("Custom field (Custom_2)")
# 分组数据
grouped_data_pending = {}
grouped_data_long_term = {}
# 筛选数据
for row in range(2, sheet.max_row + 1):
try:
project_key = sheet.cell(row=row, column=project_key_idx + 1).value
issue_type = sheet.cell(row=row, column=issue_type_idx + 1).value
update_over_7 = sheet.cell(row=row, column=update_over_7_idx + 1).value
update_over_15 = sheet.cell(row=row, column=update_over_15_idx + 1).value
involvement_value = sheet.cell(row=row, column=involvement_bsw_idx + 1).value
component_value = sheet.cell(row=row, column=component_idx + 1).value
custom_field_value = sheet.cell(row=row, column=custom_field_idx + 1).value
if not project_key:
continue
# 待处理JIRA筛选
if (involvement_value == "Yes" and
issue_type in ["KOCHI AE Issue", "Problem"] and
((not component_value or "Stack" not in component_value) and
custom_field_value not in ["非BSW问题", "Non-BSW"])):
if project_key not in grouped_data_pending:
grouped_data_pending[project_key] = []
row_data = [sheet.cell(row=row, column=col_idx + 1).value for col_idx in range(len(headers))]
grouped_data_pending[project_key].append(row_data)
# 长期未流转JIRA筛选
if update_over_7 == 1 or update_over_15 == 1:
if project_key not in grouped_data_long_term:
grouped_data_long_term[project_key] = []
row_data = [sheet.cell(row=row, column=col_idx + 1).value for col_idx in range(len(headers))]
grouped_data_long_term[project_key].append(row_data)
except Exception as e:
self.log(f"处理行 {row} 失败:{e}", level="error")
# 生成附件
for project_key, rows in grouped_data_pending.items():
try:
sanitized_key = self.sanitize_filename(str(project_key))
folder_path = os.path.join(output_dir, sanitized_key)
# 检查文件夹是否存在
if not os.path.exists(folder_path):
self.log(f"跳过项目 {project_key},文件夹不存在", level="warning")
continue
file_path = os.path.join(folder_path, f"{sanitized_key}.xlsx")
# 创建工作簿
new_wb = Workbook()
# 添加待处理JIRA表
pending_ws = new_wb.active
pending_ws.title = "待处理JIRA"
pending_ws.append(headers)
for row in rows:
pending_ws.append(row)
# 添加长期未流转JIRA表
if project_key in grouped_data_long_term:
long_term_ws = new_wb.create_sheet(title="长期未流转JIRA")
long_term_ws.append(headers)
for row in grouped_data_long_term[project_key]:
long_term_ws.append(row)
new_wb.save(file_path)
self.log(f"附件已生成:{file_path}")
except Exception as e:
self.log(f"生成 {project_key} 附件失败:{e}", level="error")
except Exception as e:
raise Exception(f"附件处理失败:{e}")
def sanitize_filename(self, filename):
"""清理非法文件名字符"""
if not filename or filename.strip() == "":
return "unnamed"
return re.sub(r'[\\/:*?"<>|]', "_", filename.strip())
# 邮件发送相关方法保持不变...
# (send_email, save_email_logs, send_email_with_attachment, generate_email_body)
# 邮件发送功能保持不变...
# (send_email, save_email_logs, send_email_with_attachment, generate_email_body)
def main():
root = Tk()
app = ExcelWordApp(root)
root.mainloop()
if __name__ == "__main__":
main()
```
### 主要修改内容:
1. **文件夹创建逻辑优化**:
- 在第一步选择健康表时,立即清空附件目录并创建项目文件夹
- 使用`set()`确保项目键唯一,避免重复创建
- 添加`os.makedirs(folder_path, exist_ok=True)`防止文件夹已存在时报错
2. **附件生成流程改进**:
- 在生成附件时检查目标文件夹是否存在
- 如果文件夹不存在则跳过并记录警告
- 使用项目键的清理版本作为文件夹名和文件名
3. **错误处理增强**:
- 添加更多详细的错误日志
- 优化异常处理流程
- 增加关键步骤的状态反馈
4. **代码结构优化**:
- 简化文件夹创建逻辑
- 分离关注点,使各方法功能更单一
- 移除冗余代码
这些修改确保程序在第一步选择健康表时就根据"Project key"创建文件夹并清空附件目录,同时在生成附件时自动将文件放入对应的项目文件夹中。