1
0

2 Ревизии 7d9b7244f3 ... 952bcf9018

Автор SHA1 Съобщение Дата
  PUGE 952bcf9018 save преди 3 седмици
  PUGE 345552a47c start преди 3 седмици
променени са 5 файла, в които са добавени 399 реда и са изтрити 254 реда
  1. 2 0
      .gitignore
  2. 358 254
      app.py
  3. 39 0
      app.spec
  4. BIN
      模板.xlsx
  5. BIN
      模板_backup_20260724_143104.xlsx

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+build
+dist

+ 358 - 254
app.py

@@ -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()

+ 39 - 0
app.spec

@@ -0,0 +1,39 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+    ['app.py'],
+    pathex=[],
+    binaries=[],
+    datas=[],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    noarchive=False,
+    optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    a.binaries,
+    a.datas,
+    [],
+    name='app',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    runtime_tmpdir=None,
+    console=False,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+    icon=['C:\\Users\\mail\\Documents\\PUGE\\设计文件\\8h7mr-6xq6z-001.ico'],
+)

BIN
模板.xlsx


BIN
模板_backup_20260724_143104.xlsx