日本工资明细转换工具
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  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 = ("employee", "company", "bank", "date", "match_status")
  93. self.file_tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
  94. # 设置列标题
  95. self.file_tree.heading("employee", text="员工姓名 (C3)")
  96. self.file_tree.heading("company", text="所属公司信息 (C2)")
  97. self.file_tree.heading("bank", text="转账银行信息 (B30)")
  98. self.file_tree.heading("date", text="日本日期 (F2)")
  99. self.file_tree.heading("match_status", text="工作表匹配状态")
  100. # 设置列宽
  101. self.file_tree.column("employee", width=120)
  102. self.file_tree.column("company", width=170)
  103. self.file_tree.column("bank", width=170)
  104. self.file_tree.column("date", width=120)
  105. self.file_tree.column("match_status", 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. file = filedialog.askopenfilename(
  145. title="选择Excel文件",
  146. filetypes=[("Excel Files", "*.xlsx")]
  147. )
  148. if file:
  149. self.import_files = [file]
  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. unmatch_employees = []
  157. # 当前只处理单个文件
  158. if not self.import_files:
  159. return
  160. file = self.import_files[0]
  161. try:
  162. # 打开工作簿
  163. wb = openpyxl.load_workbook(file)
  164. name_sheet = wb.worksheets[0]
  165. # 验证是否存在第二个工作表
  166. if len(wb.worksheets) < 2:
  167. messagebox.showerror("错误", "导入的Excel必须包含至少两个工作表")
  168. self.import_files = []
  169. return
  170. date_sheet = wb.worksheets[1]
  171. # 获取日期信息
  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. base_date = datetime(1899, 12, 30)
  179. date_obj = base_date + timedelta(days=original_date)
  180. extracted_year = date_obj.year
  181. extracted_month = date_obj.month
  182. elif isinstance(original_date, datetime):
  183. extracted_year = original_date.year
  184. extracted_month = original_date.month
  185. else:
  186. date_str = str(original_date)
  187. year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
  188. if year_month_match:
  189. extracted_year, extracted_month = map(int, year_month_match.groups())
  190. except Exception as e:
  191. logging.error(f"Error processing date: {e}")
  192. # 生成令和年号日期格式
  193. reiwa_date = ""
  194. if extracted_year and extracted_month:
  195. reiwa_year = extracted_year - 2019 + 1
  196. if reiwa_year > 0:
  197. reiwa_date = f"令和{reiwa_year}年{extracted_month}月分"
  198. # 检查工作表中是否存在员工姓名对应的工作表
  199. sheet_names = wb.sheetnames
  200. employee_sheets = sheet_names[1:] # 从第二个工作表开始
  201. # 获取所有员工数据(从C3开始的行)
  202. current_row = 3 # 从C3开始
  203. while True:
  204. name_cell = name_sheet.cell(row=current_row, column=3) # C列
  205. if not name_cell.value: # 如果C列单元格为空,说明没有更多数据
  206. break
  207. employee_name = name_cell.value
  208. if employee_name:
  209. # 清除员工姓名中的所有空格
  210. employee_name = employee_name.replace(" ", "").strip()
  211. name_sheet.cell(row=current_row, column=3).value = employee_name
  212. # 默认值
  213. company = "选择公司"
  214. bank_info = "选择银行"
  215. # 检查员工信息是否存在于员工信息维护中
  216. employee_matched = False
  217. if employee_name:
  218. for emp_info in EMPLOYEE_INFO:
  219. # 清除员工配置中姓名的空格后再比较
  220. config_name = emp_info.get("employee_name", "").replace(" ", "").strip()
  221. if config_name == employee_name:
  222. employee_matched = True
  223. # 匹配到员工信息,更新公司和银行信息
  224. company = emp_info.get("company_name", "")
  225. bank_name = emp_info.get("bank_name", "")
  226. branch_account = emp_info.get("branch_account", "")
  227. bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
  228. break
  229. # 检查员工名称是否在工作表名称中
  230. sheet_match = False
  231. for sheet_name in employee_sheets:
  232. # 清除工作表名称中的空格后再比较
  233. clean_sheet_name = sheet_name.replace(" ", "").strip()
  234. if employee_name == clean_sheet_name:
  235. sheet_match = True
  236. break
  237. # 如果未匹配,添加到未匹配列表
  238. if not employee_matched and employee_name not in unmatch_employees:
  239. unmatch_employees.append(employee_name)
  240. # 添加到文件树 - 在日本日期(F2)字段显示令和年月及工作表匹配状态
  241. matching_info = "✓ 已匹配工作表" if sheet_match else "✗ 未匹配工作表"
  242. self.file_tree.insert("", "end", values=(
  243. employee_name, # 员工姓名
  244. company, # 公司信息
  245. bank_info, # 银行信息
  246. reiwa_date, # 日期信息
  247. matching_info # 工作表匹配状态
  248. ))
  249. current_row += 1
  250. except Exception as e:
  251. logging.error(f"Error reading file {file}: {e}")
  252. # 更新文件计数
  253. total_employees = len(self.file_tree.get_children())
  254. self.file_count_label.config(text=f"已选择: {total_employees} 个员工")
  255. # 如果有未匹配的员工,显示警告消息
  256. if unmatch_employees:
  257. warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
  258. warning_msg += "\n".join(unmatch_employees)
  259. warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
  260. messagebox.showwarning("员工信息不匹配", warning_msg)
  261. # 标记未匹配的行
  262. for item in self.file_tree.get_children():
  263. values = self.file_tree.item(item, "values")
  264. if values[0] in unmatch_employees:
  265. self.file_tree.item(item, tags=("unmatch",))
  266. # 设置未匹配样式(黄色背景)
  267. self.file_tree.tag_configure("unmatch", background="#FFF9C4") # 浅黄色
  268. # 设置状态提示
  269. if total_employees > 0:
  270. self.status_label.config(text="员工信息已加载,请配置转换选项", foreground=self.text_color)
  271. else:
  272. self.status_label.config(text="准备就绪", foreground="gray")
  273. def select_export_dir(self):
  274. directory = filedialog.askdirectory(title="选择导出目录")
  275. if directory:
  276. self.export_dir = directory
  277. self.export_label.config(text=directory)
  278. def maintain_info(self):
  279. # 创建弹出窗口
  280. popup = tk.Toplevel(self.root)
  281. popup.title("员工信息维护")
  282. popup.geometry("700x500")
  283. popup.transient(self.root)
  284. popup.grab_set()
  285. # 标题
  286. title_frame = ttk.Frame(popup, padding=5)
  287. title_frame.pack(fill="x")
  288. title_label = ttk.Label(title_frame, text="员工信息维护", font=("Microsoft YaHei UI", 12, "bold"))
  289. title_label.pack(side="left")
  290. # 创建表格视图
  291. tree_frame = ttk.Frame(popup, padding=5)
  292. tree_frame.pack(fill="both", expand=True)
  293. columns = ("employee", "company", "bank", "account", "holder")
  294. tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
  295. # 设置列标题
  296. tree.heading("employee", text="员工姓名")
  297. tree.heading("company", text="所属公司")
  298. tree.heading("bank", text="振込先金融機関")
  299. tree.heading("account", text="口座番号")
  300. tree.heading("holder", text="名義人")
  301. # 设置列宽
  302. tree.column("employee", width=100)
  303. tree.column("company", width=120)
  304. tree.column("bank", width=120)
  305. tree.column("account", width=180)
  306. tree.column("holder", width=120)
  307. # 添加滚动条
  308. vscrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
  309. tree.configure(yscrollcommand=vscrollbar.set)
  310. hscrollbar = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview)
  311. tree.configure(xscrollcommand=hscrollbar.set)
  312. tree.grid(row=0, column=0, sticky="nsew")
  313. vscrollbar.grid(row=0, column=1, sticky="ns")
  314. hscrollbar.grid(row=1, column=0, sticky="ew")
  315. tree_frame.columnconfigure(0, weight=1)
  316. tree_frame.rowconfigure(0, weight=1)
  317. # 添加搜索功能
  318. search_frame = ttk.Frame(popup, padding=5)
  319. search_frame.pack(fill="x", pady=5)
  320. search_label = ttk.Label(search_frame, text="搜索:")
  321. search_label.pack(side="left", padx=5)
  322. search_var = tk.StringVar()
  323. search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
  324. search_entry.pack(side="left", fill="x", expand=True, padx=5)
  325. def search_info(*args):
  326. query = search_var.get().lower()
  327. tree.delete(*tree.get_children())
  328. for info in EMPLOYEE_INFO:
  329. # 在所有字段中搜索
  330. search_in = (
  331. info.get("employee_name", "").lower() +
  332. info.get("company_name", "").lower() +
  333. info.get("bank_name", "").lower() +
  334. info.get("branch_account", "").lower() +
  335. info.get("account_holder", "").lower()
  336. )
  337. if query in search_in:
  338. tree.insert("", "end", values=(
  339. info.get("employee_name", ""),
  340. info.get("company_name", ""),
  341. info.get("bank_name", ""),
  342. info.get("branch_account", ""),
  343. info.get("account_holder", "")
  344. ))
  345. search_var.trace("w", search_info)
  346. # 添加按钮
  347. btn_frame = ttk.Frame(popup, padding=5)
  348. btn_frame.pack(fill="x", pady=5)
  349. # 编辑功能
  350. def edit_info():
  351. selected = tree.selection()
  352. if not selected:
  353. messagebox.showinfo("提示", "请先选择一条记录")
  354. return
  355. selected_idx = tree.index(selected[0])
  356. if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
  357. messagebox.showerror("错误", "选择的记录无效")
  358. return
  359. edit_employee_info(EMPLOYEE_INFO[selected_idx])
  360. # 新增功能
  361. def add_info():
  362. edit_employee_info()
  363. # 删除功能
  364. def delete_info():
  365. selected = tree.selection()
  366. if not selected:
  367. messagebox.showinfo("提示", "请先选择一条记录")
  368. return
  369. selected_idx = tree.index(selected[0])
  370. if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
  371. messagebox.showerror("错误", "选择的记录无效")
  372. return
  373. if messagebox.askyesno("确认", "确定要删除此记录吗?"):
  374. del EMPLOYEE_INFO[selected_idx]
  375. save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
  376. refresh_tree()
  377. # 刷新表格
  378. def refresh_tree():
  379. tree.delete(*tree.get_children())
  380. for info in EMPLOYEE_INFO:
  381. tree.insert("", "end", values=(
  382. info.get("employee_name", ""),
  383. info.get("company_name", ""),
  384. info.get("bank_name", ""),
  385. info.get("branch_account", ""),
  386. info.get("account_holder", "")
  387. ))
  388. # 编辑员工信息
  389. def edit_employee_info(info=None):
  390. is_new = info is None
  391. if is_new:
  392. info = {
  393. "employee_name": "",
  394. "company_name": "",
  395. "bank_name": "",
  396. "branch_account": "",
  397. "account_holder": ""
  398. }
  399. edit_popup = tk.Toplevel(popup)
  400. edit_popup.title("编辑员工信息")
  401. edit_popup.geometry("450x300")
  402. edit_popup.transient(popup)
  403. edit_popup.grab_set()
  404. # 创建表单
  405. form_frame = ttk.Frame(edit_popup, padding=10)
  406. form_frame.pack(fill="both", expand=True)
  407. # 员工姓名
  408. name_label = ttk.Label(form_frame, text="员工姓名:")
  409. name_label.grid(row=0, column=0, sticky="w", pady=5)
  410. name_var = tk.StringVar(value=info.get("employee_name", ""))
  411. name_entry = ttk.Entry(form_frame, textvariable=name_var, width=30)
  412. name_entry.grid(row=0, column=1, sticky="ew", pady=5)
  413. # 所属公司
  414. company_label = ttk.Label(form_frame, text="所属公司:")
  415. company_label.grid(row=1, column=0, sticky="w", pady=5)
  416. company_var = tk.StringVar(value=info.get("company_name", ""))
  417. company_entry = ttk.Entry(form_frame, textvariable=company_var, width=30)
  418. company_entry.grid(row=1, column=1, sticky="ew", pady=5)
  419. # 银行名称
  420. bank_label = ttk.Label(form_frame, text="振込先金融機関:")
  421. bank_label.grid(row=2, column=0, sticky="w", pady=5)
  422. bank_var = tk.StringVar(value=info.get("bank_name", ""))
  423. bank_entry = ttk.Entry(form_frame, textvariable=bank_var, width=30)
  424. bank_entry.grid(row=2, column=1, sticky="ew", pady=5)
  425. # 账户信息
  426. account_label = ttk.Label(form_frame, text="口座番号:")
  427. account_label.grid(row=3, column=0, sticky="w", pady=5)
  428. account_var = tk.StringVar(value=info.get("branch_account", ""))
  429. account_entry = ttk.Entry(form_frame, textvariable=account_var, width=30)
  430. account_entry.grid(row=3, column=1, sticky="ew", pady=5)
  431. # 账户持有人
  432. holder_label = ttk.Label(form_frame, text="名義人:")
  433. holder_label.grid(row=4, column=0, sticky="w", pady=5)
  434. holder_var = tk.StringVar(value=info.get("account_holder", ""))
  435. holder_entry = ttk.Entry(form_frame, textvariable=holder_var, width=30)
  436. holder_entry.grid(row=4, column=1, sticky="ew", pady=5)
  437. form_frame.columnconfigure(1, weight=1)
  438. # 按钮区域
  439. btn_frame = ttk.Frame(edit_popup, padding=10)
  440. btn_frame.pack(fill="x")
  441. def save_info():
  442. # 验证
  443. if not name_var.get().strip():
  444. messagebox.showerror("错误", "员工姓名不能为空")
  445. return
  446. # 保存
  447. new_info = {
  448. "employee_name": name_var.get().strip(),
  449. "company_name": company_var.get().strip(),
  450. "bank_name": bank_var.get().strip(),
  451. "branch_account": account_var.get().strip(),
  452. "account_holder": holder_var.get().strip()
  453. }
  454. if is_new:
  455. EMPLOYEE_INFO.append(new_info)
  456. else:
  457. idx = EMPLOYEE_INFO.index(info)
  458. EMPLOYEE_INFO[idx] = new_info
  459. save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
  460. refresh_tree()
  461. edit_popup.destroy()
  462. save_btn = ttk.Button(btn_frame, text="保存", command=save_info)
  463. save_btn.pack(side="right", padx=5)
  464. cancel_btn = ttk.Button(btn_frame, text="取消", command=edit_popup.destroy)
  465. cancel_btn.pack(side="right", padx=5)
  466. name_entry.focus_set()
  467. # 添加按钮
  468. add_btn = ttk.Button(btn_frame, text="新增", command=add_info)
  469. add_btn.pack(side="left", padx=5)
  470. edit_btn = ttk.Button(btn_frame, text="编辑", command=edit_info)
  471. edit_btn.pack(side="left", padx=5)
  472. delete_btn = ttk.Button(btn_frame, text="删除", command=delete_info)
  473. delete_btn.pack(side="left", padx=5)
  474. close_btn = ttk.Button(btn_frame, text="关闭", command=popup.destroy)
  475. close_btn.pack(side="right", padx=5)
  476. # 初始化
  477. refresh_tree()
  478. search_entry.focus_set()
  479. def convert_files(self):
  480. if not self.import_files:
  481. messagebox.showerror("错误", "请先选择要导入的Excel文件")
  482. return
  483. if not self.export_dir:
  484. messagebox.showerror("错误", "请选择导出位置")
  485. return
  486. # Template file check
  487. if not os.path.exists(self.template_path):
  488. messagebox.showerror("错误", f"模板文件不存在: {self.template_path}")
  489. return
  490. # 检查是否有未匹配的员工
  491. unmatch_employees = []
  492. for item in self.file_tree.get_children():
  493. values = self.file_tree.item(item, "values")
  494. employee_name = values[0]
  495. # 检查是否有匹配
  496. if employee_name and not any(emp_info.get("employee_name") == employee_name for emp_info in EMPLOYEE_INFO):
  497. unmatch_employees.append(employee_name)
  498. # 如果有未匹配员工,提示用户先维护信息
  499. if unmatch_employees:
  500. warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
  501. warning_msg += "\n".join(unmatch_employees)
  502. warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
  503. messagebox.showwarning("员工信息不匹配", warning_msg)
  504. return
  505. # 更新状态
  506. self.status_label.config(text="正在转换文件...", foreground=self.accent_color)
  507. self.root.update()
  508. # Process each file
  509. success_count = 0
  510. error_count = 0
  511. # 确保文件树和导入文件列表长度一致
  512. tree_items = self.file_tree.get_children()
  513. for i, file_path in enumerate(self.import_files):
  514. try:
  515. # 更新状态
  516. self.status_label.config(text=f"正在处理: {os.path.basename(file_path)} ({i+1}/{len(self.import_files)})")
  517. self.root.update()
  518. # 检查索引是否有效
  519. if i < len(tree_items):
  520. item = tree_items[i]
  521. values = self.file_tree.item(item, "values")
  522. # 安全地获取值
  523. employee_name = values[0]
  524. # Process file
  525. success = self.process_file(file_path, "", "", "", employee_name)
  526. if success:
  527. success_count += 1
  528. # 设置行的背景色为成功
  529. self.file_tree.item(item, tags=("success",))
  530. else:
  531. error_count += 1
  532. # 设置行的背景色为错误
  533. self.file_tree.item(item, tags=("error",))
  534. else:
  535. # 文件树中没有对应的项,直接处理文件
  536. success = self.process_file(file_path, "", "", "", "")
  537. if success:
  538. success_count += 1
  539. else:
  540. error_count += 1
  541. except Exception as e:
  542. error_count += 1
  543. logging.error(f"处理文件 {os.path.basename(file_path)} 时出错: {str(e)}")
  544. # 不显示对话框,只在状态栏更新
  545. self.status_label.config(text=f"处理文件 {os.path.basename(file_path)} 时出错", foreground="red")
  546. # 找到对应的项并设置错误标签
  547. try:
  548. if i < len(tree_items):
  549. item = tree_items[i]
  550. self.file_tree.item(item, tags=("error",))
  551. except:
  552. pass
  553. # 设置Tag样式
  554. self.file_tree.tag_configure("success", background="#E8F8F5") # 浅绿色
  555. self.file_tree.tag_configure("error", background="#FADBD8") # 浅红色
  556. # 更新最终状态
  557. if error_count == 0:
  558. self.status_label.config(text=f"转换完成! 成功处理 {success_count} 个文件", foreground=self.accent_color)
  559. else:
  560. self.status_label.config(text=f"转换结束, 成功: {success_count}, 失败: {error_count}", foreground="red")
  561. # Show completion message
  562. messagebox.showinfo("完成", f"成功转换 {success_count} 个文件到 {self.export_dir}")
  563. def process_file(self, input_file, company, bank, other_info, employee_name=None):
  564. try:
  565. logging.debug(f"Processing file: {input_file}")
  566. # Extract name and date
  567. input_wb = openpyxl.load_workbook(input_file)
  568. name_sheet = input_wb.worksheets[0]
  569. date_sheet = input_wb.worksheets[1]
  570. date_value = date_sheet.cell(row=4, column=2).value # B4 cell for date
  571. # 获取年月信息
  572. try:
  573. extracted_year = None
  574. extracted_month = None
  575. if isinstance(date_value, (int, float)):
  576. base_date = datetime(1899, 12, 30)
  577. date_obj = base_date + timedelta(days=date_value)
  578. extracted_year = date_obj.year
  579. extracted_month = date_obj.month
  580. elif isinstance(date_value, datetime):
  581. extracted_year = date_value.year
  582. extracted_month = date_value.month
  583. else:
  584. date_str = str(date_value)
  585. year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
  586. if year_month_match:
  587. extracted_year, extracted_month = map(int, year_month_match.groups())
  588. if extracted_year is None or extracted_month is None:
  589. current_date = datetime.now()
  590. extracted_year = current_date.year
  591. extracted_month = current_date.month
  592. logging.warning(f"Could not extract year/month from date value: {date_value}, using current date")
  593. year = extracted_year
  594. month = extracted_month
  595. except Exception as e:
  596. logging.error(f"Error extracting year/month: {e}")
  597. return False
  598. # 获取所有工作表名称,用于后续匹配
  599. sheet_names = input_wb.sheetnames
  600. # 获取所有员工数据(从C3开始的行)
  601. employee_rows = []
  602. current_row = 3 # 从C3开始
  603. while True:
  604. name_cell = name_sheet.cell(row=current_row, column=3) # C列
  605. if not name_cell.value: # 如果C列单元格为空,说明没有更多数据
  606. break
  607. employee_rows.append(current_row)
  608. current_row += 1
  609. if not employee_rows:
  610. logging.error("No employee data found in the first sheet")
  611. return False
  612. # 为每个员工创建输出文件
  613. success = True
  614. for row_index in employee_rows:
  615. employee_name = name_sheet.cell(row=row_index, column=3).value
  616. if not employee_name:
  617. continue
  618. # 清除员工姓名中的所有空格
  619. employee_name = employee_name.replace(" ", "").strip()
  620. # 创建输出文件名
  621. output_filename = OUTPUT_FILENAME_FORMAT.format(
  622. year=year,
  623. month=month,
  624. name=employee_name
  625. )
  626. output_path = os.path.join(self.export_dir, output_filename)
  627. # 加载模板
  628. template_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), TEMPLATE_PATH)
  629. output_wb = openpyxl.load_workbook(template_path)
  630. # 应用单元格映射
  631. for src, dst in CELL_MAPPINGS.items():
  632. src_sheet_idx, src_row_offset, src_col = src
  633. dst_sheet_idx, dst_row, dst_col = dst
  634. try:
  635. src_sheet = input_wb.worksheets[src_sheet_idx]
  636. src_row = row_index # 使用当前员工的行
  637. # 处理合并单元格
  638. cell = src_sheet.cell(row=src_row, column=src_col)
  639. if isinstance(cell, openpyxl.cell.cell.MergedCell):
  640. for merged_range in src_sheet.merged_cells.ranges:
  641. if cell.coordinate in merged_range:
  642. top_left = merged_range.min_row, merged_range.min_col
  643. src_value = src_sheet.cell(row=top_left[0], column=top_left[1]).value
  644. break
  645. else:
  646. src_value = None
  647. else:
  648. src_value = cell.value
  649. dst_sheet = output_wb.worksheets[dst_sheet_idx]
  650. dst_sheet.cell(row=dst_row, column=dst_col).value = src_value
  651. except Exception as e:
  652. logging.error(f"Error copying cell from {src} to {dst}: {e}")
  653. continue
  654. # 设置自定义字段
  655. try:
  656. matched_info = None
  657. for emp_info in EMPLOYEE_INFO:
  658. # 清除员工配置中姓名的空格后再比较
  659. config_name = emp_info.get("employee_name", "").replace(" ", "").strip()
  660. if config_name == employee_name:
  661. matched_info = emp_info
  662. break
  663. if matched_info and matched_info.get("company_name"):
  664. company = matched_info.get("company_name")
  665. sheet_idx, row, col = CUSTOM_CELLS["company"]
  666. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = company
  667. except Exception as e:
  668. logging.error(f"Error setting company field: {e}")
  669. try:
  670. sheet_idx, row, col = CUSTOM_CELLS["bank"]
  671. # 计算转账日期(月份+1)
  672. transfer_month = month + 1
  673. transfer_year = year
  674. if transfer_month > 12:
  675. transfer_month = 1
  676. transfer_year += 1
  677. # 计算令和年号
  678. reiwa_year = transfer_year - 2019 + 1
  679. transfer_date = f"令和{reiwa_year}年{transfer_month}月25日"
  680. # 获取银行信息
  681. bank_info = bank
  682. if employee_name:
  683. # 查找匹配的员工信息
  684. matched_info = None
  685. for emp_info in EMPLOYEE_INFO:
  686. # 清除员工配置中姓名的空格后再比较
  687. config_name = emp_info.get("employee_name", "").replace(" ", "").strip()
  688. if config_name == employee_name:
  689. matched_info = emp_info
  690. break
  691. if matched_info:
  692. bank_name = matched_info.get("bank_name", "")
  693. branch_account = matched_info.get("branch_account", "")
  694. account_holder = matched_info.get("account_holder", employee_name)
  695. # 组合完整的银行转账信息
  696. bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}\n名義人:{account_holder}\n振込日:{transfer_date}\n※休日の場合は、翌営業日にお振込みします。"
  697. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = bank_info
  698. # 设置单元格自动换行
  699. cell = output_wb.worksheets[sheet_idx].cell(row=row, column=col)
  700. cell.alignment = Alignment(wrap_text=True, vertical='top')
  701. except Exception as e:
  702. logging.error(f"Error setting bank info: {e}")
  703. try:
  704. sheet_idx, row, col = CUSTOM_CELLS["other"]
  705. reiwa_date = ""
  706. if year and month:
  707. reiwa_year = year - 2019 + 1
  708. reiwa_date = f"令和{reiwa_year}年{month}月分"
  709. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = reiwa_date or other_info
  710. except Exception as e:
  711. logging.error(f"Error setting other info: {e}")
  712. # 复制第二个工作表 - 根据员工姓名匹配工作表
  713. try:
  714. if len(output_wb.worksheets) == 1:
  715. output_wb.create_sheet()
  716. output_sheet2 = output_wb.worksheets[1]
  717. output_sheet2.title = SHEET2_NAME_FORMAT.format(month=month)
  718. # 查找员工姓名匹配的工作表
  719. matched_sheet = None
  720. for sheet_name in input_wb.sheetnames:
  721. # 清除工作表名称中的空格后再比较
  722. clean_sheet_name = sheet_name.replace(" ", "").strip()
  723. if employee_name == clean_sheet_name:
  724. matched_sheet = sheet_name
  725. break
  726. if matched_sheet:
  727. logging.debug(f"Found matching sheet: {matched_sheet} for employee: {employee_name}")
  728. try:
  729. # 保存当前工作簿以便使用xlwings处理
  730. temp_output_path = os.path.join(self.export_dir, f"temp_{employee_name}.xlsx")
  731. output_wb.save(temp_output_path)
  732. # 使用xlwings处理文件 - 这样可以避免合并单元格的问题
  733. with xw.App(visible=False, add_book=False) as app:
  734. app.display_alerts = False
  735. app.screen_updating = False
  736. try:
  737. # 打开输入和输出文件
  738. input_wb_xw = app.books.open(input_file)
  739. output_wb_xw = app.books.open(temp_output_path)
  740. # 获取匹配的工作表
  741. src_sheet = None
  742. for sheet in input_wb_xw.sheets:
  743. if sheet.name == matched_sheet:
  744. src_sheet = sheet
  745. break
  746. if src_sheet:
  747. # 获取输出工作表
  748. dst_sheet = output_wb_xw.sheets[1]
  749. # 复制所有单元格的值 - 只复制数据
  750. used_range = src_sheet.used_range
  751. data_values = used_range.value # 获取所有值而不带格式
  752. # 确保data_values不为None
  753. if data_values:
  754. # 确保是二维数组
  755. if not isinstance(data_values, list):
  756. data_values = [[data_values]]
  757. elif not isinstance(data_values[0], list):
  758. data_values = [data_values]
  759. # 写入所有值
  760. dst_sheet.range('A1').value = data_values
  761. # 保存并关闭
  762. output_wb_xw.save()
  763. input_wb_xw.close()
  764. output_wb_xw.close()
  765. # 重新加载保存的文件
  766. output_wb = openpyxl.load_workbook(temp_output_path)
  767. # 删除临时文件
  768. if os.path.exists(temp_output_path):
  769. try:
  770. os.remove(temp_output_path)
  771. except:
  772. pass
  773. logging.debug(f"Successfully copied data from sheet {matched_sheet} using xlwings")
  774. except Exception as e:
  775. logging.error(f"Error in xlwings processing: {e}")
  776. # 如果xlwings失败,我们将尝试直接处理数据
  777. try:
  778. # 重新打开工作簿
  779. output_wb = openpyxl.load_workbook(template_path)
  780. if len(output_wb.worksheets) == 1:
  781. output_wb.create_sheet()
  782. output_sheet2 = output_wb.worksheets[1]
  783. output_sheet2.title = SHEET2_NAME_FORMAT.format(month=month)
  784. # 直接使用openpyxl
  785. src_sheet = input_wb[matched_sheet]
  786. # 获取所有单元格的值并只复制值
  787. max_row = src_sheet.max_row
  788. max_col = src_sheet.max_column
  789. for r in range(1, max_row + 1):
  790. for c in range(1, max_col + 1):
  791. src_cell = src_sheet.cell(row=r, column=c)
  792. # 判断是否为合并单元格
  793. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  794. # 对于合并单元格,跳过写入
  795. continue
  796. else:
  797. # 对于普通单元格,只复制值
  798. output_sheet2.cell(row=r, column=c).value = src_cell.value
  799. logging.debug(f"Successfully copied data from sheet {matched_sheet} using openpyxl fallback")
  800. except Exception as fallback_error:
  801. logging.error(f"Fallback error: {fallback_error}")
  802. raise
  803. except Exception as e:
  804. logging.error(f"Error copying sheet {matched_sheet} data: {e}")
  805. success = False
  806. else:
  807. logging.warning(f"No matching sheet found for employee: {employee_name}")
  808. except Exception as e:
  809. logging.error(f"Error setting up second sheet: {e}")
  810. success = False
  811. # 保存输出工作簿
  812. try:
  813. output_wb.save(output_path)
  814. logging.debug(f"File processed successfully for employee: {employee_name}")
  815. except Exception as e:
  816. logging.error(f"Error saving output file: {e}")
  817. success = False
  818. return success
  819. except Exception as e:
  820. logging.error(f"Error processing file: {e}")
  821. return False
  822. if __name__ == "__main__":
  823. root = tk.Tk()
  824. app = ExcelConverterApp(root)
  825. root.mainloop()