app.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import tkinter as tk
  2. from tkinter import filedialog, messagebox
  3. import pandas as pd
  4. from openpyxl import load_workbook
  5. import os
  6. import sys
  7. import re
  8. from collections import Counter
  9. class ExcelMapperApp:
  10. def __init__(self, root):
  11. self.root = root
  12. self.root.title("Excel 自动填表工具")
  13. self.root.geometry("750x550")
  14. self.root.resizable(True, True)
  15. if getattr(sys, 'frozen', False):
  16. self.base_dir = os.path.dirname(sys.executable)
  17. else:
  18. self.base_dir = os.path.dirname(os.path.abspath(__file__))
  19. self.source_file = tk.StringVar()
  20. self.header_row = tk.StringVar(value="3")
  21. self.source_header_row = tk.StringVar(value="2")
  22. self.df = None
  23. self.status_text = tk.StringVar(value="请选择来源Excel文件")
  24. self.setup_ui()
  25. def setup_ui(self):
  26. title_label = tk.Label(self.root, text="Excel 自动填表工具", font=("Arial", 16, "bold"))
  27. title_label.pack(pady=10)
  28. file_frame = tk.LabelFrame(self.root, text="文件选择", font=("Arial", 10, "bold"))
  29. file_frame.pack(pady=10, padx=20, fill="x")
  30. tk.Label(file_frame, text="来源数据Excel:").grid(row=0, column=0, sticky="w", pady=5)
  31. tk.Entry(file_frame, textvariable=self.source_file, width=50).grid(row=0, column=1, padx=5, pady=5)
  32. tk.Button(file_frame, text="浏览", command=self.select_source_file).grid(row=0, column=2, pady=5)
  33. tk.Label(file_frame, text="来源表头行号(横向格式用):").grid(row=1, column=0, sticky="w", pady=5)
  34. tk.Entry(file_frame, textvariable=self.source_header_row, width=10).grid(row=1, column=1, sticky="w", padx=5, pady=5)
  35. tk.Label(file_frame, text="(第2行是字段名)", fg="gray").grid(row=1, column=1, sticky="e", padx=5, pady=5)
  36. tk.Label(file_frame, text="模板表头行号:").grid(row=2, column=0, sticky="w", pady=5)
  37. tk.Entry(file_frame, textvariable=self.header_row, width=10).grid(row=2, column=1, sticky="w", padx=5, pady=5)
  38. tk.Label(file_frame, text="(第3行是字段名)", fg="gray").grid(row=2, column=1, sticky="e", padx=5, pady=5)
  39. template_path = os.path.join(self.base_dir, "模板.xlsx")
  40. output_path = os.path.join(self.base_dir, "输出.xlsx")
  41. tk.Label(file_frame, text=f"目标模板: {template_path}", fg="blue").grid(row=3, column=0, columnspan=3, sticky="w", pady=3)
  42. tk.Label(file_frame, text=f"输出文件: {output_path}", fg="green").grid(row=4, column=0, columnspan=3, sticky="w", pady=3)
  43. btn_frame = tk.Frame(self.root)
  44. btn_frame.pack(pady=10)
  45. tk.Button(btn_frame, text="🔍 查看来源结构", command=self.inspect_source,
  46. bg="#9C27B0", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  47. tk.Button(btn_frame, text="🚀 开始生成", command=self.generate_output,
  48. bg="#4CAF50", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  49. tk.Button(btn_frame, text="清空日志", command=self.clear_log,
  50. bg="#f44336", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  51. log_frame = tk.LabelFrame(self.root, text="运行日志", font=("Arial", 10, "bold"))
  52. log_frame.pack(pady=10, padx=20, fill="both", expand=True)
  53. self.log_text = tk.Text(log_frame, height=14, font=("Courier", 9))
  54. self.log_text.pack(side="left", fill="both", expand=True)
  55. scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
  56. scrollbar.pack(side="right", fill="y")
  57. self.log_text.config(yscrollcommand=scrollbar.set)
  58. status_bar = tk.Label(self.root, textvariable=self.status_text, relief="sunken", anchor="w",
  59. font=("Arial", 9), bg="#f0f0f0")
  60. status_bar.pack(side="bottom", fill="x")
  61. def log(self, msg):
  62. self.log_text.insert(tk.END, msg + "\n")
  63. self.log_text.see(tk.END)
  64. self.root.update()
  65. print(msg)
  66. def clear_log(self):
  67. self.log_text.delete(1.0, tk.END)
  68. def select_source_file(self):
  69. file_path = filedialog.askopenfilename(
  70. title="选择来源Excel文件",
  71. filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
  72. )
  73. if file_path:
  74. self.source_file.set(file_path)
  75. self.status_text.set(f"已选择: {os.path.basename(file_path)}")
  76. self.log(f"📁 已选择来源文件: {file_path}")
  77. def inspect_source(self):
  78. """检测并显示来源格式"""
  79. source_path = self.source_file.get()
  80. if not source_path:
  81. messagebox.showwarning("警告", "请先选择来源Excel文件!")
  82. return
  83. self.log("\n" + "="*60)
  84. self.log("🔍 分析来源Excel结构...")
  85. try:
  86. wb = load_workbook(source_path)
  87. ws = wb.active
  88. self.log(f"📊 共 {ws.max_row} 行, {ws.max_column} 列")
  89. # 判断是横向还是纵向
  90. first_row_vals = []
  91. for col in range(1, min(20, ws.max_column + 1)):
  92. val = ws.cell(row=1, column=col).value
  93. if val:
  94. first_row_vals.append(str(val))
  95. is_horizontal = len(first_row_vals) >= 3 and not any(':' in v for v in first_row_vals[:3])
  96. if is_horizontal:
  97. self.log(f"\n📌 检测到:横向格式(多列表格)")
  98. self.log(f" 第1行有 {len(first_row_vals)} 个有值单元格")
  99. self.log(f" 前几个: {first_row_vals[:5]}")
  100. # 显示第2行列名
  101. row2_vals = []
  102. for col in range(1, min(30, ws.max_column + 1)):
  103. val = ws.cell(row=2, column=col).value
  104. if val:
  105. row2_vals.append(str(val))
  106. if row2_vals:
  107. self.log(f"\n📌 第2行(列名): {row2_vals[:10]}...")
  108. else:
  109. self.log(f"\n📌 检测到:纵向格式(从上往下排列)")
  110. # 显示前20行
  111. self.log(f"\n📌 前20行内容:")
  112. for row in range(1, min(21, ws.max_row + 1)):
  113. val = ws.cell(row=row, column=1).value
  114. if val:
  115. self.log(f" 第{row}行: {str(val)[:80]}")
  116. except Exception as e:
  117. self.log(f"❌ 分析失败: {str(e)}")
  118. def parse_vertical_format(self, ws):
  119. """解析纵向格式(Key: Value 格式),相同Item Code合并"""
  120. products_dict = {} # 用字典存储,key是Item Code
  121. current_item_code = None
  122. current_product = {}
  123. field_map = {
  124. "Item Code": "Item Code",
  125. "产品名称": "产品名称",
  126. "颜色": "颜色",
  127. "材质": "材质",
  128. "组装长度 (英寸)": "组装长度(英寸)",
  129. "组装宽度 (英寸)": "组装宽度(英寸)",
  130. "组装高度 (英寸)": "组装高度(英寸)",
  131. "产品重量 (磅)": "产品重量(磅)",
  132. "长度 (英寸)": "包装尺寸-长度(英寸)",
  133. "宽度 (英寸)": "包装尺寸-宽度(英寸)",
  134. "高度 (英寸)": "包装尺寸-高度(英寸)",
  135. "重量 (磅)": "包装尺寸-重量(磅)",
  136. }
  137. price_pattern = re.compile(r'^\$?([\d.]+)$')
  138. for row in range(1, ws.max_row + 1):
  139. val = ws.cell(row=row, column=1).value
  140. if val is None:
  141. continue
  142. val_str = str(val).strip()
  143. if not val_str:
  144. continue
  145. # 检测是否是 "Key: Value" 格式
  146. if ':' in val_str:
  147. parts = val_str.split(':', 1)
  148. key = parts[0].strip()
  149. value = parts[1].strip() if len(parts) > 1 else ''
  150. if key == "Item Code":
  151. # 保存当前产品到字典
  152. if current_item_code and current_product:
  153. if current_item_code in products_dict:
  154. # 合并(不覆盖已有字段)
  155. for k, v in current_product.items():
  156. if k not in products_dict[current_item_code] or not products_dict[current_item_code][k]:
  157. products_dict[current_item_code][k] = v
  158. else:
  159. products_dict[current_item_code] = current_product.copy()
  160. # 开始新产品
  161. current_item_code = value
  162. current_product = {"Item Code": value}
  163. else:
  164. mapped_key = field_map.get(key, key)
  165. # 只有当前产品中没有该字段时才设置(保留第一次出现的值)
  166. if mapped_key not in current_product or not current_product[mapped_key]:
  167. current_product[mapped_key] = value
  168. else:
  169. # 没有冒号,可能是价格
  170. match = price_pattern.match(val_str.replace('$', '').strip())
  171. if match and current_item_code:
  172. price_val = match.group(1)
  173. # 判断是 Base Cost 还是 MSRP
  174. if current_item_code in products_dict:
  175. existing = products_dict[current_item_code]
  176. if "优惠单价" not in existing or not existing["优惠单价"]:
  177. existing["优惠单价"] = price_val
  178. elif "Manufacturer Suggested Retail Price" not in existing or not existing["Manufacturer Suggested Retail Price"]:
  179. existing["Manufacturer Suggested Retail Price"] = str(float(price_val) + 70)
  180. else:
  181. if "优惠单价" not in current_product or not current_product["优惠单价"]:
  182. current_product["优惠单价"] = price_val
  183. elif "Manufacturer Suggested Retail Price" not in current_product or not current_product["Manufacturer Suggested Retail Price"]:
  184. current_product["Manufacturer Suggested Retail Price"] = str(float(price_val) + 70)
  185. # 保存最后一个产品
  186. if current_item_code and current_product:
  187. if current_item_code in products_dict:
  188. for k, v in current_product.items():
  189. if k not in products_dict[current_item_code] or not products_dict[current_item_code][k]:
  190. products_dict[current_item_code][k] = v
  191. else:
  192. products_dict[current_item_code] = current_product
  193. # 转换为列表
  194. return list(products_dict.values())
  195. def generate_output(self):
  196. self.log("\n" + "="*60)
  197. self.log("🚀 开始处理...")
  198. source_path = self.source_file.get()
  199. template_path = os.path.join(self.base_dir, "模板.xlsx")
  200. if not source_path:
  201. messagebox.showwarning("警告", "请选择来源Excel文件!")
  202. return
  203. if not os.path.exists(template_path):
  204. self.log(f"❌ 找不到模板文件: {template_path}")
  205. messagebox.showerror("错误", f"找不到模板文件:\n{template_path}")
  206. return
  207. try:
  208. header_row = int(self.header_row.get())
  209. source_header = int(self.source_header_row.get())
  210. # ========== 1. 检测并读取来源数据 ==========
  211. wb_source = load_workbook(source_path)
  212. ws_source = wb_source.active
  213. # 检测格式
  214. first_row_vals = []
  215. for col in range(1, min(20, ws_source.max_column + 1)):
  216. val = ws_source.cell(row=1, column=col).value
  217. if val:
  218. first_row_vals.append(str(val))
  219. is_horizontal = len(first_row_vals) >= 3 and not any(':' in v for v in first_row_vals[:3])
  220. if is_horizontal:
  221. self.log(f"📌 检测到横向格式,使用 pandas 读取...")
  222. self.df = pd.read_excel(source_path, header=source_header - 1)
  223. self.log(f"✅ 成功读取 {len(self.df)} 行数据")
  224. else:
  225. self.log(f"📌 检测到纵向格式,使用解析器读取...")
  226. products = self.parse_vertical_format(ws_source)
  227. self.log(f"✅ 成功解析 {len(products)} 个产品")
  228. self.df = pd.DataFrame(products)
  229. self.log(f"📋 解析到的字段: {list(self.df.columns)}")
  230. # ========== 2. 加载模板(直接修改原文件) ==========
  231. self.log(f"\n📖 加载模板: {template_path}")
  232. wb = load_workbook(template_path)
  233. ws = wb.active
  234. # ========== 3. 找到已有数据的最后一行 ==========
  235. # 从第4行开始找,找到第一个完全空的行
  236. last_row = 3 # 从第4行开始检查
  237. for row in range(4, ws.max_row + 2):
  238. is_empty = True
  239. for col in range(1, min(20, ws.max_column + 1)): # 检查前20列
  240. if ws.cell(row=row, column=col).value is not None:
  241. is_empty = False
  242. break
  243. if is_empty:
  244. last_row = row
  245. break
  246. else:
  247. last_row = ws.max_row + 1
  248. self.log(f"📌 模板中已有数据到第 {last_row - 1} 行,从第 {last_row} 行开始追加")
  249. # ========== 4. 读取模板表头 ==========
  250. col_indices = {}
  251. for col in range(1, ws.max_column + 1):
  252. cell_value = ws.cell(row=header_row, column=col).value
  253. if cell_value:
  254. col_name = str(cell_value).strip()
  255. if col_name and not col_name.startswith("Unnamed"):
  256. col_indices[col_name] = col
  257. # ========== 5. 目标列映射 ==========
  258. target_cols = [
  259. "Supplier Part Number", "Manufacturer Part Number", "Base Cost",
  260. "Manufacturer Suggested Retail Price", "Product Weight",
  261. "Carton Weight 1", "Carton Height 1", "Carton Width 1", "Carton Depth 1",
  262. "Base Color", "Base Material", "Color", "Material",
  263. "Overall Depth - Front to Back", "Overall Height - Top to Bottom",
  264. "Overall Product Weight", "Overall Width - Side to Side",
  265. "Top Color", "Top Material"
  266. ]
  267. found_cols = {}
  268. for target in target_cols:
  269. if target in col_indices:
  270. found_cols[target] = col_indices[target]
  271. if not found_cols:
  272. self.log(f"\n❌ 没有找到任何目标列!")
  273. return
  274. # ========== 6. 匹配来源列 ==========
  275. source_mapping = {
  276. "Item Code": "Item Code",
  277. "产品名称": "产品名称",
  278. "优惠单价": "优惠单价",
  279. "产品重量": "产品重量(磅)",
  280. "包装尺寸-重量": "包装尺寸-重量(磅)",
  281. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  282. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  283. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  284. "颜色": "颜色",
  285. "材质": "材质",
  286. "组装长度": "组装长度(英寸)",
  287. "组装高度": "组装高度(英寸)",
  288. "组装宽度": "组装宽度(英寸)",
  289. }
  290. source_map = {}
  291. for key, expected in source_mapping.items():
  292. found = False
  293. for col in self.df.columns:
  294. if col == expected:
  295. source_map[key] = col
  296. found = True
  297. break
  298. if not found:
  299. # 部分匹配
  300. for col in self.df.columns:
  301. if expected in col or col in expected:
  302. source_map[key] = col
  303. found = True
  304. break
  305. if not found:
  306. source_map[key] = None
  307. # ========== 7. 填充数据 ==========
  308. def get_key(row):
  309. item = row.get("Item Code", "")
  310. name = row.get("产品名称", "")
  311. return f"{item}_{name}"
  312. keys = [get_key(row) for _, row in self.df.iterrows()]
  313. key_count = Counter(keys)
  314. key_index = Counter()
  315. row_num = last_row
  316. filled_count = 0
  317. self.log(f"\n✍️ 开始追加数据...")
  318. for idx, (_, src_row) in enumerate(self.df.iterrows()):
  319. item_code = src_row.get("Item Code", "")
  320. product_name = src_row.get("产品名称", "")
  321. if pd.isna(item_code):
  322. item_code = ""
  323. if pd.isna(product_name):
  324. product_name = ""
  325. key = get_key(src_row)
  326. if key_count[key] > 1:
  327. key_index[key] += 1
  328. suffix = f".{key_index[key] - 1}"
  329. mfr_suffix = f".{key_index[key]}"
  330. else:
  331. suffix = ""
  332. mfr_suffix = ""
  333. self.log(f"\n--- 追加第 {idx+1}/{len(self.df)} 个产品 ---")
  334. self.log(f" Item Code: '{item_code}'")
  335. self.log(f" 产品名称: '{str(product_name)[:50]}...'")
  336. fill_count = 0
  337. # Supplier Part Number
  338. if "Supplier Part Number" in found_cols:
  339. val = f"{item_code}{product_name}{suffix}"
  340. ws.cell(row=row_num, column=found_cols["Supplier Part Number"], value=val)
  341. fill_count += 1
  342. # Manufacturer Part Number
  343. if "Manufacturer Part Number" in found_cols:
  344. val = f"{item_code}{product_name}{mfr_suffix}"
  345. ws.cell(row=row_num, column=found_cols["Manufacturer Part Number"], value=val)
  346. fill_count += 1
  347. # Base Cost
  348. if "Base Cost" in found_cols:
  349. col = source_map.get("优惠单价")
  350. if col and col in src_row:
  351. val = src_row[col]
  352. if pd.notna(val):
  353. ws.cell(row=row_num, column=found_cols["Base Cost"], value=val)
  354. fill_count += 1
  355. # MSRP
  356. if "Manufacturer Suggested Retail Price" in found_cols:
  357. col = source_map.get("优惠单价")
  358. if col and col in src_row:
  359. val = src_row[col]
  360. try:
  361. num = float(val) if pd.notna(val) else 0
  362. result = num + 70
  363. ws.cell(row=row_num, column=found_cols["Manufacturer Suggested Retail Price"], value=result)
  364. fill_count += 1
  365. except:
  366. pass
  367. else:
  368. msrp_col = source_map.get("Manufacturer Suggested Retail Price")
  369. if msrp_col and msrp_col in src_row:
  370. val = src_row[msrp_col]
  371. if pd.notna(val):
  372. ws.cell(row=row_num, column=found_cols["Manufacturer Suggested Retail Price"], value=val)
  373. fill_count += 1
  374. # Product Weight
  375. if "Product Weight" in found_cols:
  376. col = source_map.get("产品重量")
  377. if col and col in src_row:
  378. val = src_row[col]
  379. if pd.notna(val):
  380. ws.cell(row=row_num, column=found_cols["Product Weight"], value=val)
  381. fill_count += 1
  382. # Carton
  383. carton_map = [
  384. ("Carton Weight 1", "包装尺寸-重量"),
  385. ("Carton Height 1", "包装尺寸-高度"),
  386. ("Carton Width 1", "包装尺寸-宽度"),
  387. ("Carton Depth 1", "包装尺寸-长度"),
  388. ]
  389. for target, src_name in carton_map:
  390. if target in found_cols:
  391. col = source_map.get(src_name)
  392. if col and col in src_row:
  393. val = src_row[col]
  394. if pd.notna(val):
  395. ws.cell(row=row_num, column=found_cols[target], value=val)
  396. fill_count += 1
  397. # Color/Material
  398. cm_map = [
  399. ("Base Color", "颜色"),
  400. ("Base Material", "材质"),
  401. ("Color", "颜色"),
  402. ("Material", "材质"),
  403. ("Top Color", "颜色"),
  404. ("Top Material", "材质"),
  405. ]
  406. for target, src_name in cm_map:
  407. if target in found_cols:
  408. col = source_map.get(src_name)
  409. if col and col in src_row:
  410. val = src_row[col]
  411. if pd.notna(val):
  412. ws.cell(row=row_num, column=found_cols[target], value=val)
  413. fill_count += 1
  414. # Overall
  415. overall_map = [
  416. ("Overall Depth - Front to Back", "组装长度"),
  417. ("Overall Height - Top to Bottom", "组装高度"),
  418. ("Overall Product Weight", "产品重量"),
  419. ("Overall Width - Side to Side", "组装宽度"),
  420. ]
  421. for target, src_name in overall_map:
  422. if target in found_cols:
  423. col = source_map.get(src_name)
  424. if col and col in src_row:
  425. val = src_row[col]
  426. if pd.notna(val):
  427. ws.cell(row=row_num, column=found_cols[target], value=val)
  428. fill_count += 1
  429. self.log(f" 📊 本行填充 {fill_count} 个字段")
  430. row_num += 1
  431. filled_count += 1
  432. # ========== 8. 直接保存到模板文件 ==========
  433. wb.save(template_path)
  434. self.log(f"\n✅ 成功追加到模板文件: {template_path}")
  435. self.log(f"📊 共追加 {filled_count} 行数据")
  436. self.status_text.set(f"✅ 已追加 {filled_count} 行到模板")
  437. messagebox.showinfo("成功", f"已追加 {filled_count} 行数据到模板文件:\n{template_path}")
  438. except Exception as e:
  439. self.log(f"\n❌ 错误: {str(e)}")
  440. import traceback
  441. self.log(traceback.format_exc())
  442. messagebox.showerror("错误", f"生成失败:\n{str(e)}")
  443. if __name__ == "__main__":
  444. root = tk.Tk()
  445. app = ExcelMapperApp(root)
  446. root.mainloop()