|
|
@@ -1,17 +1,18 @@
|
|
|
import tkinter as tk
|
|
|
-from tkinter import filedialog, messagebox
|
|
|
+from tkinter import filedialog, messagebox, ttk
|
|
|
import pandas as pd
|
|
|
from openpyxl import load_workbook
|
|
|
import os
|
|
|
import sys
|
|
|
-import re
|
|
|
+import shutil
|
|
|
from collections import Counter
|
|
|
+from datetime import datetime
|
|
|
|
|
|
class ExcelMapperApp:
|
|
|
def __init__(self, root):
|
|
|
self.root = root
|
|
|
self.root.title("Excel 自动填表工具")
|
|
|
- self.root.geometry("750x550")
|
|
|
+ self.root.geometry("800x600")
|
|
|
self.root.resizable(True, True)
|
|
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
@@ -20,9 +21,10 @@ class ExcelMapperApp:
|
|
|
self.base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
self.source_file = tk.StringVar()
|
|
|
- self.header_row = tk.StringVar(value="3")
|
|
|
+ self.header_row = tk.StringVar(value="4")
|
|
|
self.source_header_row = tk.StringVar(value="2")
|
|
|
self.df = None
|
|
|
+ self.valid_values = {} # 存储 Valid Values 工作表的数据
|
|
|
self.status_text = tk.StringVar(value="请选择来源Excel文件")
|
|
|
|
|
|
self.setup_ui()
|
|
|
@@ -38,24 +40,25 @@ class ExcelMapperApp:
|
|
|
tk.Entry(file_frame, textvariable=self.source_file, width=50).grid(row=0, column=1, padx=5, pady=5)
|
|
|
tk.Button(file_frame, text="浏览", command=self.select_source_file).grid(row=0, column=2, pady=5)
|
|
|
|
|
|
- tk.Label(file_frame, text="来源表头行号(横向格式用):").grid(row=1, column=0, sticky="w", pady=5)
|
|
|
+ tk.Label(file_frame, text="来源表头行号:").grid(row=1, column=0, sticky="w", pady=5)
|
|
|
tk.Entry(file_frame, textvariable=self.source_header_row, width=10).grid(row=1, column=1, sticky="w", padx=5, pady=5)
|
|
|
tk.Label(file_frame, text="(第2行是字段名)", fg="gray").grid(row=1, column=1, sticky="e", padx=5, pady=5)
|
|
|
|
|
|
tk.Label(file_frame, text="模板表头行号:").grid(row=2, column=0, sticky="w", pady=5)
|
|
|
tk.Entry(file_frame, textvariable=self.header_row, width=10).grid(row=2, column=1, sticky="w", padx=5, pady=5)
|
|
|
- tk.Label(file_frame, text="(第3行是字段名)", fg="gray").grid(row=2, column=1, sticky="e", padx=5, pady=5)
|
|
|
+ tk.Label(file_frame, text="(第4行是字段名)", fg="gray").grid(row=2, column=1, sticky="e", padx=5, pady=5)
|
|
|
|
|
|
template_path = os.path.join(self.base_dir, "模板.xlsx")
|
|
|
- output_path = os.path.join(self.base_dir, "输出.xlsx")
|
|
|
tk.Label(file_frame, text=f"目标模板: {template_path}", fg="blue").grid(row=3, column=0, columnspan=3, sticky="w", pady=3)
|
|
|
- tk.Label(file_frame, text=f"输出文件: {output_path}", fg="green").grid(row=4, column=0, columnspan=3, sticky="w", pady=3)
|
|
|
+ tk.Label(file_frame, text="(数据将直接追加到模板文件末尾)", fg="green").grid(row=4, column=0, columnspan=3, sticky="w", pady=3)
|
|
|
|
|
|
btn_frame = tk.Frame(self.root)
|
|
|
btn_frame.pack(pady=10)
|
|
|
tk.Button(btn_frame, text="🔍 查看来源结构", command=self.inspect_source,
|
|
|
bg="#9C27B0", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
|
|
|
- tk.Button(btn_frame, text="🚀 开始生成", command=self.generate_output,
|
|
|
+ tk.Button(btn_frame, text="🔍 检查模板表头", command=self.check_template_header,
|
|
|
+ bg="#FF9800", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
|
|
|
+ tk.Button(btn_frame, text="🚀 追加到模板", command=self.generate_output,
|
|
|
bg="#4CAF50", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
|
|
|
tk.Button(btn_frame, text="清空日志", command=self.clear_log,
|
|
|
bg="#f44336", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
|
|
|
@@ -63,7 +66,7 @@ class ExcelMapperApp:
|
|
|
log_frame = tk.LabelFrame(self.root, text="运行日志", font=("Arial", 10, "bold"))
|
|
|
log_frame.pack(pady=10, padx=20, fill="both", expand=True)
|
|
|
|
|
|
- self.log_text = tk.Text(log_frame, height=14, font=("Courier", 9))
|
|
|
+ self.log_text = tk.Text(log_frame, height=16, font=("Courier", 9))
|
|
|
self.log_text.pack(side="left", fill="both", expand=True)
|
|
|
|
|
|
scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
|
|
|
@@ -94,7 +97,6 @@ class ExcelMapperApp:
|
|
|
self.log(f"📁 已选择来源文件: {file_path}")
|
|
|
|
|
|
def inspect_source(self):
|
|
|
- """检测并显示来源格式"""
|
|
|
source_path = self.source_file.get()
|
|
|
if not source_path:
|
|
|
messagebox.showwarning("警告", "请先选择来源Excel文件!")
|
|
|
@@ -108,127 +110,109 @@ class ExcelMapperApp:
|
|
|
ws = wb.active
|
|
|
self.log(f"📊 共 {ws.max_row} 行, {ws.max_column} 列")
|
|
|
|
|
|
- # 判断是横向还是纵向
|
|
|
- first_row_vals = []
|
|
|
- for col in range(1, min(20, ws.max_column + 1)):
|
|
|
- val = ws.cell(row=1, column=col).value
|
|
|
- if val:
|
|
|
- first_row_vals.append(str(val))
|
|
|
-
|
|
|
- is_horizontal = len(first_row_vals) >= 3 and not any(':' in v for v in first_row_vals[:3])
|
|
|
-
|
|
|
- if is_horizontal:
|
|
|
- self.log(f"\n📌 检测到:横向格式(多列表格)")
|
|
|
- self.log(f" 第1行有 {len(first_row_vals)} 个有值单元格")
|
|
|
- self.log(f" 前几个: {first_row_vals[:5]}")
|
|
|
-
|
|
|
- # 显示第2行列名
|
|
|
- row2_vals = []
|
|
|
- for col in range(1, min(30, ws.max_column + 1)):
|
|
|
- val = ws.cell(row=2, column=col).value
|
|
|
- if val:
|
|
|
- row2_vals.append(str(val))
|
|
|
- if row2_vals:
|
|
|
- self.log(f"\n📌 第2行(列名): {row2_vals[:10]}...")
|
|
|
- else:
|
|
|
- self.log(f"\n📌 检测到:纵向格式(从上往下排列)")
|
|
|
-
|
|
|
- # 显示前20行
|
|
|
- self.log(f"\n📌 前20行内容:")
|
|
|
- for row in range(1, min(21, ws.max_row + 1)):
|
|
|
- val = ws.cell(row=row, column=1).value
|
|
|
+ self.log("\n📌 前3行 × 前20列:")
|
|
|
+ for row in range(1, min(4, ws.max_row + 1)):
|
|
|
+ values = []
|
|
|
+ for col in range(1, min(21, ws.max_column + 1)):
|
|
|
+ val = ws.cell(row=row, column=col).value
|
|
|
if val:
|
|
|
- self.log(f" 第{row}行: {str(val)[:80]}")
|
|
|
+ values.append(f"列{col}:{str(val)[:30]}")
|
|
|
+ if values:
|
|
|
+ self.log(f" 第{row}行: {' | '.join(values)}")
|
|
|
+
|
|
|
+ self.log(f"\n📌 第2行(列名):")
|
|
|
+ row2_vals = []
|
|
|
+ for col in range(1, min(ws.max_column + 1, 30)):
|
|
|
+ val = ws.cell(row=2, column=col).value
|
|
|
+ if val:
|
|
|
+ row2_vals.append(f"列{col}:{str(val)[:25]}")
|
|
|
+ if row2_vals:
|
|
|
+ self.log(f" {' | '.join(row2_vals)}")
|
|
|
|
|
|
except Exception as e:
|
|
|
self.log(f"❌ 分析失败: {str(e)}")
|
|
|
|
|
|
- def parse_vertical_format(self, ws):
|
|
|
- """解析纵向格式(Key: Value 格式),相同Item Code合并"""
|
|
|
- products_dict = {} # 用字典存储,key是Item Code
|
|
|
- current_item_code = None
|
|
|
- current_product = {}
|
|
|
-
|
|
|
- field_map = {
|
|
|
- "Item Code": "Item Code",
|
|
|
- "产品名称": "产品名称",
|
|
|
- "颜色": "颜色",
|
|
|
- "材质": "材质",
|
|
|
- "组装长度 (英寸)": "组装长度(英寸)",
|
|
|
- "组装宽度 (英寸)": "组装宽度(英寸)",
|
|
|
- "组装高度 (英寸)": "组装高度(英寸)",
|
|
|
- "产品重量 (磅)": "产品重量(磅)",
|
|
|
- "长度 (英寸)": "包装尺寸-长度(英寸)",
|
|
|
- "宽度 (英寸)": "包装尺寸-宽度(英寸)",
|
|
|
- "高度 (英寸)": "包装尺寸-高度(英寸)",
|
|
|
- "重量 (磅)": "包装尺寸-重量(磅)",
|
|
|
- }
|
|
|
-
|
|
|
- price_pattern = re.compile(r'^\$?([\d.]+)$')
|
|
|
-
|
|
|
- for row in range(1, ws.max_row + 1):
|
|
|
- val = ws.cell(row=row, column=1).value
|
|
|
- if val is None:
|
|
|
- continue
|
|
|
+ def check_template_header(self):
|
|
|
+ template_path = os.path.join(self.base_dir, "模板.xlsx")
|
|
|
+
|
|
|
+ if not os.path.exists(template_path):
|
|
|
+ self.log(f"❌ 找不到模板文件: {template_path}")
|
|
|
+ return
|
|
|
+
|
|
|
+ self.log("\n" + "="*60)
|
|
|
+ self.log("🔍 检查模板表头...")
|
|
|
+
|
|
|
+ try:
|
|
|
+ wb = load_workbook(template_path)
|
|
|
+ ws = wb.active
|
|
|
+ self.log(f"📊 共 {ws.max_row} 行, {ws.max_column} 列")
|
|
|
|
|
|
- val_str = str(val).strip()
|
|
|
- if not val_str:
|
|
|
- continue
|
|
|
+ # 显示前5行
|
|
|
+ for row in range(1, min(6, ws.max_row + 1)):
|
|
|
+ values = []
|
|
|
+ for col in range(1, min(31, ws.max_column + 1)):
|
|
|
+ val = ws.cell(row=row, column=col).value
|
|
|
+ if val:
|
|
|
+ values.append(f"列{col}:{str(val)[:30]}")
|
|
|
+ if values:
|
|
|
+ self.log(f"\n📌 第{row}行: {' | '.join(values[:10])}")
|
|
|
+ if len(values) > 10:
|
|
|
+ self.log(f" ... 还有 {len(values)-10} 个有值单元格")
|
|
|
+ else:
|
|
|
+ self.log(f"\n📌 第{row}行: (全空)")
|
|
|
|
|
|
- # 检测是否是 "Key: Value" 格式
|
|
|
- if ':' in val_str:
|
|
|
- parts = val_str.split(':', 1)
|
|
|
- key = parts[0].strip()
|
|
|
- value = parts[1].strip() if len(parts) > 1 else ''
|
|
|
+ # 检查 Valid Values 工作表
|
|
|
+ if "Valid Values" in wb.sheetnames:
|
|
|
+ self.log(f"\n✅ 找到 'Valid Values' 工作表")
|
|
|
+ ws_valid = wb["Valid Values"]
|
|
|
+ self.log(f" 共 {ws_valid.max_row} 行, {ws_valid.max_column} 列")
|
|
|
|
|
|
- if key == "Item Code":
|
|
|
- # 保存当前产品到字典
|
|
|
- if current_item_code and current_product:
|
|
|
- if current_item_code in products_dict:
|
|
|
- # 合并(不覆盖已有字段)
|
|
|
- for k, v in current_product.items():
|
|
|
- if k not in products_dict[current_item_code] or not products_dict[current_item_code][k]:
|
|
|
- products_dict[current_item_code][k] = v
|
|
|
- else:
|
|
|
- products_dict[current_item_code] = current_product.copy()
|
|
|
-
|
|
|
- # 开始新产品
|
|
|
- current_item_code = value
|
|
|
- current_product = {"Item Code": value}
|
|
|
- else:
|
|
|
- mapped_key = field_map.get(key, key)
|
|
|
- # 只有当前产品中没有该字段时才设置(保留第一次出现的值)
|
|
|
- if mapped_key not in current_product or not current_product[mapped_key]:
|
|
|
- current_product[mapped_key] = value
|
|
|
+ # 读取前几行
|
|
|
+ for row in range(1, min(6, ws_valid.max_row + 1)):
|
|
|
+ values = []
|
|
|
+ for col in range(1, min(10, ws_valid.max_column + 1)):
|
|
|
+ val = ws_valid.cell(row=row, column=col).value
|
|
|
+ if val:
|
|
|
+ values.append(str(val)[:20])
|
|
|
+ if values:
|
|
|
+ self.log(f" Valid Values 第{row}行: {' | '.join(values)}")
|
|
|
else:
|
|
|
- # 没有冒号,可能是价格
|
|
|
- match = price_pattern.match(val_str.replace('$', '').strip())
|
|
|
- if match and current_item_code:
|
|
|
- price_val = match.group(1)
|
|
|
- # 判断是 Base Cost 还是 MSRP
|
|
|
- if current_item_code in products_dict:
|
|
|
- existing = products_dict[current_item_code]
|
|
|
- if "优惠单价" not in existing or not existing["优惠单价"]:
|
|
|
- existing["优惠单价"] = price_val
|
|
|
- elif "Manufacturer Suggested Retail Price" not in existing or not existing["Manufacturer Suggested Retail Price"]:
|
|
|
- existing["Manufacturer Suggested Retail Price"] = str(float(price_val) + 70)
|
|
|
- else:
|
|
|
- if "优惠单价" not in current_product or not current_product["优惠单价"]:
|
|
|
- current_product["优惠单价"] = price_val
|
|
|
- elif "Manufacturer Suggested Retail Price" not in current_product or not current_product["Manufacturer Suggested Retail Price"]:
|
|
|
- current_product["Manufacturer Suggested Retail Price"] = str(float(price_val) + 70)
|
|
|
-
|
|
|
- # 保存最后一个产品
|
|
|
- if current_item_code and current_product:
|
|
|
- if current_item_code in products_dict:
|
|
|
- for k, v in current_product.items():
|
|
|
- if k not in products_dict[current_item_code] or not products_dict[current_item_code][k]:
|
|
|
- products_dict[current_item_code][k] = v
|
|
|
+ self.log(f"\n⚠️ 未找到 'Valid Values' 工作表")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.log(f"❌ 检查失败: {str(e)}")
|
|
|
+
|
|
|
+ def load_valid_values(self, wb):
|
|
|
+ """从 Valid Values 工作表读取有效值"""
|
|
|
+ valid_data = {}
|
|
|
+
|
|
|
+ if "Valid Values" not in wb.sheetnames:
|
|
|
+ self.log("⚠️ 未找到 'Valid Values' 工作表,将使用硬编码默认值")
|
|
|
+ return valid_data
|
|
|
+
|
|
|
+ ws_valid = wb["Valid Values"]
|
|
|
+
|
|
|
+ # 读取第1行作为列名
|
|
|
+ headers = []
|
|
|
+ for col in range(1, ws_valid.max_column + 1):
|
|
|
+ val = ws_valid.cell(row=1, column=col).value
|
|
|
+ if val:
|
|
|
+ headers.append(str(val).strip())
|
|
|
else:
|
|
|
- products_dict[current_item_code] = current_product
|
|
|
+ headers.append(f"Column_{col}")
|
|
|
+
|
|
|
+ # 读取数据行
|
|
|
+ for row in range(2, ws_valid.max_row + 1):
|
|
|
+ for col in range(1, len(headers) + 1):
|
|
|
+ val = ws_valid.cell(row=row, column=col).value
|
|
|
+ if val:
|
|
|
+ header = headers[col - 1]
|
|
|
+ if header not in valid_data:
|
|
|
+ valid_data[header] = []
|
|
|
+ if str(val).strip() not in valid_data[header]:
|
|
|
+ valid_data[header].append(str(val).strip())
|
|
|
|
|
|
- # 转换为列表
|
|
|
- return list(products_dict.values())
|
|
|
+ return valid_data
|
|
|
|
|
|
def generate_output(self):
|
|
|
self.log("\n" + "="*60)
|
|
|
@@ -250,53 +234,28 @@ class ExcelMapperApp:
|
|
|
header_row = int(self.header_row.get())
|
|
|
source_header = int(self.source_header_row.get())
|
|
|
|
|
|
- # ========== 1. 检测并读取来源数据 ==========
|
|
|
- wb_source = load_workbook(source_path)
|
|
|
- ws_source = wb_source.active
|
|
|
+ # ========== 1. 备份模板 ==========
|
|
|
+ backup_path = template_path.replace(".xlsx", f"_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx")
|
|
|
+ shutil.copy2(template_path, backup_path)
|
|
|
+ self.log(f"📦 已备份模板到: {backup_path}")
|
|
|
|
|
|
- # 检测格式
|
|
|
- first_row_vals = []
|
|
|
- for col in range(1, min(20, ws_source.max_column + 1)):
|
|
|
- val = ws_source.cell(row=1, column=col).value
|
|
|
- if val:
|
|
|
- first_row_vals.append(str(val))
|
|
|
-
|
|
|
- is_horizontal = len(first_row_vals) >= 3 and not any(':' in v for v in first_row_vals[:3])
|
|
|
+ # ========== 2. 读取来源数据 ==========
|
|
|
+ self.log(f"📖 读取来源数据: {source_path}")
|
|
|
+ self.log(f"📌 使用第 {source_header} 行作为列名")
|
|
|
|
|
|
- if is_horizontal:
|
|
|
- self.log(f"📌 检测到横向格式,使用 pandas 读取...")
|
|
|
- self.df = pd.read_excel(source_path, header=source_header - 1)
|
|
|
- self.log(f"✅ 成功读取 {len(self.df)} 行数据")
|
|
|
- else:
|
|
|
- self.log(f"📌 检测到纵向格式,使用解析器读取...")
|
|
|
- products = self.parse_vertical_format(ws_source)
|
|
|
- self.log(f"✅ 成功解析 {len(products)} 个产品")
|
|
|
- self.df = pd.DataFrame(products)
|
|
|
- self.log(f"📋 解析到的字段: {list(self.df.columns)}")
|
|
|
+ self.df = pd.read_excel(source_path, header=source_header - 1)
|
|
|
+ self.log(f"✅ 成功读取 {len(self.df)} 行数据")
|
|
|
|
|
|
- # ========== 2. 加载模板(直接修改原文件) ==========
|
|
|
+ # ========== 3. 加载模板并读取 Valid Values ==========
|
|
|
self.log(f"\n📖 加载模板: {template_path}")
|
|
|
wb = load_workbook(template_path)
|
|
|
ws = wb.active
|
|
|
|
|
|
- # ========== 3. 找到已有数据的最后一行 ==========
|
|
|
- # 从第4行开始找,找到第一个完全空的行
|
|
|
- last_row = 3 # 从第4行开始检查
|
|
|
- for row in range(4, ws.max_row + 2):
|
|
|
- is_empty = True
|
|
|
- for col in range(1, min(20, ws.max_column + 1)): # 检查前20列
|
|
|
- if ws.cell(row=row, column=col).value is not None:
|
|
|
- is_empty = False
|
|
|
- break
|
|
|
- if is_empty:
|
|
|
- last_row = row
|
|
|
- break
|
|
|
- else:
|
|
|
- last_row = ws.max_row + 1
|
|
|
-
|
|
|
- self.log(f"📌 模板中已有数据到第 {last_row - 1} 行,从第 {last_row} 行开始追加")
|
|
|
+ # 加载 Valid Values
|
|
|
+ self.valid_values = self.load_valid_values(wb)
|
|
|
+ self.log(f"📋 从 Valid Values 读取到 {len(self.valid_values)} 个字段的有效值")
|
|
|
|
|
|
- # ========== 4. 读取模板表头 ==========
|
|
|
+ # ========== 4. 验证模板表头 ==========
|
|
|
col_indices = {}
|
|
|
for col in range(1, ws.max_column + 1):
|
|
|
cell_value = ws.cell(row=header_row, column=col).value
|
|
|
@@ -305,27 +264,66 @@ class ExcelMapperApp:
|
|
|
if col_name and not col_name.startswith("Unnamed"):
|
|
|
col_indices[col_name] = col
|
|
|
|
|
|
- # ========== 5. 目标列映射 ==========
|
|
|
- target_cols = [
|
|
|
- "Supplier Part Number", "Manufacturer Part Number", "Base Cost",
|
|
|
- "Manufacturer Suggested Retail Price", "Product Weight",
|
|
|
- "Carton Weight 1", "Carton Height 1", "Carton Width 1", "Carton Depth 1",
|
|
|
- "Base Color", "Base Material", "Color", "Material",
|
|
|
- "Overall Depth - Front to Back", "Overall Height - Top to Bottom",
|
|
|
- "Overall Product Weight", "Overall Width - Side to Side",
|
|
|
- "Top Color", "Top Material"
|
|
|
- ]
|
|
|
+ self.log(f"📊 模板中共有 {len(col_indices)} 个有名称的列")
|
|
|
+
|
|
|
+ # ========== 5. 找到已有数据的最后一行 ==========
|
|
|
+ last_row = 5
|
|
|
+ for row in range(5, ws.max_row + 2):
|
|
|
+ is_empty = True
|
|
|
+ for col in range(1, min(20, ws.max_column + 1)):
|
|
|
+ if ws.cell(row=row, column=col).value is not None:
|
|
|
+ is_empty = False
|
|
|
+ break
|
|
|
+ if is_empty:
|
|
|
+ last_row = row
|
|
|
+ break
|
|
|
+ else:
|
|
|
+ last_row = ws.max_row + 1
|
|
|
|
|
|
- found_cols = {}
|
|
|
- for target in target_cols:
|
|
|
- if target in col_indices:
|
|
|
- found_cols[target] = col_indices[target]
|
|
|
+ self.log(f"📌 已有数据到第 {last_row - 1} 行,从第 {last_row} 行开始追加")
|
|
|
|
|
|
- if not found_cols:
|
|
|
- self.log(f"\n❌ 没有找到任何目标列!")
|
|
|
- return
|
|
|
+ # ========== 6. 定义默认值映射 ==========
|
|
|
+ # 格式: 目标列名 -> (值来源, 处理方式)
|
|
|
+ # 值来源: 'fixed' 固定值, 'valid' 从Valid Values取第一个, 'source' 从来源取, 'computed' 计算
|
|
|
+ default_mapping = {
|
|
|
+ # 固定值
|
|
|
+ "Variant Type": ("fixed", "Not Variant"),
|
|
|
+ "Minimum Order Quantity": ("fixed", 1),
|
|
|
+ "Force Quantity Multiplier": ("fixed", 1),
|
|
|
+ "Display Set Quantity": ("fixed", 1),
|
|
|
+ "Ship Type": ("valid", "Ship Type"), # 从Valid Values取第一个
|
|
|
+ "Lead Time": ("fixed", 48),
|
|
|
+ "Replacement Lead Time": ("fixed", 120),
|
|
|
+ "Flat Pack": ("fixed", "Yes"),
|
|
|
+ "Assembly Required": ("fixed", "Yes"),
|
|
|
+ "Canada Product Restriction": ("fixed", "Yes"),
|
|
|
+ "CARB Phase II Compliant (formaldehyde emissions)": ("fixed", "Yes"),
|
|
|
+ "Commercial Warranty": ("fixed", "Yes"),
|
|
|
+ "CANFER Compliant": ("fixed", "Yes"),
|
|
|
+ "Commercial Warranty Length": ("fixed", "1 Years"),
|
|
|
+ "Composite Wood Product (CWP)": ("fixed", "Yes"),
|
|
|
+ "Country Of Manufacturer": ("fixed", "China"),
|
|
|
+ "General Certificate of Conformity (GCC)": ("fixed", "Yes"),
|
|
|
+ "Hazard Class(es)": ("fixed", "Does Not Apply"),
|
|
|
+ "Hazardous Material / Dangerous Goods": ("fixed", "No"),
|
|
|
+ "Hazardous Material Weight": ("fixed", "Does Not Apply"),
|
|
|
+ "ISTA Certified": ("popup", ""), # 弹出输入框
|
|
|
+ "Level of Assembly": ("fixed", "Full Assembly Needed"),
|
|
|
+ "Packing Group": ("fixed", "Does Not Apply"),
|
|
|
+ "Reason for Restriction": ("fixed", "Does Not Apply"),
|
|
|
+ "Supplier Intended and Approved Use": ("fixed", "Residential Use"),
|
|
|
+ "TSCA Title VI Compliant (formaldehyde emissions)": ("fixed", "Yes"),
|
|
|
+ "UN or ID number": ("fixed", "Does Not Apply"),
|
|
|
+ "Uniform Packaging and Labeling Regulations (UPLR) Compliant": ("fixed", "Yes"),
|
|
|
+ "Warning Required": ("fixed", "No"),
|
|
|
+ "Warranty Length": ("fixed", "1 Years"),
|
|
|
+ "Battery or Batteries Included": ("fixed", "No"),
|
|
|
+ # 动态值(从来源取)
|
|
|
+ "Product Type": ("source", "产品类目"),
|
|
|
+ "Brand": ("valid", "Brand"), # 从Valid Values取第一个
|
|
|
+ }
|
|
|
|
|
|
- # ========== 6. 匹配来源列 ==========
|
|
|
+ # ========== 7. 匹配来源列 ==========
|
|
|
source_mapping = {
|
|
|
"Item Code": "Item Code",
|
|
|
"产品名称": "产品名称",
|
|
|
@@ -340,8 +338,10 @@ class ExcelMapperApp:
|
|
|
"组装长度": "组装长度(英寸)",
|
|
|
"组装高度": "组装高度(英寸)",
|
|
|
"组装宽度": "组装宽度(英寸)",
|
|
|
+ "产品类目": "产品类目",
|
|
|
}
|
|
|
|
|
|
+ self.log(f"\n🔍 匹配来源列:")
|
|
|
source_map = {}
|
|
|
for key, expected in source_mapping.items():
|
|
|
found = False
|
|
|
@@ -350,17 +350,13 @@ class ExcelMapperApp:
|
|
|
source_map[key] = col
|
|
|
found = True
|
|
|
break
|
|
|
- if not found:
|
|
|
- # 部分匹配
|
|
|
- for col in self.df.columns:
|
|
|
- if expected in col or col in expected:
|
|
|
- source_map[key] = col
|
|
|
- found = True
|
|
|
- break
|
|
|
- if not found:
|
|
|
+ if found:
|
|
|
+ self.log(f" ✅ '{key}' -> '{source_map[key]}'")
|
|
|
+ else:
|
|
|
+ self.log(f" ❌ '{key}' -> 未找到")
|
|
|
source_map[key] = None
|
|
|
|
|
|
- # ========== 7. 填充数据 ==========
|
|
|
+ # ========== 8. 填充数据 ==========
|
|
|
def get_key(row):
|
|
|
item = row.get("Item Code", "")
|
|
|
name = row.get("产品名称", "")
|
|
|
@@ -394,63 +390,52 @@ class ExcelMapperApp:
|
|
|
suffix = ""
|
|
|
mfr_suffix = ""
|
|
|
|
|
|
- self.log(f"\n--- 追加第 {idx+1}/{len(self.df)} 个产品 ---")
|
|
|
+ self.log(f"\n--- 第 {idx+1}/{len(self.df)} 行 ---")
|
|
|
self.log(f" Item Code: '{item_code}'")
|
|
|
self.log(f" 产品名称: '{str(product_name)[:50]}...'")
|
|
|
|
|
|
fill_count = 0
|
|
|
|
|
|
- # Supplier Part Number
|
|
|
- if "Supplier Part Number" in found_cols:
|
|
|
+ # ===== Supplier Part Number =====
|
|
|
+ if "Supplier Part Number" in col_indices:
|
|
|
val = f"{item_code}{product_name}{suffix}"
|
|
|
- ws.cell(row=row_num, column=found_cols["Supplier Part Number"], value=val)
|
|
|
+ ws.cell(row=row_num, column=col_indices["Supplier Part Number"], value=val)
|
|
|
fill_count += 1
|
|
|
|
|
|
- # Manufacturer Part Number
|
|
|
- if "Manufacturer Part Number" in found_cols:
|
|
|
+ # ===== Manufacturer Part Number =====
|
|
|
+ if "Manufacturer Part Number" in col_indices:
|
|
|
val = f"{item_code}{product_name}{mfr_suffix}"
|
|
|
- ws.cell(row=row_num, column=found_cols["Manufacturer Part Number"], value=val)
|
|
|
+ ws.cell(row=row_num, column=col_indices["Manufacturer Part Number"], value=val)
|
|
|
fill_count += 1
|
|
|
|
|
|
- # Base Cost
|
|
|
- if "Base Cost" in found_cols:
|
|
|
- col = source_map.get("优惠单价")
|
|
|
- if col and col in src_row:
|
|
|
- val = src_row[col]
|
|
|
- if pd.notna(val):
|
|
|
- ws.cell(row=row_num, column=found_cols["Base Cost"], value=val)
|
|
|
- fill_count += 1
|
|
|
+ # ===== Base Cost =====
|
|
|
+ if "Base Cost" in col_indices and source_map.get("优惠单价"):
|
|
|
+ col = source_map["优惠单价"]
|
|
|
+ val = src_row.get(col, "")
|
|
|
+ if pd.notna(val):
|
|
|
+ ws.cell(row=row_num, column=col_indices["Base Cost"], value=val)
|
|
|
+ fill_count += 1
|
|
|
|
|
|
- # MSRP
|
|
|
- if "Manufacturer Suggested Retail Price" in found_cols:
|
|
|
- col = source_map.get("优惠单价")
|
|
|
- if col and col in src_row:
|
|
|
- val = src_row[col]
|
|
|
- try:
|
|
|
- num = float(val) if pd.notna(val) else 0
|
|
|
- result = num + 70
|
|
|
- ws.cell(row=row_num, column=found_cols["Manufacturer Suggested Retail Price"], value=result)
|
|
|
- fill_count += 1
|
|
|
- except:
|
|
|
- pass
|
|
|
- else:
|
|
|
- msrp_col = source_map.get("Manufacturer Suggested Retail Price")
|
|
|
- if msrp_col and msrp_col in src_row:
|
|
|
- val = src_row[msrp_col]
|
|
|
- if pd.notna(val):
|
|
|
- ws.cell(row=row_num, column=found_cols["Manufacturer Suggested Retail Price"], value=val)
|
|
|
- fill_count += 1
|
|
|
+ # ===== MSRP =====
|
|
|
+ if "Manufacturer Suggested Retail Price" in col_indices and source_map.get("优惠单价"):
|
|
|
+ col = source_map["优惠单价"]
|
|
|
+ val = src_row.get(col, 0)
|
|
|
+ try:
|
|
|
+ num = float(val) if pd.notna(val) else 0
|
|
|
+ ws.cell(row=row_num, column=col_indices["Manufacturer Suggested Retail Price"], value=num + 70)
|
|
|
+ fill_count += 1
|
|
|
+ except:
|
|
|
+ pass
|
|
|
|
|
|
- # Product Weight
|
|
|
- if "Product Weight" in found_cols:
|
|
|
- col = source_map.get("产品重量")
|
|
|
- if col and col in src_row:
|
|
|
- val = src_row[col]
|
|
|
- if pd.notna(val):
|
|
|
- ws.cell(row=row_num, column=found_cols["Product Weight"], value=val)
|
|
|
- fill_count += 1
|
|
|
+ # ===== Product Weight =====
|
|
|
+ if "Product Weight" in col_indices and source_map.get("产品重量"):
|
|
|
+ col = source_map["产品重量"]
|
|
|
+ val = src_row.get(col, "")
|
|
|
+ if pd.notna(val):
|
|
|
+ ws.cell(row=row_num, column=col_indices["Product Weight"], value=val)
|
|
|
+ fill_count += 1
|
|
|
|
|
|
- # Carton
|
|
|
+ # ===== Carton 尺寸 =====
|
|
|
carton_map = [
|
|
|
("Carton Weight 1", "包装尺寸-重量"),
|
|
|
("Carton Height 1", "包装尺寸-高度"),
|
|
|
@@ -458,15 +443,14 @@ class ExcelMapperApp:
|
|
|
("Carton Depth 1", "包装尺寸-长度"),
|
|
|
]
|
|
|
for target, src_name in carton_map:
|
|
|
- if target in found_cols:
|
|
|
- col = source_map.get(src_name)
|
|
|
- if col and col in src_row:
|
|
|
- val = src_row[col]
|
|
|
- if pd.notna(val):
|
|
|
- ws.cell(row=row_num, column=found_cols[target], value=val)
|
|
|
- fill_count += 1
|
|
|
+ if target in col_indices and source_map.get(src_name):
|
|
|
+ col = source_map[src_name]
|
|
|
+ val = src_row.get(col, "")
|
|
|
+ if pd.notna(val):
|
|
|
+ ws.cell(row=row_num, column=col_indices[target], value=val)
|
|
|
+ fill_count += 1
|
|
|
|
|
|
- # Color/Material
|
|
|
+ # ===== Color/Material =====
|
|
|
cm_map = [
|
|
|
("Base Color", "颜色"),
|
|
|
("Base Material", "材质"),
|
|
|
@@ -476,15 +460,14 @@ class ExcelMapperApp:
|
|
|
("Top Material", "材质"),
|
|
|
]
|
|
|
for target, src_name in cm_map:
|
|
|
- if target in found_cols:
|
|
|
- col = source_map.get(src_name)
|
|
|
- if col and col in src_row:
|
|
|
- val = src_row[col]
|
|
|
- if pd.notna(val):
|
|
|
- ws.cell(row=row_num, column=found_cols[target], value=val)
|
|
|
- fill_count += 1
|
|
|
+ if target in col_indices and source_map.get(src_name):
|
|
|
+ col = source_map[src_name]
|
|
|
+ val = src_row.get(col, "")
|
|
|
+ if pd.notna(val):
|
|
|
+ ws.cell(row=row_num, column=col_indices[target], value=val)
|
|
|
+ fill_count += 1
|
|
|
|
|
|
- # Overall
|
|
|
+ # ===== Overall 尺寸 =====
|
|
|
overall_map = [
|
|
|
("Overall Depth - Front to Back", "组装长度"),
|
|
|
("Overall Height - Top to Bottom", "组装高度"),
|
|
|
@@ -492,30 +475,151 @@ class ExcelMapperApp:
|
|
|
("Overall Width - Side to Side", "组装宽度"),
|
|
|
]
|
|
|
for target, src_name in overall_map:
|
|
|
- if target in found_cols:
|
|
|
- col = source_map.get(src_name)
|
|
|
- if col and col in src_row:
|
|
|
- val = src_row[col]
|
|
|
+ if target in col_indices and source_map.get(src_name):
|
|
|
+ col = source_map[src_name]
|
|
|
+ val = src_row.get(col, "")
|
|
|
+ if pd.notna(val):
|
|
|
+ ws.cell(row=row_num, column=col_indices[target], value=val)
|
|
|
+ fill_count += 1
|
|
|
+
|
|
|
+ # ===== 默认值字段 =====
|
|
|
+ variant_type = "Not Variant" # 默认值
|
|
|
+
|
|
|
+ for target_col, (source_type, source_val) in default_mapping.items():
|
|
|
+ if target_col not in col_indices:
|
|
|
+ continue
|
|
|
+
|
|
|
+ col_idx = col_indices[target_col]
|
|
|
+
|
|
|
+ if source_type == "fixed":
|
|
|
+ # 固定值
|
|
|
+ ws.cell(row=row_num, column=col_idx, value=source_val)
|
|
|
+ fill_count += 1
|
|
|
+ self.log(f" ✅ {target_col} = '{source_val}' (固定值)")
|
|
|
+
|
|
|
+ elif source_type == "valid":
|
|
|
+ # 从 Valid Values 取第一个值
|
|
|
+ if source_val in self.valid_values and self.valid_values[source_val]:
|
|
|
+ val = self.valid_values[source_val][0]
|
|
|
+ ws.cell(row=row_num, column=col_idx, value=val)
|
|
|
+ fill_count += 1
|
|
|
+ self.log(f" ✅ {target_col} = '{val}' (从Valid Values)")
|
|
|
+ else:
|
|
|
+ self.log(f" ⚠️ {target_col}: Valid Values中无数据,使用默认值'Not Variant'")
|
|
|
+ ws.cell(row=row_num, column=col_idx, value="Not Variant")
|
|
|
+ fill_count += 1
|
|
|
+
|
|
|
+ elif source_type == "source":
|
|
|
+ # 从来源取
|
|
|
+ if source_map.get(source_val):
|
|
|
+ col = source_map[source_val]
|
|
|
+ val = src_row.get(col, "")
|
|
|
if pd.notna(val):
|
|
|
- ws.cell(row=row_num, column=found_cols[target], value=val)
|
|
|
+ ws.cell(row=row_num, column=col_idx, value=val)
|
|
|
fill_count += 1
|
|
|
+ self.log(f" ✅ {target_col} = '{val}' (从来源)")
|
|
|
+ else:
|
|
|
+ self.log(f" ⚠️ {target_col}: 来源为空")
|
|
|
+ else:
|
|
|
+ self.log(f" ⚠️ {target_col}: 找不到来源列'{source_val}'")
|
|
|
+
|
|
|
+ elif source_type == "popup":
|
|
|
+ # 弹出输入框(ISTA Certified)
|
|
|
+ self.log(f" ⏳ 请为 {target_col} 输入值...")
|
|
|
+ # 这里用简单方式:如果10秒没输入就跳过
|
|
|
+ result = self.show_input_dialog(f"请输入 {target_col}", "包装认证 (ISTA Certified):")
|
|
|
+ if result:
|
|
|
+ ws.cell(row=row_num, column=col_idx, value=result)
|
|
|
+ fill_count += 1
|
|
|
+ self.log(f" ✅ {target_col} = '{result}' (用户输入)")
|
|
|
+ else:
|
|
|
+ self.log(f" ⏭️ {target_col}: 用户跳过")
|
|
|
+
|
|
|
+ # ===== 特殊逻辑:Variant Grouping 1/2 =====
|
|
|
+ # Variant Grouping 1: 如果 Variant Type 是 Not Variant 则不填,否则填 "Color"
|
|
|
+ if "Variant Grouping 1" in col_indices:
|
|
|
+ if variant_type != "Not Variant":
|
|
|
+ ws.cell(row=row_num, column=col_indices["Variant Grouping 1"], value="Color")
|
|
|
+ fill_count += 1
|
|
|
+
|
|
|
+ if "Variant Attribute Name On Site 1" in col_indices:
|
|
|
+ if variant_type != "Not Variant":
|
|
|
+ ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 1"], value="Finish")
|
|
|
+ fill_count += 1
|
|
|
+
|
|
|
+ if "Variant Grouping 2" in col_indices:
|
|
|
+ if variant_type != "Not Variant":
|
|
|
+ ws.cell(row=row_num, column=col_indices["Variant Grouping 2"], value="Size")
|
|
|
+ fill_count += 1
|
|
|
+
|
|
|
+ if "Variant Attribute Name On Site 2" in col_indices:
|
|
|
+ if variant_type != "Not Variant":
|
|
|
+ ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 2"], value="Dimensions")
|
|
|
+ fill_count += 1
|
|
|
|
|
|
self.log(f" 📊 本行填充 {fill_count} 个字段")
|
|
|
row_num += 1
|
|
|
filled_count += 1
|
|
|
|
|
|
- # ========== 8. 直接保存到模板文件 ==========
|
|
|
+ # ========== 9. 保存 ==========
|
|
|
wb.save(template_path)
|
|
|
- self.log(f"\n✅ 成功追加到模板文件: {template_path}")
|
|
|
+ self.log(f"\n✅ 成功追加到模板: {template_path}")
|
|
|
self.log(f"📊 共追加 {filled_count} 行数据")
|
|
|
+ self.log(f"📦 备份文件: {backup_path}")
|
|
|
self.status_text.set(f"✅ 已追加 {filled_count} 行到模板")
|
|
|
- messagebox.showinfo("成功", f"已追加 {filled_count} 行数据到模板文件:\n{template_path}")
|
|
|
+ messagebox.showinfo("成功",
|
|
|
+ f"已追加 {filled_count} 行数据到模板文件:\n{template_path}\n\n"
|
|
|
+ f"备份文件保存在: {backup_path}"
|
|
|
+ )
|
|
|
|
|
|
except Exception as e:
|
|
|
self.log(f"\n❌ 错误: {str(e)}")
|
|
|
import traceback
|
|
|
self.log(traceback.format_exc())
|
|
|
messagebox.showerror("错误", f"生成失败:\n{str(e)}")
|
|
|
+
|
|
|
+ def show_input_dialog(self, title, prompt):
|
|
|
+ """显示输入对话框,10秒超时自动跳过"""
|
|
|
+ result = tk.StringVar()
|
|
|
+
|
|
|
+ dialog = tk.Toplevel(self.root)
|
|
|
+ dialog.title(title)
|
|
|
+ dialog.geometry("400x120")
|
|
|
+ dialog.transient(self.root)
|
|
|
+ dialog.grab_set()
|
|
|
+
|
|
|
+ tk.Label(dialog, text=prompt, font=("Arial", 11)).pack(pady=10)
|
|
|
+ entry = tk.Entry(dialog, width=40)
|
|
|
+ entry.pack(pady=5)
|
|
|
+ entry.focus()
|
|
|
+
|
|
|
+ # 超时计数器
|
|
|
+ timeout_seconds = 10
|
|
|
+ time_label = tk.Label(dialog, text=f"剩余 {timeout_seconds} 秒...", fg="gray")
|
|
|
+ time_label.pack(pady=5)
|
|
|
+
|
|
|
+ def on_ok():
|
|
|
+ result.set(entry.get())
|
|
|
+ dialog.destroy()
|
|
|
+
|
|
|
+ def on_cancel():
|
|
|
+ dialog.destroy()
|
|
|
+
|
|
|
+ def countdown(count):
|
|
|
+ if count <= 0:
|
|
|
+ dialog.destroy()
|
|
|
+ return
|
|
|
+ time_label.config(text=f"剩余 {count} 秒...")
|
|
|
+ dialog.after(1000, countdown, count - 1)
|
|
|
+
|
|
|
+ tk.Button(dialog, text="确定", command=on_ok, width=10).pack(side="left", padx=20, pady=10)
|
|
|
+ tk.Button(dialog, text="跳过", command=on_cancel, width=10).pack(side="right", padx=20, pady=10)
|
|
|
+
|
|
|
+ dialog.after(1000, countdown, timeout_seconds - 1)
|
|
|
+ self.root.wait_window(dialog)
|
|
|
+
|
|
|
+ return result.get()
|
|
|
+
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
root = tk.Tk()
|