PUGE 3 nedēļas atpakaļ
revīzija
7d9b7244f3
2 mainītis faili ar 523 papildinājumiem un 0 dzēšanām
  1. 523 0
      app.py
  2. BIN
      模板.xlsx

+ 523 - 0
app.py

@@ -0,0 +1,523 @@
+import tkinter as tk
+from tkinter import filedialog, messagebox
+import pandas as pd
+from openpyxl import load_workbook
+import os
+import sys
+import re
+from collections import Counter
+
+class ExcelMapperApp:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("Excel 自动填表工具")
+        self.root.geometry("750x550")
+        self.root.resizable(True, True)
+        
+        if getattr(sys, 'frozen', False):
+            self.base_dir = os.path.dirname(sys.executable)
+        else:
+            self.base_dir = os.path.dirname(os.path.abspath(__file__))
+        
+        self.source_file = tk.StringVar()
+        self.header_row = tk.StringVar(value="3")
+        self.source_header_row = tk.StringVar(value="2")
+        self.df = None
+        self.status_text = tk.StringVar(value="请选择来源Excel文件")
+        
+        self.setup_ui()
+        
+    def setup_ui(self):
+        title_label = tk.Label(self.root, text="Excel 自动填表工具", font=("Arial", 16, "bold"))
+        title_label.pack(pady=10)
+        
+        file_frame = tk.LabelFrame(self.root, text="文件选择", font=("Arial", 10, "bold"))
+        file_frame.pack(pady=10, padx=20, fill="x")
+        
+        tk.Label(file_frame, text="来源数据Excel:").grid(row=0, column=0, sticky="w", pady=5)
+        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.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)
+        
+        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)
+        
+        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,
+                  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)
+        
+        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.pack(side="left", fill="both", expand=True)
+        
+        scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
+        scrollbar.pack(side="right", fill="y")
+        self.log_text.config(yscrollcommand=scrollbar.set)
+        
+        status_bar = tk.Label(self.root, textvariable=self.status_text, relief="sunken", anchor="w",
+                              font=("Arial", 9), bg="#f0f0f0")
+        status_bar.pack(side="bottom", fill="x")
+    
+    def log(self, msg):
+        self.log_text.insert(tk.END, msg + "\n")
+        self.log_text.see(tk.END)
+        self.root.update()
+        print(msg)
+    
+    def clear_log(self):
+        self.log_text.delete(1.0, tk.END)
+    
+    def select_source_file(self):
+        file_path = filedialog.askopenfilename(
+            title="选择来源Excel文件",
+            filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
+        )
+        if file_path:
+            self.source_file.set(file_path)
+            self.status_text.set(f"已选择: {os.path.basename(file_path)}")
+            self.log(f"📁 已选择来源文件: {file_path}")
+    
+    def inspect_source(self):
+        """检测并显示来源格式"""
+        source_path = self.source_file.get()
+        if not source_path:
+            messagebox.showwarning("警告", "请先选择来源Excel文件!")
+            return
+        
+        self.log("\n" + "="*60)
+        self.log("🔍 分析来源Excel结构...")
+        
+        try:
+            wb = load_workbook(source_path)
+            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
+                    if val:
+                        self.log(f"   第{row}行: {str(val)[:80]}")
+            
+        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
+            
+            val_str = str(val).strip()
+            if not val_str:
+                continue
+            
+            # 检测是否是 "Key: Value" 格式
+            if ':' in val_str:
+                parts = val_str.split(':', 1)
+                key = parts[0].strip()
+                value = parts[1].strip() if len(parts) > 1 else ''
+                
+                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
+            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
+            else:
+                products_dict[current_item_code] = current_product
+        
+        # 转换为列表
+        return list(products_dict.values())
+    
+    def generate_output(self):
+        self.log("\n" + "="*60)
+        self.log("🚀 开始处理...")
+        
+        source_path = self.source_file.get()
+        template_path = os.path.join(self.base_dir, "模板.xlsx")
+        
+        if not source_path:
+            messagebox.showwarning("警告", "请选择来源Excel文件!")
+            return
+        
+        if not os.path.exists(template_path):
+            self.log(f"❌ 找不到模板文件: {template_path}")
+            messagebox.showerror("错误", f"找不到模板文件:\n{template_path}")
+            return
+        
+        try:
+            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
+            
+            # 检测格式
+            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])
+            
+            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)}")
+            
+            # ========== 2. 加载模板(直接修改原文件) ==========
+            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} 行开始追加")
+            
+            # ========== 4. 读取模板表头 ==========
+            col_indices = {}
+            for col in range(1, ws.max_column + 1):
+                cell_value = ws.cell(row=header_row, column=col).value
+                if cell_value:
+                    col_name = str(cell_value).strip()
+                    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"
+            ]
+            
+            found_cols = {}
+            for target in target_cols:
+                if target in col_indices:
+                    found_cols[target] = col_indices[target]
+            
+            if not found_cols:
+                self.log(f"\n❌ 没有找到任何目标列!")
+                return
+            
+            # ========== 6. 匹配来源列 ==========
+            source_mapping = {
+                "Item Code": "Item Code",
+                "产品名称": "产品名称",
+                "优惠单价": "优惠单价",
+                "产品重量": "产品重量(磅)",
+                "包装尺寸-重量": "包装尺寸-重量(磅)",
+                "包装尺寸-高度": "包装尺寸-高度(英寸)",
+                "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
+                "包装尺寸-长度": "包装尺寸-长度(英寸)",
+                "颜色": "颜色",
+                "材质": "材质",
+                "组装长度": "组装长度(英寸)",
+                "组装高度": "组装高度(英寸)",
+                "组装宽度": "组装宽度(英寸)",
+            }
+            
+            source_map = {}
+            for key, expected in source_mapping.items():
+                found = False
+                for col in self.df.columns:
+                    if col == expected:
+                        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:
+                    source_map[key] = None
+            
+            # ========== 7. 填充数据 ==========
+            def get_key(row):
+                item = row.get("Item Code", "")
+                name = row.get("产品名称", "")
+                return f"{item}_{name}"
+            
+            keys = [get_key(row) for _, row in self.df.iterrows()]
+            key_count = Counter(keys)
+            key_index = Counter()
+            
+            row_num = last_row
+            filled_count = 0
+            
+            self.log(f"\n✍️ 开始追加数据...")
+            
+            for idx, (_, src_row) in enumerate(self.df.iterrows()):
+                item_code = src_row.get("Item Code", "")
+                product_name = src_row.get("产品名称", "")
+                
+                if pd.isna(item_code):
+                    item_code = ""
+                if pd.isna(product_name):
+                    product_name = ""
+                
+                key = get_key(src_row)
+                
+                if key_count[key] > 1:
+                    key_index[key] += 1
+                    suffix = f".{key_index[key] - 1}"
+                    mfr_suffix = f".{key_index[key]}"
+                else:
+                    suffix = ""
+                    mfr_suffix = ""
+                
+                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:
+                    val = f"{item_code}{product_name}{suffix}"
+                    ws.cell(row=row_num, column=found_cols["Supplier Part Number"], value=val)
+                    fill_count += 1
+                
+                # Manufacturer Part Number
+                if "Manufacturer Part Number" in found_cols:
+                    val = f"{item_code}{product_name}{mfr_suffix}"
+                    ws.cell(row=row_num, column=found_cols["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
+                
+                # 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
+                
+                # 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
+                
+                # Carton
+                carton_map = [
+                    ("Carton Weight 1", "包装尺寸-重量"),
+                    ("Carton Height 1", "包装尺寸-高度"),
+                    ("Carton Width 1", "包装尺寸-宽度"),
+                    ("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
+                
+                # Color/Material
+                cm_map = [
+                    ("Base Color", "颜色"),
+                    ("Base Material", "材质"),
+                    ("Color", "颜色"),
+                    ("Material", "材质"),
+                    ("Top Color", "颜色"),
+                    ("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
+                
+                # Overall
+                overall_map = [
+                    ("Overall Depth - Front to Back", "组装长度"),
+                    ("Overall Height - Top to Bottom", "组装高度"),
+                    ("Overall Product Weight", "产品重量"),
+                    ("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 pd.notna(val):
+                                ws.cell(row=row_num, column=found_cols[target], value=val)
+                                fill_count += 1
+                
+                self.log(f"   📊 本行填充 {fill_count} 个字段")
+                row_num += 1
+                filled_count += 1
+            
+            # ========== 8. 直接保存到模板文件 ==========
+            wb.save(template_path)
+            self.log(f"\n✅ 成功追加到模板文件: {template_path}")
+            self.log(f"📊 共追加 {filled_count} 行数据")
+            self.status_text.set(f"✅ 已追加 {filled_count} 行到模板")
+            messagebox.showinfo("成功", f"已追加 {filled_count} 行数据到模板文件:\n{template_path}")
+            
+        except Exception as e:
+            self.log(f"\n❌ 错误: {str(e)}")
+            import traceback
+            self.log(traceback.format_exc())
+            messagebox.showerror("错误", f"生成失败:\n{str(e)}")
+
+if __name__ == "__main__":
+    root = tk.Tk()
+    app = ExcelMapperApp(root)
+    root.mainloop()

BIN
模板.xlsx