1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051 |
- import os
- import tkinter as tk
- from tkinter import filedialog, ttk, messagebox
- import pandas as pd
- import shutil
- from datetime import datetime, timedelta
- import openpyxl
- from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
- from openpyxl.utils import get_column_letter
- import xlrd
- import logging
- import xlwings as xw
- from copy import copy
- import re
-
- # Import configuration
- from config import (
- TEMPLATE_PATH,
- CELL_MAPPINGS,
- CUSTOM_CELLS,
- FILENAME_CONFIG,
- SHEET2_NAME_FORMAT,
- OUTPUT_FILENAME_FORMAT,
- EMPLOYEE_INFO,
- EMPLOYEE_CONFIG_PATH,
- COMPANY_OPTIONS,
- BANK_OPTIONS,
- save_options
- )
-
- logging.basicConfig(level=logging.DEBUG)
-
- class ExcelConverterApp:
- def __init__(self, root):
- self.root = root
- self.root.title("工资明细表转换工具")
- self.root.geometry("950x600") # 减小整体窗口尺寸
-
- # 设置主题和样式
- self.style = ttk.Style()
- if 'winnative' in self.style.theme_names():
- self.style.theme_use('winnative')
- elif 'vista' in self.style.theme_names():
- self.style.theme_use('vista')
- elif 'xpnative' in self.style.theme_names():
- self.style.theme_use('xpnative')
- elif 'clam' in self.style.theme_names():
- self.style.theme_use('clam')
-
- # 设置简约现代配色
- self.primary_color = "#4A6D8C" # 主色调 - 沉稳的蓝灰色
- self.accent_color = "#DB4D6D" # 强调色 - 适当的红色用于警告
- self.text_color = "#333333" # 文本色 - 深灰色易于阅读
- self.light_bg = "#F8F9FA" # 背景色 - 淡灰色背景
- self.light_accent = "#E9ECEF" # 浅强调 - 用于分隔和高亮
-
- # 设置基本样式
- self.style.configure("TFrame", background=self.light_bg)
- self.style.configure("TLabel", font=("Microsoft YaHei UI", 10), background=self.light_bg, foreground=self.text_color)
- self.style.configure("TLabelframe", font=("Microsoft YaHei UI", 10), background=self.light_bg)
- self.style.configure("TLabelframe.Label", font=("Microsoft YaHei UI", 10), background=self.light_bg, foreground=self.text_color)
-
- # 设置Treeview样式 - 简约样式
- self.style.configure("Treeview",
- font=("Microsoft YaHei UI", 10),
- background="white",
- foreground=self.text_color,
- fieldbackground="white")
- self.style.configure("Treeview.Heading",
- font=("Microsoft YaHei UI", 10),
- background=self.light_accent)
- self.style.map('Treeview', background=[('selected', self.primary_color)], foreground=[('selected', 'white')])
-
- # Variables
- self.import_files = []
- self.export_dir = ""
- self.template_path = TEMPLATE_PATH
-
- # Create UI
- self.create_widgets()
-
- def create_widgets(self):
- # 设置根框架背景
- self.root.configure(background=self.light_bg)
-
- # 创建主框架 - 减少内边距
- main_frame = ttk.Frame(self.root, padding=5)
- main_frame.pack(fill="both", expand=True)
-
- # 导入文件区域 - 减少内边距
- import_frame = ttk.LabelFrame(main_frame, text="导入Excel文件", padding=5)
- import_frame.pack(fill="both", expand=True, pady=5)
-
- # 按钮区域
- button_frame = ttk.Frame(import_frame)
- button_frame.pack(fill="x", pady=(0, 5))
-
- import_btn = ttk.Button(button_frame, text="选择Excel文件", command=self.select_files)
- import_btn.pack(side="left", padx=3)
-
- # 添加文件数量显示
- self.file_count_label = ttk.Label(button_frame, text="已选择: 0 个文件")
- self.file_count_label.pack(side="left", padx=10)
-
- # 文件列表区域
- list_frame = ttk.Frame(import_frame)
- list_frame.pack(fill="both", expand=True)
-
- columns = ("filename", "employee", "company", "bank", "other")
- self.file_tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
-
- # 设置列标题
- self.file_tree.heading("filename", text="文件名")
- self.file_tree.heading("employee", text="员工姓名 (C3)")
- self.file_tree.heading("company", text="所属公司信息 (C2)")
- self.file_tree.heading("bank", text="转账银行信息 (B30)")
- self.file_tree.heading("other", text="日本日期 (F2)")
-
- # 设置列宽
- self.file_tree.column("filename", width=280)
- self.file_tree.column("employee", width=110)
- self.file_tree.column("company", width=170)
- self.file_tree.column("bank", width=170)
- self.file_tree.column("other", width=120)
-
- # 添加滚动条
- scrollbar_y = ttk.Scrollbar(list_frame, orient="vertical", command=self.file_tree.yview)
- scrollbar_x = ttk.Scrollbar(list_frame, orient="horizontal", command=self.file_tree.xview)
- self.file_tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
-
- # 放置树状视图和滚动条
- self.file_tree.grid(row=0, column=0, sticky="nsew")
- scrollbar_y.grid(row=0, column=1, sticky="ns")
- scrollbar_x.grid(row=1, column=0, sticky="ew")
-
- # 配置列表框架的权重
- list_frame.columnconfigure(0, weight=1)
- list_frame.rowconfigure(0, weight=1)
-
- # 注意:移除双击编辑功能,导入列表不可修改
- # self.file_tree.bind("<Double-1>", self.edit_item)
-
- # 下部区域布局 - 减少间隔
- bottom_frame = ttk.Frame(main_frame)
- bottom_frame.pack(fill="x", pady=5)
-
- # 左侧区域 - 导出设置
- left_frame = ttk.Frame(bottom_frame)
- left_frame.pack(side="left", fill="y")
-
- export_label = ttk.Label(left_frame, text="导出位置:")
- export_label.pack(side="left", padx=(0, 3))
-
- self.export_label = ttk.Label(left_frame, text="未选择", width=28)
- self.export_label.pack(side="left")
-
- export_btn = ttk.Button(left_frame, text="选择导出位置", command=self.select_export_dir)
- export_btn.pack(side="left", padx=3)
-
- # 右侧区域 - 功能按钮
- right_frame = ttk.Frame(bottom_frame)
- right_frame.pack(side="right")
-
- info_btn = ttk.Button(right_frame, text="信息维护", command=self.maintain_info)
- info_btn.pack(side="left", padx=3)
-
- convert_btn = ttk.Button(right_frame, text="开始转换", command=self.convert_files)
- convert_btn.pack(side="left", padx=3)
-
- # 添加状态栏
- status_frame = ttk.Frame(main_frame, relief="sunken")
- status_frame.pack(fill="x", side="bottom", pady=(3, 0))
-
- self.status_label = ttk.Label(status_frame, text="准备就绪", anchor="w")
- self.status_label.pack(side="left", padx=5)
-
- def select_files(self):
- files = filedialog.askopenfilenames(
- title="选择Excel文件",
- filetypes=[("Excel Files", "*.xlsx")]
- )
-
- if files:
- self.import_files = list(files)
- self.update_file_tree()
-
- def update_file_tree(self):
- # Clear tree
- for item in self.file_tree.get_children():
- self.file_tree.delete(item)
-
- # 更新文件计数
- self.file_count_label.config(text=f"已选择: {len(self.import_files)} 个文件")
-
- # 用于存储未匹配的员工姓名
- unmatch_employees = []
-
- # Add files to tree with employee name and Japanese date
- for file in self.import_files:
- try:
- # Get employee name from C3 cell
- filename = os.path.basename(file)
- employee_name = ""
- original_date = ""
-
- # Get data from Excel file
- wb = openpyxl.load_workbook(file)
- name_sheet = wb.worksheets[0]
- employee_name = name_sheet.cell(row=3, column=3).value or ""
-
- # 获取日期单元格的原始值,不进行格式转换
- date_sheet = wb.worksheets[1]
- original_date = date_sheet.cell(row=4, column=2).value or ""
-
- # 仅为提取年月信息做最小处理(用于文件名和工作表名称),不影响显示
- extracted_year = None
- extracted_month = None
-
- try:
- if isinstance(original_date, (int, float)):
- # Excel日期数字格式
- base_date = datetime(1899, 12, 30)
- date_obj = base_date + timedelta(days=original_date)
- extracted_year = date_obj.year
- extracted_month = date_obj.month
- elif isinstance(original_date, datetime):
- # 日期对象
- extracted_year = original_date.year
- extracted_month = original_date.month
- else:
- # 字符串格式 - 尝试提取年月
- date_str = str(original_date)
- year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
- if year_month_match:
- extracted_year, extracted_month = map(int, year_month_match.groups())
- except Exception as e:
- logging.error(f"Error processing date: {e}")
-
- # 生成令和年号日期格式
- reiwa_date = ""
- if extracted_year and extracted_month:
- # 计算令和年号 (2019年为令和元年/1年)
- reiwa_year = extracted_year - 2019 + 1
- if reiwa_year > 0: # 只有2019年之后才使用令和年号
- reiwa_date = f"令和{reiwa_year}年{extracted_month}月分"
-
- # 默认值
- company = "选择公司"
- bank_info = "选择银行"
-
- # 检查员工信息是否存在于员工信息维护中
- employee_matched = False
- if employee_name:
- for emp_info in EMPLOYEE_INFO:
- if emp_info.get("employee_name") == employee_name:
- employee_matched = True
- # 匹配到员工信息,更新公司和银行信息
- company = emp_info.get("company_name", "")
- bank_name = emp_info.get("bank_name", "")
- branch_account = emp_info.get("branch_account", "")
- bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
- break
-
- # 如果未匹配,添加到未匹配列表
- if not employee_matched and employee_name not in unmatch_employees:
- unmatch_employees.append(employee_name)
-
- # 自动添加到文件树 - 在日本日期(F2)字段显示令和年月
- self.file_tree.insert("", "end", values=(filename, employee_name, company, bank_info, reiwa_date or original_date))
- except Exception as e:
- logging.error(f"Error reading file {file}: {e}")
- self.file_tree.insert("", "end", values=(filename, "", "选择公司", "选择银行", ""))
-
- # 如果有未匹配的员工,显示警告消息
- if unmatch_employees:
- warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
- warning_msg += "\n".join(unmatch_employees)
- warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
- messagebox.showwarning("员工信息不匹配", warning_msg)
-
- # 标记未匹配的行
- for item in self.file_tree.get_children():
- values = self.file_tree.item(item, "values")
- if values[1] in unmatch_employees:
- self.file_tree.item(item, tags=("unmatch",))
-
- # 设置未匹配样式(黄色背景)
- self.file_tree.tag_configure("unmatch", background="#FFF9C4") # 浅黄色
-
- # 设置状态提示
- if self.import_files:
- self.status_label.config(text="文件已加载,请配置转换选项", foreground=self.text_color)
- else:
- self.status_label.config(text="准备就绪", foreground="gray")
-
- def select_export_dir(self):
- directory = filedialog.askdirectory(title="选择导出目录")
- if directory:
- self.export_dir = directory
- self.export_label.config(text=directory)
-
- def maintain_info(self):
- # 创建弹出窗口
- popup = tk.Toplevel(self.root)
- popup.title("员工信息维护")
- popup.geometry("700x500")
- popup.transient(self.root)
- popup.grab_set()
-
- # 标题
- title_frame = ttk.Frame(popup, padding=5)
- title_frame.pack(fill="x")
-
- title_label = ttk.Label(title_frame, text="员工信息维护", font=("Microsoft YaHei UI", 12, "bold"))
- title_label.pack(side="left")
-
- # 创建表格视图
- tree_frame = ttk.Frame(popup, padding=5)
- tree_frame.pack(fill="both", expand=True)
-
- columns = ("employee", "company", "bank", "account", "holder")
- tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
-
- # 设置列标题
- tree.heading("employee", text="员工姓名")
- tree.heading("company", text="所属公司")
- tree.heading("bank", text="振込先金融機関")
- tree.heading("account", text="口座番号")
- tree.heading("holder", text="名義人")
-
- # 设置列宽
- tree.column("employee", width=100)
- tree.column("company", width=120)
- tree.column("bank", width=120)
- tree.column("account", width=180)
- tree.column("holder", width=120)
-
- # 添加滚动条
- vscrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
- tree.configure(yscrollcommand=vscrollbar.set)
- hscrollbar = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview)
- tree.configure(xscrollcommand=hscrollbar.set)
-
- tree.grid(row=0, column=0, sticky="nsew")
- vscrollbar.grid(row=0, column=1, sticky="ns")
- hscrollbar.grid(row=1, column=0, sticky="ew")
-
- tree_frame.columnconfigure(0, weight=1)
- tree_frame.rowconfigure(0, weight=1)
-
- # 添加搜索功能
- search_frame = ttk.Frame(popup, padding=5)
- search_frame.pack(fill="x", pady=5)
-
- search_label = ttk.Label(search_frame, text="搜索:")
- search_label.pack(side="left", padx=5)
-
- search_var = tk.StringVar()
- search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
- search_entry.pack(side="left", fill="x", expand=True, padx=5)
-
- def search_info(*args):
- query = search_var.get().lower()
- tree.delete(*tree.get_children())
- for info in EMPLOYEE_INFO:
- # 在所有字段中搜索
- search_in = (
- info.get("employee_name", "").lower() +
- info.get("company_name", "").lower() +
- info.get("bank_name", "").lower() +
- info.get("branch_account", "").lower() +
- info.get("account_holder", "").lower()
- )
- if query in search_in:
- tree.insert("", "end", values=(
- info.get("employee_name", ""),
- info.get("company_name", ""),
- info.get("bank_name", ""),
- info.get("branch_account", ""),
- info.get("account_holder", "")
- ))
-
- search_var.trace("w", search_info)
-
- # 添加按钮
- btn_frame = ttk.Frame(popup, padding=5)
- btn_frame.pack(fill="x", pady=5)
-
- # 编辑功能
- def edit_info():
- selected = tree.selection()
- if not selected:
- messagebox.showinfo("提示", "请先选择一条记录")
- return
-
- selected_idx = tree.index(selected[0])
- if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
- messagebox.showerror("错误", "选择的记录无效")
- return
-
- edit_employee_info(EMPLOYEE_INFO[selected_idx])
-
- # 新增功能
- def add_info():
- edit_employee_info()
-
- # 删除功能
- def delete_info():
- selected = tree.selection()
- if not selected:
- messagebox.showinfo("提示", "请先选择一条记录")
- return
-
- selected_idx = tree.index(selected[0])
- if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
- messagebox.showerror("错误", "选择的记录无效")
- return
-
- if messagebox.askyesno("确认", "确定要删除此记录吗?"):
- del EMPLOYEE_INFO[selected_idx]
- save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
- refresh_tree()
-
- # 刷新表格
- def refresh_tree():
- tree.delete(*tree.get_children())
- for info in EMPLOYEE_INFO:
- tree.insert("", "end", values=(
- info.get("employee_name", ""),
- info.get("company_name", ""),
- info.get("bank_name", ""),
- info.get("branch_account", ""),
- info.get("account_holder", "")
- ))
-
- # 编辑员工信息
- def edit_employee_info(info=None):
- is_new = info is None
- if is_new:
- info = {
- "employee_name": "",
- "company_name": "",
- "bank_name": "",
- "branch_account": "",
- "account_holder": ""
- }
-
- edit_popup = tk.Toplevel(popup)
- edit_popup.title("编辑员工信息")
- edit_popup.geometry("450x300")
- edit_popup.transient(popup)
- edit_popup.grab_set()
-
- # 创建表单
- form_frame = ttk.Frame(edit_popup, padding=10)
- form_frame.pack(fill="both", expand=True)
-
- # 员工姓名
- name_label = ttk.Label(form_frame, text="员工姓名:")
- name_label.grid(row=0, column=0, sticky="w", pady=5)
- name_var = tk.StringVar(value=info.get("employee_name", ""))
- name_entry = ttk.Entry(form_frame, textvariable=name_var, width=30)
- name_entry.grid(row=0, column=1, sticky="ew", pady=5)
-
- # 所属公司
- company_label = ttk.Label(form_frame, text="所属公司:")
- company_label.grid(row=1, column=0, sticky="w", pady=5)
- company_var = tk.StringVar(value=info.get("company_name", ""))
- company_entry = ttk.Entry(form_frame, textvariable=company_var, width=30)
- company_entry.grid(row=1, column=1, sticky="ew", pady=5)
-
- # 银行名称
- bank_label = ttk.Label(form_frame, text="振込先金融機関:")
- bank_label.grid(row=2, column=0, sticky="w", pady=5)
- bank_var = tk.StringVar(value=info.get("bank_name", ""))
- bank_entry = ttk.Entry(form_frame, textvariable=bank_var, width=30)
- bank_entry.grid(row=2, column=1, sticky="ew", pady=5)
-
- # 账户信息
- account_label = ttk.Label(form_frame, text="口座番号:")
- account_label.grid(row=3, column=0, sticky="w", pady=5)
- account_var = tk.StringVar(value=info.get("branch_account", ""))
- account_entry = ttk.Entry(form_frame, textvariable=account_var, width=30)
- account_entry.grid(row=3, column=1, sticky="ew", pady=5)
-
- # 账户持有人
- holder_label = ttk.Label(form_frame, text="名義人:")
- holder_label.grid(row=4, column=0, sticky="w", pady=5)
- holder_var = tk.StringVar(value=info.get("account_holder", ""))
- holder_entry = ttk.Entry(form_frame, textvariable=holder_var, width=30)
- holder_entry.grid(row=4, column=1, sticky="ew", pady=5)
-
- form_frame.columnconfigure(1, weight=1)
-
- # 按钮区域
- btn_frame = ttk.Frame(edit_popup, padding=10)
- btn_frame.pack(fill="x")
-
- def save_info():
- # 验证
- if not name_var.get().strip():
- messagebox.showerror("错误", "员工姓名不能为空")
- return
-
- # 保存
- new_info = {
- "employee_name": name_var.get().strip(),
- "company_name": company_var.get().strip(),
- "bank_name": bank_var.get().strip(),
- "branch_account": account_var.get().strip(),
- "account_holder": holder_var.get().strip()
- }
-
- if is_new:
- EMPLOYEE_INFO.append(new_info)
- else:
- idx = EMPLOYEE_INFO.index(info)
- EMPLOYEE_INFO[idx] = new_info
-
- save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
- refresh_tree()
- edit_popup.destroy()
-
- save_btn = ttk.Button(btn_frame, text="保存", command=save_info)
- save_btn.pack(side="right", padx=5)
-
- cancel_btn = ttk.Button(btn_frame, text="取消", command=edit_popup.destroy)
- cancel_btn.pack(side="right", padx=5)
-
- name_entry.focus_set()
-
- # 添加按钮
- add_btn = ttk.Button(btn_frame, text="新增", command=add_info)
- add_btn.pack(side="left", padx=5)
-
- edit_btn = ttk.Button(btn_frame, text="编辑", command=edit_info)
- edit_btn.pack(side="left", padx=5)
-
- delete_btn = ttk.Button(btn_frame, text="删除", command=delete_info)
- delete_btn.pack(side="left", padx=5)
-
- close_btn = ttk.Button(btn_frame, text="关闭", command=popup.destroy)
- close_btn.pack(side="right", padx=5)
-
- # 初始化
- refresh_tree()
- search_entry.focus_set()
-
- def convert_files(self):
- if not self.import_files:
- messagebox.showerror("错误", "请先选择要导入的Excel文件")
- return
-
- if not self.export_dir:
- messagebox.showerror("错误", "请选择导出位置")
- return
-
- # Template file check
- if not os.path.exists(self.template_path):
- messagebox.showerror("错误", f"模板文件不存在: {self.template_path}")
- return
-
- # 检查是否有未匹配的员工
- unmatch_employees = []
- for item in self.file_tree.get_children():
- values = self.file_tree.item(item, "values")
- employee_name = values[1]
-
- # 检查是否有匹配
- if employee_name and not any(emp_info.get("employee_name") == employee_name for emp_info in EMPLOYEE_INFO):
- unmatch_employees.append(employee_name)
-
- # 如果有未匹配员工,提示用户先维护信息
- if unmatch_employees:
- warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
- warning_msg += "\n".join(unmatch_employees)
- warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
- messagebox.showwarning("员工信息不匹配", warning_msg)
- return
-
- # 更新状态
- self.status_label.config(text="正在转换文件...", foreground=self.accent_color)
- self.root.update()
-
- # Process each file
- success_count = 0
- error_count = 0
-
- # 确保文件树和导入文件列表长度一致
- tree_items = self.file_tree.get_children()
-
- for i, file_path in enumerate(self.import_files):
- try:
- # 更新状态
- self.status_label.config(text=f"正在处理: {os.path.basename(file_path)} ({i+1}/{len(self.import_files)})")
- self.root.update()
-
- # 检查索引是否有效
- if i < len(tree_items):
- item = tree_items[i]
- values = self.file_tree.item(item, "values")
-
- # 安全地获取值
- filename = values[0] if len(values) > 0 else ""
- employee_name = values[1] if len(values) > 1 else ""
- company = values[2] if len(values) > 2 else ""
- bank = values[3] if len(values) > 3 else ""
- other_info = values[4] if len(values) > 4 else ""
-
- # Process file
- success = self.process_file(file_path, company, bank, other_info, employee_name)
- if success:
- success_count += 1
- # 设置行的背景色为成功
- self.file_tree.item(item, tags=("success",))
- else:
- error_count += 1
- # 设置行的背景色为错误
- self.file_tree.item(item, tags=("error",))
- else:
- # 文件树中没有对应的项,直接处理文件
- success = self.process_file(file_path, "", "", "", "")
- if success:
- success_count += 1
- else:
- error_count += 1
-
- except Exception as e:
- error_count += 1
- logging.error(f"处理文件 {os.path.basename(file_path)} 时出错: {str(e)}")
- # 不显示对话框,只在状态栏更新
- self.status_label.config(text=f"处理文件 {os.path.basename(file_path)} 时出错", foreground="red")
-
- # 找到对应的项并设置错误标签
- try:
- if i < len(tree_items):
- item = tree_items[i]
- self.file_tree.item(item, tags=("error",))
- except:
- pass
-
- # 设置Tag样式
- self.file_tree.tag_configure("success", background="#E8F8F5") # 浅绿色
- self.file_tree.tag_configure("error", background="#FADBD8") # 浅红色
-
- # 更新最终状态
- if error_count == 0:
- self.status_label.config(text=f"转换完成! 成功处理 {success_count} 个文件", foreground=self.accent_color)
- else:
- self.status_label.config(text=f"转换结束, 成功: {success_count}, 失败: {error_count}", foreground="red")
-
- # Show completion message
- messagebox.showinfo("完成", f"成功转换 {success_count} 个文件到 {self.export_dir}")
-
- def process_file(self, input_file, company, bank, other_info, employee_name=None):
- try:
- logging.debug(f"Processing file: {input_file}")
-
- # Extract name and date
- input_wb = openpyxl.load_workbook(input_file)
- name_sheet = input_wb.worksheets[0]
- name = employee_name or name_sheet.cell(row=3, column=3).value # Use provided name or C3 cell value
- date_sheet = input_wb.worksheets[1]
- date_value = date_sheet.cell(row=4, column=2).value # B4 cell for date - 只获取不修改
-
- logging.debug(f"Extracted name: {name}")
- logging.debug(f"Original date value: {date_value}")
-
- # 只为了文件名和标题提取年月信息,不修改原始数据
- # 提取年月用于文件名和第二个工作表标题
- try:
- # 使用不同方法尝试获取年月信息,但不修改原始单元格内容
- extracted_year = None
- extracted_month = None
-
- if isinstance(date_value, (int, float)):
- # Excel日期数字格式
- base_date = datetime(1899, 12, 30)
- date_obj = base_date + timedelta(days=date_value)
- extracted_year = date_obj.year
- extracted_month = date_obj.month
- elif isinstance(date_value, datetime):
- # 日期对象
- extracted_year = date_value.year
- extracted_month = date_value.month
- else:
- # 字符串格式 - 尝试提取年月
- date_str = str(date_value)
-
- # 尝试匹配 YYYY年M月 或 YYYY年M月D日
- year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
- if year_month_match:
- extracted_year, extracted_month = map(int, year_month_match.groups())
-
- # 如果无法提取年月,使用当前日期
- if extracted_year is None or extracted_month is None:
- current_date = datetime.now()
- extracted_year = current_date.year
- extracted_month = current_date.month
- logging.warning(f"Could not extract year/month from date value: {date_value}, using current date")
-
- # 用于文件名和工作表标题的年月
- year = extracted_year
- month = extracted_month
-
- except Exception as e:
- # 如果提取失败,使用当前日期
- current_date = datetime.now()
- year = current_date.year
- month = current_date.month
- logging.error(f"Error extracting date: {e}, using current date")
-
- logging.debug(f"Using year={year}, month={month} for filename and sheet title")
-
- # Create output filename - 确保使用employee_name
- output_filename = OUTPUT_FILENAME_FORMAT.format(year=year, month=f'{month:02}', name=name)
- output_path = os.path.join(self.export_dir, output_filename)
-
- logging.debug(f"Output path: {output_path}")
-
- # Copy template file to output
- shutil.copy(self.template_path, output_path)
-
- # Open output file
- output_wb = openpyxl.load_workbook(output_path)
-
- # Apply cell mappings from config
- for src, dst in CELL_MAPPINGS.items():
- src_sheet_idx, src_row, src_col = src
- dst_sheet_idx, dst_row, dst_col = dst
- try:
- src_sheet = input_wb.worksheets[src_sheet_idx]
-
- # 处理合并单元格问题
- cell = src_sheet.cell(row=src_row, column=src_col)
- # 如果是合并单元格,获取主单元格的值
- if isinstance(cell, openpyxl.cell.cell.MergedCell):
- # 找到包含此单元格的合并区域
- for merged_range in src_sheet.merged_cells.ranges:
- if cell.coordinate in merged_range:
- # 获取合并区域左上角单元格的值
- top_left = merged_range.min_row, merged_range.min_col
- src_value = src_sheet.cell(row=top_left[0], column=top_left[1]).value
- break
- else:
- # 如果没有找到合并区域,设为空值
- src_value = None
- else:
- # 正常单元格直接获取值
- src_value = cell.value
-
- dst_sheet = output_wb.worksheets[dst_sheet_idx]
- dst_sheet.cell(row=dst_row, column=dst_col).value = src_value
- except Exception as e:
- # 只记录错误,继续处理下一个单元格
- logging.error(f"Error copying cell from {src} to {dst}: {e}")
- continue
-
- # Set custom fields - 使用try/except包裹每个操作
- try:
- # 尝试查找匹配的员工信息
- matched_info = None
- for emp_info in EMPLOYEE_INFO:
- if emp_info.get("employee_name") == name:
- matched_info = emp_info
- break
-
- # 如果有匹配的员工信息,则使用员工信息中的公司
- if matched_info and matched_info.get("company_name"):
- company = matched_info.get("company_name")
-
- sheet_idx, row, col = CUSTOM_CELLS["company"]
- output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = company
- except Exception as e:
- logging.error(f"Error setting company field: {e}")
-
- # 计算转账日期(月份+1)
- try:
- transfer_month = month + 1
- transfer_year = year
- if transfer_month > 12:
- transfer_month = 1
- transfer_year += 1
-
- # 计算令和年号
- reiwa_year = transfer_year - 2019 + 1
- transfer_date = f"令和{reiwa_year}年{transfer_month}月25日"
-
- # 尝试寻找匹配的银行信息
- bank_info = bank
-
- # 根据员工姓名查找匹配的员工信息 - 使用EMPLOYEE_INFO
- matched_info = None
- for emp_info in EMPLOYEE_INFO:
- if emp_info.get("employee_name") == name:
- matched_info = emp_info
- break
-
- if matched_info:
- # 使用匹配到的员工信息
- bank_name = matched_info.get("bank_name", "")
- branch_account = matched_info.get("branch_account", "")
- # 使用新的格式
- bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
-
- # 组合银行转账信息 - 使用新的格式,名義人始终使用employee_name
- if name and transfer_date:
- bank_info = f"{bank_info}\n名義人:{name}\n振込日:{transfer_date}\n※休日の場合は、翌営業日にお振込みします。"
-
- sheet_idx, row, col = CUSTOM_CELLS["bank"]
- output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = bank_info
-
- # 设置单元格自动换行
- cell = output_wb.worksheets[sheet_idx].cell(row=row, column=col)
- cell.alignment = Alignment(wrap_text=True, vertical='top')
- except Exception as e:
- logging.error(f"Error setting bank info: {e}")
-
- try:
- sheet_idx, row, col = CUSTOM_CELLS["other"]
-
- # 生成令和年号日期格式
- reiwa_date = ""
- if year and month:
- # 计算令和年号 (2019年为令和元年/1年)
- reiwa_year = year - 2019 + 1
- reiwa_date = f"令和{reiwa_year}年{month}月分"
-
- # 设置F2单元格的值为令和日期格式
- output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = reiwa_date or other_info
- except Exception as e:
- logging.error(f"Error setting other info: {e}")
-
- # Copy second sheet
- try:
- if len(output_wb.worksheets) == 1:
- output_wb.create_sheet()
- output_sheet2 = output_wb.worksheets[1]
- output_sheet2.title = SHEET2_NAME_FORMAT.format(month=month)
-
- # 先保存文件,以便后续使用xlwings处理
- output_wb.save(output_path)
-
- # 尝试使用xlwings做直接复制(对日期格式更可靠)
- try:
- # 使用更安全的xlwings调用方式
- logging.debug("尝试使用xlwings复制第二个工作表...")
-
- # 使用visible=True可能会解决某些权限问题
- with xw.App(visible=False, add_book=False) as app:
- # 禁用警告和事件可以提高稳定性
- app.display_alerts = False
- app.screen_updating = False
-
- try:
- # 打开工作簿
- input_wb_xw = app.books.open(input_file)
- output_wb_xw = app.books.open(output_path)
-
- # 尝试完整复制第二个工作表内容
- src_sheet = input_wb_xw.sheets[1]
- dst_sheet = output_wb_xw.sheets[1]
-
- # 复制包含单元格值和格式的范围
- src_used = src_sheet.used_range
- if src_used:
- src_used.copy()
- dst_sheet.range('A1').paste()
-
- # 保存和关闭工作簿
- output_wb_xw.save()
- input_wb_xw.close()
- output_wb_xw.close()
-
- logging.debug("使用xlwings成功复制了第二个工作表")
- return True # 如果xlwings成功,直接返回成功
- except Exception as e:
- logging.error(f"xlwings复制过程中发生错误: {e}")
- # 关闭所有打开的工作簿
- for book in app.books:
- try:
- book.close()
- except:
- pass
- raise # 重新引发异常,将进入备选方案
- except Exception as e:
- logging.error(f"使用xlwings复制B4单元格失败: {e},将使用openpyxl备选方案")
-
- # 如果xlwings失败,使用openpyxl作为备选方案继续处理
- # 重新加载文件,因为我们之前已保存
- output_wb = openpyxl.load_workbook(output_path)
- output_sheet2 = output_wb.worksheets[1]
-
- # 获取源工作表尺寸
- max_row = date_sheet.max_row
- max_col = date_sheet.max_column
-
- # 优先复制所有合并单元格
- for merged_range in date_sheet.merged_cells.ranges:
- output_sheet2.merge_cells(str(merged_range))
-
- # 改进的合并单元格处理方式
- # 首先识别所有合并单元格的主单元格(左上角单元格)
- merged_cell_masters = {}
- for merged_range in date_sheet.merged_cells.ranges:
- min_row, min_col = merged_range.min_row, merged_range.min_col
- merged_cell_masters[(min_row, min_col)] = date_sheet.cell(row=min_row, column=min_col).value
-
- # 逐个单元格复制
- for row in range(1, max_row + 1):
- for col in range(1, max_col + 1):
- try:
- # 获取源单元格
- src_cell = date_sheet.cell(row=row, column=col)
- dst_cell = output_sheet2.cell(row=row, column=col)
-
- # 特殊处理B4单元格
- if row == 4 and col == 2:
- if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
- # 如果B4是合并单元格,找到主单元格
- for merged_range in date_sheet.merged_cells.ranges:
- if src_cell.coordinate in merged_range:
- min_row, min_col = merged_range.min_row, merged_range.min_col
- main_cell = date_sheet.cell(row=min_row, column=min_col)
- dst_cell.value = main_cell.value
-
- # 复制主单元格格式
- if hasattr(main_cell, 'number_format'):
- dst_cell.number_format = main_cell.number_format
-
- if hasattr(main_cell, 'font'):
- dst_cell.font = copy(main_cell.font)
- if hasattr(main_cell, 'border'):
- dst_cell.border = copy(main_cell.border)
- if hasattr(main_cell, 'fill'):
- dst_cell.fill = copy(main_cell.fill)
- if hasattr(main_cell, 'alignment'):
- dst_cell.alignment = copy(main_cell.alignment)
-
- logging.debug(f"B4是合并单元格,已复制主单元格值={main_cell.value}")
- break
- else:
- # 普通单元格处理
- dst_cell.value = src_cell.value
-
- # 对于日期类型,特别处理
- from datetime import date
- if isinstance(src_cell.value, (datetime, date)):
- if hasattr(src_cell, 'number_format'):
- dst_cell.number_format = src_cell.number_format
- else:
- # 设置通用日期格式
- dst_cell.number_format = "yyyy/mm/dd"
-
- # 复制格式属性
- if hasattr(src_cell, 'font'):
- dst_cell.font = copy(src_cell.font)
- if hasattr(src_cell, 'border'):
- dst_cell.border = copy(src_cell.border)
- if hasattr(src_cell, 'fill'):
- dst_cell.fill = copy(src_cell.fill)
- if hasattr(src_cell, 'alignment'):
- dst_cell.alignment = copy(src_cell.alignment)
- if hasattr(src_cell, 'number_format'):
- dst_cell.number_format = src_cell.number_format
-
- logging.debug(f"B4是普通单元格,已复制值={src_cell.value}, 类型={type(src_cell.value)}")
- else:
- # 一般单元格处理 - 区分合并单元格与普通单元格
- if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
- # 对于合并单元格中的从属单元格,值保持为None
- dst_cell.value = None
- else:
- # 普通单元格的值直接复制
- dst_cell.value = src_cell.value
-
- # 复制格式
- if hasattr(src_cell, 'font'):
- dst_cell.font = copy(src_cell.font)
- if hasattr(src_cell, 'border'):
- dst_cell.border = copy(src_cell.border)
- if hasattr(src_cell, 'fill'):
- dst_cell.fill = copy(src_cell.fill)
- if hasattr(src_cell, 'alignment'):
- dst_cell.alignment = copy(src_cell.alignment)
- if hasattr(src_cell, 'number_format'):
- dst_cell.number_format = src_cell.number_format
-
- continue
-
- # 处理合并单元格
- if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
- # 对于合并单元格,找到合并区域的左上角主单元格的值
- cell_value = None
- for merged_range in date_sheet.merged_cells.ranges:
- if src_cell.coordinate in merged_range:
- top_left = merged_range.min_row, merged_range.min_col
- cell_value = date_sheet.cell(row=top_left[0], column=top_left[1]).value
- break
- else:
- # 普通单元格直接读取值
- cell_value = src_cell.value
-
- # 设置目标单元格值
- output_sheet2.cell(row=row, column=col).value = cell_value
-
- # 复制单元格格式(如果源单元格不是合并单元格)
- if not isinstance(src_cell, openpyxl.cell.cell.MergedCell):
- if hasattr(src_cell, 'has_style') and src_cell.has_style:
- output_cell = output_sheet2.cell(row=row, column=col)
- try:
- output_cell.font = copy(src_cell.font)
- output_cell.border = copy(src_cell.border)
- output_cell.fill = copy(src_cell.fill)
- output_cell.number_format = src_cell.number_format
- output_cell.protection = copy(src_cell.protection)
- output_cell.alignment = copy(src_cell.alignment)
- except:
- # 忽略样式复制错误
- pass
-
- except Exception as e:
- # 更加详细的错误记录
- logging.warning(f"复制单元格 row={row}, col={col} 时出错: {e}", exc_info=True)
- continue
-
- except Exception as e:
- logging.error(f"复制第二个工作表时出错: {e}", exc_info=True)
- return False
- except Exception as e:
- logging.error(f"Error copying second sheet: {e}")
-
- # Save output workbook
- try:
- output_wb.save(output_path)
- logging.debug("File processed successfully")
- return True
- except Exception as e:
- logging.error(f"Error saving output file: {e}")
- return False
-
- except Exception as e:
- logging.error(f"Error processing file: {e}")
- # 不显示消息框,只记录错误
- return False
-
-
- if __name__ == "__main__":
- root = tk.Tk()
- app = ExcelConverterApp(root)
- root.mainloop()
|