1173 lines
47 KiB
Python
1173 lines
47 KiB
Python
import tkinter as tk
|
||
from tkinter import ttk, messagebox, simpledialog
|
||
from datetime import datetime
|
||
import json
|
||
from inventory_manager import InventoryManager
|
||
|
||
class InventoryUI:
|
||
def __init__(self):
|
||
self.root = tk.Tk()
|
||
self.root.title("出入库管理系统")
|
||
self.root.geometry("800x600")
|
||
self.root.resizable(False, False)
|
||
|
||
# 初始化库存管理器
|
||
self.manager = InventoryManager("inventory_data.json")
|
||
|
||
# 当前页面状态
|
||
self.current_page = "main"
|
||
self.current_product = None
|
||
|
||
# 创建主界面
|
||
self.create_main_page()
|
||
|
||
def create_main_page(self):
|
||
"""创建主页面 - 产品列表"""
|
||
self.clear_window()
|
||
|
||
# 顶部搜索栏
|
||
search_frame = tk.Frame(self.root)
|
||
search_frame.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
tk.Label(search_frame, text="搜索产品:").pack(side=tk.LEFT)
|
||
self.search_entry = tk.Entry(search_frame, width=30)
|
||
self.search_entry.pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Button(search_frame, text="搜索", command=self.search_products).pack(side=tk.LEFT, padx=2)
|
||
tk.Button(search_frame, text="清空", command=self.clear_search).pack(side=tk.LEFT, padx=2)
|
||
tk.Button(search_frame, text="添加产品分类", command=self.show_add_product_page).pack(side=tk.LEFT, padx=2)
|
||
tk.Button(search_frame, text="查询", command=self.show_query_page).pack(side=tk.LEFT, padx=2)
|
||
|
||
# 产品列表表格
|
||
columns = ("产品名", "当前剩余重量(kg)", "平均价格(元/kg)", "操作")
|
||
self.product_tree = ttk.Treeview(self.root, columns=columns, show="headings", height=15)
|
||
|
||
for col in columns:
|
||
self.product_tree.heading(col, text=col)
|
||
self.product_tree.column(col, width=180, anchor=tk.CENTER)
|
||
|
||
self.product_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||
|
||
# 绑定双击事件
|
||
self.product_tree.bind("<Double-1>", self.on_product_select)
|
||
|
||
# 底部统计信息
|
||
stats_frame = tk.Frame(self.root)
|
||
stats_frame.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
self.stats_label = tk.Label(stats_frame, text="库存总价值: 0.00 元 库存总重: 0.0 kg")
|
||
self.stats_label.pack(side=tk.RIGHT)
|
||
|
||
# 分页控件
|
||
page_frame = tk.Frame(self.root)
|
||
page_frame.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
tk.Button(page_frame, text="上一页").pack(side=tk.LEFT)
|
||
tk.Label(page_frame, text="第 1 页 / 共 1 页").pack(side=tk.LEFT, padx=10)
|
||
tk.Button(page_frame, text="下一页").pack(side=tk.LEFT)
|
||
|
||
# 加载产品数据
|
||
self.load_products()
|
||
|
||
def create_product_detail_page(self, product_name):
|
||
"""创建产品详情页面"""
|
||
self.clear_window()
|
||
self.current_product = product_name
|
||
|
||
# 顶部标题和按钮
|
||
header_frame = tk.Frame(self.root)
|
||
header_frame.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
tk.Label(header_frame, text=f"产品: {product_name}", font=("Arial", 14, "bold")).pack(side=tk.LEFT)
|
||
|
||
button_frame = tk.Frame(header_frame)
|
||
button_frame.pack(side=tk.RIGHT)
|
||
|
||
tk.Button(button_frame, text="入库", bg="lightgreen", command=self.show_inbound_dialog).pack(side=tk.LEFT, padx=2)
|
||
tk.Button(button_frame, text="出库", bg="lightcoral", command=self.show_outbound_dialog).pack(side=tk.LEFT, padx=2)
|
||
tk.Button(button_frame, text="删除产品", bg="red", fg="white", command=self.delete_product).pack(side=tk.LEFT, padx=2)
|
||
tk.Button(button_frame, text="返回主页", command=self.create_main_page).pack(side=tk.LEFT, padx=2)
|
||
|
||
# 交易记录表格
|
||
columns = ("时间", "类型", "重量(kg)", "价格(元/kg)", "总价(元)", "备注", "操作")
|
||
self.transaction_tree = ttk.Treeview(self.root, columns=columns, show="headings", height=18)
|
||
|
||
for col in columns:
|
||
self.transaction_tree.heading(col, text=col)
|
||
if col == "时间":
|
||
self.transaction_tree.column(col, width=100)
|
||
elif col == "类型":
|
||
self.transaction_tree.column(col, width=80)
|
||
elif col == "操作":
|
||
self.transaction_tree.column(col, width=80)
|
||
else:
|
||
self.transaction_tree.column(col, width=100)
|
||
|
||
self.transaction_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||
|
||
# 绑定删除事件
|
||
self.transaction_tree.bind("<Double-1>", self.delete_transaction)
|
||
|
||
# 加载交易记录
|
||
self.load_transactions(product_name)
|
||
|
||
def create_add_product_page(self):
|
||
"""创建添加产品页面"""
|
||
self.clear_window()
|
||
|
||
# 标题
|
||
tk.Label(self.root, text="添加产品大类", font=("Arial", 16, "bold")).pack(pady=20)
|
||
|
||
# 表单
|
||
form_frame = tk.Frame(self.root)
|
||
form_frame.pack(pady=50)
|
||
|
||
tk.Label(form_frame, text="产品名称:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=10)
|
||
self.product_name_entry = tk.Entry(form_frame, width=30)
|
||
self.product_name_entry.grid(row=0, column=1, padx=10, pady=10)
|
||
|
||
tk.Label(form_frame, text="产品描述:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=10)
|
||
self.product_desc_entry = tk.Entry(form_frame, width=30)
|
||
self.product_desc_entry.grid(row=1, column=1, padx=10, pady=10)
|
||
|
||
# 按钮
|
||
button_frame = tk.Frame(self.root)
|
||
button_frame.pack(pady=30)
|
||
|
||
tk.Button(button_frame, text="确认添加", bg="lightblue", command=self.add_product).pack(side=tk.LEFT, padx=10)
|
||
tk.Button(button_frame, text="取消", command=self.create_main_page).pack(side=tk.LEFT, padx=10)
|
||
|
||
# 返回按钮
|
||
tk.Button(self.root, text="返回主页", command=self.create_main_page).pack(side=tk.TOP, anchor=tk.NE, padx=10, pady=10)
|
||
|
||
# 说明文字
|
||
info_frame = tk.Frame(self.root)
|
||
info_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=20, pady=20)
|
||
|
||
tk.Label(info_frame, text="说明:", font=("Arial", 10, "bold")).pack(anchor=tk.W)
|
||
tk.Label(info_frame, text="• 产品名称不能为空且不能与现有产品重复").pack(anchor=tk.W)
|
||
tk.Label(info_frame, text="• 产品描述为可选项").pack(anchor=tk.W)
|
||
tk.Label(info_frame, text="• 添加产品后,可通过入库操作添加库存").pack(anchor=tk.W)
|
||
|
||
def create_query_page(self):
|
||
"""创建订单查询页面"""
|
||
self.clear_window()
|
||
|
||
# 标题
|
||
tk.Label(self.root, text="订单查询", font=("Arial", 16, "bold")).pack(pady=10)
|
||
|
||
# 查询条件
|
||
query_frame = tk.Frame(self.root)
|
||
query_frame.pack(fill=tk.X, padx=10, pady=10)
|
||
|
||
# 第一行
|
||
row1 = tk.Frame(query_frame)
|
||
row1.pack(fill=tk.X, pady=5)
|
||
|
||
tk.Label(row1, text="产品名称:").pack(side=tk.LEFT)
|
||
self.query_product_entry = tk.Entry(row1, width=15)
|
||
self.query_product_entry.pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Label(row1, text="交易类型:").pack(side=tk.LEFT, padx=(20,5))
|
||
self.query_type_var = tk.StringVar(value="全部")
|
||
type_combo = ttk.Combobox(row1, textvariable=self.query_type_var, values=["全部", "入库", "出库"], width=10)
|
||
type_combo.pack(side=tk.LEFT, padx=5)
|
||
|
||
# 第二行
|
||
row2 = tk.Frame(query_frame)
|
||
row2.pack(fill=tk.X, pady=5)
|
||
|
||
tk.Label(row2, text="开始时间:").pack(side=tk.LEFT)
|
||
self.start_date_entry = tk.Entry(row2, width=12)
|
||
self.start_date_entry.pack(side=tk.LEFT, padx=5)
|
||
self.start_date_entry.insert(0, "2025-06-26")
|
||
tk.Label(row2, text="(格式:2025-7-1)").pack(side=tk.LEFT)
|
||
|
||
tk.Label(row2, text="结束时间:").pack(side=tk.LEFT, padx=(20,5))
|
||
self.end_date_entry = tk.Entry(row2, width=12)
|
||
self.end_date_entry.pack(side=tk.LEFT, padx=5)
|
||
self.end_date_entry.insert(0, "2025-07-26")
|
||
tk.Label(row2, text="(格式:2025-12-31)").pack(side=tk.LEFT)
|
||
|
||
# 第三行
|
||
row3 = tk.Frame(query_frame)
|
||
row3.pack(fill=tk.X, pady=5)
|
||
|
||
tk.Label(row3, text="最小重量:").pack(side=tk.LEFT)
|
||
self.min_weight_entry = tk.Entry(row3, width=12)
|
||
self.min_weight_entry.pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Label(row3, text="最大重量:").pack(side=tk.LEFT, padx=(20,5))
|
||
self.max_weight_entry = tk.Entry(row3, width=12)
|
||
self.max_weight_entry.pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Label(row3, text="备注关键词:").pack(side=tk.LEFT, padx=(20,5))
|
||
self.note_keyword_entry = tk.Entry(row3, width=15)
|
||
self.note_keyword_entry.pack(side=tk.LEFT, padx=5)
|
||
|
||
# 查询按钮
|
||
button_frame = tk.Frame(query_frame)
|
||
button_frame.pack(fill=tk.X, pady=10)
|
||
|
||
tk.Button(button_frame, text="查询", bg="lightgreen", command=self.execute_query).pack(side=tk.LEFT, padx=5)
|
||
tk.Button(button_frame, text="清空条件", command=self.clear_query_conditions).pack(side=tk.LEFT, padx=5)
|
||
tk.Button(button_frame, text="打印查询结果", bg="lightblue", command=self.print_query_results).pack(side=tk.LEFT, padx=5)
|
||
|
||
# 快速查询按钮
|
||
quick_frame = tk.Frame(button_frame)
|
||
quick_frame.pack(side=tk.RIGHT)
|
||
|
||
tk.Label(quick_frame, text="快速查询:").pack(side=tk.LEFT, padx=(20,5))
|
||
tk.Button(quick_frame, text="今日", command=lambda: self.quick_query("today"), bg="lightyellow").pack(side=tk.LEFT, padx=2)
|
||
tk.Button(quick_frame, text="本周", command=lambda: self.quick_query("week"), bg="lightyellow").pack(side=tk.LEFT, padx=2)
|
||
tk.Button(quick_frame, text="本月", command=lambda: self.quick_query("month"), bg="lightyellow").pack(side=tk.LEFT, padx=2)
|
||
tk.Button(quick_frame, text="全部", command=lambda: self.quick_query("all"), bg="lightyellow").pack(side=tk.LEFT, padx=2)
|
||
|
||
# 查询结果表格
|
||
columns = ("时间", "产品名", "类型", "重量(kg)", "价格(元/kg)", "总价(元)", "备注")
|
||
self.query_tree = ttk.Treeview(self.root, columns=columns, show="headings", height=12)
|
||
|
||
for col in columns:
|
||
self.query_tree.heading(col, text=col)
|
||
self.query_tree.column(col, width=100, anchor=tk.CENTER)
|
||
|
||
self.query_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||
|
||
# 底部统计和分页
|
||
bottom_frame = tk.Frame(self.root)
|
||
bottom_frame.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
# 分页
|
||
page_frame = tk.Frame(bottom_frame)
|
||
page_frame.pack(side=tk.LEFT)
|
||
|
||
# 初始化分页变量
|
||
self.current_page = 1
|
||
self.page_size = 20 # 每页显示20条记录
|
||
self.total_records = 0
|
||
self.filtered_transactions = [] # 存储过滤后的交易记录
|
||
|
||
self.prev_button = tk.Button(page_frame, text="上一页", command=self.prev_page)
|
||
self.prev_button.pack(side=tk.LEFT)
|
||
|
||
self.page_info_label = tk.Label(page_frame, text="第 1 页 / 共 1 页 (共 0 条记录)")
|
||
self.page_info_label.pack(side=tk.LEFT, padx=10)
|
||
|
||
self.next_button = tk.Button(page_frame, text="下一页", command=self.next_page)
|
||
self.next_button.pack(side=tk.LEFT)
|
||
|
||
# 每页显示数量选择
|
||
tk.Label(page_frame, text="每页显示:").pack(side=tk.LEFT, padx=(20,5))
|
||
self.page_size_var = tk.StringVar(value="20")
|
||
page_size_combo = ttk.Combobox(page_frame, textvariable=self.page_size_var,
|
||
values=["10", "20", "50", "100"], width=8)
|
||
page_size_combo.pack(side=tk.LEFT, padx=5)
|
||
page_size_combo.bind("<<ComboboxSelected>>", self.on_page_size_change)
|
||
|
||
# 统计信息
|
||
self.query_stats_label = tk.Label(bottom_frame, text="查询结果: 0条记录 净重量: 0.00kg 总价值: 0.00元")
|
||
self.query_stats_label.pack(side=tk.RIGHT)
|
||
|
||
# 返回按钮
|
||
tk.Button(self.root, text="返回主页", command=self.create_main_page).pack(side=tk.TOP, anchor=tk.NE, padx=10, pady=5)
|
||
|
||
def clear_window(self):
|
||
"""清空窗口内容"""
|
||
for widget in self.root.winfo_children():
|
||
widget.destroy()
|
||
|
||
def load_products(self):
|
||
"""加载产品列表"""
|
||
try:
|
||
with open("inventory_data.json", "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
|
||
# 清空现有数据
|
||
for item in self.product_tree.get_children():
|
||
self.product_tree.delete(item)
|
||
|
||
total_value = 0
|
||
total_weight = 0
|
||
|
||
for product_name, product_data in data["products"].items():
|
||
weight = float(product_data["total_weight"])
|
||
price = float(product_data["avg_price"])
|
||
value = weight * price
|
||
|
||
total_weight += weight
|
||
total_value += value
|
||
|
||
self.product_tree.insert("", tk.END, values=(
|
||
product_name,
|
||
f"{weight:.2f}",
|
||
f"{price:.2f}",
|
||
"详情"
|
||
))
|
||
|
||
# 更新统计信息
|
||
self.stats_label.config(text=f"库存总价值: {total_value:.2f} 元 库存总重: {total_weight:.2f} kg")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"加载产品数据失败: {str(e)}")
|
||
|
||
def load_transactions(self, product_name):
|
||
"""加载指定产品的交易记录"""
|
||
try:
|
||
with open("inventory_data.json", "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
|
||
# 清空现有数据
|
||
for item in self.transaction_tree.get_children():
|
||
self.transaction_tree.delete(item)
|
||
|
||
# 筛选该产品的交易记录
|
||
for transaction in data["transactions"]:
|
||
if transaction.get("is_snapshot"):
|
||
continue
|
||
|
||
if transaction["product"] == product_name:
|
||
txn_type = transaction["type"]
|
||
weight = float(transaction["weight"])
|
||
price = float(transaction["price"])
|
||
total_price = weight * price
|
||
|
||
# 设置颜色
|
||
tag = "inbound" if txn_type == "入库" else "outbound"
|
||
|
||
item = self.transaction_tree.insert("", tk.END, values=(
|
||
transaction["time"],
|
||
txn_type,
|
||
f"{weight:.2f}",
|
||
f"{price:.2f}",
|
||
f"{total_price:.2f}",
|
||
transaction.get("note", ""),
|
||
"删除"
|
||
), tags=(tag,))
|
||
|
||
# 设置标签颜色
|
||
self.transaction_tree.tag_configure("inbound", background="lightgreen")
|
||
self.transaction_tree.tag_configure("outbound", background="lightcoral")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"加载交易记录失败: {str(e)}")
|
||
|
||
def on_product_select(self, event):
|
||
"""产品选择事件"""
|
||
selection = self.product_tree.selection()
|
||
if selection:
|
||
item = self.product_tree.item(selection[0])
|
||
product_name = item["values"][0]
|
||
self.create_product_detail_page(product_name)
|
||
|
||
def show_add_product_page(self):
|
||
"""显示添加产品页面"""
|
||
self.create_add_product_page()
|
||
|
||
def show_query_page(self):
|
||
"""显示查询页面"""
|
||
self.create_query_page()
|
||
|
||
def show_inbound_dialog(self):
|
||
"""显示入库对话框"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("入库")
|
||
dialog.geometry("350x350")
|
||
dialog.resizable(False, False)
|
||
|
||
# 居中显示
|
||
dialog.transient(self.root)
|
||
dialog.grab_set()
|
||
|
||
# 计算居中位置
|
||
x = (dialog.winfo_screenwidth() // 2) - (350 // 2)
|
||
y = (dialog.winfo_screenheight() // 2) - (350 // 2)
|
||
dialog.geometry(f"350x350+{x}+{y}")
|
||
|
||
tk.Label(dialog, text=f"产品: {self.current_product}", font=("Arial", 12, "bold")).pack(pady=15)
|
||
|
||
tk.Label(dialog, text="重量(kg):").pack()
|
||
weight_entry = tk.Entry(dialog, width=20)
|
||
weight_entry.pack(pady=5)
|
||
|
||
tk.Label(dialog, text="单价(元/kg):").pack()
|
||
price_entry = tk.Entry(dialog, width=20)
|
||
price_entry.pack(pady=5)
|
||
|
||
tk.Label(dialog, text="日期(YYYY-MM-DD):").pack()
|
||
date_entry = tk.Entry(dialog, width=20)
|
||
date_entry.insert(0, datetime.now().strftime("%Y-%m-%d")) # 默认今天日期
|
||
date_entry.pack(pady=5)
|
||
|
||
tk.Label(dialog, text="备注:").pack()
|
||
note_entry = tk.Entry(dialog, width=20)
|
||
note_entry.pack(pady=5)
|
||
|
||
# 设置回车键切换输入框
|
||
def focus_next_entry(event, next_entry):
|
||
next_entry.focus_set()
|
||
return "break"
|
||
|
||
def confirm_on_enter(event):
|
||
confirm_inbound()
|
||
return "break"
|
||
|
||
weight_entry.bind("<Return>", lambda e: focus_next_entry(e, price_entry))
|
||
price_entry.bind("<Return>", lambda e: focus_next_entry(e, date_entry))
|
||
date_entry.bind("<Return>", lambda e: focus_next_entry(e, note_entry))
|
||
note_entry.bind("<Return>", confirm_on_enter)
|
||
|
||
# 默认焦点在重量输入框
|
||
weight_entry.focus_set()
|
||
|
||
def confirm_inbound():
|
||
try:
|
||
weight = float(weight_entry.get())
|
||
price = float(price_entry.get())
|
||
note = note_entry.get()
|
||
date_str = date_entry.get().strip()
|
||
|
||
# 验证日期格式
|
||
try:
|
||
datetime.strptime(date_str, "%Y-%m-%d")
|
||
except ValueError:
|
||
messagebox.showerror("错误", "日期格式不正确,请使用YYYY-MM-DD格式")
|
||
return
|
||
|
||
transaction = {
|
||
"product": self.current_product,
|
||
"type": "入库",
|
||
"weight": f"{weight:.8f}",
|
||
"price": f"{price:.8f}",
|
||
"note": note,
|
||
"time": date_str
|
||
}
|
||
|
||
# 处理交易记录,支持进度回调
|
||
def progress_callback(current, total, message):
|
||
# 简单的进度显示,可以后续扩展为进度条
|
||
print(f"进度: {current}/{total} - {message}")
|
||
|
||
result = self.manager.process_transaction(transaction, progress_callback)
|
||
|
||
if result["success"]:
|
||
if result["recalculated_count"] > 0:
|
||
messagebox.showinfo("成功", f"入库记录添加成功!\n重计算了{result['recalculated_count']}条记录")
|
||
else:
|
||
messagebox.showinfo("成功", "入库记录添加成功!")
|
||
else:
|
||
messagebox.showerror("错误", f"入库失败:{result['message']}")
|
||
return
|
||
|
||
dialog.destroy()
|
||
self.create_product_detail_page(self.current_product)
|
||
|
||
except ValueError:
|
||
messagebox.showerror("错误", "请输入有效的数字")
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"入库失败: {str(e)}")
|
||
|
||
# 按钮框架
|
||
button_frame = tk.Frame(dialog)
|
||
button_frame.pack(pady=20)
|
||
|
||
tk.Button(button_frame, text="确认", command=confirm_inbound, bg="lightgreen", width=8).pack(side=tk.LEFT, padx=5)
|
||
tk.Button(button_frame, text="取消", command=dialog.destroy, width=8).pack(side=tk.LEFT, padx=5)
|
||
|
||
def show_outbound_dialog(self):
|
||
"""显示出库对话框"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("出库")
|
||
dialog.geometry("350x300")
|
||
dialog.resizable(False, False)
|
||
|
||
# 居中显示
|
||
dialog.transient(self.root)
|
||
dialog.grab_set()
|
||
|
||
# 计算居中位置
|
||
x = (dialog.winfo_screenwidth() // 2) - (350 // 2)
|
||
y = (dialog.winfo_screenheight() // 2) - (300 // 2)
|
||
dialog.geometry(f"350x300+{x}+{y}")
|
||
|
||
tk.Label(dialog, text=f"产品: {self.current_product}", font=("Arial", 12, "bold")).pack(pady=15)
|
||
|
||
tk.Label(dialog, text="重量(kg):").pack()
|
||
weight_entry = tk.Entry(dialog, width=20)
|
||
weight_entry.pack(pady=5)
|
||
|
||
tk.Label(dialog, text="日期(YYYY-MM-DD):").pack()
|
||
date_entry = tk.Entry(dialog, width=20)
|
||
date_entry.insert(0, datetime.now().strftime("%Y-%m-%d")) # 默认今天日期
|
||
date_entry.pack(pady=5)
|
||
|
||
tk.Label(dialog, text="备注:").pack()
|
||
note_entry = tk.Entry(dialog, width=20)
|
||
note_entry.pack(pady=5)
|
||
|
||
# 设置回车键切换输入框
|
||
def focus_next_entry(event, next_entry):
|
||
next_entry.focus_set()
|
||
return "break"
|
||
|
||
def confirm_on_enter(event):
|
||
confirm_outbound()
|
||
return "break"
|
||
|
||
weight_entry.bind("<Return>", lambda e: focus_next_entry(e, date_entry))
|
||
date_entry.bind("<Return>", lambda e: focus_next_entry(e, note_entry))
|
||
note_entry.bind("<Return>", confirm_on_enter)
|
||
|
||
# 默认焦点在重量输入框
|
||
weight_entry.focus_set()
|
||
|
||
def confirm_outbound():
|
||
try:
|
||
weight = float(weight_entry.get())
|
||
note = note_entry.get()
|
||
date_str = date_entry.get().strip()
|
||
|
||
# 验证日期格式
|
||
try:
|
||
datetime.strptime(date_str, "%Y-%m-%d")
|
||
except ValueError:
|
||
messagebox.showerror("错误", "日期格式不正确,请使用YYYY-MM-DD格式")
|
||
return
|
||
|
||
# 出库记录,价格将在process_transaction中自动计算
|
||
transaction = {
|
||
"product": self.current_product,
|
||
"type": "出库",
|
||
"weight": f"{weight:.8f}",
|
||
"note": note,
|
||
"time": date_str
|
||
}
|
||
|
||
# 处理交易记录,支持进度回调
|
||
def progress_callback(current, total, message):
|
||
# 简单的进度显示,可以后续扩展为进度条
|
||
print(f"进度: {current}/{total} - {message}")
|
||
|
||
result = self.manager.process_transaction(transaction, progress_callback)
|
||
|
||
if result["success"]:
|
||
if result["recalculated_count"] > 0:
|
||
messagebox.showinfo("成功", f"出库记录添加成功!\n重计算了{result['recalculated_count']}条记录")
|
||
else:
|
||
messagebox.showinfo("成功", "出库记录添加成功!")
|
||
else:
|
||
if result['message'] == "库存不足":
|
||
messagebox.showerror("错误", "库存不足,无法出库!")
|
||
else:
|
||
messagebox.showerror("错误", f"出库失败:{result['message']}")
|
||
return
|
||
|
||
dialog.destroy()
|
||
self.create_product_detail_page(self.current_product)
|
||
|
||
except ValueError:
|
||
messagebox.showerror("错误", "请输入有效的数字")
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"出库失败: {str(e)}")
|
||
|
||
# 按钮框架
|
||
button_frame = tk.Frame(dialog)
|
||
button_frame.pack(pady=20)
|
||
|
||
tk.Button(button_frame, text="确认", command=confirm_outbound, bg="lightcoral", width=8).pack(side=tk.LEFT, padx=5)
|
||
tk.Button(button_frame, text="取消", command=dialog.destroy, width=8).pack(side=tk.LEFT, padx=5)
|
||
|
||
def add_product(self):
|
||
"""添加新产品"""
|
||
name = self.product_name_entry.get().strip()
|
||
desc = self.product_desc_entry.get().strip()
|
||
|
||
if not name:
|
||
messagebox.showerror("错误", "产品名称不能为空")
|
||
return
|
||
|
||
try:
|
||
with open("inventory_data.json", "r+", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
|
||
if name in data["products"]:
|
||
messagebox.showerror("错误", "产品已存在")
|
||
return
|
||
|
||
data["products"][name] = {
|
||
"total_weight": "0.00000000",
|
||
"avg_price": "0.00000000"
|
||
}
|
||
|
||
f.seek(0)
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
f.truncate()
|
||
|
||
messagebox.showinfo("成功", "产品添加成功")
|
||
self.create_main_page()
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"添加产品失败: {str(e)}")
|
||
|
||
def search_products(self):
|
||
"""搜索产品"""
|
||
keyword = self.search_entry.get().strip()
|
||
if not keyword:
|
||
self.load_products() # 如果没有关键词,显示所有产品
|
||
return
|
||
|
||
try:
|
||
with open("inventory_data.json", "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
|
||
# 清空现有数据
|
||
for item in self.product_tree.get_children():
|
||
self.product_tree.delete(item)
|
||
|
||
total_value = 0
|
||
total_weight = 0
|
||
found_count = 0
|
||
|
||
# 过滤产品
|
||
for product_name, product_data in data["products"].items():
|
||
if keyword.lower() in product_name.lower():
|
||
weight = float(product_data["total_weight"])
|
||
price = float(product_data["avg_price"])
|
||
value = weight * price
|
||
|
||
total_weight += weight
|
||
total_value += value
|
||
found_count += 1
|
||
|
||
self.product_tree.insert("", tk.END, values=(
|
||
product_name,
|
||
f"{weight:.2f}",
|
||
f"{price:.2f}",
|
||
"详情"
|
||
))
|
||
|
||
# 更新统计信息
|
||
self.stats_label.config(text=f"搜索结果: {found_count}个产品 库存总价值: {total_value:.2f} 元 库存总重: {total_weight:.2f} kg")
|
||
|
||
if found_count == 0:
|
||
messagebox.showinfo("搜索结果", f"没有找到包含'{keyword}'的产品")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"搜索失败: {str(e)}")
|
||
|
||
def clear_search(self):
|
||
"""清空搜索"""
|
||
self.search_entry.delete(0, tk.END)
|
||
self.load_products()
|
||
|
||
def delete_product(self):
|
||
"""删除产品"""
|
||
if messagebox.askyesno("确认", f"确定要删除产品 {self.current_product} 吗?\n\n注意:删除产品将同时删除该产品的所有交易记录!\n如果有大量交易记录,重计算可能需要一些时间。"):
|
||
try:
|
||
# 进度回调函数
|
||
def progress_callback(current, total, message):
|
||
print(f"删除产品进度: {current}/{total} - {message}")
|
||
|
||
# 使用manager的删除产品方法
|
||
result = self.manager.delete_product(self.current_product, progress_callback)
|
||
|
||
if result["success"]:
|
||
if result["deleted_transactions"] > 0:
|
||
messagebox.showinfo("成功", f"产品删除成功!\n删除了{result['deleted_transactions']}条相关交易记录")
|
||
else:
|
||
messagebox.showinfo("成功", "产品删除成功!")
|
||
|
||
self.create_main_page()
|
||
else:
|
||
messagebox.showerror("错误", f"删除产品失败:{result['message']}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"删除产品失败: {str(e)}")
|
||
|
||
def delete_transaction(self, event):
|
||
"""删除交易记录"""
|
||
selection = self.transaction_tree.selection()
|
||
if selection:
|
||
# 获取选中的交易记录信息
|
||
item = self.transaction_tree.item(selection[0])
|
||
values = item["values"]
|
||
|
||
if len(values) < 6:
|
||
messagebox.showerror("错误", "无法获取交易记录信息")
|
||
return
|
||
|
||
transaction_time = values[0]
|
||
transaction_type = values[1]
|
||
weight = float(values[2])
|
||
|
||
if messagebox.askyesno("确认", f"确定要删除这条记录吗?\n\n时间: {transaction_time}\n类型: {transaction_type}\n重量: {weight}kg\n\n注意:删除记录将触发重计算,可能需要一些时间。"):
|
||
try:
|
||
# 查找交易记录的索引
|
||
transaction_index = self.manager.find_transaction_index(
|
||
self.current_product, transaction_time, transaction_type, weight
|
||
)
|
||
|
||
if transaction_index == -1:
|
||
messagebox.showerror("错误", "未找到对应的交易记录")
|
||
return
|
||
|
||
# 进度回调函数
|
||
def progress_callback(current, total, message):
|
||
print(f"删除进度: {current}/{total} - {message}")
|
||
|
||
# 删除交易记录
|
||
result = self.manager.delete_transaction(transaction_index, progress_callback)
|
||
|
||
if result["success"]:
|
||
if result["recalculated_count"] > 0:
|
||
messagebox.showinfo("成功", f"交易记录删除成功!\n重计算了{result['recalculated_count']}条记录")
|
||
else:
|
||
messagebox.showinfo("成功", "交易记录删除成功!")
|
||
|
||
# 刷新页面
|
||
self.create_product_detail_page(self.current_product)
|
||
else:
|
||
messagebox.showerror("错误", f"删除失败:{result['message']}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"删除交易记录失败: {str(e)}")
|
||
|
||
def execute_query(self):
|
||
"""执行查询"""
|
||
try:
|
||
with open("inventory_data.json", "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
|
||
# 获取查询条件
|
||
product_name = self.query_product_entry.get().strip()
|
||
transaction_type = self.query_type_var.get()
|
||
start_date = self.start_date_entry.get().strip()
|
||
end_date = self.end_date_entry.get().strip()
|
||
min_weight = self.min_weight_entry.get().strip()
|
||
max_weight = self.max_weight_entry.get().strip()
|
||
note_keyword = self.note_keyword_entry.get().strip()
|
||
|
||
# 过滤交易记录
|
||
self.filtered_transactions = []
|
||
for transaction in data["transactions"]:
|
||
if transaction.get("is_snapshot"):
|
||
continue
|
||
|
||
# 应用过滤条件
|
||
if self._match_query_conditions(transaction, product_name, transaction_type,
|
||
start_date, end_date, min_weight, max_weight, note_keyword):
|
||
self.filtered_transactions.append(transaction)
|
||
|
||
# 按时间倒序排序(最新的在前面)
|
||
self.filtered_transactions.sort(key=lambda x: x["time"], reverse=True)
|
||
|
||
self.total_records = len(self.filtered_transactions)
|
||
self.current_page = 1 # 重置到第一页
|
||
|
||
# 显示当前页的数据
|
||
self._display_current_page()
|
||
|
||
if self.total_records == 0:
|
||
messagebox.showinfo("查询结果", "没有找到符合条件的记录")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"查询失败: {str(e)}")
|
||
|
||
def _display_current_page(self):
|
||
"""显示当前页的数据"""
|
||
# 清空现有数据
|
||
for item in self.query_tree.get_children():
|
||
self.query_tree.delete(item)
|
||
|
||
# 计算分页
|
||
start_index = (self.current_page - 1) * self.page_size
|
||
end_index = min(start_index + self.page_size, self.total_records)
|
||
|
||
total_weight = 0
|
||
total_value = 0
|
||
|
||
# 显示当前页的记录
|
||
for i in range(start_index, end_index):
|
||
transaction = self.filtered_transactions[i]
|
||
weight = float(transaction["weight"])
|
||
price = float(transaction["price"])
|
||
total_price = weight * price
|
||
|
||
# 计算统计信息(入库为正,出库为负)
|
||
if transaction["type"] == "入库":
|
||
total_weight += weight
|
||
else:
|
||
total_weight -= weight
|
||
total_value += total_price
|
||
|
||
# 设置行颜色
|
||
tag = "inbound" if transaction["type"] == "入库" else "outbound"
|
||
|
||
self.query_tree.insert("", tk.END, values=(
|
||
transaction["time"],
|
||
transaction["product"],
|
||
transaction["type"],
|
||
f"{weight:.2f}",
|
||
f"{price:.2f}",
|
||
f"{total_price:.2f}",
|
||
transaction.get("note", "")
|
||
), tags=(tag,))
|
||
|
||
# 设置标签颜色
|
||
self.query_tree.tag_configure("inbound", background="lightgreen")
|
||
self.query_tree.tag_configure("outbound", background="lightcoral")
|
||
|
||
# 更新分页信息
|
||
total_pages = (self.total_records + self.page_size - 1) // self.page_size if self.total_records > 0 else 1
|
||
self.page_info_label.config(text=f"第 {self.current_page} 页 / 共 {total_pages} 页 (共 {self.total_records} 条记录)")
|
||
|
||
# 更新按钮状态
|
||
self.prev_button.config(state=tk.NORMAL if self.current_page > 1 else tk.DISABLED)
|
||
self.next_button.config(state=tk.NORMAL if self.current_page < total_pages else tk.DISABLED)
|
||
|
||
# 计算全部记录的统计信息
|
||
all_total_weight = 0
|
||
all_total_value = 0
|
||
if self.filtered_transactions: # 只有当有筛选结果时才计算
|
||
for transaction in self.filtered_transactions:
|
||
weight = float(transaction["weight"])
|
||
price = float(transaction["price"])
|
||
total_price = weight * price
|
||
|
||
if transaction["type"] == "入库":
|
||
all_total_weight += weight
|
||
else:
|
||
all_total_weight -= weight
|
||
all_total_value += total_price
|
||
|
||
# 更新统计信息
|
||
self.query_stats_label.config(text=f"查询结果: {self.total_records}条记录 净重量: {all_total_weight:.2f}kg 总价值: {all_total_value:.2f}元")
|
||
|
||
def prev_page(self):
|
||
"""上一页"""
|
||
if self.current_page > 1:
|
||
self.current_page -= 1
|
||
self._display_current_page()
|
||
|
||
def next_page(self):
|
||
"""下一页"""
|
||
total_pages = (self.total_records + self.page_size - 1) // self.page_size if self.total_records > 0 else 1
|
||
if self.current_page < total_pages:
|
||
self.current_page += 1
|
||
self._display_current_page()
|
||
|
||
def on_page_size_change(self, event):
|
||
"""每页显示数量改变"""
|
||
try:
|
||
self.page_size = int(self.page_size_var.get())
|
||
self.current_page = 1 # 重置到第一页
|
||
if hasattr(self, 'filtered_transactions'):
|
||
self._display_current_page()
|
||
except ValueError:
|
||
pass
|
||
|
||
def quick_query(self, period):
|
||
"""快速查询"""
|
||
from datetime import datetime, timedelta
|
||
|
||
# 清空查询条件
|
||
self.clear_query_conditions()
|
||
|
||
today = datetime.now()
|
||
|
||
if period == "today":
|
||
# 今日
|
||
date_str = today.strftime("%Y-%m-%d")
|
||
self.start_date_entry.delete(0, tk.END)
|
||
self.start_date_entry.insert(0, date_str)
|
||
self.end_date_entry.delete(0, tk.END)
|
||
self.end_date_entry.insert(0, date_str)
|
||
elif period == "week":
|
||
# 本周(周一到今天)
|
||
days_since_monday = today.weekday()
|
||
monday = today - timedelta(days=days_since_monday)
|
||
self.start_date_entry.delete(0, tk.END)
|
||
self.start_date_entry.insert(0, monday.strftime("%Y-%m-%d"))
|
||
self.end_date_entry.delete(0, tk.END)
|
||
self.end_date_entry.insert(0, today.strftime("%Y-%m-%d"))
|
||
elif period == "month":
|
||
# 本月(月初到今天)
|
||
month_start = today.replace(day=1)
|
||
self.start_date_entry.delete(0, tk.END)
|
||
self.start_date_entry.insert(0, month_start.strftime("%Y-%m-%d"))
|
||
self.end_date_entry.delete(0, tk.END)
|
||
self.end_date_entry.insert(0, today.strftime("%Y-%m-%d"))
|
||
elif period == "all":
|
||
# 全部记录
|
||
self.start_date_entry.delete(0, tk.END)
|
||
self.start_date_entry.insert(0, "2020-01-01")
|
||
self.end_date_entry.delete(0, tk.END)
|
||
self.end_date_entry.insert(0, "2030-12-31")
|
||
|
||
# 执行查询
|
||
self.execute_query()
|
||
|
||
def clear_query_conditions(self):
|
||
"""清空查询条件"""
|
||
self.query_product_entry.delete(0, tk.END)
|
||
self.query_type_var.set("全部")
|
||
self.start_date_entry.delete(0, tk.END)
|
||
self.start_date_entry.insert(0, "2025-06-26")
|
||
self.end_date_entry.delete(0, tk.END)
|
||
self.end_date_entry.insert(0, "2025-07-26")
|
||
self.min_weight_entry.delete(0, tk.END)
|
||
self.max_weight_entry.delete(0, tk.END)
|
||
self.note_keyword_entry.delete(0, tk.END)
|
||
|
||
def _match_query_conditions(self, transaction, product_name, transaction_type,
|
||
start_date, end_date, min_weight, max_weight, note_keyword):
|
||
"""检查交易记录是否匹配查询条件"""
|
||
from datetime import datetime
|
||
|
||
# 产品名称过滤
|
||
if product_name and product_name.lower() not in transaction["product"].lower():
|
||
return False
|
||
|
||
# 交易类型过滤
|
||
if transaction_type != "全部" and transaction["type"] != transaction_type:
|
||
return False
|
||
|
||
# 时间范围过滤
|
||
try:
|
||
if start_date:
|
||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||
txn_dt = datetime.strptime(transaction["time"].split()[0], "%Y-%m-%d")
|
||
if txn_dt < start_dt:
|
||
return False
|
||
except ValueError:
|
||
pass # 忽略日期格式错误
|
||
|
||
try:
|
||
if end_date:
|
||
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||
txn_dt = datetime.strptime(transaction["time"].split()[0], "%Y-%m-%d")
|
||
if txn_dt > end_dt:
|
||
return False
|
||
except ValueError:
|
||
pass # 忽略日期格式错误
|
||
|
||
# 重量范围过滤
|
||
weight = float(transaction["weight"])
|
||
try:
|
||
if min_weight and weight < float(min_weight):
|
||
return False
|
||
except ValueError:
|
||
pass
|
||
|
||
try:
|
||
if max_weight and weight > float(max_weight):
|
||
return False
|
||
except ValueError:
|
||
pass
|
||
|
||
# 备注关键词过滤
|
||
if note_keyword:
|
||
note = transaction.get("note", "")
|
||
if note_keyword.lower() not in note.lower():
|
||
return False
|
||
|
||
return True
|
||
|
||
def print_query_results(self):
|
||
"""生成HTML文件并用浏览器打开打印"""
|
||
try:
|
||
# 获取当前查询结果
|
||
results = []
|
||
for item in self.query_tree.get_children():
|
||
values = self.query_tree.item(item)["values"]
|
||
results.append(values)
|
||
|
||
if not results:
|
||
messagebox.showwarning("提示", "没有查询结果可以打印")
|
||
return
|
||
|
||
# 生成HTML内容
|
||
from datetime import datetime
|
||
import os
|
||
import webbrowser
|
||
import tempfile
|
||
|
||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
stats_text = self.query_stats_label.cget("text")
|
||
|
||
html_content = f"""<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>库存管理系统 - 查询结果报表</title>
|
||
<style>
|
||
body {{
|
||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||
margin: 20px;
|
||
background-color: #f5f5f5;
|
||
}}
|
||
.container {{
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background-color: white;
|
||
padding: 30px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}}
|
||
.header {{
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
border-bottom: 2px solid #4CAF50;
|
||
padding-bottom: 20px;
|
||
}}
|
||
.header h1 {{
|
||
color: #2c3e50;
|
||
margin: 0;
|
||
font-size: 28px;
|
||
}}
|
||
.header .time {{
|
||
color: #7f8c8d;
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
}}
|
||
table {{
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 20px 0;
|
||
font-size: 14px;
|
||
}}
|
||
th, td {{
|
||
border: 1px solid #ddd;
|
||
padding: 12px 8px;
|
||
text-align: center;
|
||
}}
|
||
th {{
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
font-weight: bold;
|
||
}}
|
||
tr:nth-child(even) {{
|
||
background-color: #f9f9f9;
|
||
}}
|
||
tr:hover {{
|
||
background-color: #f5f5f5;
|
||
}}
|
||
.inbound {{
|
||
background-color: #e8f5e8 !important;
|
||
}}
|
||
.outbound {{
|
||
background-color: #ffe8e8 !important;
|
||
}}
|
||
.stats {{
|
||
margin-top: 30px;
|
||
padding: 20px;
|
||
background-color: #ecf0f1;
|
||
border-radius: 5px;
|
||
border-left: 4px solid #3498db;
|
||
}}
|
||
.stats h3 {{
|
||
margin: 0 0 10px 0;
|
||
color: #2c3e50;
|
||
}}
|
||
.print-btn {{
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||
}}
|
||
.print-btn:hover {{
|
||
background-color: #45a049;
|
||
}}
|
||
@media print {{
|
||
body {{
|
||
background-color: white;
|
||
margin: 0;
|
||
}}
|
||
.container {{
|
||
box-shadow: none;
|
||
margin: 0;
|
||
padding: 20px;
|
||
}}
|
||
.print-btn {{
|
||
display: none;
|
||
}}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="print-btn" onclick="window.print()">打印</button>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>库存管理系统 - 查询结果报表</h1>
|
||
<div class="time">生成时间: {current_time}</div>
|
||
</div>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>产品名</th>
|
||
<th>类型</th>
|
||
<th>重量(kg)</th>
|
||
<th>价格(元/kg)</th>
|
||
<th>总价(元)</th>
|
||
<th>备注</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>"""
|
||
|
||
# 添加数据行
|
||
for values in results:
|
||
row_class = "inbound" if values[2] == "入库" else "outbound"
|
||
html_content += f""" <tr class="{row_class}">
|
||
<td>{values[0]}</td>
|
||
<td>{values[1]}</td>
|
||
<td>{values[2]}</td>
|
||
<td>{values[3]}</td>
|
||
<td>{values[4]}</td>
|
||
<td>{values[5]}</td>
|
||
<td>{values[6]}</td>
|
||
</tr>
|
||
"""
|
||
|
||
html_content += f""" </tbody>
|
||
</table>
|
||
|
||
<div class="stats">
|
||
<h3>统计信息</h3>
|
||
<p>{stats_text}</p>
|
||
<p>记录总数: {len(results)}条</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>"""
|
||
|
||
# 创建HTML文件到软件根目录
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
html_file = os.path.join(current_dir, f"{datetime.now().strftime('%Y%m%d')}_{len(results)}条.html")
|
||
|
||
with open(html_file, 'w', encoding='utf-8') as f:
|
||
f.write(html_content)
|
||
|
||
# 用浏览器打开HTML文件
|
||
webbrowser.open(f'file:///{html_file.replace(os.sep, "/")}')
|
||
|
||
messagebox.showinfo("成功", f"报表已生成并在浏览器中打开\n文件位置: {html_file}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"生成HTML报表失败: {str(e)}")
|
||
|
||
def run(self):
|
||
"""运行应用"""
|
||
self.root.mainloop()
|
||
|
||
if __name__ == "__main__":
|
||
app = InventoryUI()
|
||
app.run() |