Simple_Enterprise_Warehouse.../Simple_Enterprise_Warehouse_Management.py

999 lines
47 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.

#本产品含AI量高达98%
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
import json
import os
class InventoryApp:
def __init__(self, root):
self.root = root
self.root.title("出入库管理系统")
self.root.geometry("800x600")
# 数据存储
self.data_file = "inventory_data.json"
self.load_data()
# 当前页面状态
self.current_page = "main"
self.current_product = None
self.current_main_page = 1
self.current_detail_page = 1
self.current_query_page = 1
self.items_per_page = 14
self.query_results = []
# 窗口大小相关
self.last_window_height = 600
# 创建主框架
self.main_frame = tk.Frame(root)
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 绑定窗口大小变化事件
self.root.bind('<Configure>', self.on_window_resize)
# 显示主页面
self.show_main_page()
def load_data(self):
"""加载数据"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
self.data = json.load(f)
except:
self.data = {"products": {}, "transactions": []}
else:
# 初始化空数据结构
self.data = {"products": {}, "transactions": []}
def save_data(self):
"""保存数据"""
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=2)
def clear_frame(self):
"""清空当前框架"""
for widget in self.main_frame.winfo_children():
widget.destroy()
def calculate_items_per_page(self):
"""根据窗口高度动态计算每页显示的条目数"""
window_height = self.root.winfo_height()
# 减去顶部搜索区域(约60px)、表头(约30px)、底部分页区域(约50px)的高度
available_height = window_height - 140
# 每个条目大约占用35px高度
item_height = 35
# 计算可显示的条目数最少5个最多20个
items_count = max(5, min(20, available_height // item_height))
return items_count
def on_window_resize(self, event):
"""窗口大小变化时的处理"""
# 只处理主窗口的大小变化事件
if event.widget == self.root:
current_height = self.root.winfo_height()
# 如果高度变化超过50px重新计算每页条目数
if abs(current_height - self.last_window_height) > 50:
self.last_window_height = current_height
new_items_per_page = self.calculate_items_per_page()
if new_items_per_page != self.items_per_page:
self.items_per_page = new_items_per_page
# 重新刷新当前页面的列表
if self.current_page == "main":
self.update_product_list()
# 更新页码标签
total_products = len(self.data["products"])
max_pages = (total_products + self.items_per_page - 1) // self.items_per_page
if self.current_main_page > max_pages and max_pages > 0:
self.current_main_page = max_pages
self.main_page_label.config(text=f"{self.current_main_page} 页 / 共 {max_pages}")
elif self.current_page == "detail":
self.update_detail_list()
# 更新页码标签
product_transactions = [t for t in self.data["transactions"] if t["product"] == self.current_product]
max_pages = (len(product_transactions) + self.items_per_page - 1) // self.items_per_page
if self.current_detail_page > max_pages and max_pages > 0:
self.current_detail_page = max_pages
self.detail_page_label.config(text=f"{self.current_detail_page} 页 / 共 {max_pages}")
def show_main_page(self):
"""显示主页面"""
self.current_page = "main"
self.clear_frame()
# 如果是首次显示主页面保持默认的14条否则根据窗口大小计算
if not hasattr(self, 'main_page_initialized'):
self.main_page_initialized = True
else:
self.items_per_page = self.calculate_items_per_page()
# 顶部搜索功能和添加产品按钮
search_frame = tk.Frame(self.main_frame)
search_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Label(search_frame, text="搜索产品:", font=("Arial", 12)).pack(side=tk.LEFT)
self.search_var = tk.StringVar()
search_entry = tk.Entry(search_frame, textvariable=self.search_var, font=("Arial", 12))
search_entry.pack(side=tk.LEFT, padx=(10, 5), fill=tk.X, expand=True)
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_category_page, bg="lightblue", font=("Arial", 12)).pack(side=tk.LEFT, padx=5)
tk.Button(search_frame, text="查询", command=self.show_query_page, bg="lightcyan", font=("Arial", 12)).pack(side=tk.LEFT, padx=5)
# 产品列表
list_frame = tk.Frame(self.main_frame)
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表头
header_frame = tk.Frame(list_frame)
header_frame.pack(fill=tk.X)
tk.Label(header_frame, text="产品名", width=15, relief=tk.RIDGE, font=("Arial", 12, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="当前剩余重量(kg)", width=15, relief=tk.RIDGE, font=("Arial", 12, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="平均价格(元/kg)", width=15, relief=tk.RIDGE, font=("Arial", 12, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="操作", width=15, relief=tk.RIDGE, font=("Arial", 12, "bold")).pack(side=tk.LEFT)
# 产品条目
self.product_list_frame = tk.Frame(list_frame)
self.product_list_frame.pack(fill=tk.BOTH, expand=True)
self.update_product_list()
# 页数控制
page_frame = tk.Frame(self.main_frame)
page_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Button(page_frame, text="上一页", command=self.prev_main_page).pack(side=tk.LEFT)
total_products = len(self.data["products"])
max_pages = max(1, (total_products + self.items_per_page - 1) // self.items_per_page)
self.main_page_label = tk.Label(page_frame, text=f"{self.current_main_page} 页 / 共 {max_pages}")
self.main_page_label.pack(side=tk.LEFT, padx=20)
tk.Button(page_frame, text="下一页", command=self.next_main_page).pack(side=tk.LEFT)
def search_products(self):
"""搜索产品"""
# 重置到第一页
self.current_main_page = 1
self.update_product_list()
def clear_search(self):
"""清空搜索"""
self.search_var.set("")
self.current_main_page = 1
self.update_product_list()
def update_product_list(self):
"""更新产品列表"""
for widget in self.product_list_frame.winfo_children():
widget.destroy()
# 获取搜索词并过滤产品
search_term = self.search_var.get().strip().lower()
all_products = list(self.data["products"].items())
if search_term:
# 模糊搜索:产品名包含搜索词
filtered_products = [(name, data) for name, data in all_products
if search_term in name.lower()]
else:
filtered_products = all_products
# 分页处理
start_idx = (self.current_main_page - 1) * self.items_per_page
end_idx = start_idx + self.items_per_page
page_products = filtered_products[start_idx:end_idx]
# 显示产品列表
for product_name, product_data in page_products:
row_frame = tk.Frame(self.product_list_frame)
row_frame.pack(fill=tk.X, pady=1)
tk.Label(row_frame, text=product_name, width=21, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{product_data['total_weight']:.1f}", width=22, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{product_data['avg_price']:.2f}", width=21, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Button(row_frame, text="详情", width=21,
command=lambda p=product_name: self.show_detail_page(p)).pack(side=tk.LEFT)
# 更新分页信息
total_products = len(filtered_products)
max_pages = max(1, (total_products + self.items_per_page - 1) // self.items_per_page)
if hasattr(self, 'main_page_label') and self.main_page_label.winfo_exists():
try:
if search_term:
self.main_page_label.config(text=f"{self.current_main_page} 页 / 共 {max_pages} 页 (找到 {total_products} 个结果)")
else:
self.main_page_label.config(text=f"{self.current_main_page} 页 / 共 {max_pages}")
except tk.TclError:
pass # 忽略已销毁的组件错误
# 如果没有搜索结果,显示提示
if not page_products and search_term:
no_result_frame = tk.Frame(self.product_list_frame)
no_result_frame.pack(fill=tk.X, pady=20)
tk.Label(no_result_frame, text=f"未找到包含 '{search_term}' 的产品",
font=("Arial", 12), fg="gray").pack()
def prev_main_page(self):
"""上一页"""
if self.current_main_page > 1:
self.current_main_page -= 1
self.update_product_list()
def next_main_page(self):
"""下一页"""
# 获取当前过滤后的产品数量
search_term = self.search_var.get().strip().lower()
all_products = list(self.data["products"].items())
if search_term:
filtered_products = [(name, data) for name, data in all_products
if search_term in name.lower()]
else:
filtered_products = all_products
total_products = len(filtered_products)
max_pages = max(1, (total_products + self.items_per_page - 1) // self.items_per_page)
if self.current_main_page < max_pages:
self.current_main_page += 1
self.update_product_list()
def show_detail_page(self, product_name):
"""显示详细条目页面"""
self.current_page = "detail"
self.current_product = product_name
self.current_detail_page = 1
self.clear_frame()
# 如果是首次显示详情页面保持默认的14条否则根据窗口大小计算
if not hasattr(self, 'detail_page_initialized'):
self.detail_page_initialized = True
else:
self.items_per_page = self.calculate_items_per_page()
# 顶部产品名和按钮
top_frame = tk.Frame(self.main_frame)
top_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Label(top_frame, text=f"产品: {product_name}", font=("Arial", 16, "bold")).pack(side=tk.LEFT)
button_frame = tk.Frame(top_frame)
button_frame.pack(side=tk.RIGHT)
tk.Button(button_frame, text="入库", command=self.show_inbound_page, bg="lightgreen").pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="出库", command=self.show_outbound_page, bg="lightcoral").pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="删除产品", command=self.delete_product_category, bg="darkred", fg="white").pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="返回主页", command=self.show_main_page).pack(side=tk.LEFT, padx=5)
# 详细条目列表
detail_frame = tk.Frame(self.main_frame)
detail_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表头
header_frame = tk.Frame(detail_frame)
header_frame.pack(fill=tk.X)
tk.Label(header_frame, text="时间", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="类型", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="重量(kg)", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="价格(元/kg)", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="总价(元)", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="备注", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="操作", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
# 交易记录
self.detail_list_frame = tk.Frame(detail_frame)
self.detail_list_frame.pack(fill=tk.BOTH, expand=True)
self.update_detail_list()
# 页数控制
page_frame = tk.Frame(self.main_frame)
page_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Button(page_frame, text="上一页", command=self.prev_detail_page).pack(side=tk.LEFT)
product_transactions = [t for t in self.data["transactions"] if t["product"] == self.current_product]
max_pages = max(1, (len(product_transactions) + self.items_per_page - 1) // self.items_per_page)
self.detail_page_label = tk.Label(page_frame, text=f"{self.current_detail_page} 页 / 共 {max_pages}")
self.detail_page_label.pack(side=tk.LEFT, padx=20)
tk.Button(page_frame, text="下一页", command=self.next_detail_page).pack(side=tk.LEFT)
def update_detail_list(self):
"""更新详细条目列表"""
for widget in self.detail_list_frame.winfo_children():
widget.destroy()
# 筛选当前产品的交易记录
product_transactions = [t for t in self.data["transactions"] if t["product"] == self.current_product]
product_transactions.sort(key=lambda x: x["time"], reverse=True)
start_idx = (self.current_detail_page - 1) * self.items_per_page
end_idx = start_idx + self.items_per_page
page_transactions = product_transactions[start_idx:end_idx]
for i, transaction in enumerate(page_transactions):
row_frame = tk.Frame(self.detail_list_frame)
row_frame.pack(fill=tk.X, pady=1)
total_price = transaction['weight'] * transaction['price']
tk.Label(row_frame, text=transaction["time"], width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
color = "lightgreen" if transaction["type"] == "入库" else "lightcoral"
tk.Label(row_frame, text=transaction["type"], width=15, relief=tk.RIDGE, bg=color).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{transaction['weight']:.1f}", width=14, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{transaction['price']:.2f}", width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{total_price:.2f}", width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=transaction["note"], width=14, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Button(row_frame, text="删除", width=15, bg="red", fg="white",
command=lambda t=transaction: self.delete_transaction(t)).pack(side=tk.LEFT)
def prev_detail_page(self):
"""上一页"""
if self.current_detail_page > 1:
self.current_detail_page -= 1
self.update_detail_list()
product_transactions = [t for t in self.data["transactions"] if t["product"] == self.current_product]
max_pages = max(1, (len(product_transactions) + self.items_per_page - 1) // self.items_per_page)
self.detail_page_label.config(text=f"{self.current_detail_page} 页 / 共 {max_pages}")
def next_detail_page(self):
"""详情页下一页"""
product_transactions = [t for t in self.data["transactions"] if t["product"] == self.current_product]
max_pages = (len(product_transactions) + self.items_per_page - 1) // self.items_per_page
if self.current_detail_page < max_pages:
self.current_detail_page += 1
self.update_detail_list()
self.detail_page_label.config(text=f"{self.current_detail_page} 页 / 共 {max_pages}")
def show_inbound_page(self):
"""显示入库页面"""
self.current_page = "inbound"
self.clear_frame()
# 标题
title_frame = tk.Frame(self.main_frame)
title_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Label(title_frame, text=f"入库 - {self.current_product}", font=("Arial", 16, "bold")).pack(side=tk.LEFT)
tk.Button(title_frame, text="返回详情", command=lambda: self.show_detail_page(self.current_product)).pack(side=tk.RIGHT)
# 输入表单
form_frame = tk.Frame(self.main_frame)
form_frame.pack(fill=tk.X, padx=50, pady=50)
# 入库重量
tk.Label(form_frame, text="入库重量(kg):", font=("Arial", 12)).grid(row=0, column=0, sticky="w", pady=10)
self.inbound_weight_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.inbound_weight_var, font=("Arial", 12), width=20).grid(row=0, column=1, padx=10, pady=10)
# 入库价格
tk.Label(form_frame, text="入库价格(元/kg):", font=("Arial", 12)).grid(row=1, column=0, sticky="w", pady=10)
self.inbound_price_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.inbound_price_var, font=("Arial", 12), width=20).grid(row=1, column=1, padx=10, pady=10)
# 入库日期
tk.Label(form_frame, text="入库日期:", font=("Arial", 12)).grid(row=2, column=0, sticky="w", pady=10)
self.inbound_date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d"))
date_entry = tk.Entry(form_frame, textvariable=self.inbound_date_var, font=("Arial", 12), width=20)
date_entry.grid(row=2, column=1, padx=10, pady=10)
tk.Label(form_frame, text="(格式: 2024-01-01)", font=("Arial", 8), fg="gray").grid(row=2, column=2, sticky="w", padx=5)
# 备注
tk.Label(form_frame, text="备注:", font=("Arial", 12)).grid(row=3, column=0, sticky="w", pady=10)
self.inbound_note_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.inbound_note_var, font=("Arial", 12), width=20).grid(row=3, column=1, padx=10, pady=10)
# 提交按钮
button_frame = tk.Frame(form_frame)
button_frame.grid(row=4, column=0, columnspan=2, pady=20)
tk.Button(button_frame, text="确认入库", command=self.process_inbound, bg="lightgreen", font=("Arial", 12)).pack(side=tk.LEFT, padx=10)
tk.Button(button_frame, text="取消", command=lambda: self.show_detail_page(self.current_product), font=("Arial", 12)).pack(side=tk.LEFT, padx=10)
def show_outbound_page(self):
"""显示出库页面"""
self.current_page = "outbound"
self.clear_frame()
# 标题
title_frame = tk.Frame(self.main_frame)
title_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Label(title_frame, text=f"出库 - {self.current_product}", font=("Arial", 16, "bold")).pack(side=tk.LEFT)
tk.Button(title_frame, text="返回详情", command=lambda: self.show_detail_page(self.current_product)).pack(side=tk.RIGHT)
# 输入表单
form_frame = tk.Frame(self.main_frame)
form_frame.pack(fill=tk.X, padx=50, pady=50)
# 出库重量
tk.Label(form_frame, text="出库重量(kg):", font=("Arial", 12)).grid(row=0, column=0, sticky="w", pady=10)
self.outbound_weight_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.outbound_weight_var, font=("Arial", 12), width=20).grid(row=0, column=1, padx=10, pady=10)
# 出库价格(自动计算)
tk.Label(form_frame, text="出库价格(元/kg):", font=("Arial", 12)).grid(row=1, column=0, sticky="w", pady=10)
current_price = self.data["products"][self.current_product]["avg_price"]
self.outbound_price_var = tk.StringVar(value=str(current_price))
price_entry = tk.Entry(form_frame, textvariable=self.outbound_price_var, font=("Arial", 12), width=20, state="readonly")
price_entry.grid(row=1, column=1, padx=10, pady=10)
# 出库日期
tk.Label(form_frame, text="出库日期:", font=("Arial", 12)).grid(row=2, column=0, sticky="w", pady=10)
self.outbound_date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d"))
date_entry = tk.Entry(form_frame, textvariable=self.outbound_date_var, font=("Arial", 12), width=20)
date_entry.grid(row=2, column=1, padx=10, pady=10)
tk.Label(form_frame, text="(格式: 2024-01-01)", font=("Arial", 8), fg="gray").grid(row=2, column=2, sticky="w", padx=5)
# 备注
tk.Label(form_frame, text="备注:", font=("Arial", 12)).grid(row=3, column=0, sticky="w", pady=10)
self.outbound_note_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.outbound_note_var, font=("Arial", 12), width=20).grid(row=3, column=1, padx=10, pady=10)
# 提交按钮
button_frame = tk.Frame(form_frame)
button_frame.grid(row=4, column=0, columnspan=2, pady=20)
tk.Button(button_frame, text="确认出库", command=self.process_outbound, bg="lightcoral", font=("Arial", 12)).pack(side=tk.LEFT, padx=10)
tk.Button(button_frame, text="取消", command=lambda: self.show_detail_page(self.current_product), font=("Arial", 12)).pack(side=tk.LEFT, padx=10)
def process_inbound(self):
"""处理入库"""
try:
weight = float(self.inbound_weight_var.get())
price = float(self.inbound_price_var.get())
note = self.inbound_note_var.get().strip()
date_str = self.inbound_date_var.get().strip()
if weight <= 0 or price <= 0:
messagebox.showerror("错误", "重量和价格必须大于0")
return
# 验证日期格式
try:
datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
messagebox.showerror("错误", "请输入正确的日期格式 (YYYY-MM-DD)")
return
# 更新产品数据
if self.current_product not in self.data["products"]:
self.data["products"][self.current_product] = {"total_weight": 0, "avg_price": 0}
product_data = self.data["products"][self.current_product]
old_total_weight = product_data["total_weight"]
old_avg_price = product_data["avg_price"]
# 计算新的平均价格
new_total_weight = old_total_weight + weight
new_avg_price = (old_total_weight * old_avg_price + weight * price) / new_total_weight
product_data["total_weight"] = new_total_weight
product_data["avg_price"] = new_avg_price
# 添加交易记录
transaction = {
"product": self.current_product,
"type": "入库",
"weight": weight,
"price": price,
"note": note,
"time": date_str
}
self.data["transactions"].append(transaction)
# 保存数据
self.save_data()
messagebox.showinfo("成功", "入库成功!")
self.show_detail_page(self.current_product)
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
def process_outbound(self):
"""处理出库"""
try:
weight = float(self.outbound_weight_var.get())
price = float(self.outbound_price_var.get())
note = self.outbound_note_var.get().strip()
date_str = self.outbound_date_var.get().strip()
if weight <= 0:
messagebox.showerror("错误", "重量必须大于0")
return
# 验证日期格式
try:
datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
messagebox.showerror("错误", "请输入正确的日期格式 (YYYY-MM-DD)")
return
# 检查库存是否足够
current_weight = self.data["products"][self.current_product]["total_weight"]
if weight > current_weight:
messagebox.showerror("错误", f"库存不足!当前库存: {current_weight:.1f}kg")
return
# 更新产品数据
self.data["products"][self.current_product]["total_weight"] -= weight
# 添加交易记录
transaction = {
"product": self.current_product,
"type": "出库",
"weight": weight,
"price": price,
"note": note,
"time": date_str
}
self.data["transactions"].append(transaction)
# 保存数据
self.save_data()
messagebox.showinfo("成功", "出库成功!")
self.show_detail_page(self.current_product)
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
def show_add_category_page(self):
"""显示添加产品大类页面"""
self.current_page = "add_category"
self.clear_frame()
# 标题
title_frame = tk.Frame(self.main_frame)
title_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Label(title_frame, text="添加产品大类", font=("Arial", 16, "bold")).pack(side=tk.LEFT)
tk.Button(title_frame, text="返回主页", command=self.show_main_page).pack(side=tk.RIGHT)
# 输入表单
form_frame = tk.Frame(self.main_frame)
form_frame.pack(fill=tk.X, padx=50, pady=50)
# 产品名称
tk.Label(form_frame, text="产品名称:", font=("Arial", 12)).grid(row=0, column=0, sticky="w", pady=10)
self.category_name_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.category_name_var, font=("Arial", 12), width=30).grid(row=0, column=1, padx=10, pady=10)
# 初始重量
tk.Label(form_frame, text="初始重量(kg):", font=("Arial", 12)).grid(row=1, column=0, sticky="w", pady=10)
self.category_weight_var = tk.StringVar(value="0")
tk.Entry(form_frame, textvariable=self.category_weight_var, font=("Arial", 12), width=30).grid(row=1, column=1, padx=10, pady=10)
# 初始价格
tk.Label(form_frame, text="初始价格(元/kg):", font=("Arial", 12)).grid(row=2, column=0, sticky="w", pady=10)
self.category_price_var = tk.StringVar(value="0")
tk.Entry(form_frame, textvariable=self.category_price_var, font=("Arial", 12), width=30).grid(row=2, column=1, padx=10, pady=10)
# 产品描述
tk.Label(form_frame, text="产品描述:", font=("Arial", 12)).grid(row=3, column=0, sticky="w", pady=10)
self.category_desc_var = tk.StringVar()
tk.Entry(form_frame, textvariable=self.category_desc_var, font=("Arial", 12), width=30).grid(row=3, column=1, padx=10, pady=10)
# 提交按钮
button_frame = tk.Frame(form_frame)
button_frame.grid(row=4, column=0, columnspan=2, pady=20)
tk.Button(button_frame, text="确认添加", command=self.process_add_category, bg="lightblue", font=("Arial", 12)).pack(side=tk.LEFT, padx=10)
tk.Button(button_frame, text="取消", command=self.show_main_page, font=("Arial", 12)).pack(side=tk.LEFT, padx=10)
# 说明文字
info_frame = tk.Frame(self.main_frame)
info_frame.pack(fill=tk.X, padx=50, pady=10)
info_text = "说明:\n• 产品名称不能为空且不能与现有产品重复\n• 初始重量和价格可以设为0后续通过入库操作添加\n• 产品描述为可选项"
tk.Label(info_frame, text=info_text, font=("Arial", 10), justify=tk.LEFT, fg="gray").pack(anchor="w")
def process_add_category(self):
"""处理添加产品大类"""
try:
name = self.category_name_var.get().strip()
weight = float(self.category_weight_var.get())
price = float(self.category_price_var.get())
description = self.category_desc_var.get().strip()
# 验证输入
if not name:
messagebox.showerror("错误", "产品名称不能为空")
return
if name in self.data["products"]:
messagebox.showerror("错误", f"产品 '{name}' 已存在,请使用其他名称")
return
if weight < 0 or price < 0:
messagebox.showerror("错误", "重量和价格不能为负数")
return
# 添加新产品
self.data["products"][name] = {
"total_weight": weight,
"avg_price": price
}
# 如果有初始重量和价格,添加初始入库记录
if weight > 0 and price > 0:
transaction = {
"product": name,
"type": "入库",
"weight": weight,
"price": price,
"note": f"初始库存 - {description}" if description else "初始库存",
"time": datetime.now().strftime("%Y-%m-%d %H:%M")
}
self.data["transactions"].append(transaction)
# 保存数据
self.save_data()
messagebox.showinfo("成功", f"产品大类 '{name}' 添加成功!")
self.show_main_page()
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
def show_query_page(self):
"""显示查询页面"""
self.current_page = "query"
self.current_query_page = 1
self.clear_frame()
# 标题
title_frame = tk.Frame(self.main_frame)
title_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Label(title_frame, text="订单查询", font=("Arial", 16, "bold")).pack(side=tk.LEFT)
tk.Button(title_frame, text="返回主页", command=self.show_main_page).pack(side=tk.RIGHT)
# 查询条件
query_frame = tk.Frame(self.main_frame)
query_frame.pack(fill=tk.X, padx=10, pady=10)
# 第一行:产品名称和交易类型
row1_frame = tk.Frame(query_frame)
row1_frame.pack(fill=tk.X, pady=5)
tk.Label(row1_frame, text="产品名称:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_product_var = tk.StringVar()
tk.Entry(row1_frame, textvariable=self.query_product_var, font=("Arial", 10), width=15).pack(side=tk.LEFT, padx=(5, 20))
tk.Label(row1_frame, text="交易类型:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_type_var = tk.StringVar()
type_combo = tk.OptionMenu(row1_frame, self.query_type_var, "全部", "入库", "出库")
type_combo.config(font=("Arial", 10))
self.query_type_var.set("全部")
type_combo.pack(side=tk.LEFT, padx=5)
# 第二行:时间范围
row2_frame = tk.Frame(query_frame)
row2_frame.pack(fill=tk.X, pady=5)
tk.Label(row2_frame, text="开始时间:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_start_date_var = tk.StringVar()
tk.Entry(row2_frame, textvariable=self.query_start_date_var, font=("Arial", 10), width=12).pack(side=tk.LEFT, padx=(5, 10))
tk.Label(row2_frame, text="(格式: 2024-01-01)", font=("Arial", 8), fg="gray").pack(side=tk.LEFT, padx=(0, 20))
tk.Label(row2_frame, text="结束时间:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_end_date_var = tk.StringVar()
tk.Entry(row2_frame, textvariable=self.query_end_date_var, font=("Arial", 10), width=12).pack(side=tk.LEFT, padx=(5, 10))
tk.Label(row2_frame, text="(格式: 2024-12-31)", font=("Arial", 8), fg="gray").pack(side=tk.LEFT)
# 第三行:重量和价格范围
row3_frame = tk.Frame(query_frame)
row3_frame.pack(fill=tk.X, pady=5)
tk.Label(row3_frame, text="最小重量:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_min_weight_var = tk.StringVar()
tk.Entry(row3_frame, textvariable=self.query_min_weight_var, font=("Arial", 10), width=10).pack(side=tk.LEFT, padx=(5, 20))
tk.Label(row3_frame, text="最大重量:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_max_weight_var = tk.StringVar()
tk.Entry(row3_frame, textvariable=self.query_max_weight_var, font=("Arial", 10), width=10).pack(side=tk.LEFT, padx=(5, 20))
tk.Label(row3_frame, text="备注关键词:", font=("Arial", 10)).pack(side=tk.LEFT)
self.query_note_var = tk.StringVar()
tk.Entry(row3_frame, textvariable=self.query_note_var, font=("Arial", 10), width=15).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="查询", command=self.execute_query, bg="lightgreen", font=("Arial", 12)).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="清空条件", command=self.clear_query_conditions, font=("Arial", 12)).pack(side=tk.LEFT, padx=5)
# 查询结果列表
result_frame = tk.Frame(self.main_frame)
result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表头
header_frame = tk.Frame(result_frame)
header_frame.pack(fill=tk.X)
tk.Label(header_frame, text="时间", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="产品名", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="类型", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="重量(kg)", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="价格(元/kg)", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="总价(元)", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
tk.Label(header_frame, text="备注", width=13, relief=tk.RIDGE, font=("Arial", 10, "bold")).pack(side=tk.LEFT)
# 查询结果
self.query_result_frame = tk.Frame(result_frame)
self.query_result_frame.pack(fill=tk.BOTH, expand=True)
# 分页控制
self.query_page_frame = tk.Frame(self.main_frame)
self.query_page_frame.pack(fill=tk.X, padx=10, pady=10)
tk.Button(self.query_page_frame, text="上一页", command=self.prev_query_page).pack(side=tk.LEFT)
self.query_page_label = tk.Label(self.query_page_frame, text="第 1 页 / 共 1 页")
self.query_page_label.pack(side=tk.LEFT, padx=20)
tk.Button(self.query_page_frame, text="下一页", command=self.next_query_page).pack(side=tk.LEFT)
# 设置默认查询条件:最近一个月
from datetime import datetime, timedelta
today = datetime.now()
one_month_ago = today - timedelta(days=30)
self.query_start_date_var.set(one_month_ago.strftime("%Y-%m-%d"))
self.query_end_date_var.set(today.strftime("%Y-%m-%d"))
# 初始显示最近一个月的交易记录
self.execute_query()
def execute_query(self):
"""执行查询"""
# 获取查询条件
product_name = self.query_product_var.get().strip().lower()
transaction_type = self.query_type_var.get()
start_date = self.query_start_date_var.get().strip()
end_date = self.query_end_date_var.get().strip()
min_weight = self.query_min_weight_var.get().strip()
max_weight = self.query_max_weight_var.get().strip()
note_keyword = self.query_note_var.get().strip().lower()
# 过滤交易记录
filtered_transactions = []
for transaction in self.data["transactions"]:
# 产品名称过滤
if product_name and product_name not in transaction["product"].lower():
continue
# 交易类型过滤
if transaction_type != "全部" and transaction["type"] != transaction_type:
continue
# 时间范围过滤
if start_date:
try:
trans_date = transaction["time"][:10] # 取日期部分
if trans_date < start_date:
continue
except:
pass
if end_date:
try:
trans_date = transaction["time"][:10] # 取日期部分
if trans_date > end_date:
continue
except:
pass
# 重量范围过滤
if min_weight:
try:
if transaction["weight"] < float(min_weight):
continue
except ValueError:
pass
if max_weight:
try:
if transaction["weight"] > float(max_weight):
continue
except ValueError:
pass
# 备注关键词过滤
if note_keyword and note_keyword not in transaction["note"].lower():
continue
filtered_transactions.append(transaction)
# 按时间倒序排列
filtered_transactions.sort(key=lambda x: x["time"], reverse=True)
# 保存查询结果
self.query_results = filtered_transactions
# 重置到第一页
self.current_query_page = 1
# 更新查询结果显示
self.update_query_results()
def update_query_results(self):
"""更新查询结果显示"""
for widget in self.query_result_frame.winfo_children():
widget.destroy()
# 分页处理
start_idx = (self.current_query_page - 1) * self.items_per_page
end_idx = start_idx + self.items_per_page
page_results = self.query_results[start_idx:end_idx]
# 显示查询结果
for transaction in page_results:
row_frame = tk.Frame(self.query_result_frame)
row_frame.pack(fill=tk.X, pady=1)
# 计算总价
total_price = transaction["weight"] * transaction["price"]
tk.Label(row_frame, text=transaction["time"], width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=transaction["product"], width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
color = "lightgreen" if transaction["type"] == "入库" else "lightcoral"
tk.Label(row_frame, text=transaction["type"], width=14, relief=tk.RIDGE, bg=color).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{transaction['weight']:.1f}", width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{transaction['price']:.2f}", width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=f"{total_price:.2f}", width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
tk.Label(row_frame, text=transaction["note"], width=15, relief=tk.RIDGE).pack(side=tk.LEFT)
# 更新分页信息
total_results = len(self.query_results)
max_pages = max(1, (total_results + self.items_per_page - 1) // self.items_per_page)
self.query_page_label.config(text=f"{self.current_query_page} 页 / 共 {max_pages} 页 (共 {total_results} 条记录)")
# 计算统计信息
total_weight = 0
total_price = 0
for transaction in self.query_results:
total_weight += transaction["weight"]
total_price += transaction["weight"] * transaction["price"]
# 在分页信息右侧显示统计信息
stats_text = f"总重量: {total_weight:.2f}kg 总价: {total_price:.2f}"
if hasattr(self, 'query_stats_label') and self.query_stats_label.winfo_exists():
try:
self.query_stats_label.config(text=stats_text)
except tk.TclError:
# 如果标签已被销毁,重新创建
self.query_stats_label = tk.Label(self.query_page_frame, text=stats_text, font=("Arial", 10, "bold"), fg="blue")
self.query_stats_label.pack(side=tk.RIGHT)
else:
self.query_stats_label = tk.Label(self.query_page_frame, text=stats_text, font=("Arial", 10, "bold"), fg="blue")
self.query_stats_label.pack(side=tk.RIGHT)
# 如果没有查询结果,显示提示
if not page_results:
no_result_frame = tk.Frame(self.query_result_frame)
no_result_frame.pack(fill=tk.X, pady=20)
tk.Label(no_result_frame, text="未找到符合条件的记录",
font=("Arial", 12), fg="gray").pack()
def clear_query_conditions(self):
"""清空查询条件"""
self.query_product_var.set("")
self.query_type_var.set("全部")
self.query_start_date_var.set("")
self.query_end_date_var.set("")
self.query_min_weight_var.set("")
self.query_max_weight_var.set("")
self.query_note_var.set("")
self.execute_query()
def prev_query_page(self):
"""查询结果上一页"""
if self.current_query_page > 1:
self.current_query_page -= 1
self.update_query_results()
def next_query_page(self):
"""查询结果下一页"""
total_results = len(self.query_results)
max_pages = max(1, (total_results + self.items_per_page - 1) // self.items_per_page)
if self.current_query_page < max_pages:
self.current_query_page += 1
self.update_query_results()
def delete_transaction(self, transaction):
"""删除交易记录"""
# 确认删除
result = messagebox.askyesno("确认删除",
f"确定要删除这条交易记录吗?\n\n时间: {transaction['time']}\n类型: {transaction['type']}\n重量: {transaction['weight']:.1f}kg\n价格: {transaction['price']:.2f}元/kg")
if not result:
return
try:
# 从交易记录中移除
self.data["transactions"].remove(transaction)
# 重新计算产品数据
self.recalculate_product_data(self.current_product)
# 保存数据
self.save_data()
messagebox.showinfo("成功", "交易记录删除成功!")
# 刷新详情页面
self.show_detail_page(self.current_product)
except ValueError:
messagebox.showerror("错误", "删除失败,请重试")
def recalculate_product_data(self, product_name):
"""重新计算产品数据"""
# 获取该产品的所有交易记录
product_transactions = [t for t in self.data["transactions"] if t["product"] == product_name]
if not product_transactions:
# 如果没有交易记录,重置产品数据
self.data["products"][product_name] = {"total_weight": 0, "avg_price": 0}
return
# 重新计算总重量和平均价格
total_weight = 0
total_value = 0
for transaction in product_transactions:
if transaction["type"] == "入库":
total_weight += transaction["weight"]
total_value += transaction["weight"] * transaction["price"]
else: # 出库
total_weight -= transaction["weight"]
# 计算平均价格(只考虑入库记录)
inbound_transactions = [t for t in product_transactions if t["type"] == "入库"]
if inbound_transactions:
total_inbound_weight = sum(t["weight"] for t in inbound_transactions)
total_inbound_value = sum(t["weight"] * t["price"] for t in inbound_transactions)
avg_price = total_inbound_value / total_inbound_weight if total_inbound_weight > 0 else 0
else:
avg_price = 0
# 更新产品数据
self.data["products"][product_name] = {
"total_weight": max(0, total_weight), # 确保重量不为负
"avg_price": avg_price
}
def delete_product_category(self):
"""删除整个产品分类"""
# 确认删除
result = messagebox.askyesno("确认删除",
f"确定要删除产品分类 '{self.current_product}' 吗?\n\n这将删除该产品的所有交易记录,此操作不可撤销!")
if not result:
return
try:
# 删除该产品的所有交易记录
self.data["transactions"] = [t for t in self.data["transactions"] if t["product"] != self.current_product]
# 删除产品数据
if self.current_product in self.data["products"]:
del self.data["products"][self.current_product]
# 保存数据
self.save_data()
messagebox.showinfo("成功", f"产品分类 '{self.current_product}' 已删除!")
# 返回主页
self.show_main_page()
except Exception as e:
messagebox.showerror("错误", f"删除失败:{str(e)}")
def main():
root = tk.Tk()
app = InventoryApp(root)
root.mainloop()
if __name__ == "__main__":
main()