📘 SKU Map 編輯器 使用說明 📖 簡介 SKU Map 編輯器 是一個基於 Python Tkinter 開發的桌面應用程式, 主要功能是管理 SKU 對應表 (SKU Map) ,支援 JSON 檔案 與 Excel 檔案 的匯入匯出, 並可即時從線上取得產品清單(含價格與溫層資訊)。 此工具可協助快速維護多平台商品 SKU 對應關係,並計算總金額。 🛠 系統需求 Windows 10 / 11 Python 3.10 以上版本 已安裝模組: tkinter requests openpyxl 安裝缺少的套件: pip install requests openpyxl 📂 程式結構 sku_editor_v1.1.py        # 本程式主檔 程式會自動從以下網址載入產品清單: https://wu:wu2266228@ec.zfun.com.tw/plist.json 🚀 功能說明 1. 檔案操作 讀取 JSON 檔案 :開啟並載入 SKU Map。 儲存 JSON :將當前資料存回已開啟的 JSON 檔。 另存 JSON :將 SKU Map 另存為新的 JSON 檔。 匯入 Excel :讀取 Excel 檔案並轉換成 SKU Map。 匯出 Excel :將 SKU Map 以 Excel 格式匯出,並自動套用底色區隔不同原始 SKU。 重新載入產品清單 :從線上重新載入商品資料。 2. SKU 清單管理 搜尋原始 SKU :即時過濾 SKU 清單。 新增原始 SKU :輸入新的 SKU 並加入清單。 修改選取 SKU :將目前選取的 SKU 重新命名。 刪除選取 SKU :刪除當前選取的 SKU 及其子項目。 3. 子項目管理 每個原始 SKU 可對應多個子項目,包含以下欄位: 新SKU 產品名稱 (自動從線上清單帶出,含溫層資訊) 新數量 新進價 可執行操作: 新增子項 儲存子項修改 刪除子項 4. 總金額計算 自動計算所有子項 (數量 × 單價) 的合計。 顯示於畫面下方標籤,並四捨五入顯示。 📐 Excel 匯入/匯出格式 必要欄位 原始SKU 新SKU 產品名稱 新數量 新進價 匯出檔案 每個 原始 SKU 的子項會分組顯示。 系統會為不同原始 SKU 的區塊套用不同底色(淺檸檬色、淺青色、淺玫瑰色等)。 📦 資料結構範例 JSON 範例 { "A123": [ { "新SKU": "B456", "新數量": "10", "新進價": "150" }, { "新SKU": "C789", "新數量": "5", "新進價": "200" } ] } Excel 範例 原始SKU 新SKU 產品名稱 新數量 新進價 A123 B456 牛肉火鍋片(冷凍) 10 150 A123 C789 豬肉火鍋片(冷凍) 5 200 ⚙️ 程式原始碼重點 import json import tkinter as tk from tkinter import ttk, messagebox, filedialog import requests from openpyxl import Workbook, load_workbook from openpyxl.styles import PatternFill class SKUEditor:   def __init__(self, root):       self.root = root       self.root.title("SKU Map 編輯器")       self.file_path = None       self.sku_map = {}       self.product_list = []       self.product_lookup = {}       self.selected_sku = None       self.selected_item_index = None       self.setup_ui()       self.load_product_list_from_url()  # 程式啟動時自動載入產品列表   def load_product_list_from_url(self):       url = "https://wu:wu2266228@ec.zfun.com.tw/plist.json"       try:           response = requests.get(url)           response.raise_for_status()  # 檢查 HTTP 狀態碼           data = response.json()                   self.product_list = []           self.product_lookup = {}           for item in data:               sku = str(item.get("sku", "")).strip()               name = str(item.get("name", "")).strip()               price = str(item.get("price", "")).strip()               temp = str(item.get("temp", "")).strip()  # 取得溫層資訊               # 如果 temp 不為空,則在產品名稱後加上 (溫層)               if temp:                   display = f"{name}({temp})"               else:                   display = name               self.product_list.append((sku, display, price))               self.product_lookup[sku] = {"name": name, "price": price, "temp": temp}           self.update_sku_combobox("")           messagebox.showinfo("成功", "成功從線上載入產品清單!")       except Exception as e:           messagebox.showerror("讀取產品清單失敗", f"無法從 {url} 載入資料:{str(e)}")           self.product_list = []           self.product_lookup = {}   def update_sku_combobox(self, keyword):       keywords = keyword.lower().split()       filtered = []       for sku, display, price in self.product_list:           product_name = display.lower()           if all(k in product_name for k in keywords):               filtered.append(display)       self.sku_combo["values"] = filtered   def load_json(self, file_path):       try:           with open(file_path, "r", encoding="utf-8") as f:               self.sku_map = json.load(f)               self.file_path = file_path               self.refresh_sku_list()               self.tree.delete(*self.tree.get_children())               messagebox.showinfo("成功", f"成功讀取:{file_path}")       except Exception as e:           messagebox.showerror("錯誤", f"讀取 JSON 檔案失敗:{e}")   def save_json(self):       if not self.file_path:           messagebox.showwarning("未指定檔案", "請先讀取一個 JSON 檔案")           return       try:           with open(self.file_path, "w", encoding="utf-8") as f:               json.dump(self.sku_map, f, ensure_ascii=False, indent=4)           messagebox.showinfo("成功", f"資料已儲存至 {self.file_path}")       except Exception as e:           messagebox.showerror("儲存失敗", str(e))   def save_as_json(self):       file_path = filedialog.asksaveasfilename(           defaultextension=".json",           filetypes=[("JSON Files", "*.json")],           title="另存 JSON 檔案"       )       if not file_path:           return       try:           with open(file_path, "w", encoding="utf-8") as f:               json.dump(self.sku_map, f, ensure_ascii=False, indent=4)           self.file_path = file_path  # 更新當前檔案路徑           messagebox.showinfo("成功", f"JSON 檔案已儲存至 {file_path}")       except Exception as e:           messagebox.showerror("儲存失敗", f"無法儲存 JSON 檔案:{str(e)}")   def import_from_xlsx(self):       file_path = filedialog.askopenfilename(           filetypes=[("Excel Files", "*.xlsx")],           title="選擇 Excel 檔案"       )       if not file_path:           return       try:           wb = load_workbook(file_path)           ws = wb.active           headers = [cell.value for cell in ws[1]]           expected_headers = ["原始SKU", "新SKU", "產品名稱", "新數量", "新進價"]           if not all(h in headers for h in expected_headers):               messagebox.showerror("格式錯誤", "Excel 檔案缺少必要的欄位:原始SKU, 新SKU, 產品名稱, 新數量, 新進價")               return           self.sku_map = {}           for row in ws.iter_rows(min_row=2, values_only=True):               row_dict = dict(zip(headers, row))               original_sku = str(row_dict["原始SKU"]).strip()               new_sku = str(row_dict["新SKU"]).strip()               quantity = str(row_dict["新數量"]).strip()               price = str(row_dict["新進價"]).strip()               # 驗證新SKU是否存在於 product_lookup               if new_sku not in self.product_lookup:                   messagebox.showwarning("警告", f"SKU {new_sku} 不存在於產品清單中,將被忽略")                   continue               # 建立資料結構,不包含產品名稱               item = {                   "新SKU": new_sku,                   "新數量": quantity,                   "新進價": price               }               if original_sku not in self.sku_map:                   self.sku_map[original_sku] = []               self.sku_map[original_sku].append(item)           self.file_path = None  # 重置檔案路徑,因為這是新匯入的資料           self.refresh_sku_list()           self.tree.delete(*self.tree.get_children())           messagebox.showinfo("成功", f"成功從 {file_path} 匯入資料")       except Exception as e:           messagebox.showerror("匯入失敗", f"無法匯入 Excel 檔案:{str(e)}")   def export_to_xlsx(self):       file_path = filedialog.asksaveasfilename(           defaultextension=".xlsx",           filetypes=[("Excel Files", "*.xlsx")],           title="儲存 Excel 檔案"       )       if not file_path:           return       try:           wb = Workbook()           ws = wb.active           ws.title = "SKU Map"           # 寫入標頭           headers = ["原始SKU", "新SKU", "產品名稱", "新數量", "新進價"]           ws.append(headers)           # 定義顏色列表 (淺色調,16進位格式,無需前綴"0x")           colors = [               "FFFACD",  # 淺檸檬色               "E0FFFF",  # 淺青色               "FFE4E1",  # 淺玫瑰色               "E6E6FA",  # 淺薰衣草色               "F0FFF0",  # 蜂蜜露色           ]           # 收集所有資料並按原始SKU排序           data_rows = []           for original_sku, items in sorted(self.sku_map.items()):               for item in items:                   new_sku = item["新SKU"]                   product_info = self.product_lookup.get(new_sku, {})                   product_name = product_info.get("name", "未知產品")                   temp = product_info.get("temp", "")                   display_name = f"{product_name}({temp})" if temp else product_name                   quantity = item["新數量"]                   price = item["新進價"]                   data_rows.append([original_sku, new_sku, display_name, quantity, price])           # 追蹤當前原始SKU和顏色索引           current_sku = None           color_index = 0           for row_idx, row_data in enumerate(data_rows, start=2):  # 從第2行開始 (第1行是標頭)               original_sku = row_data[0]               # 如果原始SKU改變,切換到下一個顏色               if original_sku != current_sku:                   current_sku = original_sku                   color_index = (color_index + 1) % len(colors)  # 循環使用顏色               # 寫入資料行               ws.append(row_data)               # 應用顏色到整行               fill = PatternFill(start_color=colors[color_index], end_color=colors[color_index], fill_type="solid")               for col_idx in range(1, len(headers) + 1):                   ws.cell(row=row_idx, column=col_idx).fill = fill           # 儲存檔案           wb.save(file_path)           messagebox.showinfo("成功", f"Excel 檔案已匯出至 {file_path}")       except Exception as e:           messagebox.showerror("匯出失敗", f"無法匯出 Excel 檔案:{str(e)}")   def setup_ui(self):       top_frame = tk.Frame(self.root)       top_frame.pack(fill=tk.X, pady=5)       tk.Button(top_frame, text="讀取 JSON 檔案", command=self.select_json_file).pack(side=tk.LEFT, padx=5)       tk.Button(top_frame, text="儲存 JSON", command=self.save_json).pack(side=tk.LEFT, padx=5)       tk.Button(top_frame, text="另存 JSON", command=self.save_as_json).pack(side=tk.LEFT, padx=5)       tk.Button(top_frame, text="匯入 Excel", command=self.import_from_xlsx).pack(side=tk.LEFT, padx=5)       tk.Button(top_frame, text="匯出 Excel", command=self.export_to_xlsx).pack(side=tk.LEFT, padx=5)       tk.Button(top_frame, text="重新載入產品清單", command=self.load_product_list_from_url).pack(side=tk.LEFT, padx=5)       # 使用 PanedWindow 來讓左邊框架可以左右拖曳調整大小       self.paned_window = tk.PanedWindow(self.root, orient=tk.HORIZONTAL)       self.paned_window.pack(fill=tk.BOTH, expand=True)       self.left_frame = tk.Frame(self.paned_window)       self.center_frame = tk.Frame(self.paned_window)       self.right_frame = tk.Frame(self.paned_window)       self.paned_window.add(self.left_frame, minsize=150)       self.paned_window.add(self.center_frame, minsize=300)       self.paned_window.add(self.right_frame, minsize=150)       tk.Label(self.left_frame, text="搜尋原始 SKU").pack()       self.sku_search_var = tk.StringVar()       self.sku_search_entry = tk.Entry(self.left_frame, textvariable=self.sku_search_var)       self.sku_search_entry.pack()       self.sku_search_var.trace("w", lambda *args: self.search_sku())       tk.Label(self.left_frame, text="原始 SKU").pack()       self.sku_listbox = tk.Listbox(self.left_frame)       self.sku_listbox.pack(fill=tk.BOTH, expand=True)       self.sku_listbox.bind("<>", self.on_sku_select)       tk.Label(self.left_frame, text="➕ 新增原始 SKU").pack(pady=2)       self.new_sku_entry = tk.Entry(self.left_frame)       self.new_sku_entry.pack()       tk.Button(self.left_frame, text="新增 SKU", command=self.add_new_sku).pack(pady=2)       tk.Label(self.left_frame, text="✏️ 修改選取 SKU").pack(pady=2)       self.rename_sku_entry = tk.Entry(self.left_frame)       self.rename_sku_entry.pack()       tk.Button(self.left_frame, text="修改 SKU 名稱", command=self.rename_sku).pack(pady=2)       tk.Button(self.left_frame, text="❌ 刪除選取 SKU", command=self.delete_sku).pack(pady=5)       tk.Label(self.center_frame, text="子項目").pack()       self.tree = ttk.Treeview(self.center_frame, columns=("新SKU", "產品名稱", "新數量", "新進價"), show="headings")       for col in ("新SKU", "產品名稱", "新數量", "新進價"):           self.tree.heading(col, text=col)           self.tree.column(col, width=100)  # 設定欄位寬度       self.tree.pack(fill=tk.BOTH, expand=True)       self.tree.bind("<>", self.on_item_select)       # 總金額總和標籤       self.total_label = tk.Label(self.center_frame, text="總金額總和:0")       self.total_label.pack(pady=5)       tk.Label(self.right_frame, text="編輯子項目").pack(pady=5)       self.entries = {}       tk.Label(self.right_frame, text="搜尋 SKU").pack()       self.search_var = tk.StringVar()       self.search_entry = tk.Entry(self.right_frame, textvariable=self.search_var)       self.search_entry.pack()       self.search_var.trace("w", lambda *args: self.update_sku_combobox(self.search_var.get()))       self.sku_var = tk.StringVar()       self.sku_combo = ttk.Combobox(self.right_frame, textvariable=self.sku_var)       self.sku_combo.bind("<>", self.on_sku_selected)       tk.Label(self.right_frame, text="新SKU").pack()       self.sku_combo.pack()       self.entries["新SKU"] = self.sku_combo       for field in ("新數量", "新進價"):           tk.Label(self.right_frame, text=field).pack()           entry = tk.Entry(self.right_frame)           entry.pack()           self.entries[field] = entry       tk.Button(self.right_frame, text="儲存子項修改", command=self.save_item).pack(pady=5)       tk.Button(self.right_frame, text="新增子項", command=self.add_item).pack(pady=5)       tk.Button(self.right_frame, text="刪除子項", command=self.delete_item).pack(pady=5)   def search_sku(self):       keyword = self.sku_search_var.get().lower()       self.sku_listbox.delete(0, tk.END)       for sku in self.sku_map.keys():           if keyword in sku.lower():               self.sku_listbox.insert(tk.END, sku)   def select_json_file(self):       file_path = filedialog.askopenfilename(           filetypes=[("JSON Files", "*.json")],           title="選擇 JSON 檔案"       )       if file_path:           self.load_json(file_path)   def refresh_sku_list(self):       self.search_sku()  # Use search_sku to populate list with current search term   def refresh_items(self):       self.tree.delete(*self.tree.get_children())       total_sum = 0.0  # 用浮點數計算總金額總和       if self.selected_sku:           for item in self.sku_map[self.selected_sku]:               new_sku = item["新SKU"]               # 從 product_lookup 中獲取產品名稱和溫層               product_info = self.product_lookup.get(new_sku, {})               product_name = product_info.get("name", "未知產品")               temp = product_info.get("temp", "")               # 如果 temp 不為空,則在產品名稱後加上 (溫層)               if temp:                   display_name = f"{product_name}({temp})"               else:                   display_name = product_name               quantity = item["新數量"]               price = item["新進價"]               # 計算總金額,使用浮點數               try:                   total = float(quantity) * float(price)                   total_sum += total               except (ValueError, TypeError):                   total = 0.0               self.tree.insert("", tk.END, values=(new_sku, display_name, quantity, price))       # 四捨五入總金額總和       total_sum = round(total_sum)       self.total_label.config(text=f"總金額總和:{total_sum}")   def on_sku_select(self, event):       try:           index = self.sku_listbox.curselection()[0]           self.selected_sku = self.sku_listbox.get(index)           self.refresh_items()       except IndexError:           return   def on_item_select(self, event):       selected = self.tree.selection()       if selected:           self.selected_item_index = self.tree.index(selected[0])           values = self.tree.item(selected[0], "values")           self.sku_var.set(values[0])           self.entries["新數量"].delete(0, tk.END)           self.entries["新數量"].insert(0, values[2])           self.entries["新進價"].delete(0, tk.END)           self.entries["新進價"].insert(0, values[3])   def on_sku_selected(self, event):       selected = self.sku_var.get()       # 從顯示格式中提取 SKU       for sku, display, price in self.product_list:           if display == selected:               if sku in self.product_lookup:                   price = self.product_lookup[sku]["price"]                   self.entries["新進價"].delete(0, tk.END)                   self.entries["新進價"].insert(0, price)               break   def get_clean_entry(self, key):       if key == "新SKU":           selected = self.entries[key].get()           for sku, display, _ in self.product_list:               if display == selected:                   return sku           return selected       else:           return self.entries[key].get()   def save_item(self):       if self.selected_sku is None or self.selected_item_index is None:           return       new_data = {key: self.get_clean_entry(key) for key in self.entries}       self.sku_map[self.selected_sku][self.selected_item_index] = new_data       self.refresh_items()   def add_item(self):       if self.selected_sku is None:           return       new_data = {key: self.get_clean_entry(key) for key in self.entries}       self.sku_map[self.selected_sku].append(new_data)       self.refresh_items()   def delete_item(self):       if self.selected_sku is None or self.selected_item_index is None:           return       del self.sku_map[self.selected_sku][self.selected_item_index]       self.refresh_items()   def add_new_sku(self):       new_sku = self.new_sku_entry.get().strip()       if not new_sku:           return       if new_sku in self.sku_map:           messagebox.showwarning("已存在", "該 SKU 已存在")           return       self.sku_map[new_sku] = []       self.refresh_sku_list()       self.new_sku_entry.delete(0, tk.END)   def rename_sku(self):       new_name = self.rename_sku_entry.get().strip()       if not self.selected_sku or not new_name:           return       if new_name in self.sku_map:           messagebox.showwarning("名稱衝突", "新的 SKU 名稱已存在")           return       self.sku_map[new_name] = self.sku_map.pop(self.selected_sku)       self.selected_sku = new_name       self.refresh_sku_list()       self.rename_sku_entry.delete(0, tk.END)   def delete_sku(self):       if not self.selected_sku:           return       del self.sku_map[self.selected_sku]       self.selected_sku = None       self.refresh_sku_list()       self.tree.delete(*self.tree.get_children()) if __name__ == "__main__":   root = tk.Tk()   app = SKUEditor(root)   root.mainloop() 📷 介面說明 左側 :原始 SKU 清單(可搜尋/新增/刪除/修改)。 中間 :子項列表(支援排序與檢視)。 右側 :編輯區(可選取 SKU、輸入數量與進價)。 上方 :功能按鈕列(檔案操作與產品清單管理)。 ⚠️ 注意事項 產品清單來源需可連線 ,若讀取失敗,SKU 搜尋可能無法帶出產品名稱。 Excel 格式必須正確 ,若缺少必要欄位將匯入失敗。 編輯完成後記得 手動儲存 JSON 或匯出 Excel ,避免資料遺失。 子項輸入的數量與價格須為數字,否則計算總金額會失敗。