日本工资明细转换工具
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app.py 95KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052
  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() import os
  826. import tkinter as tk
  827. from tkinter import filedialog, ttk, messagebox
  828. import pandas as pd
  829. import shutil
  830. from datetime import datetime, timedelta
  831. import openpyxl
  832. from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
  833. from openpyxl.utils import get_column_letter
  834. import xlrd
  835. import logging
  836. import xlwings as xw
  837. from copy import copy
  838. import re
  839. # Import configuration
  840. from config import (
  841. TEMPLATE_PATH,
  842. CELL_MAPPINGS,
  843. CUSTOM_CELLS,
  844. FILENAME_CONFIG,
  845. SHEET2_NAME_FORMAT,
  846. OUTPUT_FILENAME_FORMAT,
  847. EMPLOYEE_INFO,
  848. EMPLOYEE_CONFIG_PATH,
  849. COMPANY_OPTIONS,
  850. BANK_OPTIONS,
  851. save_options
  852. )
  853. logging.basicConfig(level=logging.DEBUG)
  854. class ExcelConverterApp:
  855. def __init__(self, root):
  856. self.root = root
  857. self.root.title("工资明细表转换工具")
  858. self.root.geometry("950x600") # 减小整体窗口尺寸
  859. # 设置主题和样式
  860. self.style = ttk.Style()
  861. if 'winnative' in self.style.theme_names():
  862. self.style.theme_use('winnative')
  863. elif 'vista' in self.style.theme_names():
  864. self.style.theme_use('vista')
  865. elif 'xpnative' in self.style.theme_names():
  866. self.style.theme_use('xpnative')
  867. elif 'clam' in self.style.theme_names():
  868. self.style.theme_use('clam')
  869. # 设置简约现代配色
  870. self.primary_color = "#4A6D8C" # 主色调 - 沉稳的蓝灰色
  871. self.accent_color = "#DB4D6D" # 强调色 - 适当的红色用于警告
  872. self.text_color = "#333333" # 文本色 - 深灰色易于阅读
  873. self.light_bg = "#F8F9FA" # 背景色 - 淡灰色背景
  874. self.light_accent = "#E9ECEF" # 浅强调 - 用于分隔和高亮
  875. # 设置基本样式
  876. self.style.configure("TFrame", background=self.light_bg)
  877. self.style.configure("TLabel", font=("Microsoft YaHei UI", 10), background=self.light_bg, foreground=self.text_color)
  878. self.style.configure("TLabelframe", font=("Microsoft YaHei UI", 10), background=self.light_bg)
  879. self.style.configure("TLabelframe.Label", font=("Microsoft YaHei UI", 10), background=self.light_bg, foreground=self.text_color)
  880. # 设置Treeview样式 - 简约样式
  881. self.style.configure("Treeview",
  882. font=("Microsoft YaHei UI", 10),
  883. background="white",
  884. foreground=self.text_color,
  885. fieldbackground="white")
  886. self.style.configure("Treeview.Heading",
  887. font=("Microsoft YaHei UI", 10),
  888. background=self.light_accent)
  889. self.style.map('Treeview', background=[('selected', self.primary_color)], foreground=[('selected', 'white')])
  890. # Variables
  891. self.import_files = []
  892. self.export_dir = ""
  893. self.template_path = TEMPLATE_PATH
  894. # Create UI
  895. self.create_widgets()
  896. def create_widgets(self):
  897. # 设置根框架背景
  898. self.root.configure(background=self.light_bg)
  899. # 创建主框架 - 减少内边距
  900. main_frame = ttk.Frame(self.root, padding=5)
  901. main_frame.pack(fill="both", expand=True)
  902. # 导入文件区域 - 减少内边距
  903. import_frame = ttk.LabelFrame(main_frame, text="导入Excel文件", padding=5)
  904. import_frame.pack(fill="both", expand=True, pady=5)
  905. # 按钮区域
  906. button_frame = ttk.Frame(import_frame)
  907. button_frame.pack(fill="x", pady=(0, 5))
  908. import_btn = ttk.Button(button_frame, text="选择Excel文件", command=self.select_files)
  909. import_btn.pack(side="left", padx=3)
  910. # 添加文件数量显示
  911. self.file_count_label = ttk.Label(button_frame, text="已选择: 0 个文件")
  912. self.file_count_label.pack(side="left", padx=10)
  913. # 文件列表区域
  914. list_frame = ttk.Frame(import_frame)
  915. list_frame.pack(fill="both", expand=True)
  916. columns = ("filename", "employee", "company", "bank", "other")
  917. self.file_tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
  918. # 设置列标题
  919. self.file_tree.heading("filename", text="文件名")
  920. self.file_tree.heading("employee", text="员工姓名 (C3)")
  921. self.file_tree.heading("company", text="所属公司信息 (C2)")
  922. self.file_tree.heading("bank", text="转账银行信息 (B30)")
  923. self.file_tree.heading("other", text="日本日期 (F2)")
  924. # 设置列宽
  925. self.file_tree.column("filename", width=280)
  926. self.file_tree.column("employee", width=110)
  927. self.file_tree.column("company", width=170)
  928. self.file_tree.column("bank", width=170)
  929. self.file_tree.column("other", width=120)
  930. # 添加滚动条
  931. scrollbar_y = ttk.Scrollbar(list_frame, orient="vertical", command=self.file_tree.yview)
  932. scrollbar_x = ttk.Scrollbar(list_frame, orient="horizontal", command=self.file_tree.xview)
  933. self.file_tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
  934. # 放置树状视图和滚动条
  935. self.file_tree.grid(row=0, column=0, sticky="nsew")
  936. scrollbar_y.grid(row=0, column=1, sticky="ns")
  937. scrollbar_x.grid(row=1, column=0, sticky="ew")
  938. # 配置列表框架的权重
  939. list_frame.columnconfigure(0, weight=1)
  940. list_frame.rowconfigure(0, weight=1)
  941. # 注意:移除双击编辑功能,导入列表不可修改
  942. # self.file_tree.bind("<Double-1>", self.edit_item)
  943. # 下部区域布局 - 减少间隔
  944. bottom_frame = ttk.Frame(main_frame)
  945. bottom_frame.pack(fill="x", pady=5)
  946. # 左侧区域 - 导出设置
  947. left_frame = ttk.Frame(bottom_frame)
  948. left_frame.pack(side="left", fill="y")
  949. export_label = ttk.Label(left_frame, text="导出位置:")
  950. export_label.pack(side="left", padx=(0, 3))
  951. self.export_label = ttk.Label(left_frame, text="未选择", width=28)
  952. self.export_label.pack(side="left")
  953. export_btn = ttk.Button(left_frame, text="选择导出位置", command=self.select_export_dir)
  954. export_btn.pack(side="left", padx=3)
  955. # 右侧区域 - 功能按钮
  956. right_frame = ttk.Frame(bottom_frame)
  957. right_frame.pack(side="right")
  958. info_btn = ttk.Button(right_frame, text="信息维护", command=self.maintain_info)
  959. info_btn.pack(side="left", padx=3)
  960. convert_btn = ttk.Button(right_frame, text="开始转换", command=self.convert_files)
  961. convert_btn.pack(side="left", padx=3)
  962. # 添加状态栏
  963. status_frame = ttk.Frame(main_frame, relief="sunken")
  964. status_frame.pack(fill="x", side="bottom", pady=(3, 0))
  965. self.status_label = ttk.Label(status_frame, text="准备就绪", anchor="w")
  966. self.status_label.pack(side="left", padx=5)
  967. def select_files(self):
  968. files = filedialog.askopenfilenames(
  969. title="选择Excel文件",
  970. filetypes=[("Excel Files", "*.xlsx")]
  971. )
  972. if files:
  973. self.import_files = list(files)
  974. self.update_file_tree()
  975. def update_file_tree(self):
  976. # Clear tree
  977. for item in self.file_tree.get_children():
  978. self.file_tree.delete(item)
  979. # 更新文件计数
  980. self.file_count_label.config(text=f"已选择: {len(self.import_files)} 个文件")
  981. # 用于存储未匹配的员工姓名
  982. unmatch_employees = []
  983. # Add files to tree with employee name and Japanese date
  984. for file in self.import_files:
  985. try:
  986. # Get employee name from C3 cell
  987. filename = os.path.basename(file)
  988. employee_name = ""
  989. original_date = ""
  990. # Get data from Excel file
  991. wb = openpyxl.load_workbook(file)
  992. name_sheet = wb.worksheets[0]
  993. employee_name = name_sheet.cell(row=3, column=3).value or ""
  994. # 获取日期单元格的原始值,不进行格式转换
  995. date_sheet = wb.worksheets[1]
  996. original_date = date_sheet.cell(row=4, column=2).value or ""
  997. # 仅为提取年月信息做最小处理(用于文件名和工作表名称),不影响显示
  998. extracted_year = None
  999. extracted_month = None
  1000. try:
  1001. if isinstance(original_date, (int, float)):
  1002. # Excel日期数字格式
  1003. base_date = datetime(1899, 12, 30)
  1004. date_obj = base_date + timedelta(days=original_date)
  1005. extracted_year = date_obj.year
  1006. extracted_month = date_obj.month
  1007. elif isinstance(original_date, datetime):
  1008. # 日期对象
  1009. extracted_year = original_date.year
  1010. extracted_month = original_date.month
  1011. else:
  1012. # 字符串格式 - 尝试提取年月
  1013. date_str = str(original_date)
  1014. year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
  1015. if year_month_match:
  1016. extracted_year, extracted_month = map(int, year_month_match.groups())
  1017. except Exception as e:
  1018. logging.error(f"Error processing date: {e}")
  1019. # 生成令和年号日期格式
  1020. reiwa_date = ""
  1021. if extracted_year and extracted_month:
  1022. # 计算令和年号 (2019年为令和元年/1年)
  1023. reiwa_year = extracted_year - 2019 + 1
  1024. if reiwa_year > 0: # 只有2019年之后才使用令和年号
  1025. reiwa_date = f"令和{reiwa_year}年{extracted_month}月分"
  1026. # 默认值
  1027. company = "选择公司"
  1028. bank_info = "选择银行"
  1029. # 检查员工信息是否存在于员工信息维护中
  1030. employee_matched = False
  1031. if employee_name:
  1032. for emp_info in EMPLOYEE_INFO:
  1033. if emp_info.get("employee_name") == employee_name:
  1034. employee_matched = True
  1035. # 匹配到员工信息,更新公司和银行信息
  1036. company = emp_info.get("company_name", "")
  1037. bank_name = emp_info.get("bank_name", "")
  1038. branch_account = emp_info.get("branch_account", "")
  1039. bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
  1040. break
  1041. # 如果未匹配,添加到未匹配列表
  1042. if not employee_matched and employee_name not in unmatch_employees:
  1043. unmatch_employees.append(employee_name)
  1044. # 自动添加到文件树 - 在日本日期(F2)字段显示令和年月
  1045. self.file_tree.insert("", "end", values=(filename, employee_name, company, bank_info, reiwa_date or original_date))
  1046. except Exception as e:
  1047. logging.error(f"Error reading file {file}: {e}")
  1048. self.file_tree.insert("", "end", values=(filename, "", "选择公司", "选择银行", ""))
  1049. # 如果有未匹配的员工,显示警告消息
  1050. if unmatch_employees:
  1051. warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
  1052. warning_msg += "\n".join(unmatch_employees)
  1053. warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
  1054. messagebox.showwarning("员工信息不匹配", warning_msg)
  1055. # 标记未匹配的行
  1056. for item in self.file_tree.get_children():
  1057. values = self.file_tree.item(item, "values")
  1058. if values[1] in unmatch_employees:
  1059. self.file_tree.item(item, tags=("unmatch",))
  1060. # 设置未匹配样式(黄色背景)
  1061. self.file_tree.tag_configure("unmatch", background="#FFF9C4") # 浅黄色
  1062. # 设置状态提示
  1063. if self.import_files:
  1064. self.status_label.config(text="文件已加载,请配置转换选项", foreground=self.text_color)
  1065. else:
  1066. self.status_label.config(text="准备就绪", foreground="gray")
  1067. def select_export_dir(self):
  1068. directory = filedialog.askdirectory(title="选择导出目录")
  1069. if directory:
  1070. self.export_dir = directory
  1071. self.export_label.config(text=directory)
  1072. def maintain_info(self):
  1073. # 创建弹出窗口
  1074. popup = tk.Toplevel(self.root)
  1075. popup.title("员工信息维护")
  1076. popup.geometry("700x500")
  1077. popup.transient(self.root)
  1078. popup.grab_set()
  1079. # 标题
  1080. title_frame = ttk.Frame(popup, padding=5)
  1081. title_frame.pack(fill="x")
  1082. title_label = ttk.Label(title_frame, text="员工信息维护", font=("Microsoft YaHei UI", 12, "bold"))
  1083. title_label.pack(side="left")
  1084. # 创建表格视图
  1085. tree_frame = ttk.Frame(popup, padding=5)
  1086. tree_frame.pack(fill="both", expand=True)
  1087. columns = ("employee", "company", "bank", "account", "holder")
  1088. tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
  1089. # 设置列标题
  1090. tree.heading("employee", text="员工姓名")
  1091. tree.heading("company", text="所属公司")
  1092. tree.heading("bank", text="振込先金融機関")
  1093. tree.heading("account", text="口座番号")
  1094. tree.heading("holder", text="名義人")
  1095. # 设置列宽
  1096. tree.column("employee", width=100)
  1097. tree.column("company", width=120)
  1098. tree.column("bank", width=120)
  1099. tree.column("account", width=180)
  1100. tree.column("holder", width=120)
  1101. # 添加滚动条
  1102. vscrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
  1103. tree.configure(yscrollcommand=vscrollbar.set)
  1104. hscrollbar = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview)
  1105. tree.configure(xscrollcommand=hscrollbar.set)
  1106. tree.grid(row=0, column=0, sticky="nsew")
  1107. vscrollbar.grid(row=0, column=1, sticky="ns")
  1108. hscrollbar.grid(row=1, column=0, sticky="ew")
  1109. tree_frame.columnconfigure(0, weight=1)
  1110. tree_frame.rowconfigure(0, weight=1)
  1111. # 添加搜索功能
  1112. search_frame = ttk.Frame(popup, padding=5)
  1113. search_frame.pack(fill="x", pady=5)
  1114. search_label = ttk.Label(search_frame, text="搜索:")
  1115. search_label.pack(side="left", padx=5)
  1116. search_var = tk.StringVar()
  1117. search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
  1118. search_entry.pack(side="left", fill="x", expand=True, padx=5)
  1119. def search_info(*args):
  1120. query = search_var.get().lower()
  1121. tree.delete(*tree.get_children())
  1122. for info in EMPLOYEE_INFO:
  1123. # 在所有字段中搜索
  1124. search_in = (
  1125. info.get("employee_name", "").lower() +
  1126. info.get("company_name", "").lower() +
  1127. info.get("bank_name", "").lower() +
  1128. info.get("branch_account", "").lower() +
  1129. info.get("account_holder", "").lower()
  1130. )
  1131. if query in search_in:
  1132. tree.insert("", "end", values=(
  1133. info.get("employee_name", ""),
  1134. info.get("company_name", ""),
  1135. info.get("bank_name", ""),
  1136. info.get("branch_account", ""),
  1137. info.get("account_holder", "")
  1138. ))
  1139. search_var.trace("w", search_info)
  1140. # 添加按钮
  1141. btn_frame = ttk.Frame(popup, padding=5)
  1142. btn_frame.pack(fill="x", pady=5)
  1143. # 编辑功能
  1144. def edit_info():
  1145. selected = tree.selection()
  1146. if not selected:
  1147. messagebox.showinfo("提示", "请先选择一条记录")
  1148. return
  1149. selected_idx = tree.index(selected[0])
  1150. if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
  1151. messagebox.showerror("错误", "选择的记录无效")
  1152. return
  1153. edit_employee_info(EMPLOYEE_INFO[selected_idx])
  1154. # 新增功能
  1155. def add_info():
  1156. edit_employee_info()
  1157. # 删除功能
  1158. def delete_info():
  1159. selected = tree.selection()
  1160. if not selected:
  1161. messagebox.showinfo("提示", "请先选择一条记录")
  1162. return
  1163. selected_idx = tree.index(selected[0])
  1164. if selected_idx < 0 or selected_idx >= len(EMPLOYEE_INFO):
  1165. messagebox.showerror("错误", "选择的记录无效")
  1166. return
  1167. if messagebox.askyesno("确认", "确定要删除此记录吗?"):
  1168. del EMPLOYEE_INFO[selected_idx]
  1169. save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
  1170. refresh_tree()
  1171. # 刷新表格
  1172. def refresh_tree():
  1173. tree.delete(*tree.get_children())
  1174. for info in EMPLOYEE_INFO:
  1175. tree.insert("", "end", values=(
  1176. info.get("employee_name", ""),
  1177. info.get("company_name", ""),
  1178. info.get("bank_name", ""),
  1179. info.get("branch_account", ""),
  1180. info.get("account_holder", "")
  1181. ))
  1182. # 编辑员工信息
  1183. def edit_employee_info(info=None):
  1184. is_new = info is None
  1185. if is_new:
  1186. info = {
  1187. "employee_name": "",
  1188. "company_name": "",
  1189. "bank_name": "",
  1190. "branch_account": "",
  1191. "account_holder": ""
  1192. }
  1193. edit_popup = tk.Toplevel(popup)
  1194. edit_popup.title("编辑员工信息")
  1195. edit_popup.geometry("450x300")
  1196. edit_popup.transient(popup)
  1197. edit_popup.grab_set()
  1198. # 创建表单
  1199. form_frame = ttk.Frame(edit_popup, padding=10)
  1200. form_frame.pack(fill="both", expand=True)
  1201. # 员工姓名
  1202. name_label = ttk.Label(form_frame, text="员工姓名:")
  1203. name_label.grid(row=0, column=0, sticky="w", pady=5)
  1204. name_var = tk.StringVar(value=info.get("employee_name", ""))
  1205. name_entry = ttk.Entry(form_frame, textvariable=name_var, width=30)
  1206. name_entry.grid(row=0, column=1, sticky="ew", pady=5)
  1207. # 所属公司
  1208. company_label = ttk.Label(form_frame, text="所属公司:")
  1209. company_label.grid(row=1, column=0, sticky="w", pady=5)
  1210. company_var = tk.StringVar(value=info.get("company_name", ""))
  1211. company_entry = ttk.Entry(form_frame, textvariable=company_var, width=30)
  1212. company_entry.grid(row=1, column=1, sticky="ew", pady=5)
  1213. # 银行名称
  1214. bank_label = ttk.Label(form_frame, text="振込先金融機関:")
  1215. bank_label.grid(row=2, column=0, sticky="w", pady=5)
  1216. bank_var = tk.StringVar(value=info.get("bank_name", ""))
  1217. bank_entry = ttk.Entry(form_frame, textvariable=bank_var, width=30)
  1218. bank_entry.grid(row=2, column=1, sticky="ew", pady=5)
  1219. # 账户信息
  1220. account_label = ttk.Label(form_frame, text="口座番号:")
  1221. account_label.grid(row=3, column=0, sticky="w", pady=5)
  1222. account_var = tk.StringVar(value=info.get("branch_account", ""))
  1223. account_entry = ttk.Entry(form_frame, textvariable=account_var, width=30)
  1224. account_entry.grid(row=3, column=1, sticky="ew", pady=5)
  1225. # 账户持有人
  1226. holder_label = ttk.Label(form_frame, text="名義人:")
  1227. holder_label.grid(row=4, column=0, sticky="w", pady=5)
  1228. holder_var = tk.StringVar(value=info.get("account_holder", ""))
  1229. holder_entry = ttk.Entry(form_frame, textvariable=holder_var, width=30)
  1230. holder_entry.grid(row=4, column=1, sticky="ew", pady=5)
  1231. form_frame.columnconfigure(1, weight=1)
  1232. # 按钮区域
  1233. btn_frame = ttk.Frame(edit_popup, padding=10)
  1234. btn_frame.pack(fill="x")
  1235. def save_info():
  1236. # 验证
  1237. if not name_var.get().strip():
  1238. messagebox.showerror("错误", "员工姓名不能为空")
  1239. return
  1240. # 保存
  1241. new_info = {
  1242. "employee_name": name_var.get().strip(),
  1243. "company_name": company_var.get().strip(),
  1244. "bank_name": bank_var.get().strip(),
  1245. "branch_account": account_var.get().strip(),
  1246. "account_holder": holder_var.get().strip()
  1247. }
  1248. if is_new:
  1249. EMPLOYEE_INFO.append(new_info)
  1250. else:
  1251. idx = EMPLOYEE_INFO.index(info)
  1252. EMPLOYEE_INFO[idx] = new_info
  1253. save_options(EMPLOYEE_CONFIG_PATH, EMPLOYEE_INFO)
  1254. refresh_tree()
  1255. edit_popup.destroy()
  1256. save_btn = ttk.Button(btn_frame, text="保存", command=save_info)
  1257. save_btn.pack(side="right", padx=5)
  1258. cancel_btn = ttk.Button(btn_frame, text="取消", command=edit_popup.destroy)
  1259. cancel_btn.pack(side="right", padx=5)
  1260. name_entry.focus_set()
  1261. # 添加按钮
  1262. add_btn = ttk.Button(btn_frame, text="新增", command=add_info)
  1263. add_btn.pack(side="left", padx=5)
  1264. edit_btn = ttk.Button(btn_frame, text="编辑", command=edit_info)
  1265. edit_btn.pack(side="left", padx=5)
  1266. delete_btn = ttk.Button(btn_frame, text="删除", command=delete_info)
  1267. delete_btn.pack(side="left", padx=5)
  1268. close_btn = ttk.Button(btn_frame, text="关闭", command=popup.destroy)
  1269. close_btn.pack(side="right", padx=5)
  1270. # 初始化
  1271. refresh_tree()
  1272. search_entry.focus_set()
  1273. def convert_files(self):
  1274. if not self.import_files:
  1275. messagebox.showerror("错误", "请先选择要导入的Excel文件")
  1276. return
  1277. if not self.export_dir:
  1278. messagebox.showerror("错误", "请选择导出位置")
  1279. return
  1280. # Template file check
  1281. if not os.path.exists(self.template_path):
  1282. messagebox.showerror("错误", f"模板文件不存在: {self.template_path}")
  1283. return
  1284. # 检查是否有未匹配的员工
  1285. unmatch_employees = []
  1286. for item in self.file_tree.get_children():
  1287. values = self.file_tree.item(item, "values")
  1288. employee_name = values[1]
  1289. # 检查是否有匹配
  1290. if employee_name and not any(emp_info.get("employee_name") == employee_name for emp_info in EMPLOYEE_INFO):
  1291. unmatch_employees.append(employee_name)
  1292. # 如果有未匹配员工,提示用户先维护信息
  1293. if unmatch_employees:
  1294. warning_msg = "以下员工信息未在员工信息维护中找到匹配记录:\n"
  1295. warning_msg += "\n".join(unmatch_employees)
  1296. warning_msg += "\n\n请先在【信息维护】中添加这些员工信息,然后再进行转换。"
  1297. messagebox.showwarning("员工信息不匹配", warning_msg)
  1298. return
  1299. # 更新状态
  1300. self.status_label.config(text="正在转换文件...", foreground=self.accent_color)
  1301. self.root.update()
  1302. # Process each file
  1303. success_count = 0
  1304. error_count = 0
  1305. # 确保文件树和导入文件列表长度一致
  1306. tree_items = self.file_tree.get_children()
  1307. for i, file_path in enumerate(self.import_files):
  1308. try:
  1309. # 更新状态
  1310. self.status_label.config(text=f"正在处理: {os.path.basename(file_path)} ({i+1}/{len(self.import_files)})")
  1311. self.root.update()
  1312. # 检查索引是否有效
  1313. if i < len(tree_items):
  1314. item = tree_items[i]
  1315. values = self.file_tree.item(item, "values")
  1316. # 安全地获取值
  1317. filename = values[0] if len(values) > 0 else ""
  1318. employee_name = values[1] if len(values) > 1 else ""
  1319. company = values[2] if len(values) > 2 else ""
  1320. bank = values[3] if len(values) > 3 else ""
  1321. other_info = values[4] if len(values) > 4 else ""
  1322. # Process file
  1323. success = self.process_file(file_path, company, bank, other_info, employee_name)
  1324. if success:
  1325. success_count += 1
  1326. # 设置行的背景色为成功
  1327. self.file_tree.item(item, tags=("success",))
  1328. else:
  1329. error_count += 1
  1330. # 设置行的背景色为错误
  1331. self.file_tree.item(item, tags=("error",))
  1332. else:
  1333. # 文件树中没有对应的项,直接处理文件
  1334. success = self.process_file(file_path, "", "", "", "")
  1335. if success:
  1336. success_count += 1
  1337. else:
  1338. error_count += 1
  1339. except Exception as e:
  1340. error_count += 1
  1341. logging.error(f"处理文件 {os.path.basename(file_path)} 时出错: {str(e)}")
  1342. # 不显示对话框,只在状态栏更新
  1343. self.status_label.config(text=f"处理文件 {os.path.basename(file_path)} 时出错", foreground="red")
  1344. # 找到对应的项并设置错误标签
  1345. try:
  1346. if i < len(tree_items):
  1347. item = tree_items[i]
  1348. self.file_tree.item(item, tags=("error",))
  1349. except:
  1350. pass
  1351. # 设置Tag样式
  1352. self.file_tree.tag_configure("success", background="#E8F8F5") # 浅绿色
  1353. self.file_tree.tag_configure("error", background="#FADBD8") # 浅红色
  1354. # 更新最终状态
  1355. if error_count == 0:
  1356. self.status_label.config(text=f"转换完成! 成功处理 {success_count} 个文件", foreground=self.accent_color)
  1357. else:
  1358. self.status_label.config(text=f"转换结束, 成功: {success_count}, 失败: {error_count}", foreground="red")
  1359. # Show completion message
  1360. messagebox.showinfo("完成", f"成功转换 {success_count} 个文件到 {self.export_dir}")
  1361. def process_file(self, input_file, company, bank, other_info, employee_name=None):
  1362. try:
  1363. logging.debug(f"Processing file: {input_file}")
  1364. # Extract name and date
  1365. input_wb = openpyxl.load_workbook(input_file)
  1366. name_sheet = input_wb.worksheets[0]
  1367. name = employee_name or name_sheet.cell(row=3, column=3).value # Use provided name or C3 cell value
  1368. date_sheet = input_wb.worksheets[1]
  1369. date_value = date_sheet.cell(row=4, column=2).value # B4 cell for date - 只获取不修改
  1370. logging.debug(f"Extracted name: {name}")
  1371. logging.debug(f"Original date value: {date_value}")
  1372. # 只为了文件名和标题提取年月信息,不修改原始数据
  1373. # 提取年月用于文件名和第二个工作表标题
  1374. try:
  1375. # 使用不同方法尝试获取年月信息,但不修改原始单元格内容
  1376. extracted_year = None
  1377. extracted_month = None
  1378. if isinstance(date_value, (int, float)):
  1379. # Excel日期数字格式
  1380. base_date = datetime(1899, 12, 30)
  1381. date_obj = base_date + timedelta(days=date_value)
  1382. extracted_year = date_obj.year
  1383. extracted_month = date_obj.month
  1384. elif isinstance(date_value, datetime):
  1385. # 日期对象
  1386. extracted_year = date_value.year
  1387. extracted_month = date_value.month
  1388. else:
  1389. # 字符串格式 - 尝试提取年月
  1390. date_str = str(date_value)
  1391. # 尝试匹配 YYYY年M月 或 YYYY年M月D日
  1392. year_month_match = re.search(r'(\d{4})年(\d{1,2})月', date_str)
  1393. if year_month_match:
  1394. extracted_year, extracted_month = map(int, year_month_match.groups())
  1395. # 如果无法提取年月,使用当前日期
  1396. if extracted_year is None or extracted_month is None:
  1397. current_date = datetime.now()
  1398. extracted_year = current_date.year
  1399. extracted_month = current_date.month
  1400. logging.warning(f"Could not extract year/month from date value: {date_value}, using current date")
  1401. # 用于文件名和工作表标题的年月
  1402. year = extracted_year
  1403. month = extracted_month
  1404. except Exception as e:
  1405. # 如果提取失败,使用当前日期
  1406. current_date = datetime.now()
  1407. year = current_date.year
  1408. month = current_date.month
  1409. logging.error(f"Error extracting date: {e}, using current date")
  1410. logging.debug(f"Using year={year}, month={month} for filename and sheet title")
  1411. # Create output filename - 确保使用employee_name
  1412. output_filename = OUTPUT_FILENAME_FORMAT.format(year=year, month=f'{month:02}', name=name)
  1413. output_path = os.path.join(self.export_dir, output_filename)
  1414. logging.debug(f"Output path: {output_path}")
  1415. # Copy template file to output
  1416. shutil.copy(self.template_path, output_path)
  1417. # Open output file
  1418. output_wb = openpyxl.load_workbook(output_path)
  1419. # Apply cell mappings from config
  1420. for src, dst in CELL_MAPPINGS.items():
  1421. src_sheet_idx, src_row, src_col = src
  1422. dst_sheet_idx, dst_row, dst_col = dst
  1423. try:
  1424. src_sheet = input_wb.worksheets[src_sheet_idx]
  1425. # 处理合并单元格问题
  1426. cell = src_sheet.cell(row=src_row, column=src_col)
  1427. # 如果是合并单元格,获取主单元格的值
  1428. if isinstance(cell, openpyxl.cell.cell.MergedCell):
  1429. # 找到包含此单元格的合并区域
  1430. for merged_range in src_sheet.merged_cells.ranges:
  1431. if cell.coordinate in merged_range:
  1432. # 获取合并区域左上角单元格的值
  1433. top_left = merged_range.min_row, merged_range.min_col
  1434. src_value = src_sheet.cell(row=top_left[0], column=top_left[1]).value
  1435. break
  1436. else:
  1437. # 如果没有找到合并区域,设为空值
  1438. src_value = None
  1439. else:
  1440. # 正常单元格直接获取值
  1441. src_value = cell.value
  1442. dst_sheet = output_wb.worksheets[dst_sheet_idx]
  1443. dst_sheet.cell(row=dst_row, column=dst_col).value = src_value
  1444. except Exception as e:
  1445. # 只记录错误,继续处理下一个单元格
  1446. logging.error(f"Error copying cell from {src} to {dst}: {e}")
  1447. continue
  1448. # Set custom fields - 使用try/except包裹每个操作
  1449. try:
  1450. # 尝试查找匹配的员工信息
  1451. matched_info = None
  1452. for emp_info in EMPLOYEE_INFO:
  1453. if emp_info.get("employee_name") == name:
  1454. matched_info = emp_info
  1455. break
  1456. # 如果有匹配的员工信息,则使用员工信息中的公司
  1457. if matched_info and matched_info.get("company_name"):
  1458. company = matched_info.get("company_name")
  1459. sheet_idx, row, col = CUSTOM_CELLS["company"]
  1460. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = company
  1461. except Exception as e:
  1462. logging.error(f"Error setting company field: {e}")
  1463. # 计算转账日期(月份+1)
  1464. try:
  1465. transfer_month = month + 1
  1466. transfer_year = year
  1467. if transfer_month > 12:
  1468. transfer_month = 1
  1469. transfer_year += 1
  1470. # 计算令和年号
  1471. reiwa_year = transfer_year - 2019 + 1
  1472. transfer_date = f"令和{reiwa_year}年{transfer_month}月25日"
  1473. # 尝试寻找匹配的银行信息
  1474. bank_info = bank
  1475. # 根据员工姓名查找匹配的员工信息 - 使用EMPLOYEE_INFO
  1476. matched_info = None
  1477. for emp_info in EMPLOYEE_INFO:
  1478. if emp_info.get("employee_name") == name:
  1479. matched_info = emp_info
  1480. break
  1481. if matched_info:
  1482. # 使用匹配到的员工信息
  1483. bank_name = matched_info.get("bank_name", "")
  1484. branch_account = matched_info.get("branch_account", "")
  1485. # 使用新的格式
  1486. bank_info = f"振込先金融機関:{bank_name}\n口座番号:{branch_account}"
  1487. # 组合银行转账信息 - 使用新的格式,名義人始终使用employee_name
  1488. if name and transfer_date:
  1489. bank_info = f"{bank_info}\n名義人:{name}\n振込日:{transfer_date}\n※休日の場合は、翌営業日にお振込みします。"
  1490. sheet_idx, row, col = CUSTOM_CELLS["bank"]
  1491. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = bank_info
  1492. # 设置单元格自动换行
  1493. cell = output_wb.worksheets[sheet_idx].cell(row=row, column=col)
  1494. cell.alignment = Alignment(wrap_text=True, vertical='top')
  1495. except Exception as e:
  1496. logging.error(f"Error setting bank info: {e}")
  1497. try:
  1498. sheet_idx, row, col = CUSTOM_CELLS["other"]
  1499. # 生成令和年号日期格式
  1500. reiwa_date = ""
  1501. if year and month:
  1502. # 计算令和年号 (2019年为令和元年/1年)
  1503. reiwa_year = year - 2019 + 1
  1504. reiwa_date = f"令和{reiwa_year}年{month}月分"
  1505. # 设置F2单元格的值为令和日期格式
  1506. output_wb.worksheets[sheet_idx].cell(row=row, column=col).value = reiwa_date or other_info
  1507. except Exception as e:
  1508. logging.error(f"Error setting other info: {e}")
  1509. # Copy second sheet
  1510. try:
  1511. if len(output_wb.worksheets) == 1:
  1512. output_wb.create_sheet()
  1513. output_sheet2 = output_wb.worksheets[1]
  1514. output_sheet2.title = SHEET2_NAME_FORMAT.format(month=month)
  1515. # 先保存文件,以便后续使用xlwings处理
  1516. output_wb.save(output_path)
  1517. # 尝试使用xlwings做直接复制(对日期格式更可靠)
  1518. try:
  1519. # 使用更安全的xlwings调用方式
  1520. logging.debug("尝试使用xlwings复制第二个工作表...")
  1521. # 使用visible=True可能会解决某些权限问题
  1522. with xw.App(visible=False, add_book=False) as app:
  1523. # 禁用警告和事件可以提高稳定性
  1524. app.display_alerts = False
  1525. app.screen_updating = False
  1526. try:
  1527. # 打开工作簿
  1528. input_wb_xw = app.books.open(input_file)
  1529. output_wb_xw = app.books.open(output_path)
  1530. # 尝试完整复制第二个工作表内容
  1531. src_sheet = input_wb_xw.sheets[1]
  1532. dst_sheet = output_wb_xw.sheets[1]
  1533. # 复制包含单元格值和格式的范围
  1534. src_used = src_sheet.used_range
  1535. if src_used:
  1536. src_used.copy()
  1537. dst_sheet.range('A1').paste()
  1538. # 保存和关闭工作簿
  1539. output_wb_xw.save()
  1540. input_wb_xw.close()
  1541. output_wb_xw.close()
  1542. logging.debug("使用xlwings成功复制了第二个工作表")
  1543. return True # 如果xlwings成功,直接返回成功
  1544. except Exception as e:
  1545. logging.error(f"xlwings复制过程中发生错误: {e}")
  1546. # 关闭所有打开的工作簿
  1547. for book in app.books:
  1548. try:
  1549. book.close()
  1550. except:
  1551. pass
  1552. raise # 重新引发异常,将进入备选方案
  1553. except Exception as e:
  1554. logging.error(f"使用xlwings复制B4单元格失败: {e},将使用openpyxl备选方案")
  1555. # 如果xlwings失败,使用openpyxl作为备选方案继续处理
  1556. # 重新加载文件,因为我们之前已保存
  1557. output_wb = openpyxl.load_workbook(output_path)
  1558. output_sheet2 = output_wb.worksheets[1]
  1559. # 获取源工作表尺寸
  1560. max_row = date_sheet.max_row
  1561. max_col = date_sheet.max_column
  1562. # 优先复制所有合并单元格
  1563. for merged_range in date_sheet.merged_cells.ranges:
  1564. output_sheet2.merge_cells(str(merged_range))
  1565. # 改进的合并单元格处理方式
  1566. # 首先识别所有合并单元格的主单元格(左上角单元格)
  1567. merged_cell_masters = {}
  1568. for merged_range in date_sheet.merged_cells.ranges:
  1569. min_row, min_col = merged_range.min_row, merged_range.min_col
  1570. merged_cell_masters[(min_row, min_col)] = date_sheet.cell(row=min_row, column=min_col).value
  1571. # 逐个单元格复制
  1572. for row in range(1, max_row + 1):
  1573. for col in range(1, max_col + 1):
  1574. try:
  1575. # 获取源单元格
  1576. src_cell = date_sheet.cell(row=row, column=col)
  1577. dst_cell = output_sheet2.cell(row=row, column=col)
  1578. # 特殊处理B4单元格
  1579. if row == 4 and col == 2:
  1580. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  1581. # 如果B4是合并单元格,找到主单元格
  1582. for merged_range in date_sheet.merged_cells.ranges:
  1583. if src_cell.coordinate in merged_range:
  1584. min_row, min_col = merged_range.min_row, merged_range.min_col
  1585. main_cell = date_sheet.cell(row=min_row, column=min_col)
  1586. dst_cell.value = main_cell.value
  1587. # 复制主单元格格式
  1588. if hasattr(main_cell, 'number_format'):
  1589. dst_cell.number_format = main_cell.number_format
  1590. if hasattr(main_cell, 'font'):
  1591. dst_cell.font = copy(main_cell.font)
  1592. if hasattr(main_cell, 'border'):
  1593. dst_cell.border = copy(main_cell.border)
  1594. if hasattr(main_cell, 'fill'):
  1595. dst_cell.fill = copy(main_cell.fill)
  1596. if hasattr(main_cell, 'alignment'):
  1597. dst_cell.alignment = copy(main_cell.alignment)
  1598. logging.debug(f"B4是合并单元格,已复制主单元格值={main_cell.value}")
  1599. break
  1600. else:
  1601. # 普通单元格处理
  1602. dst_cell.value = src_cell.value
  1603. # 对于日期类型,特别处理
  1604. from datetime import date
  1605. if isinstance(src_cell.value, (datetime, date)):
  1606. if hasattr(src_cell, 'number_format'):
  1607. dst_cell.number_format = src_cell.number_format
  1608. else:
  1609. # 设置通用日期格式
  1610. dst_cell.number_format = "yyyy/mm/dd"
  1611. # 复制格式属性
  1612. if hasattr(src_cell, 'font'):
  1613. dst_cell.font = copy(src_cell.font)
  1614. if hasattr(src_cell, 'border'):
  1615. dst_cell.border = copy(src_cell.border)
  1616. if hasattr(src_cell, 'fill'):
  1617. dst_cell.fill = copy(src_cell.fill)
  1618. if hasattr(src_cell, 'alignment'):
  1619. dst_cell.alignment = copy(src_cell.alignment)
  1620. if hasattr(src_cell, 'number_format'):
  1621. dst_cell.number_format = src_cell.number_format
  1622. logging.debug(f"B4是普通单元格,已复制值={src_cell.value}, 类型={type(src_cell.value)}")
  1623. else:
  1624. # 一般单元格处理 - 区分合并单元格与普通单元格
  1625. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  1626. # 对于合并单元格中的从属单元格,值保持为None
  1627. dst_cell.value = None
  1628. else:
  1629. # 普通单元格的值直接复制
  1630. dst_cell.value = src_cell.value
  1631. # 复制格式
  1632. if hasattr(src_cell, 'font'):
  1633. dst_cell.font = copy(src_cell.font)
  1634. if hasattr(src_cell, 'border'):
  1635. dst_cell.border = copy(src_cell.border)
  1636. if hasattr(src_cell, 'fill'):
  1637. dst_cell.fill = copy(src_cell.fill)
  1638. if hasattr(src_cell, 'alignment'):
  1639. dst_cell.alignment = copy(src_cell.alignment)
  1640. if hasattr(src_cell, 'number_format'):
  1641. dst_cell.number_format = src_cell.number_format
  1642. continue
  1643. # 处理合并单元格
  1644. if isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  1645. # 对于合并单元格,找到合并区域的左上角主单元格的值
  1646. cell_value = None
  1647. for merged_range in date_sheet.merged_cells.ranges:
  1648. if src_cell.coordinate in merged_range:
  1649. top_left = merged_range.min_row, merged_range.min_col
  1650. cell_value = date_sheet.cell(row=top_left[0], column=top_left[1]).value
  1651. break
  1652. else:
  1653. # 普通单元格直接读取值
  1654. cell_value = src_cell.value
  1655. # 设置目标单元格值
  1656. output_sheet2.cell(row=row, column=col).value = cell_value
  1657. # 复制单元格格式(如果源单元格不是合并单元格)
  1658. if not isinstance(src_cell, openpyxl.cell.cell.MergedCell):
  1659. if hasattr(src_cell, 'has_style') and src_cell.has_style:
  1660. output_cell = output_sheet2.cell(row=row, column=col)
  1661. try:
  1662. output_cell.font = copy(src_cell.font)
  1663. output_cell.border = copy(src_cell.border)
  1664. output_cell.fill = copy(src_cell.fill)
  1665. output_cell.number_format = src_cell.number_format
  1666. output_cell.protection = copy(src_cell.protection)
  1667. output_cell.alignment = copy(src_cell.alignment)
  1668. except:
  1669. # 忽略样式复制错误
  1670. pass
  1671. except Exception as e:
  1672. # 更加详细的错误记录
  1673. logging.warning(f"复制单元格 row={row}, col={col} 时出错: {e}", exc_info=True)
  1674. continue
  1675. except Exception as e:
  1676. logging.error(f"复制第二个工作表时出错: {e}", exc_info=True)
  1677. return False
  1678. except Exception as e:
  1679. logging.error(f"Error copying second sheet: {e}")
  1680. # Save output workbook
  1681. try:
  1682. output_wb.save(output_path)
  1683. logging.debug("File processed successfully")
  1684. return True
  1685. except Exception as e:
  1686. logging.error(f"Error saving output file: {e}")
  1687. return False
  1688. except Exception as e:
  1689. logging.error(f"Error processing file: {e}")
  1690. # 不显示消息框,只记录错误
  1691. return False
  1692. if __name__ == "__main__":
  1693. root = tk.Tk()
  1694. app = ExcelConverterApp(root)
  1695. root.mainloop()