Simple_Enterprise_Warehouse.../Simple_Enterprise_Warehouse_Management.py

1056 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="总重量: 360.0kg 总价: 11680.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:.8f}",
f"{price:.8f}",
"详情"
))
# 更新统计信息
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
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') and 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):
"""打印查询结果"""
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
# 创建打印预览窗口
print_window = tk.Toplevel(self.root)
print_window.title("打印预览")
print_window.geometry("800x600")
print_window.resizable(True, True)
# 创建文本框显示打印内容
text_frame = tk.Frame(print_window)
text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text_widget = tk.Text(text_frame, wrap=tk.NONE, font=("Courier", 10))
scrollbar_y = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview)
scrollbar_x = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_widget.xview)
text_widget.config(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
text_widget.pack(fill=tk.BOTH, expand=True)
# 生成打印内容
from datetime import datetime
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print_content = f"""库存管理系统 - 查询结果报表
生成时间: {current_time}
{'='*80}
"""
# 表头
header = f"{'时间':<20} {'产品名':<15} {'类型':<8} {'重量(kg)':<10} {'价格(元/kg)':<12} {'总价(元)':<12} {'备注':<20}"
print_content += header + "\n"
print_content += "-" * 80 + "\n"
# 数据行
for values in results:
line = f"{values[0]:<20} {values[1]:<15} {values[2]:<8} {values[3]:<10} {values[4]:<12} {values[5]:<12} {values[6]:<20}"
print_content += line + "\n"
# 统计信息
stats_text = self.query_stats_label.cget("text")
print_content += "\n" + "="*80 + "\n"
print_content += f"统计信息: {stats_text}\n"
print_content += f"记录总数: {len(results)}\n"
text_widget.insert(tk.END, print_content)
text_widget.config(state=tk.DISABLED)
# 按钮框架
button_frame = tk.Frame(print_window)
button_frame.pack(pady=10)
def copy_to_clipboard():
print_window.clipboard_clear()
print_window.clipboard_append(print_content)
messagebox.showinfo("成功", "内容已复制到剪贴板")
tk.Button(button_frame, text="复制到剪贴板", command=copy_to_clipboard, bg="lightblue").pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="关闭", command=print_window.destroy).pack(side=tk.LEFT, padx=5)
except Exception as e:
messagebox.showerror("错误", f"生成打印预览失败: {str(e)}")
def run(self):
"""运行应用"""
self.root.mainloop()
if __name__ == "__main__":
app = InventoryUI()
app.run()