日本工资明细转换工具
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. import os
  2. import tkinter as tk
  3. from tkinter import filedialog, ttk, messagebox
  4. import pandas as pd
  5. import shutil
  6. from datetime import datetime, timedelta
  7. import openpyxl
  8. from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
  9. from openpyxl.utils import get_column_letter
  10. import xlrd
  11. import logging
  12. import xlwings as xw
  13. from copy import copy
  14. import re
  15. # Import configuration
  16. from config import (
  17. TEMPLATE_PATH,
  18. CELL_MAPPINGS,
  19. CUSTOM_CELLS,
  20. FILENAME_CONFIG,
  21. SHEET2_NAME_FORMAT,
  22. OUTPUT_FILENAME_FORMAT,
  23. EMPLOYEE_INFO,
  24. EMPLOYEE_CONFIG_PATH,
  25. COMPANY_OPTIONS,
  26. BANK_OPTIONS,
  27. save_options
  28. )
  29. logging.basicConfig(level=logging.DEBUG)
  30. class ExcelConverterApp:
  31. def __init__(self, root):
  32. self.root = root
  33. self.root.title("工资明细表转换工具")
  34. self.root.geometry("950x600") # 减小整体窗口尺寸
  35. # 设置主题和样式
  36. self.style = ttk.Style()
  37. if 'winnative' in self.style.theme_names():
  38. self.style.theme_use('winnative')
  39. elif 'vista' in self.style.theme_names():
  40. self.style.theme_use('vista')
  41. elif 'xpnative' in self.style.theme_names():
  42. self.style.theme_use('xpnative')
  43. elif 'clam' in self.style.theme_names():
  44. self.style.theme_use('clam')
  45. # 设置简约现代配色
  46. self.primary_color = "#4A6D8C" # 主色调 - 沉稳的蓝灰色
  47. self.accent_color = "#DB4D6D" # 强调色 - 适当的红色用于警告
  48. self.text_color = "#333333" # 文本色 - 深灰色易于阅读
  49. self.light_bg = "#F8F9FA" # 背景色 - 淡灰色背景
  50. self.light_accent = "#E9ECEF" # 浅强调 - 用于分隔和高亮
  51. # 设置基本样式
  52. self.style.configure("TFrame", background=self.light_bg)
  53. self.style.configure("TLabel", font=("Microsoft YaHei UI", 10), background=self.light_bg, foreground=self.text_color)
  54. self.style.configure("TLabelframe", font=("Microsoft YaHei UI", 10), background=self.light_bg)
  55. self.style.configure("TLabelframe.Label", font=("Microsoft YaHei UI", 10), background=self.light_bg, foreground=self.text_color)
  56. # 设置Treeview样式 - 简约样式
  57. self.style.configure("Treeview",
  58. font=("Microsoft YaHei UI", 10),
  59. background="white",
  60. foreground=self.text_color,
  61. fieldbackground="white")
  62. self.style.configure("Treeview.Heading",
  63. font=("Microsoft YaHei UI", 10),
  64. background=self.light_accent)
  65. self.style.map('Treeview', background=[('selected', self.primary_color)], foreground=[('selected', 'white')])
  66. # Variables
  67. self.import_files = []
  68. self.export_dir = ""
  69. self.template_path = TEMPLATE_PATH
  70. # Create UI
  71. self.create_widgets()
  72. def create_widgets(self):
  73. # 设置根框架背景
  74. self.root.configure(background=self.light_bg)
  75. # 创建主框架 - 减少内边距
  76. main_frame = ttk.Frame(self.root, padding=5)
  77. main_frame.pack(fill="both", expand=True)
  78. # 导入文件区域 - 减少内边距
  79. import_frame = ttk.LabelFrame(main_frame, text="导入Excel文件", padding=5)
  80. import_frame.pack(fill="both", expand=True, pady=5)
  81. # 按钮区域
  82. button_frame = ttk.Frame(import_frame)
  83. button_frame.pack(fill="x", pady=(0, 5))
  84. import_btn = ttk.Button(button_frame, text="选择Excel文件", command=self.select_files)
  85. import_btn.pack(side="left", padx=3)
  86. # 添加文件数量显示
  87. self.file_count_label = ttk.Label(button_frame, text="已选择: 0 个文件")
  88. self.file_count_label.pack(side="left", padx=10)
  89. # 文件列表区域
  90. list_frame = ttk.Frame(import_frame)
  91. list_frame.pack(fill="both", expand=True)
  92. columns = ("filename", "employee", "company", "bank", "other")
  93. self.file_tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
  94. # 设置列标题
  95. self.file_tree.heading("filename", text="文件名")
  96. self.file_tree.heading("employee", text="员工姓名 (C3)")
  97. self.file_tree.heading("company", text="所属公司信息 (C2)")
  98. self.file_tree.heading("bank", text="转账银行信息 (B30)")
  99. self.file_tree.heading("other", text="日本日期 (F2)")
  100. # 设置列宽
  101. self.file_tree.column("filename", width=280)
  102. self.file_tree.column("employee", width=110)
  103. self.file_tree.column("company", width=170)
  104. self.file_tree.column("bank", width=170)
  105. self.file_tree.column("other", width=120)
  106. # 添加滚动条
  107. scrollbar_y = ttk.Scrollbar(list_frame, orient="vertical", command=self.file_tree.yview)
  108. scrollbar_x = ttk.Scrollbar(list_frame, orient="horizontal", command=self.file_tree.xview)
  109. self.file_tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
  110. # 放置树状视图和滚动条
  111. self.file_tree.grid(row=0, column=0, sticky="nsew")
  112. scrollbar_y.grid(row=0, column=1, sticky="ns")
  113. scrollbar_x.grid(row=1, column=0, sticky="ew")
  114. # 配置列表框架的权重
  115. list_frame.columnconfigure(0, weight=1)
  116. list_frame.rowconfigure(0, weight=1)
  117. # 注意:移除双击编辑功能,导入列表不可修改
  118. # self.file_tree.bind("<Double-1>", self.edit_item)
  119. # 下部区域布局 - 减少间隔
  120. bottom_frame = ttk.Frame(main_frame)
  121. bottom_frame.pack(fill="x", pady=5)
  122. # 左侧区域 - 导出设置
  123. left_frame = ttk.Frame(bottom_frame)
  124. left_frame.pack(side="left", fill="y")
  125. export_label = ttk.Label(left_frame, text="导出位置:")
  126. export_label.pack(side="left", padx=(0, 3))
  127. self.export_label = ttk.Label(left_frame, text="未选择", width=28)
  128. self.export_label.pack(side="left")
  129. export_btn = ttk.Button(left_frame, text="选择导出位置", command=self.select_export_dir)
  130. export_btn.pack(side="left", padx=3)
  131. # 右侧区域 - 功能按钮
  132. right_frame = ttk.Frame(bottom_frame)
  133. right_frame.pack(side="right")
  134. info_btn = ttk.Button(right_frame, text="信息维护", command=self.maintain_info)
  135. info_btn.pack(side="left", padx=3)
  136. convert_btn = ttk.Button(right_frame, text="开始转换", command=self.convert_files)
  137. convert_btn.pack(side="left", padx=3)
  138. # 添加状态栏
  139. status_frame = ttk.Frame(main_frame, relief="sunken")
  140. status_frame.pack(fill="x", side="bottom", pady=(3, 0))
  141. self.status_label = ttk.Label(status_frame, text="准备就绪", anchor="w")
  142. self.status_label.pack(side="left", padx=5)
  143. def select_files(self):
  144. files = filedialog.askopenfilenames(
  145. title="选择Excel文件",
  146. filetypes=[("Excel Files", "*.xlsx")]
  147. )
  148. if files:
  149. self.import_files = list(files)
  150. self.update_file_tree()
  151. def update_file_tree(self):
  152. # Clear tree
  153. for item in self.file_tree.get_children():
  154. self.file_tree.delete(item)
  155. # 更新文件计数
  156. self.file_count_label.config(text=f"已选择: {len(self.import_files)} 个文件")
  157. # 用于存储未匹配的员工姓名
  158. unmatch_employees = []
  159. # Add files to tree with employee name and Japanese date
  160. for file in self.import_files:
  161. try:
  162. # Get employee name from C3 cell
  163. filename = os.path.basename(file)
  164. employee_name = ""
  165. original_date = ""
  166. # Get data from Excel file
  167. wb = openpyxl.load_workbook(file)
  168. name_sheet = wb.worksheets[0]
  169. employee_name = name_sheet.cell(row=3, column=3).value or ""
  170. # 获取日期单元格的原始值,不进行格式转换
  171. date_sheet = wb.worksheets[1]
  172. original_date = date_sheet.cell(row=4, column=2).value or ""
  173. # 仅为提取年月信息做最小处理(用于文件名和工作表名称),不影响显示
  174. extracted_year = None
  175. extracted_month = None
  176. try:
  177. if isinstance(original_date, (int, float)):
  178. # Excel日期数字格式
  179. base_date = datetime(1899, 12, 30)
  180. date_obj = base_date + timedelta(days=original_date)
  181. extracted_year = date_obj.year
  182. extracted_month = date_obj.month
  183. elif isinstance(original_date, datetime):
  184. # 日期对象
  185. extracted_year = original_date.year
  186. extracted_month = original_date.month
  187. else:
  188. # 字符串格式 - 尝试提取年月
  189. date_str = str(original_date)
  190. year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
  191. if year_month_match:
  192. extracted_year, extracted_month = map(int, year_month_match.groups())
  193. except Exception as e:
  194. logging.error(f"Error processing date: {e}")
  195. # 生成令和年号日期格式
  196. reiwa_date = ""
  197. if extracted_year and extracted_month:
  198. # 计算令和年号 (2019年为令和元年/1年)
  199. reiwa_year = extracted_year - 2019 + 1
  200. if reiwa_year > 0: # 只有2019年之后才使用令和年号
  201. reiwa_date = f"令和{reiwa_year}年{extracted_month}月分"
  202. # 默认值
  203. company = "选择公司"
  204. bank_info = "选择银行"
  205. # 检查员工信息是否存在于员工信息维护中
  206. employee_matched = False
  207. if employee_name:
  208. for emp_info in EMPLOYEE_INFO:
  209. if emp_info.get("employee_name") == employee_name:
  210. employee_matched = True
  211. # 匹配到员工信息,更新公司和银行信息
  212. company = emp_info.get("company_name", "")
  213. bank_name = emp_info.get("bank_name", "")
  214. branch_account = emp_info.get("branch_account", "")
  215. bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
  216. break
  217. # 如果未匹配,添加到未匹配列表
  218. if not employee_matched and employee_name not in unmatch_employees:
  219. unmatch_employees.append(employee_name)
  220. # 自动添加到文件树 - 在日本日期(F2)字段显示令和年月
  221. self.file_tree.insert("", "end", values=(filename, employee_name, company, bank_info, reiwa_date or original_date))
  222. except Exception as e:
  223. logging.error(f"Error reading file {file}: {e}")
  224. self.file_tree.insert("", "end", values=(filename, "", "选择公司", "选择银行", ""))
  225. # 如果有未匹配的员工,显示警告消息
  226. if unmatch_employees:
  227. warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
  228. warning_msg += "\n".join(unmatch_employees)
  229. warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
  230. messagebox.showwarning("员工信息不匹配", warning_msg)
  231. # 标记未匹配的行
  232. for item in self.file_tree.get_children():
  233. values = self.file_tree.item(item, "values")
  234. if values[1] in unmatch_employees:
  235. self.file_tree.item(item, tags=("unmatch",))
  236. # 设置未匹配样式(黄色背景)
  237. self.file_tree.tag_configure("unmatch", background="#FFF9C4") # 浅黄色
  238. # 设置状态提示
  239. if self.import_files:
  240. self.status_label.config(text="文件已加载,请配置转换选项", foreground=self.text_color)
  241. else:
  242. self.status_label.config(text="准备就绪", foreground="gray")
  243. def select_export_dir(self):
  244. directory = filedialog.askdirectory(title="选择导出目录")
  245. if directory:
  246. self.export_dir = directory
  247. self.export_label.config(text=directory)
  248. def maintain_info(self):
  249. # 创建弹出窗口
  250. popup = tk.Toplevel(self.root)
  251. popup.title("员工信息维护")
  252. popup.geometry("700x500")
  253. popup.transient(self.root)
  254. popup.grab_set()
  255. # 标题
  256. title_frame = ttk.Frame(popup, padding=5)
  257. title_frame.pack(fill="x")
  258. title_label = ttk.Label(title_frame, text="员工信息维护", font=("Microsoft YaHei UI", 12, "bold"))
  259. title_label.pack(side="left")
  260. # 创建表格视图
  261. tree_frame = ttk.Frame(popup, padding=5)
  262. tree_frame.pack(fill="both", expand=True)
  263. columns = ("employee", "company", "bank", "account", "holder")
  264. tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
  265. # 设置列标题
  266. tree.heading("employee", text="员工姓名")
  267. tree.heading("company", text="所属公司")
  268. tree.heading("bank", text="振込先金融機関")
  269. tree.heading("account", text="口座番号")
  270. tree.heading("holder", text="名義人")
  271. # 设置列宽
  272. tree.column("employee", width=100)
  273. tree.column("company", width=120)
  274. tree.column("bank", width=120)
  275. tree.column("account", width=180)
  276. tree.column("holder", width=120)
  277. # 添加滚动条
  278. vscrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
  279. tree.configure(yscrollcommand=vscrollbar.set)
  280. hscrollbar = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview)
  281. tree.configure(xscrollcommand=hscrollbar.set)
  282. tree.grid(row=0, column=0, sticky="nsew")
  283. vscrollbar.grid(row=0, column=1, sticky="ns")
  284. hscrollbar.grid(row=1, column=0, sticky="ew")
  285. tree_frame.columnconfigure(0, weight=1)
  286. tree_frame.rowconfigure(0, weight=1)
  287. # 添加搜索功能
  288. search_frame = ttk.Frame(popup, padding=5)
  289. search_frame.pack(fill="x", pady=5)
  290. search_label = ttk.Label(search_frame, text="搜索:")
  291. search_label.pack(side="left", padx=5)
  292. search_var = tk.StringVar()
  293. search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
  294. search_entry.pack(side="left", fill="x", expand=True, padx=5)
  295. def search_info(*args):
  296. query = search_var.get().lower()
  297. tree.delete(*tree.get_children())
  298. for info in EMPLOYEE_INFO:
  299. # 在所有字段中搜索
  300. search_in = (
  301. info.get("employee_name", "").lower() +
  302. info.get("company_name", "").lower() +
  303. info.get("bank_name", "").lower() +
  304. info.get("branch_account", "").lower() +
  305. info.get("account_holder", "").lower()
  306. )
  307. if query in search_in:
  308. tree.insert("", "end", values=(
  309. info.get("employee_name", ""),
  310. info.get("company_name", ""),
  311. info.get("bank_name", ""),
  312. info.get("branch_account", ""),
  313. info.get("account_holder", "")
  314. ))
  315. search_var.trace("w", search_info)
  316. # 添加按钮
  317. btn_frame = ttk.Frame(popup, padding=5)
  318. btn_frame.pack(fill="x", pady=5)
  319. # 编辑功能
  320. def edit_info():
  321. selected = tree.selection()
  322. if not selected:
  323. messagebox.showinfo("提示", "请先选择一条记录")
  324. return
  325. selected_idx = tree.index(selected[0])
  326. if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
  327. messagebox.showerror("错误", "选择的记录无效")
  328. return
  329. edit_employee_info(EMPLOYEE_INFO[selected_idx])
  330. # 新增功能
  331. def add_info():
  332. edit_employee_info()
  333. # 删除功能
  334. def delete_info():
  335. selected = tree.selection()
  336. if not selected:
  337. messagebox.showinfo("提示", "请先选择一条记录")
  338. return
  339. selected_idx = tree.index(selected[0])
  340. if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
  341. messagebox.showerror("错误", "选择的记录无效")
  342. return
  343. if messagebox.askyesno("确认", "确定要删除此记录吗?"):
  344. del EMPLOYEE_INFO[selected_idx]
  345. save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
  346. refresh_tree()
  347. # 刷新表格
  348. def refresh_tree():
  349. tree.delete(*tree.get_children())
  350. for info in EMPLOYEE_INFO:
  351. tree.insert("", "end", values=(
  352. info.get("employee_name", ""),
  353. info.get("company_name", ""),
  354. info.get("bank_name", ""),
  355. info.get("branch_account", ""),
  356. info.get("account_holder", "")
  357. ))
  358. # 编辑员工信息
  359. def edit_employee_info(info=None):
  360. is_new = info is None
  361. if is_new:
  362. info = {
  363. "employee_name": "",
  364. "company_name": "",
  365. "bank_name": "",
  366. "branch_account": "",
  367. "account_holder": ""
  368. }
  369. edit_popup = tk.Toplevel(popup)
  370. edit_popup.title("编辑员工信息")
  371. edit_popup.geometry("450x300")
  372. edit_popup.transient(popup)
  373. edit_popup.grab_set()
  374. # 创建表单
  375. form_frame = ttk.Frame(edit_popup, padding=10)
  376. form_frame.pack(fill="both", expand=True)
  377. # 员工姓名
  378. name_label = ttk.Label(form_frame, text="员工姓名:")
  379. name_label.grid(row=0, column=0, sticky="w", pady=5)
  380. name_var = tk.StringVar(value=info.get("employee_name", ""))
  381. name_entry = ttk.Entry(form_frame, textvariable=name_var, width=30)
  382. name_entry.grid(row=0, column=1, sticky="ew", pady=5)
  383. # 所属公司
  384. company_label = ttk.Label(form_frame, text="所属公司:")
  385. company_label.grid(row=1, column=0, sticky="w", pady=5)
  386. company_var = tk.StringVar(value=info.get("company_name", ""))
  387. company_entry = ttk.Entry(form_frame, textvariable=company_var, width=30)
  388. company_entry.grid(row=1, column=1, sticky="ew", pady=5)
  389. # 银行名称
  390. bank_label = ttk.Label(form_frame, text="振込先金融機関:")
  391. bank_label.grid(row=2, column=0, sticky="w", pady=5)
  392. bank_var = tk.StringVar(value=info.get("bank_name", ""))
  393. bank_entry = ttk.Entry(form_frame, textvariable=bank_var, width=30)
  394. bank_entry.grid(row=2, column=1, sticky="ew", pady=5)
  395. # 账户信息
  396. account_label = ttk.Label(form_frame, text="口座番号:")
  397. account_label.grid(row=3, column=0, sticky="w", pady=5)
  398. account_var = tk.StringVar(value=info.get("branch_account", ""))
  399. account_entry = ttk.Entry(form_frame, textvariable=account_var, width=30)
  400. account_entry.grid(row=3, column=1, sticky="ew", pady=5)
  401. # 账户持有人
  402. holder_label = ttk.Label(form_frame, text="名義人:")
  403. holder_label.grid(row=4, column=0, sticky="w", pady=5)
  404. holder_var = tk.StringVar(value=info.get("account_holder", ""))
  405. holder_entry = ttk.Entry(form_frame, textvariable=holder_var, width=30)
  406. holder_entry.grid(row=4, column=1, sticky="ew", pady=5)
  407. form_frame.columnconfigure(1, weight=1)
  408. # 按钮区域
  409. btn_frame = ttk.Frame(edit_popup, padding=10)
  410. btn_frame.pack(fill="x")
  411. def save_info():
  412. # 验证
  413. if not name_var.get().strip():
  414. messagebox.showerror("错误", "员工姓名不能为空")
  415. return
  416. # 保存
  417. new_info = {
  418. "employee_name": name_var.get().strip(),
  419. "company_name": company_var.get().strip(),
  420. "bank_name": bank_var.get().strip(),
  421. "branch_account": account_var.get().strip(),
  422. "account_holder": holder_var.get().strip()
  423. }
  424. if is_new:
  425. EMPLOYEE_INFO.append(new_info)
  426. else:
  427. idx = EMPLOYEE_INFO.index(info)
  428. EMPLOYEE_INFO[idx] = new_info
  429. save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
  430. refresh_tree()
  431. edit_popup.destroy()
  432. save_btn = ttk.Button(btn_frame, text="保存", command=save_info)
  433. save_btn.pack(side="right", padx=5)
  434. cancel_btn = ttk.Button(btn_frame, text="取消", command=edit_popup.destroy)
  435. cancel_btn.pack(side="right", padx=5)
  436. name_entry.focus_set()
  437. # 添加按钮
  438. add_btn = ttk.Button(btn_frame, text="新增", command=add_info)
  439. add_btn.pack(side="left", padx=5)
  440. edit_btn = ttk.Button(btn_frame, text="编辑", command=edit_info)
  441. edit_btn.pack(side="left", padx=5)
  442. delete_btn = ttk.Button(btn_frame, text="删除", command=delete_info)
  443. delete_btn.pack(side="left", padx=5)
  444. close_btn = ttk.Button(btn_frame, text="关闭", command=popup.destroy)
  445. close_btn.pack(side="right", padx=5)
  446. # 初始化
  447. refresh_tree()
  448. search_entry.focus_set()
  449. def convert_files(self):
  450. if not self.import_files:
  451. messagebox.showerror("错误", "请先选择要导入的Excel文件")
  452. return
  453. if not self.export_dir:
  454. messagebox.showerror("错误", "请选择导出位置")
  455. return
  456. # Template file check
  457. if not os.path.exists(self.template_path):
  458. messagebox.showerror("错误", f"模板文件不存在: {self.template_path}")
  459. return
  460. # 检查是否有未匹配的员工
  461. unmatch_employees = []
  462. for item in self.file_tree.get_children():
  463. values = self.file_tree.item(item, "values")
  464. employee_name = values[1]
  465. # 检查是否有匹配
  466. if employee_name and not any(emp_info.get("employee_name") == employee_name for emp_info in EMPLOYEE_INFO):
  467. unmatch_employees.append(employee_name)
  468. # 如果有未匹配员工,提示用户先维护信息
  469. if unmatch_employees:
  470. warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
  471. warning_msg += "\n".join(unmatch_employees)
  472. warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
  473. messagebox.showwarning("员工信息不匹配", warning_msg)
  474. return
  475. # 更新状态
  476. self.status_label.config(text="正在转换文件...", foreground=self.accent_color)
  477. self.root.update()
  478. # Process each file
  479. success_count = 0
  480. error_count = 0
  481. # 确保文件树和导入文件列表长度一致
  482. tree_items = self.file_tree.get_children()
  483. for i, file_path in enumerate(self.import_files):
  484. try:
  485. # 更新状态
  486. self.status_label.config(text=f"正在处理: {os.path.basename(file_path)} ({i+1}/{len(self.import_files)})")
  487. self.root.update()
  488. # 检查索引是否有效
  489. if i < len(tree_items):
  490. item = tree_items[i]
  491. values = self.file_tree.item(item, "values")
  492. # 安全地获取值
  493. filename = values[0] if len(values) > 0 else ""
  494. employee_name = values[1] if len(values) > 1 else ""
  495. company = values[2] if len(values) > 2 else ""
  496. bank = values[3] if len(values) > 3 else ""
  497. other_info = values[4] if len(values) > 4 else ""
  498. # Process file
  499. success = self.process_file(file_path, company, bank, other_info, employee_name)
  500. if success:
  501. success_count += 1
  502. # 设置行的背景色为成功
  503. self.file_tree.item(item, tags=("success",))
  504. else:
  505. error_count += 1
  506. # 设置行的背景色为错误
  507. self.file_tree.item(item, tags=("error",))
  508. else:
  509. # 文件树中没有对应的项,直接处理文件
  510. success = self.process_file(file_path, "", "", "", "")
  511. if success:
  512. success_count += 1
  513. else:
  514. error_count += 1
  515. except Exception as e:
  516. error_count += 1
  517. logging.error(f"处理文件 {os.path.basename(file_path)} 时出错: {str(e)}")
  518. # 不显示对话框,只在状态栏更新
  519. self.status_label.config(text=f"处理文件 {os.path.basename(file_path)} 时出错", foreground="red")
  520. # 找到对应的项并设置错误标签
  521. try:
  522. if i < len(tree_items):
  523. item = tree_items[i]
  524. self.file_tree.item(item, tags=("error",))
  525. except:
  526. pass
  527. # 设置Tag样式
  528. self.file_tree.tag_configure("success", background="#E8F8F5") # 浅绿色
  529. self.file_tree.tag_configure("error", background="#FADBD8") # 浅红色
  530. # 更新最终状态
  531. if error_count == 0:
  532. self.status_label.config(text=f"转换完成! 成功处理 {success_count} 个文件", foreground=self.accent_color)
  533. else:
  534. self.status_label.config(text=f"转换结束, 成功: {success_count}, 失败: {error_count}", foreground="red")
  535. # Show completion message
  536. messagebox.showinfo("完成", f"成功转换 {success_count} 个文件到 {self.export_dir}")
  537. def process_file(self, input_file, company, bank, other_info, employee_name=None):
  538. try:
  539. logging.debug(f"Processing file: {input_file}")
  540. # Extract name and date
  541. input_wb = openpyxl.load_workbook(input_file)
  542. name_sheet = input_wb.worksheets[0]
  543. name = employee_name or name_sheet.cell(row=3, column=3).value # Use provided name or C3 cell value
  544. date_sheet = input_wb.worksheets[1]
  545. date_value = date_sheet.cell(row=4, column=2).value # B4 cell for date - 只获取不修改
  546. logging.debug(f"Extracted name: {name}")
  547. logging.debug(f"Original date value: {date_value}")
  548. # 只为了文件名和标题提取年月信息,不修改原始数据
  549. # 提取年月用于文件名和第二个工作表标题
  550. try:
  551. # 使用不同方法尝试获取年月信息,但不修改原始单元格内容
  552. extracted_year = None
  553. extracted_month = None
  554. if isinstance(date_value, (int, float)):
  555. # Excel日期数字格式
  556. base_date = datetime(1899, 12, 30)
  557. date_obj = base_date + timedelta(days=date_value)
  558. extracted_year = date_obj.year
  559. extracted_month = date_obj.month
  560. elif isinstance(date_value, datetime):
  561. # 日期对象
  562. extracted_year = date_value.year
  563. extracted_month = date_value.month
  564. else:
  565. # 字符串格式 - 尝试提取年月
  566. date_str = str(date_value)
  567. # 尝试匹配 YYYY年M月 或 YYYY年M月D日
  568. year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
  569. if year_month_match:
  570. extracted_year, extracted_month = map(int, year_month_match.groups())
  571. # 如果无法提取年月,使用当前日期
  572. if extracted_year is None or extracted_month is None:
  573. current_date = datetime.now()
  574. extracted_year = current_date.year
  575. extracted_month = current_date.month
  576. logging.warning(f"Could not extract year/month from date value: {date_value}, using current date")
  577. # 用于文件名和工作表标题的年月
  578. year = extracted_year
  579. month = extracted_month
  580. except Exception as e:
  581. # 如果提取失败,使用当前日期
  582. current_date = datetime.now()
  583. year = current_date.year
  584. month = current_date.month
  585. logging.error(f"Error extracting date: {e}, using current date")
  586. logging.debug(f"Using year={year}, month={month} for filename and sheet title")
  587. # Create output filename - 确保使用employee_name
  588. output_filename = OUTPUT_FILENAME_FORMAT.format(year=year, month=f'{month:02}', name=name)
  589. output_path = os.path.join(self.export_dir, output_filename)
  590. logging.debug(f"Output path: {output_path}")
  591. # Copy template file to output
  592. shutil.copy(self.template_path, output_path)
  593. # Open output file
  594. output_wb = openpyxl.load_workbook(output_path)
  595. # Apply cell mappings from config
  596. for src, dst in CELL_MAPPINGS.items():
  597. src_sheet_idx, src_row, src_col = src
  598. dst_sheet_idx, dst_row, dst_col = dst
  599. try:
  600. src_sheet = input_wb.worksheets[src_sheet_idx]
  601. # 处理合并单元格问题
  602. cell = src_sheet.cell(row=src_row, column=src_col)
  603. # 如果是合并单元格,获取主单元格的值
  604. if isinstance(cell, openpyxl.cell.cell.MergedCell):
  605. # 找到包含此单元格的合并区域
  606. for merged_range in src_sheet.merged_cells.ranges:
  607. if cell.coordinate in merged_range:
  608. # 获取合并区域左上角单元格的值
  609. top_left = merged_range.min_row, merged_range.min_col
  610. src_value = src_sheet.cell(row=top_left[0], column=top_left[1]).value
  611. break
  612. else:
  613. # 如果没有找到合并区域,设为空值
  614. src_value = None
  615. else:
  616. # 正常单元格直接获取值
  617. src_value = cell.value
  618. dst_sheet = output_wb.worksheets[dst_sheet_idx]
  619. dst_sheet.cell(row=dst_row, column=dst_col).value = src_value
  620. except Exception as e:
  621. # 只记录错误,继续处理下一个单元格
  622. logging.error(f"Error copying cell from {src} to {dst}: {e}")
  623. continue
  624. # Set custom fields - 使用try/except包裹每个操作
  625. try:
  626. # 尝试查找匹配的员工信息
  627. matched_info = None
  628. for emp_info in EMPLOYEE_INFO:
  629. if emp_info.get("employee_name") == name:
  630. matched_info = emp_info
  631. break
  632. # 如果有匹配的员工信息,则使用员工信息中的公司
  633. if matched_info and matched_info.get("company_name"):
  634. company = matched_info.get("company_name")
  635. sheet_idx, row, col = CUSTOM_CELLS["company"]
  636. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = company
  637. except Exception as e:
  638. logging.error(f"Error setting company field: {e}")
  639. # 计算转账日期(月份+1)
  640. try:
  641. transfer_month = month + 1
  642. transfer_year = year
  643. if transfer_month > 12:
  644. transfer_month = 1
  645. transfer_year += 1
  646. # 计算令和年号
  647. reiwa_year = transfer_year - 2019 + 1
  648. transfer_date = f"令和{reiwa_year}年{transfer_month}月25日"
  649. # 尝试寻找匹配的银行信息
  650. bank_info = bank
  651. # 根据员工姓名查找匹配的员工信息 - 使用EMPLOYEE_INFO
  652. matched_info = None
  653. for emp_info in EMPLOYEE_INFO:
  654. if emp_info.get("employee_name") == name:
  655. matched_info = emp_info
  656. break
  657. if matched_info:
  658. # 使用匹配到的员工信息
  659. bank_name = matched_info.get("bank_name", "")
  660. branch_account = matched_info.get("branch_account", "")
  661. # 使用新的格式
  662. bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
  663. # 组合银行转账信息 - 使用新的格式,名義人始终使用employee_name
  664. if name and transfer_date:
  665. bank_info = f"{bank_info}\n名義人:{name}\n振込日:{transfer_date}\n※休日の場合は、翌営業日にお振込みします。"
  666. sheet_idx, row, col = CUSTOM_CELLS["bank"]
  667. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = bank_info
  668. # 设置单元格自动换行
  669. cell = output_wb.worksheets[sheet_idx].cell(row=row, column=col)
  670. cell.alignment = Alignment(wrap_text=True, vertical='top')
  671. except Exception as e:
  672. logging.error(f"Error setting bank info: {e}")
  673. try:
  674. sheet_idx, row, col = CUSTOM_CELLS["other"]
  675. # 生成令和年号日期格式
  676. reiwa_date = ""
  677. if year and month:
  678. # 计算令和年号 (2019年为令和元年/1年)
  679. reiwa_year = year - 2019 + 1
  680. reiwa_date = f"令和{reiwa_year}年{month}月分"
  681. # 设置F2单元格的值为令和日期格式
  682. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = reiwa_date or other_info
  683. except Exception as e:
  684. logging.error(f"Error setting other info: {e}")
  685. # Copy second sheet
  686. try:
  687. if len(output_wb.worksheets) == 1:
  688. output_wb.create_sheet()
  689. output_sheet2 = output_wb.worksheets[1]
  690. output_sheet2.title = SHEET2_NAME_FORMAT.format(month=month)
  691. # 先保存文件,以便后续使用xlwings处理
  692. output_wb.save(output_path)
  693. # 尝试使用xlwings做直接复制(对日期格式更可靠)
  694. try:
  695. # 使用更安全的xlwings调用方式
  696. logging.debug("尝试使用xlwings复制第二个工作表...")
  697. # 使用visible=True可能会解决某些权限问题
  698. with xw.App(visible=False, add_book=False) as app:
  699. # 禁用警告和事件可以提高稳定性
  700. app.display_alerts = False
  701. app.screen_updating = False
  702. try:
  703. # 打开工作簿
  704. input_wb_xw = app.books.open(input_file)
  705. output_wb_xw = app.books.open(output_path)
  706. # 尝试完整复制第二个工作表内容
  707. src_sheet = input_wb_xw.sheets[1]
  708. dst_sheet = output_wb_xw.sheets[1]
  709. # 复制包含单元格值和格式的范围
  710. src_used = src_sheet.used_range
  711. if src_used:
  712. src_used.copy()
  713. dst_sheet.range('A1').paste()
  714. # 保存和关闭工作簿
  715. output_wb_xw.save()
  716. input_wb_xw.close()
  717. output_wb_xw.close()
  718. logging.debug("使用xlwings成功复制了第二个工作表")
  719. return True # 如果xlwings成功,直接返回成功
  720. except Exception as e:
  721. logging.error(f"xlwings复制过程中发生错误: {e}")
  722. # 关闭所有打开的工作簿
  723. for book in app.books:
  724. try:
  725. book.close()
  726. except:
  727. pass
  728. raise # 重新引发异常,将进入备选方案
  729. except Exception as e:
  730. logging.error(f"使用xlwings复制B4单元格失败: {e},将使用openpyxl备选方案")
  731. # 如果xlwings失败,使用openpyxl作为备选方案继续处理
  732. # 重新加载文件,因为我们之前已保存
  733. output_wb = openpyxl.load_workbook(output_path)
  734. output_sheet2 = output_wb.worksheets[1]
  735. # 获取源工作表尺寸
  736. max_row = date_sheet.max_row
  737. max_col = date_sheet.max_column
  738. # 优先复制所有合并单元格
  739. for merged_range in date_sheet.merged_cells.ranges:
  740. output_sheet2.merge_cells(str(merged_range))
  741. # 改进的合并单元格处理方式
  742. # 首先识别所有合并单元格的主单元格(左上角单元格)
  743. merged_cell_masters = {}
  744. for merged_range in date_sheet.merged_cells.ranges:
  745. min_row, min_col = merged_range.min_row, merged_range.min_col
  746. merged_cell_masters[(min_row, min_col)] = date_sheet.cell(row=min_row, column=min_col).value
  747. # 逐个单元格复制
  748. for row in range(1, max_row + 1):
  749. for col in range(1, max_col + 1):
  750. try:
  751. # 获取源单元格
  752. src_cell = date_sheet.cell(row=row, column=col)
  753. dst_cell = output_sheet2.cell(row=row, column=col)
  754. # 特殊处理B4单元格
  755. if row == 4 and col == 2:
  756. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  757. # 如果B4是合并单元格,找到主单元格
  758. for merged_range in date_sheet.merged_cells.ranges:
  759. if src_cell.coordinate in merged_range:
  760. min_row, min_col = merged_range.min_row, merged_range.min_col
  761. main_cell = date_sheet.cell(row=min_row, column=min_col)
  762. dst_cell.value = main_cell.value
  763. # 复制主单元格格式
  764. if hasattr(main_cell, 'number_format'):
  765. dst_cell.number_format = main_cell.number_format
  766. if hasattr(main_cell, 'font'):
  767. dst_cell.font = copy(main_cell.font)
  768. if hasattr(main_cell, 'border'):
  769. dst_cell.border = copy(main_cell.border)
  770. if hasattr(main_cell, 'fill'):
  771. dst_cell.fill = copy(main_cell.fill)
  772. if hasattr(main_cell, 'alignment'):
  773. dst_cell.alignment = copy(main_cell.alignment)
  774. logging.debug(f"B4是合并单元格,已复制主单元格值={main_cell.value}")
  775. break
  776. else:
  777. # 普通单元格处理
  778. dst_cell.value = src_cell.value
  779. # 对于日期类型,特别处理
  780. from datetime import date
  781. if isinstance(src_cell.value, (datetime, date)):
  782. if hasattr(src_cell, 'number_format'):
  783. dst_cell.number_format = src_cell.number_format
  784. else:
  785. # 设置通用日期格式
  786. dst_cell.number_format = "yyyy/mm/dd"
  787. # 复制格式属性
  788. if hasattr(src_cell, 'font'):
  789. dst_cell.font = copy(src_cell.font)
  790. if hasattr(src_cell, 'border'):
  791. dst_cell.border = copy(src_cell.border)
  792. if hasattr(src_cell, 'fill'):
  793. dst_cell.fill = copy(src_cell.fill)
  794. if hasattr(src_cell, 'alignment'):
  795. dst_cell.alignment = copy(src_cell.alignment)
  796. if hasattr(src_cell, 'number_format'):
  797. dst_cell.number_format = src_cell.number_format
  798. logging.debug(f"B4是普通单元格,已复制值={src_cell.value}, 类型={type(src_cell.value)}")
  799. else:
  800. # 一般单元格处理 - 区分合并单元格与普通单元格
  801. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  802. # 对于合并单元格中的从属单元格,值保持为None
  803. dst_cell.value = None
  804. else:
  805. # 普通单元格的值直接复制
  806. dst_cell.value = src_cell.value
  807. # 复制格式
  808. if hasattr(src_cell, 'font'):
  809. dst_cell.font = copy(src_cell.font)
  810. if hasattr(src_cell, 'border'):
  811. dst_cell.border = copy(src_cell.border)
  812. if hasattr(src_cell, 'fill'):
  813. dst_cell.fill = copy(src_cell.fill)
  814. if hasattr(src_cell, 'alignment'):
  815. dst_cell.alignment = copy(src_cell.alignment)
  816. if hasattr(src_cell, 'number_format'):
  817. dst_cell.number_format = src_cell.number_format
  818. continue
  819. # 处理合并单元格
  820. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  821. # 对于合并单元格,找到合并区域的左上角主单元格的值
  822. cell_value = None
  823. for merged_range in date_sheet.merged_cells.ranges:
  824. if src_cell.coordinate in merged_range:
  825. top_left = merged_range.min_row, merged_range.min_col
  826. cell_value = date_sheet.cell(row=top_left[0], column=top_left[1]).value
  827. break
  828. else:
  829. # 普通单元格直接读取值
  830. cell_value = src_cell.value
  831. # 设置目标单元格值
  832. output_sheet2.cell(row=row, column=col).value = cell_value
  833. # 复制单元格格式(如果源单元格不是合并单元格)
  834. if not isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  835. if hasattr(src_cell, 'has_style') and src_cell.has_style:
  836. output_cell = output_sheet2.cell(row=row, column=col)
  837. try:
  838. output_cell.font = copy(src_cell.font)
  839. output_cell.border = copy(src_cell.border)
  840. output_cell.fill = copy(src_cell.fill)
  841. output_cell.number_format = src_cell.number_format
  842. output_cell.protection = copy(src_cell.protection)
  843. output_cell.alignment = copy(src_cell.alignment)
  844. except:
  845. # 忽略样式复制错误
  846. pass
  847. except Exception as e:
  848. # 更加详细的错误记录
  849. logging.warning(f"复制单元格 row={row}, col={col} 时出错: {e}", exc_info=True)
  850. continue
  851. except Exception as e:
  852. logging.error(f"复制第二个工作表时出错: {e}", exc_info=True)
  853. return False
  854. except Exception as e:
  855. logging.error(f"Error copying second sheet: {e}")
  856. # Save output workbook
  857. try:
  858. output_wb.save(output_path)
  859. logging.debug("File processed successfully")
  860. return True
  861. except Exception as e:
  862. logging.error(f"Error saving output file: {e}")
  863. return False
  864. except Exception as e:
  865. logging.error(f"Error processing file: {e}")
  866. # 不显示消息框,只记录错误
  867. return False
  868. if __name__ == "__main__":
  869. root = tk.Tk()
  870. app = ExcelConverterApp(root)
  871. root.mainloop()