请大师帮我解决问题
```python
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
import sqlite3
import os
import shutil
import datetime
import webbrowser
import tempfile
from PIL import Image, ImageTk
import pandas as pd
from openpyxl import Workbook
from openpyxl.drawing.image import Image as XLImage
from openpyxl.utils.dataframe import dataframe_to_rows
import io
========== 数据库路径 ==========
DB_NAME = os.path.join(os.getcwd(), “flower_data.db”)
IMG_FOLDER = os.path.join(os.getcwd(), “product_images”)
LOGO_FOLDER = os.path.join(os.getcwd(), “company_logo”)
========== 初始化数据库(自动升级) ==========
def init_env():
if not os.path.exists(IMG_FOLDER):
os.makedirs(IMG_FOLDER)
if not os.path.exists(LOGO_FOLDER):
os.makedirs(LOGO_FOLDER)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
# ---- 客户表 ----
c.execute('''CREATE TABLE IF NOT EXISTS customers
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
contact_person TEXT,
phone TEXT,
address TEXT,
memo TEXT)''')
# ---- 供应商表 ----
c.execute('''CREATE TABLE IF NOT EXISTS suppliers
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
contact_person TEXT,
phone TEXT,
address TEXT,
memo TEXT)''')
# ---- 产品表(新增字段) ----
c.execute('''CREATE TABLE IF NOT EXISTS products
(id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE,
name TEXT,
img_path TEXT,
sale_price REAL DEFAULT 0.0,
supplier_id INTEGER,
customer_code TEXT, -- 客户货号
weight REAL DEFAULT 0, -- 重量(g)
size TEXT, -- 产品尺寸
inner_box_qty INTEGER DEFAULT 0,
outer_box_qty INTEGER DEFAULT 0,
outer_box_size TEXT,
inner_box_size TEXT,
FOREIGN KEY(supplier_id) REFERENCES suppliers(id))''')
# ---- 物料分类表(三级结构) ----
c.execute('''CREATE TABLE IF NOT EXISTS material_categories
(id INTEGER PRIMARY KEY AUTOINCREMENT,
level1 TEXT,
level2 TEXT,
level3 TEXT,
unit TEXT,
price REAL DEFAULT 0.0,
is_fabric BOOLEAN DEFAULT 1,
UNIQUE(level1, level2, level3))''')
# ---- 模具表 ----
c.execute('''CREATE TABLE IF NOT EXISTS molds
(id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE,
name TEXT,
cuts_per_yard REAL,
cost REAL DEFAULT 0.0)''')
# ---- 工序主表 ----
c.execute('''CREATE TABLE IF NOT EXISTS processes
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
description TEXT)''')
# ---- 工序选项表 ----
c.execute('''CREATE TABLE IF NOT EXISTS process_options
(id INTEGER PRIMARY KEY AUTOINCREMENT,
process_id INTEGER,
option_name TEXT,
unit_price REAL,
unit_desc TEXT,
FOREIGN KEY(process_id) REFERENCES processes(id))''')
# ---- BOM表(增加管理费标记) ----
c.execute('''CREATE TABLE IF NOT EXISTS bom
(id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER,
layer_code TEXT,
layer_name TEXT,
category_id INTEGER, -- 关联 material_categories.id
mold_id INTEGER,
qty REAL,
fabric_usage REAL,
unit_price REAL,
sort_order INTEGER,
remark TEXT,
is_management_fee BOOLEAN DEFAULT 0,
FOREIGN KEY(product_id) REFERENCES products(id),
FOREIGN KEY(category_id) REFERENCES material_categories(id),
FOREIGN KEY(mold_id) REFERENCES molds(id))''')
# ---- 工艺路线表 ----
c.execute('''CREATE TABLE IF NOT EXISTS product_processes
(id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER,
process_id INTEGER,
option_id INTEGER,
process_qty REAL,
unit_price REAL,
total_price REAL,
sort_order INTEGER,
FOREIGN KEY(product_id) REFERENCES products(id),
FOREIGN KEY(process_id) REFERENCES processes(id),
FOREIGN KEY(option_id) REFERENCES process_options(id))''')
# ---- 公司信息表 ----
c.execute('''CREATE TABLE IF NOT EXISTS company_info
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
address TEXT,
phone TEXT,
contact TEXT,
logo_path TEXT,
quote_title TEXT)''')
# ---- 报价单记录表(可选) ----
c.execute('''CREATE TABLE IF NOT EXISTS quote_records
(id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER,
quote_date TEXT,
products TEXT,
total_amount REAL,
memo TEXT,
FOREIGN KEY(customer_id) REFERENCES customers(id))''')
# ========== 强制修复旧数据库结构(新增列) ==========
try:
c.execute("ALTER TABLE products ADD COLUMN customer_code TEXT")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN weight REAL DEFAULT 0")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN size TEXT")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN inner_box_qty INTEGER DEFAULT 0")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN outer_box_qty INTEGER DEFAULT 0")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN outer_box_size TEXT")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN inner_box_size TEXT")
except: pass
try:
c.execute("ALTER TABLE bom ADD COLUMN category_id INTEGER")
except: pass
try:
c.execute("ALTER TABLE bom ADD COLUMN is_management_fee BOOLEAN DEFAULT 0")
except: pass
try:
c.execute("ALTER TABLE bom ADD COLUMN remark TEXT")
except: pass
try:
c.execute("ALTER TABLE materials ADD COLUMN type TEXT DEFAULT 'fabric'")
except: pass
try:
c.execute("ALTER TABLE materials ADD COLUMN unit TEXT DEFAULT '码'")
except: pass
try:
c.execute("ALTER TABLE products ADD COLUMN supplier_id INTEGER")
except: pass
conn.commit()
conn.close()
class App:
def init(self, root):
self.root = root
root.title(“仿真花产品管家 V7.0 - 完整版”)
root.geometry(“1400x980”)
self.current_product_id = None
self.img_tk = None
self.supplier_map = {}
self.material_map = {} # 用于BOM添加时快速查找分类
self.mold_map = {}
self.process_map = {}
self.option_map = {}
self.current_external_order = ""
# ---- 顶部工具栏 ----
frame_top = tk.Frame(root)
frame_top.pack(pady=5, fill=tk.X, padx=10)
tk.Label(frame_top, text="货号/名称:").pack(side=tk.LEFT)
self.entry_search = tk.Entry(frame_top, width=20)
self.entry_search.pack(side=tk.LEFT, padx=5)
self.entry_search.bind("<Return>", lambda e: self.search_product())
tk.Button(frame_top, text="搜索", command=self.search_product).pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="新增产品", command=self.add_product).pack(side=tk.LEFT, padx=5)
tk.Button(frame_top, text="🧺 原料管理", command=self.manage_materials, bg="#ffe6b3").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="🔧 模具管理", command=self.manage_molds, bg="#d9d9f0").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="⚙️ 工序管理", command=self.manage_processes_v2, bg="#b3d9ff").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="👥 客户管理", command=self.manage_customers, bg="#d9f0d9").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="🏢 供应商管理", command=self.manage_suppliers, bg="#f0d9d9").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="📞 缺货找供应商", command=self.find_supplier, bg="yellow").pack(side=tk.LEFT, padx=5)
tk.Button(frame_top, text="🏢 公司设置", command=self.manage_company, bg="#d9d9ff").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="📊 报价单", command=self.generate_quote, bg="#ffcc80").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="📦 导出全部数据", command=self.export_all_data_to_excel, bg="#ffcc80").pack(side=tk.LEFT, padx=2)
tk.Button(frame_top, text="刷新列表", command=self.load_product_list).pack(side=tk.LEFT, padx=2)
# ---- 主体分割 ----
main_pane = tk.PanedWindow(root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED, sashwidth=5)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# 左侧产品列表
left_frame = tk.Frame(main_pane)
main_pane.add(left_frame, width=250)
self.tree = ttk.Treeview(left_frame, columns=("code",), show="tree headings")
self.tree.heading("#0", text="ID")
self.tree.heading("code", text="货号")
self.tree.column("code", width=120)
self.tree.pack(fill=tk.BOTH, expand=True)
self.tree.bind('<<TreeviewSelect>>', self.on_select_product)
# 右侧详情面板
right_frame = tk.Frame(main_pane)
main_pane.add(right_frame, width=950)
# ---- 图片 & 基本信息 ----
info_frame = tk.Frame(right_frame)
info_frame.pack(fill=tk.X, pady=5)
self.img_label = tk.Label(info_frame, text="双击图片放大", bg="#eee", width=30, height=10)
self.img_label.pack(side=tk.LEFT, padx=10)
self.img_label.bind("<Double-Button-1>", self.show_big_image)
meta_frame = tk.Frame(info_frame)
meta_frame.pack(side=tk.LEFT, padx=20, fill=tk.BOTH)
tk.Label(meta_frame, text="货号:").grid(row=0, column=0, sticky='e')
self.lbl_code = tk.Label(meta_frame, text="", font=('Arial', 12, 'bold'))
self.lbl_code.grid(row=0, column=1, sticky='w')
tk.Label(meta_frame, text="名称:").grid(row=1, column=0, sticky='e')
self.lbl_name = tk.Label(meta_frame, text="")
self.lbl_name.grid(row=1, column=1, sticky='w')
tk.Label(meta_frame, text="销售单价:").grid(row=2, column=0, sticky='e')
self.entry_sale_price = tk.Entry(meta_frame, width=10)
self.entry_sale_price.grid(row=2, column=1, sticky='w')
tk.Button(meta_frame, text="💾 保存单价", command=self.save_sale_price).grid(row=2, column=2, padx=5)
tk.Label(meta_frame, text="首选供应商:").grid(row=3, column=0, sticky='e')
self.combo_supplier = ttk.Combobox(meta_frame, width=20, state="readonly")
self.combo_supplier.grid(row=3, column=1, sticky='w')
self.combo_supplier.bind('<<ComboboxSelected>>', self.on_supplier_select)
tk.Label(meta_frame, text="客户货号:").grid(row=4, column=0, sticky='e')
self.entry_customer_code = tk.Entry(meta_frame, width=15)
self.entry_customer_code.grid(row=4, column=1, sticky='w')
tk.Button(meta_frame, text="💾 保存客户货号", command=self.save_customer_code).grid(row=4, column=2, padx=5)
tk.Button(meta_frame, text="📷 选图片", command=self.select_image).grid(row=5, column=0, columnspan=2, pady=5)
tk.Button(meta_frame, text="删除此产品", command=self.delete_product, fg="red").grid(row=6, column=0, columnspan=2)
# ---- 分页标签 ----
self.notebook = ttk.Notebook(right_frame)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=10)
# ---- 页签1:BOM物料清单 ----
bom_frame = tk.Frame(self.notebook)
self.notebook.add(bom_frame, text="📋 ① BOM(物料清单)")
# ---- BOM 表头 ----
bom_header_frame = tk.Frame(bom_frame, bg="#f0f0f0", relief=tk.RIDGE, bd=2)
bom_header_frame.pack(fill=tk.X, padx=5, pady=5)
tk.Label(bom_header_frame, text="产品货号:", font=('Arial', 10, 'bold'), bg="#f0f0f0").pack(side=tk.LEFT, padx=5)
self.bom_header_code = tk.Label(bom_header_frame, text="(请选择产品)", font=('Arial', 10, 'bold'), fg="blue", bg="#f0f0f0")
self.bom_header_code.pack(side=tk.LEFT, padx=2)
tk.Label(bom_header_frame, text=" 名称:", font=('Arial', 10, 'bold'), bg="#f0f0f0").pack(side=tk.LEFT, padx=5)
self.bom_header_name = tk.Label(bom_header_frame, text="(请选择产品)", font=('Arial', 10, 'bold'), fg="blue", bg="#f0f0f0")
self.bom_header_name.pack(side=tk.LEFT, padx=2)
tk.Label(bom_header_frame, text=" 外部订单号:", font=('Arial', 10, 'bold'), bg="#f0f0f0").pack(side=tk.LEFT, padx=(20, 2))
self.entry_external_order = tk.Entry(bom_header_frame, width=20, font=('Arial', 10))
self.entry_external_order.pack(side=tk.LEFT, padx=2)
self.entry_external_order.bind("<KeyRelease>", self.on_external_order_change)
# ---- BOM 操作按钮区 ----
bom_entry_frame = tk.Frame(bom_frame)
bom_entry_frame.pack(fill=tk.X, pady=5)
self.var_layer = tk.StringVar(value="L1")
self.var_qty = tk.DoubleVar(value=1.0)
self.var_mold_id = tk.IntVar(value=0)
self.var_category_id = tk.IntVar(value=0)
self.var_insert_after = tk.StringVar(value="末尾")
tk.Label(bom_entry_frame, text="层级:").pack(side=tk.LEFT)
self.entry_layer = tk.Entry(bom_entry_frame, textvariable=self.var_layer, width=6)
self.entry_layer.pack(side=tk.LEFT)
tk.Label(bom_entry_frame, text="分类:").pack(side=tk.LEFT, padx=(5,0))
self.combo_level1 = ttk.Combobox(bom_entry_frame, width=10, state="readonly")
self.combo_level1.pack(side=tk.LEFT)
self.combo_level1.bind('<<ComboboxSelected>>', self.on_level1_select)
self.combo_level2 = ttk.Combobox(bom_entry_frame, width=10, state="readonly")
self.combo_level2.pack(side=tk.LEFT)
self.combo_level2.bind('<<ComboboxSelected>>', self.on_level2_select)
self.combo_level3 = ttk.Combobox(bom_entry_frame, width=15, state="readonly")
self.combo_level3.pack(side=tk.LEFT)
self.combo_level3.bind('<<ComboboxSelected>>', self.on_level3_select)
tk.Label(bom_entry_frame, text="数量:").pack(side=tk.LEFT, padx=(5,0))
self.entry_qty = tk.Entry(bom_entry_frame, textvariable=self.var_qty, width=5)
self.entry_qty.pack(side=tk.LEFT)
tk.Label(bom_entry_frame, text="插入位置:").pack(side=tk.LEFT, padx=(5,0))
self.combo_insert_pos = ttk.Combobox(bom_entry_frame, width=8, state="readonly")
self.combo_insert_pos.pack(side=tk.LEFT)
tk.Label(bom_entry_frame, text="备注:").pack(side=tk.LEFT, padx=(5,0))
self.entry_bom_remark = tk.Entry(bom_entry_frame, width=12)
self.entry_bom_remark.pack(side=tk.LEFT)
btn_group = tk.Frame(bom_entry_frame)
btn_group.pack(side=tk.LEFT, padx=10)
tk.Button(btn_group, text="➕ 添加", command=self.add_bom_row, bg="#90ee90", width=6).pack(side=tk.LEFT, padx=1)
tk.Button(btn_group, text="✏️ 编辑", command=self.edit_bom_row, bg="#ffd700", width=6).pack(side=tk.LEFT, padx=1)
tk.Button(btn_group, text="🗑️ 删除", command=self.delete_bom_row, fg="red", width=6).pack(side=tk.LEFT, padx=1)
tk.Button(btn_group, text="➕ 管理费", command=self.add_management_fee, bg="#ffb3b3", width=8).pack(side=tk.LEFT, padx=1)
tk.Button(btn_group, text="清空", command=self.clear_bom, fg="red", width=6).pack(side=tk.LEFT, padx=1)
# ---- BOM 表格 ----
cols_bom = ('层级', '分类', '物料名称', '数量', '单价', '用布(码)', '成本', '备注')
self.bom_tree = ttk.Treeview(bom_frame, columns=cols_bom, show='headings', height=8)
col_w = [80, 100, 150, 60, 80, 80, 80, 120]
for i, col in enumerate(cols_bom):
self.bom_tree.heading(col, text=col)
self.bom_tree.column(col, width=col_w[i])
# 滚动条
bom_table_frame = tk.Frame(bom_frame)
bom_table_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
scroll_y = ttk.Scrollbar(bom_table_frame, orient=tk.VERTICAL, command=self.bom_tree.yview)
scroll_x = ttk.Scrollbar(bom_table_frame, orient=tk.HORIZONTAL, command=self.bom_tree.xview)
self.bom_tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
self.bom_tree.grid(row=0, column=0, sticky="nsew")
scroll_y.grid(row=0, column=1, sticky="ns")
scroll_x.grid(row=1, column=0, sticky="ew")
bom_table_frame.grid_rowconfigure(0, weight=1)
bom_table_frame.grid_columnconfigure(0, weight=1)
self.bom_tree.bind("<Double-Button-1>", lambda e: self.edit_bom_row())
# ========== BOM 操作函数 ==========
def load_bom(self, pid):
for i in self.bom_tree.get_children():
self.bom_tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("""
SELECT b.layer_code,
mc.level1 || '/' || mc.level2 AS category,
mc.level3 AS material,
b.qty,
b.unit_price,
b.fabric_usage,
b.qty * b.unit_price AS cost,
b.remark,
b.is_management_fee
FROM bom b
LEFT JOIN material_categories mc ON b.category_id = mc.id
WHERE b.product_id=? ORDER BY
CASE WHEN b.layer_code GLOB 'L[0-9]*' THEN 1 ELSE 2 END,
b.layer_code
""", (pid,))
rows = c.fetchall()
for row in rows:
if row[9]: # 管理费行
layer_display = "管理费"
else:
layer_display = row[0]
self.bom_tree.insert("", "end", values=(
layer_display,
row[1] if row[1] else "",
row[2] if row[2] else "",
row[3] if row[3] else 0,
f"{row[4]:.4f}" if row[4] else "0.0000",
f"{row[5]:.4f}" if row[5] else "0.0000",
f"{row[6]:.2f}" if row[6] else "0.00",
row[7] if row[7] else ""
))
conn.close()
self.calc_total()
self.update_insert_positions()
def add_bom_row(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选择产品")
return
category_id = self.var_category_id.get()
qty = self.var_qty.get()
layer_code = self.var_layer.get().strip()
if not layer_code:
messagebox.showwarning("提示", "请输入层级代码(如 L1)")
return
if category_id == 0:
messagebox.showwarning("提示", "请选择完整的物料分类")
return
# 获取分类信息
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT level1, level2, level3, unit, price, is_fabric FROM material_categories WHERE id=?", (category_id,))
cat = c.fetchone()
if not cat:
conn.close()
return
is_fabric = cat[5]
# 如果是布料,必须关联模具
mold_id = 0
unit_price = 0
fabric_usage = 0
if is_fabric:
# 简单起见,使用默认的模具(提示用户选择)
mold_id = self.var_mold_id.get()
if mold_id == 0:
messagebox.showwarning("提示", "布料必须选择模具")
conn.close()
return
c.execute("SELECT cuts_per_yard FROM molds WHERE id=?", (mold_id,))
cuts = c.fetchone()
if cuts and cuts[0] > 0:
fabric_usage = qty / cuts[0]
unit_price = fabric_usage * cat[4] # 布料单价
else:
messagebox.showerror("错误", "模具冲片数无效")
conn.close()
return
else:
# 配件直接按单价计算
unit_price = cat[4] # 单价
fabric_usage = 0
# 计算排序号
insert_pos = self.combo_insert_pos.get()
if insert_pos == "末尾":
c.execute("SELECT MAX(sort_order) FROM bom WHERE product_id=?", (self.current_product_id,))
max_order = c.fetchone()[0] or 0
new_order = max_order + 1
else:
c.execute("SELECT sort_order FROM bom WHERE product_id=? AND layer_code=? ORDER BY sort_order LIMIT 1",
(self.current_product_id, insert_pos))
row = c.fetchone()
if row:
target_order = row[0]
c.execute("UPDATE bom SET sort_order = sort_order + 1 WHERE product_id=? AND sort_order >= ?",
(self.current_product_id, target_order))
new_order = target_order
else:
new_order = 0
remark = self.entry_bom_remark.get().strip()
c.execute("""INSERT INTO bom
(product_id, layer_code, category_id, mold_id, qty, fabric_usage, unit_price, sort_order, remark, is_management_fee)
VALUES (?,?,?,?,?,?,?,?,?,0)""",
(self.current_product_id, layer_code, category_id, mold_id, qty, fabric_usage, unit_price, new_order, remark))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
self.var_qty.set(1.0)
self.entry_qty.focus_set()
self.entry_qty.select_range(0, tk.END)
messagebox.showinfo("成功", "BOM行已添加")
def add_management_fee(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选择产品")
return
# 弹窗输入管理费金额
fee = simpledialog.askfloat("管理费", "请输入管理费金额(元/朵):", minvalue=0)
if fee is None:
return
if fee < 0:
messagebox.showerror("错误", "金额不能为负数")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
# 检查是否已有管理费行
c.execute("SELECT id FROM bom WHERE product_id=? AND is_management_fee=1", (self.current_product_id,))
exist = c.fetchone()
if exist:
c.execute("UPDATE bom SET unit_price=? WHERE id=?", (fee, exist[0]))
messagebox.showinfo("更新", "管理费已更新")
else:
# 获取最大排序号
c.execute("SELECT MAX(sort_order) FROM bom WHERE product_id=?", (self.current_product_id,))
max_order = c.fetchone()[0] or 0
c.execute("""INSERT INTO bom
(product_id, layer_code, category_id, qty, unit_price, sort_order, is_management_fee, remark)
VALUES (?, 'MGT', NULL, 1, ?, ?, 1, '管理费')""",
(self.current_product_id, fee, max_order+1))
messagebox.showinfo("成功", "管理费已添加")
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
def edit_bom_row(self):
selected = self.bom_tree.focus()
if not selected:
return
# 简化编辑:直接删除后重新添加(或弹窗编辑)
# 为简化,此处略,实际可做完整编辑功能
messagebox.showinfo("提示", "编辑功能:双击行后,您可以删除并重新添加")
def delete_bom_row(self):
selected = self.bom_tree.focus()
if not selected:
return
vals = self.bom_tree.item(selected)['values']
if not vals:
return
layer = vals[0]
if layer == "管理费":
# 删除管理费行
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM bom WHERE product_id=? AND is_management_fee=1", (self.current_product_id,))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
return
# 否则根据层级和物料删除
if messagebox.askyesno("确认", f"确定删除层级 {layer} 吗?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM bom WHERE product_id=? AND layer_code=?", (self.current_product_id, layer))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
def clear_bom(self):
if not self.current_product_id:
return
if messagebox.askyesno("确认", "清空所有BOM行?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM bom WHERE product_id=?", (self.current_product_id,))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
def update_insert_positions(self):
if not self.current_product_id:
self.combo_insert_pos['values'] = ["末尾"]
self.combo_insert_pos.current(0)
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT layer_code FROM bom WHERE product_id=? AND is_management_fee=0 ORDER BY sort_order", (self.current_product_id,))
rows = c.fetchall()
conn.close()
positions = ["末尾"] + [row[0] for row in rows]
self.combo_insert_pos['values'] = positions
if positions:
self.combo_insert_pos.current(0)
def calc_total(self):
total_material = 0.0
for child in self.bom_tree.get_children():
vals = self.bom_tree.item(child)['values']
if len(vals) >= 7 and vals[6]:
total_material += float(vals[6])
# 显示在底部,但底部栏在后面定义,先略
self.lbl_total_cost.config(text=f"💰 总成本: {total_material:.2f} 元")
# ========== 报价单生成器 ==========
def generate_quote(self):
"""报价单生成窗口"""
win = tk.Toplevel(self.root)
win.title("生成报价单")
win.geometry("1100x750")
# 选择客户
tk.Label(win, text="选择客户:").grid(row=0, column=0, padx=10, pady=10, sticky='e')
combo_customer = ttk.Combobox(win, width=30, state="readonly")
combo_customer.grid(row=0, column=1, padx=10, pady=10, sticky='w')
# 加载客户列表
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, name FROM customers ORDER BY name")
customers = c.fetchall()
conn.close()
customer_names = [row[1] for row in customers]
combo_customer['values'] = customer_names
if customer_names:
combo_customer.current(0)
# 选择产品(多选)
tk.Label(win, text="选择产品(按住Ctrl多选):").grid(row=1, column=0, padx=10, pady=10, sticky='ne')
# 产品列表(多选列表框)
from tkinter import Listbox, MULTIPLE
listbox_products = Listbox(win, selectmode=MULTIPLE, width=60, height=12)
listbox_products.grid(row=1, column=1, padx=10, pady=10, sticky='w')
# 加载产品
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, code, name FROM products ORDER BY code")
products = c.fetchall()
conn.close()
product_map = {}
for p in products:
display = f"{p[1]} - {p[2]}"
listbox_products.insert(tk.END, display)
product_map[display] = p[0]
# 利润率设置
tk.Label(win, text="统一利润率(%):").grid(row=2, column=0, padx=10, pady=10, sticky='e')
entry_profit = tk.Entry(win, width=10)
entry_profit.insert(0, "30")
entry_profit.grid(row=2, column=1, padx=10, pady=10, sticky='w')
# 备注
tk.Label(win, text="备注:").grid(row=3, column=0, padx=10, pady=10, sticky='ne')
text_memo = tk.Text(win, width=50, height=4)
text_memo.grid(row=3, column=1, padx=10, pady=10, sticky='w')
text_memo.insert(tk.END, "报价不包含盆器\n运费到盐田")
# 生成按钮
def do_generate():
# 获取选中的客户
customer_name = combo_customer.get()
if not customer_name:
messagebox.showwarning("提示", "请选择客户")
return
# 获取选中的产品
selected_indices = listbox_products.curselection()
if not selected_indices:
messagebox.showwarning("提示", "请至少选择一个产品")
return
selected_products = []
for idx in selected_indices:
display = listbox_products.get(idx)
pid = product_map[display]
selected_products.append(pid)
# 获取利润率
try:
profit_rate = float(entry_profit.get()) / 100
except:
messagebox.showerror("错误", "利润率请输入数字")
return
# 获取备注
memo = text_memo.get("1.0", tk.END).strip()
# 生成报价单(预览窗口)
self.preview_quote(customer_name, selected_products, profit_rate, memo)
tk.Button(win, text="📊 生成报价单", command=do_generate, bg="#4CAF50", fg="white", font=('Arial', 12, 'bold')).grid(row=4, column=0, columnspan=2, pady=20)
def preview_quote(self, customer_name, product_ids, profit_rate, memo):
"""预览报价单"""
# 获取公司信息
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT name, address, phone, contact, logo_path, quote_title FROM company_info LIMIT 1")
company = c.fetchone()
conn.close()
company_name = company[0] if company and company[0] else "您的公司名称"
logo_path = company[4] if company and company[4] else ""
quote_title = company[5] if company and company[5] else "报价单"
# 收集产品数据
product_data = []
total_amount = 0.0
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
for pid in product_ids:
c.execute("SELECT code, name, customer_code, weight, size, inner_box_qty, outer_box_qty, outer_box_size, img_path FROM products WHERE id=?", (pid,))
prod = c.fetchone()
if not prod:
continue
# 计算该产品的成本(BOM总成本)
c.execute("SELECT SUM(unit_price * qty) FROM bom WHERE product_id=? AND is_management_fee=0", (pid,))
mat_cost = c.fetchone()[0] or 0.0
c.execute("SELECT unit_price FROM bom WHERE product_id=? AND is_management_fee=1", (pid,))
mgmt = c.fetchone()
mgmt_fee = mgmt[0] if mgmt else 0.0
total_cost = mat_cost + mgmt_fee
# 利润率可单独调整(这里用统一)
price = total_cost * (1 + profit_rate)
# 含税价
price_tax = price * 1.13
total_amount += price_tax
# 获取产品图片(用于显示)
img_path = prod[8] if prod[8] and os.path.exists(prod[8]) else ""
product_data.append({
'code': prod[0],
'name': prod[1],
'customer_code': prod[2] if prod[2] else '',
'weight': prod[3] if prod[3] else 0,
'size': prod[4] if prod[4] else '',
'inner_qty': prod[5] if prod[5] else 0,
'outer_qty': prod[6] if prod[6] else 0,
'outer_size': prod[7] if prod[7] else '',
'img_path': img_path,
'price': price,
'price_tax': price_tax,
'cost': total_cost
})
conn.close()
# 创建预览窗口
preview_win = tk.Toplevel(self.root)
preview_win.title("报价单预览")
preview_win.geometry("1200x800")
# 使用HTML预览(浏览器打印更美观)
html_content = self.build_quote_html(company_name, logo_path, quote_title, customer_name, product_data, profit_rate, memo, total_amount)
# 写入临时HTML文件
temp_html = tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8')
temp_html.write(html_content)
temp_html.close()
# 在窗口中显示HTML(使用WebView或直接打开浏览器)
# 由于Tkinter没有内置WebView,使用浏览器打开
webbrowser.open(temp_html.name)
# 同时提供导出Excel按钮
def export_excel():
self.export_quote_excel(company_name, customer_name, product_data, memo, total_amount)
btn_frame = tk.Frame(preview_win)
btn_frame.pack(pady=10)
tk.Button(btn_frame, text="📤 导出Excel", command=export_excel, bg="#ffcc80").pack(side=tk.LEFT, padx=10)
tk.Button(btn_frame, text="🔄 刷新", command=lambda: webbrowser.open(temp_html.name)).pack(side=tk.LEFT, padx=10)
tk.Button(btn_frame, text="❌ 关闭", command=preview_win.destroy).pack(side=tk.LEFT, padx=10)
def build_quote_html(self, company_name, logo_path, quote_title, customer_name, product_data, profit_rate, memo, total_amount):
"""构建报价单HTML"""
# 处理Logo
logo_img = ""
if logo_path and os.path.exists(logo_path):
logo_img = f'<img src="{logo_path}" height="60" style="float:left; margin-right:20px;">'
rows = ""
for idx, p in enumerate(product_data, 1):
rows += f"""
<tr>
<td>{idx}</td>
<td>{p['customer_code']}</td>
<td>{p['code']}</td>
<td>{p['name']}</td>
<td>{p['weight']}</td>
<td>{p['size']}</td>
<td>{p['inner_qty']}</td>
<td>{p['outer_qty']}</td>
<td>{p['outer_size']}</td>
<td>{p['price']:.2f}</td>
<td>{p['price_tax']:.2f}</td>
</tr>
"""
html = f"""
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>报价单</title>
<style>
body {{ font-family: "Microsoft YaHei", Arial, sans-serif; margin: 40px; }}
.header {{ display: flex; align-items: center; margin-bottom: 20px; }}
.header img {{ max-height: 60px; margin-right: 20px; }}
.company-name {{ font-size: 28px; font-weight: bold; }}
.title {{ font-size: 22px; font-weight: bold; text-align: center; margin: 10px 0; }}
.info {{ margin-bottom: 20px; }}
table {{ border-collapse: collapse; width: 100%; font-size: 12px; }}
th, td {{ border: 1px solid #333; padding: 6px 8px; text-align: center; }}
th {{ background-color: #e8e8e8; font-weight: bold; }}
.total {{ font-weight: bold; background-color: #f5f5f5; }}
.memo {{ margin-top: 20px; white-space: pre-wrap; }}
.footer {{ margin-top: 30px; text-align: right; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="header">
{logo_img}
<div><span class="company-name">{company_name}</span></div>
</div>
<div class="title">{quote_title}</div>
<div class="info">
<strong>TO:</strong>{customer_name}
<strong>日期:</strong>{datetime.datetime.now().strftime('%Y-%m-%d')}
</div>
<table>
<thead>
<tr>
<th>序号</th><th>客户货号</th><th>工厂货号</th><th>名称</th>
<th>重量(g)</th><th>尺寸</th><th>内盒装数</th><th>外箱装数</th>
<th>外箱尺寸(CM)</th><th>不含税价</th><th>含税价(13%)</th>
</tr>
</thead>
<tbody>
{rows}
<tr class="total">
<td colspan="10" style="text-align:right;"><strong>总金额:</strong></td>
<td><strong>{total_amount:.2f}</strong></td>
</tr>
</tbody>
</table>
<div class="memo"><strong>备注:</strong>{memo}</div>
<div class="footer">—— 本报价单由「仿真花产品管家」生成 ——</div>
</body>
</html>
"""
return html
def export_quote_excel(self, company_name, customer_name, product_data, memo, total_amount):
"""导出报价单到Excel(含图片)"""
from tkinter import filedialog
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel 文件", "*.xlsx")],
initialfile=f"报价单_{customer_name}_{datetime.datetime.now().strftime('%Y%m%d')}.xlsx"
)
if not file_path:
return
wb = Workbook()
ws = wb.active
ws.title = "报价单"
# 写入公司名称和标题
ws['A1'] = company_name
ws.merge_cells('A1:K1')
ws['A1'].font = wb.Font(size=20, bold=True)
ws['A2'] = "报价单"
ws.merge_cells('A2:K2')
ws['A2'].font = wb.Font(size=16, bold=True)
ws['A3'] = f"TO:{customer_name}"
ws['A3'].font = wb.Font(size=12)
ws['A3'] = f"日期:{datetime.datetime.now().strftime('%Y-%m-%d')}"
# 表头
headers = ['序号', '客户货号', '工厂货号', '名称', '重量(g)', '尺寸', '内盒装数', '外箱装数', '外箱尺寸(CM)', '不含税价', '含税价(13%)']
for col, header in enumerate(headers, 1):
cell = ws.cell(row=4, column=col, value=header)
cell.font = wb.Font(bold=True)
cell.alignment = wb.Alignment(horizontal='center', vertical='center')
# 写入数据
for idx, p in enumerate(product_data, 1):
row = 4 + idx
ws.cell(row=row, column=1, value=idx)
ws.cell(row=row, column=2, value=p['customer_code'])
ws.cell(row=row, column=3, value=p['code'])
ws.cell(row=row, column=4, value=p['name'])
ws.cell(row=row, column=5, value=p['weight'])
ws.cell(row=row, column=6, value=p['size'])
ws.cell(row=row, column=7, value=p['inner_qty'])
ws.cell(row=row, column=8, value=p['outer_qty'])
ws.cell(row=row, column=9, value=p['outer_size'])
ws.cell(row=row, column=10, value=p['price'])
ws.cell(row=row, column=11, value=p['price_tax'])
# 插入图片(如果有)
if p['img_path'] and os.path.exists(p['img_path']):
try:
img = XLImage(p['img_path'])
img.width = 80
img.height = 80
# 图片放在M列(第12列)
ws.add_image(img, f'M{row}')
except:
pass
# 合计行
total_row = 5 + len(product_data)
ws.cell(row=total_row, column=10, value="总金额:")
ws.cell(row=total_row, column=11, value=total_amount)
ws.cell(row=total_row, column=10).font = wb.Font(bold=True)
ws.cell(row=total_row, column=11).font = wb.Font(bold=True)
# 备注
memo_row = total_row + 2
ws.cell(row=memo_row, column=1, value="备注:")
ws.merge_cells(start_row=memo_row, start_column=1, end_row=memo_row, end_column=11)
ws.cell(row=memo_row, column=1, value=memo)
# 调整列宽
for col in range(1, 12):
ws.column_dimensions[chr(64+col)].width = 15
wb.save(file_path)
messagebox.showinfo("成功", f"报价单已导出:{file_path}")
# ========== 其他管理窗口(简写,您已有完整实现) ==========
def manage_materials(self):
“””原料管理窗口 - 全中文界面 + 键盘流录入”””
win = tk.Toplevel(self.root)
win.title(“原材料管理”)
win.geometry(“700x500”)
tree = ttk.Treeview(win, columns=('ID','名称','单位','单价','类型'), show='headings')
tree.heading('ID', text='ID')
tree.heading('名称', text='原料名称')
tree.heading('单位', text='单位')
tree.heading('单价', text='单价(元)')
tree.heading('类型', text='类型')
tree.column('ID', width=30)
tree.column('名称', width=150)
tree.column('单位', width=80)
tree.column('单价', width=80)
tree.column('类型', width=80)
tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
entry_frame = tk.Frame(win)
entry_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(entry_frame, text="名称:").grid(row=0, column=0, padx=2, pady=2)
entry_name = tk.Entry(entry_frame, width=15)
entry_name.grid(row=0, column=1, padx=2)
tk.Label(entry_frame, text="单位:").grid(row=0, column=2, padx=2)
entry_unit = tk.Entry(entry_frame, width=8)
entry_unit.grid(row=0, column=3, padx=2)
tk.Label(entry_frame, text="单价:").grid(row=0, column=4, padx=2)
entry_price = tk.Entry(entry_frame, width=10)
entry_price.grid(row=0, column=5, padx=2)
tk.Label(entry_frame, text="类型:").grid(row=0, column=6, padx=2)
combo_type = ttk.Combobox(entry_frame, values=["布料", "辅料"], width=8)
combo_type.grid(row=0, column=7, padx=2)
combo_type.set("布料")
def load_data():
for i in tree.get_children():
tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, name, unit, price, type FROM materials")
for row in c.fetchall():
display_type = "布料" if row[4] == "fabric" else "辅料"
tree.insert("", "end", values=(row[0], row[1], row[2], row[3], display_type))
conn.close()
def clear_entries():
entry_name.delete(0, tk.END)
entry_unit.delete(0, tk.END)
entry_price.delete(0, tk.END)
combo_type.set("布料")
entry_name.focus_set()
def add_material():
name = entry_name.get().strip()
unit = entry_unit.get().strip()
price_str = entry_price.get().strip()
mat_type_display = combo_type.get().strip() or "布料"
mat_type = "fabric" if mat_type_display == "布料" else "accessory"
if not name or not unit or not price_str:
messagebox.showwarning("提示", "请完整填写名称、单位和单价")
return
try:
price = float(price_str)
except:
messagebox.showerror("错误", "单价请输入数字")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("INSERT INTO materials (name, unit, price, type) VALUES (?,?,?,?)",
(name, unit, price, mat_type))
conn.commit()
messagebox.showinfo("成功", f"已添加:{name}")
load_data()
self.load_material_combobox()
clear_entries()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"原料「{name}」已存在")
conn.close()
def on_double_click(event):
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
edit_win = tk.Toplevel(win)
edit_win.title(f"修改原料 - {vals[1]}")
edit_win.geometry("350x280")
edit_win.transient(win)
tk.Label(edit_win, text="名称:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
edit_name = tk.Entry(edit_win, width=20)
edit_name.insert(0, vals[1])
edit_name.grid(row=0, column=1, padx=10)
tk.Label(edit_win, text="单位:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
edit_unit = tk.Entry(edit_win, width=20)
edit_unit.insert(0, vals[2])
edit_unit.grid(row=1, column=1, padx=10)
tk.Label(edit_win, text="单价:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
edit_price = tk.Entry(edit_win, width=20)
edit_price.insert(0, vals[3])
edit_price.grid(row=2, column=1, padx=10)
tk.Label(edit_win, text="类型:").grid(row=3, column=0, padx=10, pady=5, sticky='e')
edit_combo = ttk.Combobox(edit_win, values=["布料", "辅料"], width=18)
current_type_display = "布料" if vals[4] == "布料" else "辅料"
edit_combo.set(current_type_display)
edit_combo.grid(row=3, column=1, padx=10)
def save_edit():
new_name = edit_name.get().strip()
new_unit = edit_unit.get().strip()
try:
new_price = float(edit_price.get().strip())
except:
messagebox.showerror("错误", "单价请输入数字")
return
type_display = edit_combo.get().strip() or "布料"
new_type = "fabric" if type_display == "布料" else "accessory"
if not new_name or not new_unit:
messagebox.showwarning("提示", "名称和单位不能为空")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("UPDATE materials SET name=?, unit=?, price=?, type=? WHERE id=?",
(new_name, new_unit, new_price, new_type, vals[0]))
conn.commit()
messagebox.showinfo("成功", "已更新")
edit_win.destroy()
load_data()
self.load_material_combobox()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"名称「{new_name}」已被使用")
conn.close()
edit_name.bind("<Return>", lambda e: edit_unit.focus_set())
edit_unit.bind("<Return>", lambda e: edit_price.focus_set())
edit_price.bind("<Return>", lambda e: edit_combo.focus_set())
edit_combo.bind("<Return>", lambda e: btn_save.focus_set())
btn_save = tk.Button(edit_win, text="💾 保存修改", command=save_edit, bg="#90ee90")
btn_save.grid(row=4, column=0, columnspan=2, pady=15)
btn_save.bind("<Return>", lambda e: save_edit())
edit_win.bind("<Return>", lambda e: btn_save.focus_set())
edit_win.after(100, edit_name.focus_set)
tree.bind("<Double-Button-1>", on_double_click)
def delete_selected():
selected = tree.focus()
if not selected:
messagebox.showwarning("提示", "请先点击选中要删除的行")
return
vals = tree.item(selected)['values']
if not vals:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM bom WHERE material_id=?", (vals[0],))
count = c.fetchone()[0]
conn.close()
if count > 0:
messagebox.showwarning("无法删除", f"该原料被 {count} 个BOM引用,请先删除相关BOM再删除原料")
return
if messagebox.askyesno("确认删除", f"确定要删除原料「{vals[1]}」吗?\n(单价:{vals[3]}元/{vals[2]})"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM materials WHERE id=?", (vals[0],))
conn.commit()
conn.close()
load_data()
self.load_material_combobox()
messagebox.showinfo("已删除", f"已删除原料「{vals[1]}」")
button_frame = tk.Frame(win)
button_frame.pack(fill=tk.X, padx=10, pady=10)
btn_add = tk.Button(button_frame, text="➕ 添加", command=add_material, bg="#90ee90", width=10)
btn_add.pack(side=tk.LEFT, padx=5)
btn_add.bind("<Return>", lambda e: add_material())
tk.Button(button_frame, text="🗑️ 删除选中", command=delete_selected, fg="red", width=15).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="🔄 刷新", command=load_data, width=10).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="❌ 关闭", command=win.destroy, width=10).pack(side=tk.LEFT, padx=5)
entry_name.bind("<Return>", lambda e: entry_unit.focus_set())
entry_unit.bind("<Return>", lambda e: entry_price.focus_set())
entry_price.bind("<Return>", lambda e: combo_type.focus_set())
combo_type.bind("<Return>", lambda e: btn_add.focus_set())
win.after(100, entry_name.focus_set)
load_data()
def manage_molds(self):
"""模具管理窗口 - 键盘流录入 + 双击修改"""
win = tk.Toplevel(self.root)
win.title("模具/刀模管理 - 键盘流录入")
win.geometry("750x500")
tree = ttk.Treeview(win, columns=('ID','编号','名称','每码冲片数','制作费'), show='headings')
for col in ['ID','编号','名称','每码冲片数','制作费']:
tree.heading(col, text=col)
tree.column(col, width=120)
tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
entry_frame = tk.Frame(win)
entry_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(entry_frame, text="编号:").grid(row=0, column=0, padx=2, pady=2)
entry_code = tk.Entry(entry_frame, width=15)
entry_code.grid(row=0, column=1, padx=2)
tk.Label(entry_frame, text="名称:").grid(row=0, column=2, padx=2)
entry_name = tk.Entry(entry_frame, width=15)
entry_name.grid(row=0, column=3, padx=2)
tk.Label(entry_frame, text="每码冲片数:").grid(row=0, column=4, padx=2)
entry_cuts = tk.Entry(entry_frame, width=10)
entry_cuts.grid(row=0, column=5, padx=2)
tk.Label(entry_frame, text="制作费(元):").grid(row=0, column=6, padx=2)
entry_cost = tk.Entry(entry_frame, width=10)
entry_cost.grid(row=0, column=7, padx=2)
def load_data():
for i in tree.get_children():
tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT * FROM molds ORDER BY code")
for row in c.fetchall():
tree.insert("", "end", values=row)
conn.close()
def clear_entries():
entry_code.delete(0, tk.END)
entry_name.delete(0, tk.END)
entry_cuts.delete(0, tk.END)
entry_cost.delete(0, tk.END)
entry_code.focus_set()
def add_mold():
code = entry_code.get().strip()
name = entry_name.get().strip()
cuts_str = entry_cuts.get().strip()
cost_str = entry_cost.get().strip()
if not code or not name or not cuts_str:
messagebox.showwarning("提示", "请完整填写编号、名称和每码冲片数")
return
try:
cuts = float(cuts_str)
except:
messagebox.showerror("错误", "每码冲片数请输入数字")
return
try:
cost = float(cost_str) if cost_str else 0
except:
messagebox.showerror("错误", "制作费请输入数字")
return
if cuts <= 0:
messagebox.showerror("错误", "每码冲片数必须大于0")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("INSERT INTO molds (code, name, cuts_per_yard, cost) VALUES (?,?,?,?)",
(code, name, cuts, cost))
conn.commit()
messagebox.showinfo("成功", f"模具「{code}」已添加")
load_data()
self.load_mold_combobox()
clear_entries()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"模具编号「{code}」已存在")
conn.close()
def on_double_click(event):
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
edit_win = tk.Toplevel(win)
edit_win.title(f"修改模具 - {vals[1]}")
edit_win.geometry("360x300")
edit_win.transient(win)
tk.Label(edit_win, text="编号:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
edit_code = tk.Entry(edit_win, width=20)
edit_code.insert(0, vals[1])
edit_code.grid(row=0, column=1, padx=10)
tk.Label(edit_win, text="名称:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
edit_name = tk.Entry(edit_win, width=20)
edit_name.insert(0, vals[2])
edit_name.grid(row=1, column=1, padx=10)
tk.Label(edit_win, text="每码冲片数:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
edit_cuts = tk.Entry(edit_win, width=20)
edit_cuts.insert(0, vals[3])
edit_cuts.grid(row=2, column=1, padx=10)
tk.Label(edit_win, text="制作费(元):").grid(row=3, column=0, padx=10, pady=5, sticky='e')
edit_cost = tk.Entry(edit_win, width=20)
edit_cost.insert(0, vals[4] if vals[4] else 0)
edit_cost.grid(row=3, column=1, padx=10)
def save_edit():
new_code = edit_code.get().strip()
new_name = edit_name.get().strip()
try:
new_cuts = float(edit_cuts.get().strip())
except:
messagebox.showerror("错误", "每码冲片数请输入数字")
return
try:
new_cost = float(edit_cost.get().strip() or 0)
except:
messagebox.showerror("错误", "制作费请输入数字")
return
if not new_code or not new_name:
messagebox.showwarning("提示", "编号和名称不能为空")
return
if new_cuts <= 0:
messagebox.showerror("错误", "每码冲片数必须大于0")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("UPDATE molds SET code=?, name=?, cuts_per_yard=?, cost=? WHERE id=?",
(new_code, new_name, new_cuts, new_cost, vals[0]))
conn.commit()
messagebox.showinfo("成功", "模具已更新")
edit_win.destroy()
load_data()
self.load_mold_combobox()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"编号「{new_code}」已被使用")
conn.close()
edit_code.bind("<Return>", lambda e: edit_name.focus_set())
edit_name.bind("<Return>", lambda e: edit_cuts.focus_set())
edit_cuts.bind("<Return>", lambda e: edit_cost.focus_set())
edit_cost.bind("<Return>", lambda e: btn_save.focus_set())
btn_save = tk.Button(edit_win, text="💾 保存修改", command=save_edit, bg="#90ee90")
btn_save.grid(row=4, column=0, columnspan=2, pady=15)
btn_save.bind("<Return>", lambda e: save_edit())
edit_win.after(100, edit_code.focus_set)
tree.bind("<Double-Button-1>", on_double_click)
def delete_mold():
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM bom WHERE mold_id=?", (vals[0],))
count = c.fetchone()[0]
conn.close()
if count > 0:
messagebox.showwarning("无法删除", f"该模具被 {count} 个BOM引用")
return
if messagebox.askyesno("确认删除", f"确定删除模具 {vals[1]}({vals[2]})吗?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM molds WHERE id=?", (vals[0],))
conn.commit()
conn.close()
load_data()
self.load_mold_combobox()
button_frame = tk.Frame(win)
button_frame.pack(fill=tk.X, padx=10, pady=10)
btn_add = tk.Button(button_frame, text="➕ 添加", command=add_mold, bg="#90ee90", width=10)
btn_add.pack(side=tk.LEFT, padx=5)
btn_add.bind("<Return>", lambda e: add_mold())
tk.Button(button_frame, text="🗑️ 删除选中", command=delete_mold, fg="red", width=15).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="🔄 刷新", command=load_data, width=10).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="❌ 关闭", command=win.destroy, width=10).pack(side=tk.LEFT, padx=5)
entry_code.bind("<Return>", lambda e: entry_name.focus_set())
entry_name.bind("<Return>", lambda e: entry_cuts.focus_set())
entry_cuts.bind("<Return>", lambda e: entry_cost.focus_set())
entry_cost.bind("<Return>", lambda e: btn_add.focus_set())
win.after(100, entry_code.focus_set)
load_data()
def manage_customers(self):
“””客户管理窗口…”””
win = tk.Toplevel(self.root)
win.title(“客户信息管理 “)
win.geometry(“650x500”)
tree = ttk.Treeview(win, columns=('ID','名称','联系人','电话','地址','备注'), show='headings')
for col in ['ID','名称','联系人','电话','地址','备注']:
tree.heading(col, text=col)
tree.column(col, width=100)
tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
entry_frame = tk.Frame(win)
entry_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(entry_frame, text="名称:").grid(row=0, column=0, padx=2, pady=2)
entry_name = tk.Entry(entry_frame, width=15)
entry_name.grid(row=0, column=1, padx=2)
tk.Label(entry_frame, text="联系人:").grid(row=0, column=2, padx=2)
entry_contact = tk.Entry(entry_frame, width=12)
entry_contact.grid(row=0, column=3, padx=2)
tk.Label(entry_frame, text="电话:").grid(row=0, column=4, padx=2)
entry_phone = tk.Entry(entry_frame, width=15)
entry_phone.grid(row=0, column=5, padx=2)
tk.Label(entry_frame, text="地址:").grid(row=1, column=0, padx=2, pady=2)
entry_address = tk.Entry(entry_frame, width=40)
entry_address.grid(row=1, column=1, columnspan=5, padx=2)
def load_data():
for i in tree.get_children():
tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT * FROM customers")
for row in c.fetchall():
tree.insert("", "end", values=row)
conn.close()
def clear_entries():
entry_name.delete(0, tk.END)
entry_contact.delete(0, tk.END)
entry_phone.delete(0, tk.END)
entry_address.delete(0, tk.END)
entry_name.focus_set()
def add_customer():
name = entry_name.get().strip()
contact = entry_contact.get().strip()
phone = entry_phone.get().strip()
address = entry_address.get().strip()
if not name:
messagebox.showwarning("提示", "客户名称不能为空")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("INSERT INTO customers (name, contact_person, phone, address) VALUES (?,?,?,?)",
(name, contact, phone, address))
conn.commit()
messagebox.showinfo("成功", f"客户「{name}」已添加")
load_data()
clear_entries()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"客户「{name}」已存在")
conn.close()
def on_double_click(event):
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
edit_win = tk.Toplevel(win)
edit_win.title(f"修改客户 - {vals[1]}")
edit_win.geometry("400x320")
edit_win.transient(win)
tk.Label(edit_win, text="名称:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
edit_name = tk.Entry(edit_win, width=25)
edit_name.insert(0, vals[1])
edit_name.grid(row=0, column=1, padx=10)
tk.Label(edit_win, text="联系人:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
edit_contact = tk.Entry(edit_win, width=25)
edit_contact.insert(0, vals[2] if vals[2] else "")
edit_contact.grid(row=1, column=1, padx=10)
tk.Label(edit_win, text="电话:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
edit_phone = tk.Entry(edit_win, width=25)
edit_phone.insert(0, vals[3] if vals[3] else "")
edit_phone.grid(row=2, column=1, padx=10)
tk.Label(edit_win, text="地址:").grid(row=3, column=0, padx=10, pady=5, sticky='e')
edit_address = tk.Entry(edit_win, width=25)
edit_address.insert(0, vals[4] if vals[4] else "")
edit_address.grid(row=3, column=1, padx=10)
tk.Label(edit_win, text="备注:").grid(row=4, column=0, padx=10, pady=5, sticky='e')
edit_memo = tk.Entry(edit_win, width=25)
edit_memo.insert(0, vals[5] if vals[5] else "")
edit_memo.grid(row=4, column=1, padx=10)
def save_edit():
new_name = edit_name.get().strip()
if not new_name:
messagebox.showwarning("提示", "客户名称不能为空")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("UPDATE customers SET name=?, contact_person=?, phone=?, address=?, memo=? WHERE id=?",
(new_name, edit_contact.get().strip(), edit_phone.get().strip(),
edit_address.get().strip(), edit_memo.get().strip(), vals[0]))
conn.commit()
messagebox.showinfo("成功", "客户已更新")
edit_win.destroy()
load_data()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"名称「{new_name}」已被使用")
conn.close()
edit_name.bind("<Return>", lambda e: edit_contact.focus_set())
edit_contact.bind("<Return>", lambda e: edit_phone.focus_set())
edit_phone.bind("<Return>", lambda e: edit_address.focus_set())
edit_address.bind("<Return>", lambda e: edit_memo.focus_set())
edit_memo.bind("<Return>", lambda e: btn_save.focus_set())
btn_save = tk.Button(edit_win, text="💾 保存修改", command=save_edit, bg="#90ee90")
btn_save.grid(row=5, column=0, columnspan=2, pady=15)
btn_save.bind("<Return>", lambda e: save_edit())
edit_win.after(100, edit_name.focus_set)
tree.bind("<Double-Button-1>", on_double_click)
def delete_customer():
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
if messagebox.askyesno("确认删除", f"确定删除客户「{vals[1]}」吗?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM customers WHERE id=?", (vals[0],))
conn.commit()
conn.close()
load_data()
button_frame = tk.Frame(win)
button_frame.pack(fill=tk.X, padx=10, pady=10)
btn_add = tk.Button(button_frame, text="➕ 添加", command=add_customer, bg="#90ee90", width=10)
btn_add.pack(side=tk.LEFT, padx=5)
btn_add.bind("<Return>", lambda e: add_customer())
tk.Button(button_frame, text="🗑️ 删除选中", command=delete_customer, fg="red", width=15).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="🔄 刷新", command=load_data, width=10).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="❌ 关闭", command=win.destroy, width=10).pack(side=tk.LEFT, padx=5)
entry_name.bind("<Return>", lambda e: entry_contact.focus_set())
entry_contact.bind("<Return>", lambda e: entry_phone.focus_set())
entry_phone.bind("<Return>", lambda e: entry_address.focus_set())
entry_address.bind("<Return>", lambda e: btn_add.focus_set())
win.after(100, entry_name.focus_set)
load_data()
def manage_suppliers(self):
"""供应商管理窗口 - 键盘流录入 + 双击修改"""
win = tk.Toplevel(self.root)
win.title("供应商信息管理 - 键盘流录入")
win.geometry("650x500")
tree = ttk.Treeview(win, columns=('ID','名称','联系人','电话','地址','备注'), show='headings')
for col in ['ID','名称','联系人','电话','地址','备注']:
tree.heading(col, text=col)
tree.column(col, width=100)
tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
entry_frame = tk.Frame(win)
entry_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(entry_frame, text="名称:").grid(row=0, column=0, padx=2, pady=2)
entry_name = tk.Entry(entry_frame, width=15)
entry_name.grid(row=0, column=1, padx=2)
tk.Label(entry_frame, text="联系人:").grid(row=0, column=2, padx=2)
entry_contact = tk.Entry(entry_frame, width=12)
entry_contact.grid(row=0, column=3, padx=2)
tk.Label(entry_frame, text="电话:").grid(row=0, column=4, padx=2)
entry_phone = tk.Entry(entry_frame, width=15)
entry_phone.grid(row=0, column=5, padx=2)
tk.Label(entry_frame, text="地址:").grid(row=1, column=0, padx=2, pady=2)
entry_address = tk.Entry(entry_frame, width=40)
entry_address.grid(row=1, column=1, columnspan=5, padx=2)
def load_data():
for i in tree.get_children():
tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT * FROM suppliers")
for row in c.fetchall():
tree.insert("", "end", values=row)
conn.close()
def clear_entries():
entry_name.delete(0, tk.END)
entry_contact.delete(0, tk.END)
entry_phone.delete(0, tk.END)
entry_address.delete(0, tk.END)
entry_name.focus_set()
def add_supplier():
name = entry_name.get().strip()
contact = entry_contact.get().strip()
phone = entry_phone.get().strip()
address = entry_address.get().strip()
if not name:
messagebox.showwarning("提示", "供应商名称不能为空")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("INSERT INTO suppliers (name, contact_person, phone, address) VALUES (?,?,?,?)",
(name, contact, phone, address))
conn.commit()
messagebox.showinfo("成功", f"供应商「{name}」已添加")
load_data()
self.load_supplier_combobox()
clear_entries()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"供应商「{name}」已存在")
conn.close()
def on_double_click(event):
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
edit_win = tk.Toplevel(win)
edit_win.title(f"修改供应商 - {vals[1]}")
edit_win.geometry("400x320")
edit_win.transient(win)
tk.Label(edit_win, text="名称:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
edit_name = tk.Entry(edit_win, width=25)
edit_name.insert(0, vals[1])
edit_name.grid(row=0, column=1, padx=10)
tk.Label(edit_win, text="联系人:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
edit_contact = tk.Entry(edit_win, width=25)
edit_contact.insert(0, vals[2] if vals[2] else "")
edit_contact.grid(row=1, column=1, padx=10)
tk.Label(edit_win, text="电话:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
edit_phone = tk.Entry(edit_win, width=25)
edit_phone.insert(0, vals[3] if vals[3] else "")
edit_phone.grid(row=2, column=1, padx=10)
tk.Label(edit_win, text="地址:").grid(row=3, column=0, padx=10, pady=5, sticky='e')
edit_address = tk.Entry(edit_win, width=25)
edit_address.insert(0, vals[4] if vals[4] else "")
edit_address.grid(row=3, column=1, padx=10)
tk.Label(edit_win, text="备注:").grid(row=4, column=0, padx=10, pady=5, sticky='e')
edit_memo = tk.Entry(edit_win, width=25)
edit_memo.insert(0, vals[5] if vals[5] else "")
edit_memo.grid(row=4, column=1, padx=10)
def save_edit():
new_name = edit_name.get().strip()
if not new_name:
messagebox.showwarning("提示", "供应商名称不能为空")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("UPDATE suppliers SET name=?, contact_person=?, phone=?, address=?, memo=? WHERE id=?",
(new_name, edit_contact.get().strip(), edit_phone.get().strip(),
edit_address.get().strip(), edit_memo.get().strip(), vals[0]))
conn.commit()
messagebox.showinfo("成功", "供应商已更新")
edit_win.destroy()
load_data()
self.load_supplier_combobox()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"名称「{new_name}」已被使用")
conn.close()
edit_name.bind("<Return>", lambda e: edit_contact.focus_set())
edit_contact.bind("<Return>", lambda e: edit_phone.focus_set())
edit_phone.bind("<Return>", lambda e: edit_address.focus_set())
edit_address.bind("<Return>", lambda e: edit_memo.focus_set())
edit_memo.bind("<Return>", lambda e: btn_save.focus_set())
btn_save = tk.Button(edit_win, text="💾 保存修改", command=save_edit, bg="#90ee90")
btn_save.grid(row=5, column=0, columnspan=2, pady=15)
btn_save.bind("<Return>", lambda e: save_edit())
edit_win.after(100, edit_name.focus_set)
tree.bind("<Double-Button-1>", on_double_click)
def delete_supplier():
selected = tree.focus()
if not selected:
return
vals = tree.item(selected)['values']
if not vals:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM products WHERE supplier_id=?", (vals[0],))
count = c.fetchone()[0]
conn.close()
if count > 0:
messagebox.showwarning("无法删除", f"该供应商被 {count} 个产品引用")
return
if messagebox.askyesno("确认删除", f"确定删除供应商「{vals[1]}」吗?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM suppliers WHERE id=?", (vals[0],))
conn.commit()
conn.close()
load_data()
self.load_supplier_combobox()
button_frame = tk.Frame(win)
button_frame.pack(fill=tk.X, padx=10, pady=10)
btn_add = tk.Button(button_frame, text="➕ 添加", command=add_supplier, bg="#90ee90", width=10)
btn_add.pack(side=tk.LEFT, padx=5)
btn_add.bind("<Return>", lambda e: add_supplier())
tk.Button(button_frame, text="🗑️ 删除选中", command=delete_supplier, fg="red", width=15).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="🔄 刷新", command=load_data, width=10).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="❌ 关闭", command=win.destroy, width=10).pack(side=tk.LEFT, padx=5)
entry_name.bind("<Return>", lambda e: entry_contact.focus_set())
entry_contact.bind("<Return>", lambda e: entry_phone.focus_set())
entry_phone.bind("<Return>", lambda e: entry_address.focus_set())
entry_address.bind("<Return>", lambda e: btn_add.focus_set())
win.after(100, entry_name.focus_set)
load_data()
def manage_processes_v2(self):
“””工序管理窗口 - 独立容器版(顺序修正)”””
win = tk.Toplevel(self.root)
win.title(“工序与选项管理 - 连续录入”)
win.geometry(“900x600”)
win.minsize(800, 500)
# ---- 1. 主容器 ----
main_frame = tk.Frame(win)
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# ---- 2. 表格区域 ----
table_frame = tk.Frame(main_frame)
table_frame.pack(fill=tk.BOTH, expand=True)
# 左侧工序列表
left_frame = tk.Frame(table_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2)
tk.Label(left_frame, text="工序列表", font=('Arial', 10, 'bold')).pack(anchor='w')
process_tree = ttk.Treeview(left_frame, columns=('ID', '名称'), show='headings', height=14)
process_tree.heading('ID', text='ID')
process_tree.heading('名称', text='工序名称')
process_tree.column('ID', width=40)
process_tree.column('名称', width=150)
process_tree.pack(fill=tk.BOTH, expand=True, pady=5)
# 右侧选项列表
right_frame = tk.Frame(table_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=2)
tk.Label(right_frame, text="工序选项(双击修改)", font=('Arial', 10, 'bold')).pack(anchor='w')
option_tree = ttk.Treeview(right_frame, columns=('ID', '选项名', '单价', '单位'), show='headings', height=14)
option_tree.heading('ID', text='ID')
option_tree.heading('选项名', text='选项名')
option_tree.heading('单价', text='单价(元)')
option_tree.heading('单位', text='计价单位')
option_tree.column('ID', width=40)
option_tree.column('选项名', width=120)
option_tree.column('单价', width=80)
option_tree.column('单位', width=80)
option_tree.pack(fill=tk.BOTH, expand=True, pady=5)
# ---- 3. 底部操作栏 ----
bottom_frame = tk.Frame(main_frame, height=130, bg='#f0f0f0')
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5)
bottom_frame.pack_propagate(False)
# ---- ★★★ 先定义所有功能函数(顺序调整) ★★★ ----
def load_processes():
for i in process_tree.get_children():
process_tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, name FROM processes ORDER BY name")
for row in c.fetchall():
process_tree.insert("", "end", values=row)
conn.close()
def load_options(proc_id):
for i in option_tree.get_children():
option_tree.delete(i)
if proc_id is None:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, option_name, unit_price, unit_desc FROM process_options WHERE process_id=? ORDER BY option_name", (proc_id,))
for row in c.fetchall():
option_tree.insert("", "end", values=row)
conn.close()
def on_select_process(event):
selected = process_tree.focus()
if selected:
vals = process_tree.item(selected)['values']
if vals:
load_options(vals[0])
process_tree.bind('<<TreeviewSelect>>', on_select_process)
def add_process():
name = entry_process_name.get().strip()
if not name:
messagebox.showwarning("提示", "请输入工序名称")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("INSERT INTO processes (name) VALUES (?)", (name,))
conn.commit()
messagebox.showinfo("成功", f"工序「{name}」已添加")
entry_process_name.delete(0, tk.END)
entry_process_name.focus_set()
load_processes()
self.load_process_combobox()
except sqlite3.IntegrityError:
messagebox.showerror("错误", f"工序「{name}」已存在")
conn.close()
def delete_process():
selected = process_tree.focus()
if not selected:
messagebox.showwarning("提示", "请先选中要删除的工序")
return
vals = process_tree.item(selected)['values']
if not vals:
return
if messagebox.askyesno("确认删除", f"确定要删除工序「{vals[1]}」及其所有选项吗?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM process_options WHERE process_id=?", (vals[0],))
c.execute("DELETE FROM processes WHERE id=?", (vals[0],))
conn.commit()
conn.close()
load_processes()
self.load_process_combobox()
for i in option_tree.get_children():
option_tree.delete(i)
messagebox.showinfo("已删除", f"工序「{vals[1]}」已删除")
def add_option():
selected = process_tree.focus()
if not selected:
messagebox.showwarning("提示", "请先在左侧选中一个工序")
return
proc_vals = process_tree.item(selected)['values']
if not proc_vals:
return
opt_name = entry_option_name.get().strip()
price_str = entry_option_price.get().strip()
unit = entry_option_unit.get().strip()
if not opt_name or not price_str or not unit:
messagebox.showwarning("提示", "请完整填写选项名、单价和单位")
return
try:
price = float(price_str)
except:
messagebox.showerror("错误", "单价请输入数字")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
try:
c.execute("INSERT INTO process_options (process_id, option_name, unit_price, unit_desc) VALUES (?,?,?,?)",
(proc_vals[0], opt_name, price, unit))
conn.commit()
messagebox.showinfo("成功", f"选项「{opt_name}」已添加")
entry_option_name.delete(0, tk.END)
entry_option_price.delete(0, tk.END)
entry_option_unit.delete(0, tk.END)
entry_option_name.focus_set()
load_options(proc_vals[0])
self.load_process_combobox()
except Exception as e:
messagebox.showerror("错误", f"添加失败:{str(e)}")
conn.close()
def delete_option():
selected_option = option_tree.focus()
if not selected_option:
messagebox.showwarning("提示", "请先选中要删除的选项")
return
vals = option_tree.item(selected_option)['values']
if not vals:
return
if messagebox.askyesno("确认删除", f"确定要删除选项「{vals[1]}」吗?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM process_options WHERE id=?", (vals[0],))
conn.commit()
conn.close()
selected = process_tree.focus()
if selected:
proc_vals = process_tree.item(selected)['values']
if proc_vals:
load_options(proc_vals[0])
self.load_process_combobox()
messagebox.showinfo("已删除", f"选项「{vals[1]}」已删除")
def on_option_double_click():
selected_option = option_tree.focus()
if not selected_option:
messagebox.showwarning("提示", "请先选中要修改的选项")
return
vals = option_tree.item(selected_option)['values']
if not vals:
return
edit_win = tk.Toplevel(win)
edit_win.title(f"修改选项 - {vals[1]}")
edit_win.geometry("340x260")
edit_win.transient(win)
edit_win.grab_set()
tk.Label(edit_win, text="选项名:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
edit_name = tk.Entry(edit_win, width=20)
edit_name.insert(0, vals[1])
edit_name.grid(row=0, column=1, padx=10, pady=5)
tk.Label(edit_win, text="单价(元):").grid(row=1, column=0, padx=10, pady=5, sticky='e')
edit_price = tk.Entry(edit_win, width=20)
edit_price.insert(0, vals[2])
edit_price.grid(row=1, column=1, padx=10, pady=5)
tk.Label(edit_win, text="计价单位:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
edit_unit = tk.Entry(edit_win, width=20)
edit_unit.insert(0, vals[3])
edit_unit.grid(row=2, column=1, padx=10, pady=5)
def save_option_edit():
new_name = edit_name.get().strip()
try:
new_price = float(edit_price.get().strip())
except:
messagebox.showerror("错误", "单价请输入数字")
return
new_unit = edit_unit.get().strip()
if not new_name or not new_unit:
messagebox.showwarning("提示", "选项名和单位不能为空")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("UPDATE process_options SET option_name=?, unit_price=?, unit_desc=? WHERE id=?",
(new_name, new_price, new_unit, vals[0]))
conn.commit()
conn.close()
messagebox.showinfo("成功", "选项已更新")
edit_win.destroy()
selected = process_tree.focus()
if selected:
proc_vals = process_tree.item(selected)['values']
if proc_vals:
load_options(proc_vals[0])
self.load_process_combobox()
edit_name.bind("<Return>", lambda e: edit_price.focus_set())
edit_price.bind("<Return>", lambda e: edit_unit.focus_set())
edit_unit.bind("<Return>", lambda e: btn_save.focus_set())
btn_save = tk.Button(edit_win, text="💾 保存修改", command=save_option_edit, bg="#90ee90")
btn_save.grid(row=3, column=0, columnspan=2, pady=15)
btn_save.bind("<Return>", lambda e: save_option_edit())
edit_win.after(100, edit_name.focus_set)
# ---- 绑定双击事件 ----
option_tree.bind("<Double-Button-1>", lambda e: on_option_double_click())
# ---- ★★★ 现在定义按钮(此时所有功能函数已存在) ★★★ ----
# 工序操作行
process_op_frame = tk.Frame(bottom_frame, bg='#f0f0f0')
process_op_frame.pack(fill=tk.X, pady=3)
tk.Label(process_op_frame, text="新增工序名称:", bg='#f0f0f0').pack(side=tk.LEFT, padx=2)
entry_process_name = tk.Entry(process_op_frame, width=15)
entry_process_name.pack(side=tk.LEFT, padx=2)
btn_add_process = tk.Button(process_op_frame, text="➕ 添加工序", command=add_process, bg="#90ee90", width=12)
btn_add_process.pack(side=tk.LEFT, padx=2)
tk.Button(process_op_frame, text="🗑️ 删除工序", command=delete_process, fg="red", width=15).pack(side=tk.LEFT, padx=2)
# 选项操作行
option_op_frame = tk.Frame(bottom_frame, bg='#f0f0f0')
option_op_frame.pack(fill=tk.X, pady=3)
tk.Label(option_op_frame, text="为选中工序添加选项:", bg='#f0f0f0').pack(side=tk.LEFT, padx=2)
entry_option_name = tk.Entry(option_op_frame, width=12)
entry_option_name.pack(side=tk.LEFT, padx=2)
tk.Label(option_op_frame, text="单价:", bg='#f0f0f0').pack(side=tk.LEFT, padx=2)
entry_option_price = tk.Entry(option_op_frame, width=8)
entry_option_price.pack(side=tk.LEFT, padx=2)
tk.Label(option_op_frame, text="单位:", bg='#f0f0f0').pack(side=tk.LEFT, padx=2)
entry_option_unit = tk.Entry(option_op_frame, width=6)
entry_option_unit.pack(side=tk.LEFT, padx=2)
btn_add_option = tk.Button(option_op_frame, text="➕ 添加选项", command=add_option, bg="#90ee90", width=12)
btn_add_option.pack(side=tk.LEFT, padx=2)
tk.Button(option_op_frame, text="🗑️ 删除选项", command=delete_option, fg="red", width=15).pack(side=tk.LEFT, padx=2)
# 关闭按钮
tk.Button(bottom_frame, text="❌ 关闭", command=win.destroy, bg="#f0f0f0", width=10).pack(pady=5)
# ---- 键盘流绑定 ----
entry_process_name.bind("<Return>", lambda e: btn_add_process.focus_set())
btn_add_process.bind("<Return>", lambda e: add_process())
entry_option_name.bind("<Return>", lambda e: entry_option_price.focus_set())
entry_option_price.bind("<Return>", lambda e: entry_option_unit.focus_set())
entry_option_unit.bind("<Return>", lambda e: btn_add_option.focus_set())
btn_add_option.bind("<Return>", lambda e: add_option())
# ---- 窗口打开后默认聚焦 ----
win.after(100, entry_process_name.focus_set)
load_processes()
def manage_company(self):
"""公司信息设置窗口"""
win = tk.Toplevel(self.root)
win.title("公司信息设置")
win.geometry("500x400")
# 读取当前信息
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT name, address, phone, contact, logo_path, quote_title FROM company_info LIMIT 1")
info = c.fetchone()
conn.close()
if not info:
info = ("", "", "", "", "", "报价单")
# 输入框
tk.Label(win, text="公司名称:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
entry_name = tk.Entry(win, width=30)
entry_name.insert(0, info[0] if info[0] else "")
entry_name.grid(row=0, column=1, padx=10, pady=5)
tk.Label(win, text="地址:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
entry_address = tk.Entry(win, width=30)
entry_address.insert(0, info[1] if info[1] else "")
entry_address.grid(row=1, column=1, padx=10, pady=5)
tk.Label(win, text="电话:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
entry_phone = tk.Entry(win, width=30)
entry_phone.insert(0, info[2] if info[2] else "")
entry_phone.grid(row=2, column=1, padx=10, pady=5)
tk.Label(win, text="联系人:").grid(row=3, column=0, padx=10, pady=5, sticky='e')
entry_contact = tk.Entry(win, width=30)
entry_contact.insert(0, info[3] if info[3] else "")
entry_contact.grid(row=3, column=1, padx=10, pady=5)
tk.Label(win, text="报价单标题:").grid(row=4, column=0, padx=10, pady=5, sticky='e')
entry_title = tk.Entry(win, width=30)
entry_title.insert(0, info[5] if info[5] else "报价单")
entry_title.grid(row=4, column=1, padx=10, pady=5)
tk.Label(win, text="Logo(PNG/JPG):").grid(row=5, column=0, padx=10, pady=5, sticky='e')
lbl_logo = tk.Label(win, text="未选择")
lbl_logo.grid(row=5, column=1, padx=10, pady=5, sticky='w')
def select_logo():
path = filedialog.askopenfilename(filetypes=[("图片", "*.png *.jpg *.jpeg")])
if path:
# 复制到公司logo文件夹
ext = os.path.splitext(path)[1]
dest = os.path.join(LOGO_FOLDER, f"logo{ext}")
shutil.copyfile(path, dest)
lbl_logo.config(text=dest)
tk.Button(win, text="选择Logo", command=select_logo).grid(row=5, column=2, padx=5)
def save_company():
name = entry_name.get().strip()
address = entry_address.get().strip()
phone = entry_phone.get().strip()
contact = entry_contact.get().strip()
title = entry_title.get().strip()
logo_path = lbl_logo.cget("text")
if logo_path == "未选择":
logo_path = ""
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM company_info")
c.execute("INSERT INTO company_info (name, address, phone, contact, logo_path, quote_title) VALUES (?,?,?,?,?,?)",
(name, address, phone, contact, logo_path, title))
conn.commit()
conn.close()
messagebox.showinfo("成功", "公司信息已保存")
win.destroy()
tk.Button(win, text="💾 保存", command=save_company, bg="#90ee90").grid(row=6, column=0, columnspan=3, pady=20)
# ========== 辅助函数(产品、搜索、图片、导出等) ==========
# 请保留您之前已调试通过的所有辅助函数(如 load_product_list, search_product, add_product, on_select_product, select_image, show_big_image, delete_product, find_supplier, on_external_order_change, save_sale_price, save_customer_code, load_mold_combobox, on_mold_select, on_supplier_select 等)
# 由于篇幅,此处省略,但必须保留完整。
# 建议您从之前的备份中将这些函数迁移过来。
def load_supplier_combobox(self):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute(“SELECT id, name FROM suppliers”)
rows = c.fetchall()
conn.close()
display_list = []
self.supplier_map = {}
for row in rows:
display_list.append(row[1])
self.supplier_map[row[1]] = row[0]
self.combo_supplier[‘values’] = display_list
if display_list:
self.combo_supplier.current(0)
def on_supplier_select(self, event):
if not self.current_product_id:
return
selected = self.combo_supplier.get()
if selected and selected in self.supplier_map:
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("UPDATE products SET supplier_id=? WHERE id=?", (self.supplier_map[selected], self.current_product_id))
conn.commit()
conn.close()
def load_material_combobox(self):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, name, unit, price FROM materials")
rows = c.fetchall()
conn.close()
display_list = []
self.material_map = {}
for row in rows:
text = f"{row[1]} ({row[3]}元/{row[2]})"
display_list.append(text)
self.material_map[text] = row[0]
self.combo_bom_material['values'] = display_list
if display_list:
self.combo_bom_material.current(0)
self.on_bom_material_select(None)
def on_bom_material_select(self, event):
selected = self.combo_bom_material.get()
if selected and selected in self.material_map:
self.var_material_id.set(self.material_map[selected])
def load_mold_combobox(self):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, code, name FROM molds ORDER BY code")
rows = c.fetchall()
conn.close()
display_list = []
self.mold_map = {}
for row in rows:
text = f"{row[1]} - {row[2]}"
display_list.append(text)
self.mold_map[text] = row[0]
self.combo_mold['values'] = display_list
if display_list:
self.combo_mold.current(0)
self.on_mold_select(None)
def on_mold_select(self, event):
selected = self.combo_mold.get()
if selected and selected in self.mold_map:
self.var_mold_id.set(self.mold_map[selected])
def load_process_combobox(self):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, name FROM processes")
rows = c.fetchall()
conn.close()
display_list = []
self.process_map = {}
for row in rows:
display_list.append(row[1])
self.process_map[row[1]] = row[0]
self.combo_process['values'] = display_list
if display_list:
self.combo_process.current(0)
self.on_process_select(None)
def on_process_select(self, event):
proc_name = self.combo_process.get()
if not proc_name or proc_name not in self.process_map:
self.combo_option['values'] = []
return
proc_id = self.process_map[proc_name]
self.var_process_id.set(proc_id)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, option_name, unit_price, unit_desc FROM process_options WHERE process_id=?", (proc_id,))
rows = c.fetchall()
conn.close()
display_list = []
self.option_map = {}
for row in rows:
text = f"{row[1]} ({row[2]}元/{row[3]})"
display_list.append(text)
self.option_map[text] = row[0]
self.combo_option['values'] = display_list
if display_list:
self.combo_option.current(0)
self.on_option_select(None)
def on_option_select(self, event):
selected = self.combo_option.get()
if selected and selected in self.option_map:
self.var_option_id.set(self.option_map[selected])
def load_product_list(self, keyword=""):
for i in self.tree.get_children():
self.tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
if keyword:
c.execute("SELECT id, code, name FROM products WHERE code LIKE ? OR name LIKE ?",
('%'+keyword+'%', '%'+keyword+'%'))
else:
c.execute("SELECT id, code, name FROM products")
rows = c.fetchall()
for row in rows:
self.tree.insert("", "end", text=row[0], values=(row[1],))
conn.close()
def search_product(self):
self.load_product_list(self.entry_search.get().strip())
def add_product(self):
win = tk.Toplevel(self.root)
win.title("新增产品")
tk.Label(win, text="货号(唯一):").grid(row=0, column=0)
entry_code = tk.Entry(win)
entry_code.grid(row=0, column=1)
tk.Label(win, text="名称:").grid(row=1, column=0)
entry_name = tk.Entry(win)
entry_name.grid(row=1, column=1)
def save():
code = entry_code.get().strip()
name = entry_name.get().strip()
if not code:
messagebox.showerror("错误", "货号不能为空")
return
try:
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("INSERT INTO products (code, name) VALUES (?,?)", (code, name))
conn.commit()
conn.close()
win.destroy()
self.load_product_list()
messagebox.showinfo("成功", "产品已添加")
except sqlite3.IntegrityError:
messagebox.showerror("错误", "货号已存在!")
tk.Button(win, text="保存", command=save).grid(row=2, column=0, columnspan=2, pady=10)
def on_select_product(self, event):
selected = self.tree.focus()
if not selected:
return
pid = self.tree.item(selected)['text']
self.current_product_id = pid
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT code, name, img_path, sale_price, supplier_id FROM products WHERE id=?", (pid,))
row = c.fetchone()
if row:
self.lbl_code.config(text=row[0])
self.lbl_name.config(text=row[1])
self.bom_header_code.config(text=row[0])
self.bom_header_name.config(text=row[1])
self.entry_sale_price.delete(0, tk.END)
self.entry_sale_price.insert(0, str(row[3] if row[3] else 0.0))
if row[4]:
c.execute("SELECT name FROM suppliers WHERE id=?", (row[4],))
sup = c.fetchone()
if sup:
self.combo_supplier.set(sup[0])
else:
self.combo_supplier.set('')
if row[2] and os.path.exists(row[2]):
img = Image.open(row[2])
img.thumbnail((200, 200))
self.img_tk = ImageTk.PhotoImage(img)
self.img_label.config(image=self.img_tk, text="")
else:
self.img_label.config(image="", text="无图片")
self.load_bom(pid)
self.load_processes(pid)
self.update_insert_positions()
conn.close()
def update_insert_positions(self):
if not self.current_product_id:
self.combo_insert_pos['values'] = ["末尾"]
self.combo_insert_pos.current(0)
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT layer_code FROM bom WHERE product_id=? ORDER BY sort_order", (self.current_product_id,))
rows = c.fetchall()
conn.close()
positions = ["末尾"] + [row[0] for row in rows]
self.combo_insert_pos['values'] = positions
if positions:
self.combo_insert_pos.current(0)
def load_bom(self, pid):
for i in self.bom_tree.get_children():
self.bom_tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("""
SELECT b.layer_code, m.code, m.name, b.qty,
mat.name, m.cuts_per_yard, b.fabric_usage, b.unit_price,
b.remark
FROM bom b
JOIN molds m ON b.mold_id = m.id
LEFT JOIN materials mat ON b.material_id = mat.id
WHERE b.product_id=? ORDER BY b.sort_order
""", (pid,))
rows = c.fetchall()
for row in rows:
subtotal = row[7] if row[7] else 0
mold_display = f"{row[1]} - {row[2]}" if row[1] else "未关联"
material_name = row[4] if row[4] else "无"
cuts = row[5] if row[5] else 0
fabric_usage = row[6] if row[6] else 0
unit_price = row[7] if row[7] else 0
remark = row[8] if row[8] else "" # ★ 新增备注
self.bom_tree.insert("", "end", values=(
row[0], mold_display, row[3], material_name, cuts,
f"{fabric_usage:.4f}", f"{unit_price:.4f}", f"{subtotal:.2f}", remark
))
conn.close()
self.calc_total()
def add_bom_row(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选中产品")
return
mold_id = self.var_mold_id.get()
material_id = self.var_material_id.get()
qty = self.var_bom_qty.get()
layer_code = self.var_layer.get()
layer_name = self.var_layer_name.get()
insert_pos = self.combo_insert_pos.get()
if mold_id == 0 or material_id == 0 or qty <= 0:
messagebox.showwarning("提示", "请完整选择刀模、布料,并输入正数片数")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT cuts_per_yard FROM molds WHERE id=?", (mold_id,))
cuts_row = c.fetchone()
if not cuts_row or cuts_row[0] <= 0:
conn.close()
messagebox.showerror("错误", "该刀模未设置每码冲片数")
return
c.execute("SELECT price FROM materials WHERE id=?", (material_id,))
price_row = c.fetchone()
if not price_row:
conn.close()
messagebox.showerror("错误", "布料未设置单价")
return
fabric_usage = qty / cuts_row[0]
unit_price = fabric_usage * price_row[0]
if insert_pos == "末尾":
c.execute("SELECT MAX(sort_order) FROM bom WHERE product_id=?", (self.current_product_id,))
max_order = c.fetchone()[0] or 0
new_order = max_order + 1
else:
c.execute("SELECT sort_order FROM bom WHERE product_id=? AND layer_code=? ORDER BY sort_order LIMIT 1",
(self.current_product_id, insert_pos))
order_row = c.fetchone()
if order_row:
target_order = order_row[0]
c.execute("UPDATE bom SET sort_order = sort_order + 1 WHERE product_id=? AND sort_order >= ?",
(self.current_product_id, target_order))
new_order = target_order
else:
new_order = 0
c.execute("""INSERT INTO bom
(product_id, layer_code, layer_name, mold_id, qty, material_id, fabric_usage, unit_price, sort_order, remark)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(self.current_product_id, layer_code, layer_name, mold_id, qty, material_id, fabric_usage, unit_price, new_order,
self.entry_bom_remark.get().strip()))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
self.update_insert_positions()
self.var_bom_qty.set(1)
self.entry_bom_qty.focus_set()
self.entry_bom_qty.select_range(0, tk.END)
messagebox.showinfo("成功", f"已添加:用布 {fabric_usage:.4f} 码,布料成本 {unit_price:.4f} 元")
def edit_bom_row(self):
selected = self.bom_tree.focus()
if not selected:
messagebox.showwarning("提示", "请先在BOM表中点击选中一行")
return
vals = self.bom_tree.item(selected)['values']
if not vals or len(vals) < 2:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
mold_display = vals[1]
if ' - ' in mold_display:
mold_code = mold_display.split(' - ')[0]
else:
mold_code = mold_display
c.execute("""SELECT b.id, b.layer_code, b.layer_name, b.mold_id, b.qty, b.material_id
FROM bom b
JOIN molds m ON b.mold_id = m.id
WHERE b.product_id=? AND b.layer_code=? AND m.code=?
ORDER BY b.sort_order LIMIT 1""",
(self.current_product_id, vals[0], mold_code))
bom_row = c.fetchone()
conn.close()
if not bom_row:
messagebox.showerror("错误", "无法定位该行数据")
return
edit_win = tk.Toplevel(self.root)
edit_win.title(f"编辑BOM行 - {vals[1]}")
edit_win.geometry("420x320")
tk.Label(edit_win, text="层级代码:").grid(row=0, column=0, padx=10, pady=5, sticky='e')
layer_entry = ttk.Combobox(edit_win, values=["L1","L2","L3","L4","L5","L6","L7","L8","L9","L10"], width=15)
layer_entry.set(bom_row[1])
layer_entry.grid(row=0, column=1, sticky='w')
tk.Label(edit_win, text="层级名称:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
layer_name_entry = ttk.Combobox(edit_win, values=["外瓣","中瓣","内瓣","花心","花托","枝干","叶片","装饰","底座"], width=10)
layer_name_entry.set(bom_row[2])
layer_name_entry.grid(row=1, column=1, sticky='w')
tk.Label(edit_win, text="片数:").grid(row=2, column=0, padx=10, pady=5, sticky='e')
qty_entry = tk.Entry(edit_win, width=10)
qty_entry.insert(0, str(bom_row[4]))
qty_entry.grid(row=2, column=1, sticky='w')
tk.Label(edit_win, text="布料:").grid(row=3, column=0, padx=10, pady=5, sticky='e')
material_combo = ttk.Combobox(edit_win, values=list(self.material_map.keys()), width=20)
material_combo.grid(row=3, column=1, sticky='w')
for key, val in self.material_map.items():
if val == bom_row[5]:
material_combo.set(key)
break
tk.Label(edit_win, text="刀模:").grid(row=4, column=0, padx=10, pady=5, sticky='e')
mold_combo = ttk.Combobox(edit_win, values=list(self.mold_map.keys()), width=20)
mold_combo.grid(row=4, column=1, sticky='w')
for key, val in self.mold_map.items():
if val == bom_row[3]:
mold_combo.set(key)
break
def save_edit():
try:
new_qty = float(qty_entry.get())
if new_qty <= 0:
messagebox.showerror("错误", "片数必须为正数")
return
new_layer = layer_entry.get()
new_layer_name = layer_name_entry.get()
new_material_key = material_combo.get()
new_mold_key = mold_combo.get()
if not new_layer or not new_material_key or not new_mold_key:
messagebox.showerror("错误", "请完整填写所有字段")
return
new_material_id = self.material_map[new_material_key]
new_mold_id = self.mold_map[new_mold_key]
conn2 = sqlite3.connect(DB_NAME)
c2 = conn2.cursor()
c2.execute("SELECT cuts_per_yard FROM molds WHERE id=?", (new_mold_id,))
cuts = c2.fetchone()
c2.execute("SELECT price FROM materials WHERE id=?", (new_material_id,))
price = c2.fetchone()
if not cuts or not price:
conn2.close()
messagebox.showerror("错误", "刀模或布料数据异常")
return
new_usage = new_qty / cuts[0]
new_unit_price = new_usage * price[0]
c2.execute("""UPDATE bom SET
layer_code=?, layer_name=?, mold_id=?, qty=?, material_id=?,
fabric_usage=?, unit_price=?
WHERE id=?""",
(new_layer, new_layer_name, new_mold_id, new_qty, new_material_id, new_usage, new_unit_price, bom_row[0]))
conn2.commit()
conn2.close()
edit_win.destroy()
self.load_bom(self.current_product_id)
self.update_insert_positions()
messagebox.showinfo("成功", f"已更新,新布料成本: {new_unit_price:.4f} 元")
except Exception as e:
messagebox.showerror("错误", f"修改失败: {str(e)}")
tk.Button(edit_win, text="💾 保存修改", command=save_edit, bg="#90ee90").grid(row=5, column=0, columnspan=2, pady=15)
tk.Button(edit_win, text="取消", command=edit_win.destroy).grid(row=6, column=0, columnspan=2)
def delete_bom_row(self):
selected = self.bom_tree.focus()
if not selected:
messagebox.showwarning("提示", "请先选中一行")
return
vals = self.bom_tree.item(selected)['values']
if not vals:
return
if not messagebox.askyesno("确认", f"确定删除 {vals[0]} 层({vals[1]})吗?"):
return
mold_display = vals[1]
if ' - ' in mold_display:
mold_code = mold_display.split(' - ')[0]
else:
mold_code = mold_display
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("""DELETE FROM bom
WHERE product_id=? AND layer_code=? AND mold_id=(SELECT id FROM molds WHERE code=?)""",
(self.current_product_id, vals[0], mold_code))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
self.update_insert_positions()
messagebox.showinfo("成功", "已删除该层")
def clear_bom(self):
if not self.current_product_id:
return
if messagebox.askyesno("确认", "删除当前所有BOM?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM bom WHERE product_id=?", (self.current_product_id,))
conn.commit()
conn.close()
self.load_bom(self.current_product_id)
self.update_insert_positions()
def refresh_all_bom(self):
if not messagebox.askyesno("确认", "重新计算所有BOM的布料成本?"):
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, mold_id, material_id, qty FROM bom")
rows = c.fetchall()
updated = 0
for bom_id, mold_id, material_id, qty in rows:
c.execute("SELECT cuts_per_yard FROM molds WHERE id=?", (mold_id,))
cuts = c.fetchone()
c.execute("SELECT price FROM materials WHERE id=?", (material_id,))
price = c.fetchone()
if cuts and price and cuts[0] > 0:
fabric_usage = qty / cuts[0]
unit_price = fabric_usage * price[0]
c.execute("UPDATE bom SET fabric_usage=?, unit_price=? WHERE id=?", (fabric_usage, unit_price, bom_id))
updated += 1
conn.commit()
conn.close()
if self.current_product_id:
self.load_bom(self.current_product_id)
messagebox.showinfo("完成", f"已刷新 {updated} 条BOM记录")
def load_processes(self, pid):
for i in self.process_tree.get_children():
self.process_tree.delete(i)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("""
SELECT p.name, po.option_name, po.unit_desc, pp.unit_price, pp.process_qty, pp.total_price
FROM product_processes pp
JOIN processes p ON pp.process_id = p.id
JOIN process_options po ON pp.option_id = po.id
WHERE pp.product_id=? ORDER BY pp.sort_order
""", (pid,))
rows = c.fetchall()
for row in rows:
self.process_tree.insert("", "end", values=(row[0], row[1], row[2], f"{row[3]:.4f}", row[4], f"{row[5]:.2f}"))
conn.close()
self.calc_total()
def add_process_row(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选中产品")
return
option_id = self.var_option_id.get()
qty = self.var_process_qty.get()
if option_id == 0 or qty <= 0:
messagebox.showwarning("提示", "请选择完整的工序和数量")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT unit_price FROM process_options WHERE id=?", (option_id,))
row = c.fetchone()
if not row:
conn.close()
return
unit_price = row[0]
total_price = unit_price * qty
c.execute("SELECT MAX(sort_order) FROM product_processes WHERE product_id=?", (self.current_product_id,))
max_order = c.fetchone()[0] or 0
new_order = max_order + 1
c.execute("""INSERT INTO product_processes
(product_id, process_id, option_id, process_qty, unit_price, total_price, sort_order)
VALUES (?,?,?,?,?,?,?)""",
(self.current_product_id, self.var_process_id.get(), option_id, qty, unit_price, total_price, new_order))
conn.commit()
conn.close()
self.load_processes(self.current_product_id)
messagebox.showinfo("成功", f"工序已添加,加工费: {total_price:.2f}")
def clear_processes(self):
if not self.current_product_id:
return
if messagebox.askyesno("确认", "删除当前所有工艺路线?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM product_processes WHERE product_id=?", (self.current_product_id,))
conn.commit()
conn.close()
self.load_processes(self.current_product_id)
def refresh_all_process_prices(self):
if not messagebox.askyesno("确认", "刷新所有工序单价?"):
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, option_id, process_qty FROM product_processes")
rows = c.fetchall()
updated = 0
for pp_id, opt_id, qty in rows:
c.execute("SELECT unit_price FROM process_options WHERE id=?", (opt_id,))
pr = c.fetchone()
if pr:
new_price = pr[0] * qty
c.execute("UPDATE product_processes SET unit_price=?, total_price=? WHERE id=?", (pr[0], new_price, pp_id))
updated += 1
conn.commit()
conn.close()
if self.current_product_id:
self.load_processes(self.current_product_id)
messagebox.showinfo("完成", f"刷新了 {updated} 条工艺单价")
def calc_total(self):
fabric_total = 0.0
for child in self.bom_tree.get_children():
vals = self.bom_tree.item(child)['values']
if len(vals) >= 8 and vals[7]:
fabric_total += float(vals[7])
self.lbl_fabric_cost.config(text=f"🧵 布料成本: {fabric_total:.2f} 元")
process_total = 0.0
for child in self.process_tree.get_children():
vals = self.process_tree.item(child)['values']
if len(vals) >= 6 and vals[5]:
process_total += float(vals[5])
self.lbl_process_cost.config(text=f"🔧 加工成本: {process_total:.2f} 元")
total = fabric_total + process_total
self.lbl_total_cost.config(text=f"💰 总成本: {total:.2f} 元")
def save_sale_price(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选中产品")
return
try:
price = float(self.entry_sale_price.get())
except:
messagebox.showerror("错误", "请输入数字")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("UPDATE products SET sale_price=? WHERE id=?", (price, self.current_product_id))
conn.commit()
conn.close()
messagebox.showinfo("成功", f"销售单价已设为 {price} 元")
def select_image(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选中产品")
return
file_path = filedialog.askopenfilename(filetypes=[("图片", "*.jpg *.jpeg *.png *.bmp *.gif")])
if file_path:
ext = os.path.splitext(file_path)[1]
new_name = f"{self.lbl_code.cget('text')}{ext}"
dest_path = os.path.join(IMG_FOLDER, new_name)
shutil.copyfile(file_path, dest_path)
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("UPDATE products SET img_path=? WHERE id=?", (dest_path, self.current_product_id))
conn.commit()
conn.close()
self.on_select_product(None)
def show_big_image(self, event):
if not self.current_product_id:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT img_path FROM products WHERE id=?", (self.current_product_id,))
row = c.fetchone()
conn.close()
if row and row[0] and os.path.exists(row[0]):
big_win = tk.Toplevel(self.root)
big_win.title("产品大图")
img = Image.open(row[0])
base_width = 600
w_percent = base_width / float(img.size[0])
h_size = int(float(img.size[1]) * w_percent)
img_resized = img.resize((base_width, h_size), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img_resized)
label = tk.Label(big_win, image=photo)
label.image = photo
label.pack()
else:
messagebox.showinfo("提示", "无图片")
def delete_product(self):
if not self.current_product_id:
return
if messagebox.askyesno("警告", f"确定删除货号 {self.lbl_code.cget('text')} 及其所有数据?"):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("DELETE FROM bom WHERE product_id=?", (self.current_product_id,))
c.execute("DELETE FROM product_processes WHERE product_id=?", (self.current_product_id,))
c.execute("DELETE FROM products WHERE id=?", (self.current_product_id,))
conn.commit()
conn.close()
self.current_product_id = None
self.lbl_code.config(text="")
self.lbl_name.config(text="")
self.bom_header_code.config(text="(请选择产品)")
self.bom_header_name.config(text="(请选择产品)")
self.entry_sale_price.delete(0, tk.END)
self.combo_supplier.set('')
self.img_label.config(image="", text="已删除")
for i in self.bom_tree.get_children():
self.bom_tree.delete(i)
for i in self.process_tree.get_children():
self.process_tree.delete(i)
self.load_product_list()
messagebox.showinfo("已删除")
def find_supplier(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先在左侧点击选中一个产品")
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("""
SELECT s.name, s.contact_person, s.phone, s.address, s.memo
FROM products p
LEFT JOIN suppliers s ON p.supplier_id = s.id
WHERE p.id = ?
""", (self.current_product_id,))
row = c.fetchone()
conn.close()
if not row or not row[0]:
messagebox.showinfo("提示", "该产品尚未绑定供应商")
return
win = tk.Toplevel(self.root)
win.title("📞 缺货联系供应商")
win.geometry("400x300")
tk.Label(win, text=f"供应商: {row[0]}", font=('Arial', 14, 'bold'), fg="blue").pack(pady=10)
tk.Label(win, text=f"联系人: {row[1] if row[1] else '未填写'}").pack(pady=2)
tk.Label(win, text=f"电 话: {row[2] if row[2] else '未填写'}", font=('Arial', 12), fg="red").pack(pady=5)
tk.Label(win, text=f"地 址: {row[3] if row[3] else '未填写'}").pack(pady=2)
tk.Label(win, text=f"备 注: {row[4] if row[4] else '无'}").pack(pady=2)
tk.Button(win, text="关闭", command=win.destroy).pack(pady=20)
def generate_quote(self):
selected = self.tree.selection()
if not selected:
messagebox.showwarning("提示", "请按住Ctrl多选产品")
return
quote_win = tk.Toplevel(self.root)
quote_win.title("客户报价单")
quote_win.geometry("800x500")
row_idx = 0
tk.Label(quote_win, text="货号", font=('Arial', 10, 'bold')).grid(row=row_idx, column=0, padx=5)
tk.Label(quote_win, text="名称", font=('Arial', 10, 'bold')).grid(row=row_idx, column=1, padx=5)
tk.Label(quote_win, text="布料成本", font=('Arial', 10, 'bold')).grid(row=row_idx, column=2, padx=5)
tk.Label(quote_win, text="加工成本", font=('Arial', 10, 'bold')).grid(row=row_idx, column=3, padx=5)
tk.Label(quote_win, text="总成本", font=('Arial', 10, 'bold')).grid(row=row_idx, column=4, padx=5)
tk.Label(quote_win, text="销售单价", font=('Arial', 10, 'bold')).grid(row=row_idx, column=5, padx=5)
tk.Label(quote_win, text="利润", font=('Arial', 10, 'bold')).grid(row=row_idx, column=6, padx=5)
row_idx += 1
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
grand_total = 0.0
for item in selected:
pid = self.tree.item(item)['text']
c.execute("SELECT code, name, sale_price FROM products WHERE id=?", (pid,))
prod = c.fetchone()
if not prod:
continue
c.execute("SELECT SUM(unit_price) FROM bom WHERE product_id=?", (pid,))
fabric_sum = c.fetchone()[0] or 0.0
c.execute("SELECT SUM(total_price) FROM product_processes WHERE product_id=?", (pid,))
proc_sum = c.fetchone()[0] or 0.0
total_cost = fabric_sum + proc_sum
sale = prod[2] or 0.0
profit = sale - total_cost
tk.Label(quote_win, text=prod[0]).grid(row=row_idx, column=0, padx=5)
tk.Label(quote_win, text=prod[1]).grid(row=row_idx, column=1, padx=5)
tk.Label(quote_win, text=f"{fabric_sum:.2f}").grid(row=row_idx, column=2, padx=5)
tk.Label(quote_win, text=f"{proc_sum:.2f}").grid(row=row_idx, column=3, padx=5)
tk.Label(quote_win, text=f"{total_cost:.2f}").grid(row=row_idx, column=4, padx=5)
tk.Label(quote_win, text=f"{sale:.2f}").grid(row=row_idx, column=5, padx=5)
tk.Label(quote_win, text=f"{profit:.2f}", fg="red" if profit < 0 else "green").grid(row=row_idx, column=6, padx=5)
row_idx += 1
grand_total += total_cost
conn.close()
tk.Label(quote_win, text="总成本合计:", font=('Arial', 12, 'bold')).grid(row=row_idx, column=4, pady=15)
tk.Label(quote_win, text=f"{grand_total:.2f} 元", font=('Arial', 12, 'bold'), fg="blue").grid(row=row_idx, column=5, pady=15)
def on_external_order_change(self, event):
self.current_external_order = self.entry_external_order.get().strip()
def export_all_data_to_excel(self):
"""将所有基础数据导出到一个 Excel 文件的多个 Sheet"""
try:
import pandas as pd
from tkinter import filedialog
except ImportError:
messagebox.showerror("错误", "请先安装 pandas 和 openpyxl:\npip install pandas openpyxl")
return
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel 文件", "*.xlsx")],
initialfile=f"全部数据_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
)
if not file_path:
return
conn = sqlite3.connect(DB_NAME)
try:
tables = {
"产品": "SELECT * FROM products",
"BOM明细": "SELECT * FROM bom",
"原料": "SELECT * FROM materials",
"模具": "SELECT * FROM molds",
"客户": "SELECT * FROM customers",
"供应商": "SELECT * FROM suppliers",
"工序": "SELECT * FROM processes",
"工序选项": "SELECT * FROM process_options",
"工艺路线": "SELECT * FROM product_processes"
}
with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
for sheet_name, sql in tables.items():
try:
df = pd.read_sql_query(sql, conn)
if not df.empty:
df.to_excel(writer, sheet_name=sheet_name[:31], index=False)
else:
pd.DataFrame({'提示': ['该表无数据']}).to_excel(writer, sheet_name=sheet_name[:31], index=False)
except Exception as e:
print(f"导出表 {sheet_name} 失败: {e}")
messagebox.showinfo("导出成功", f"所有数据已导出到:\n{file_path}")
except Exception as e:
messagebox.showerror("导出失败", str(e))
finally:
conn.close()
# 特别提醒:需要新增 save_customer_code 函数
def save_customer_code(self):
if not self.current_product_id:
messagebox.showwarning("提示", "请先选择产品")
return
code = self.entry_customer_code.get().strip()
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("UPDATE products SET customer_code=? WHERE id=?", (code, self.current_product_id))
conn.commit()
conn.close()
messagebox.showinfo("成功", "客户货号已保存")
# 导出全部数据(含新表)
def export_all_data_to_excel(self):
# 请保留之前已实现的导出功能,并增加新表 material_categories, company_info 等
pass
# 其他函数(如 load_material_combobox, on_level1_select 等)需新增
def load_category_combos(self):
# 加载一级大类列表
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT DISTINCT level1 FROM material_categories ORDER BY level1")
level1_list = [row[0] for row in c.fetchall()]
self.combo_level1['values'] = level1_list
conn.close()
def on_level1_select(self, event):
# 根据一级加载二级
level1 = self.combo_level1.get()
if not level1:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT DISTINCT level2 FROM material_categories WHERE level1=? ORDER BY level2", (level1,))
level2_list = [row[0] for row in c.fetchall()]
self.combo_level2['values'] = level2_list
self.combo_level2.set('')
self.combo_level3.set('')
self.var_category_id.set(0)
conn.close()
def on_level2_select(self, event):
# 根据二级加载三级
level1 = self.combo_level1.get()
level2 = self.combo_level2.get()
if not level1 or not level2:
return
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT id, level3, unit, price FROM material_categories WHERE level1=? AND level2=? ORDER BY level3", (level1, level2))
rows = c.fetchall()
display_list = []
self.category_map = {}
for row in rows:
display = f"{row[1]} ({row[2]}, {row[3]}元)"
display_list.append(display)
self.category_map[display] = row[0]
self.combo_level3['values'] = display_list
self.combo_level3.set('')
self.var_category_id.set(0)
conn.close()
def on_level3_select(self, event):
# 选择具体物料,自动填充分类id
selected = self.combo_level3.get()
if selected in self.category_map:
self.var_category_id.set(self.category_map[selected])
# 自动带出单价(可显示)
# ========== 启动 ==========
if name == “main“:
root = tk.Tk()
app = App(root)
root.mainloop()
```python “D:\人造花管理系统\flower_manage.py”
关于 LearnKu
推荐文章: