This commit is contained in:
2025-12-23 00:30:36 +08:00
parent 192c05707a
commit 033a1acef3
16 changed files with 2870 additions and 1342 deletions

633
raw_material_dialog.py Normal file
View File

@@ -0,0 +1,633 @@
"""
原料库管理模块 - 处理面料和原材料的管理
"""
import os
from datetime import datetime
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit,
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox, QTabWidget, QWidget, QDoubleSpinBox, QTextEdit
)
from PyQt5.QtCore import Qt
from database import get_db_connection
from stock_dialog import StockInDialog
class RawMaterialLibraryDialog(QDialog):
def __init__(self, db_path, is_admin=False):
super().__init__()
self.db_path = db_path
self.is_admin = is_admin
self.current_edit_model = None
self.setWindowTitle("原料库管理")
self.resize(1400, 700)
self.setup_ui()
self.refresh_filters_and_table()
self.load_add_major_categories()
self.load_stock_table()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
# 工具栏
toolbar = QHBoxLayout()
stock_in_btn = QPushButton("📥 原料入库管理(独立)")
stock_in_btn.clicked.connect(self.open_stock_in_dialog)
stock_in_btn.setStyleSheet("background-color: #ff5722; color: white; padding: 10px; font-weight: bold;")
toolbar.addWidget(stock_in_btn)
toolbar.addStretch()
layout.addLayout(toolbar)
# 标签页
tabs = QTabWidget()
layout.addWidget(tabs)
# 原料列表标签页
list_tab = self.create_list_tab()
tabs.addTab(list_tab, "原料列表")
# 新增/编辑标签页
add_tab = self.create_add_tab()
tabs.addTab(add_tab, "新增/编辑原料")
# 库存跟踪标签页
stock_tab = self.create_stock_tab()
tabs.addTab(stock_tab, "库存跟踪")
def create_list_tab(self):
"""创建原料列表标签页"""
list_tab = QWidget()
list_layout = QVBoxLayout(list_tab)
# 过滤器区域
filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("类目筛选:"))
self.major_combo = QComboBox()
self.major_combo.addItem("全部类目")
self.major_combo.currentIndexChanged.connect(self.load_sub_categories)
filter_layout.addWidget(self.major_combo)
filter_layout.addWidget(QLabel("类型筛选:"))
self.sub_combo = QComboBox()
self.sub_combo.addItem("全部类型")
self.sub_combo.currentIndexChanged.connect(self.load_table)
filter_layout.addWidget(self.sub_combo)
filter_layout.addWidget(QLabel("供应商筛选:"))
self.supplier_combo = QComboBox()
self.supplier_combo.addItem("全部供应商")
self.supplier_combo.currentIndexChanged.connect(self.load_table)
filter_layout.addWidget(self.supplier_combo)
filter_layout.addWidget(QLabel("搜索型号/名称:"))
self.search_input = QLineEdit()
self.search_input.textChanged.connect(self.load_table)
filter_layout.addWidget(self.search_input)
refresh_btn = QPushButton("刷新")
refresh_btn.clicked.connect(self.refresh_filters_and_table)
filter_layout.addWidget(refresh_btn)
list_layout.addLayout(filter_layout)
# 数据表格
headers = ["类目", "类型", "型号", "供应商", "颜色", "幅宽(cm)", "克重(g/m²)", "单位", "散剪价", "大货价(单位)", "米价", "码价", "操作"]
self.table = QTableWidget()
self.table.setColumnCount(len(headers))
self.table.setHorizontalHeaderLabels(headers)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
list_layout.addWidget(self.table)
# 隐藏价格列(非管理员)
if not self.is_admin:
self.table.setColumnHidden(9, True)
self.table.setColumnHidden(10, True)
self.table.setColumnHidden(11, True)
return list_tab
def create_add_tab(self):
"""创建新增/编辑标签页"""
add_tab = QWidget()
add_layout = QGridLayout(add_tab)
# 类目选择
add_layout.addWidget(QLabel("类目:"), 0, 0, Qt.AlignRight)
self.add_major_category = QComboBox()
self.add_major_category.setEditable(True)
self.add_major_category.currentTextChanged.connect(self.on_major_changed)
add_layout.addWidget(self.add_major_category, 0, 1)
add_layout.addWidget(QLabel("类型:"), 0, 2, Qt.AlignRight)
self.add_sub_category = QLineEdit()
self.add_sub_category.textChanged.connect(self.on_sub_changed)
add_layout.addWidget(self.add_sub_category, 0, 3)
add_layout.addWidget(QLabel("完整分类(显示用):"), 1, 0, Qt.AlignRight)
self.full_category_label = QLabel("布料-")
add_layout.addWidget(self.full_category_label, 1, 1, 1, 3)
# 基本信息
add_layout.addWidget(QLabel("型号:"), 2, 0, Qt.AlignRight)
self.add_model = QLineEdit()
add_layout.addWidget(self.add_model, 2, 1, 1, 3)
add_layout.addWidget(QLabel("供应商:"), 3, 0, Qt.AlignRight)
self.add_supplier = QComboBox()
self.add_supplier.setEditable(True)
add_layout.addWidget(self.add_supplier, 3, 1, 1, 3)
add_layout.addWidget(QLabel("颜色:"), 4, 0, Qt.AlignRight)
self.add_color = QLineEdit()
add_layout.addWidget(self.add_color, 4, 1, 1, 3)
# 规格信息
add_layout.addWidget(QLabel("幅宽 (cm):"), 5, 0, Qt.AlignRight)
self.add_width = QDoubleSpinBox()
self.add_width.setRange(0, 300)
self.add_width.setValue(0)
add_layout.addWidget(self.add_width, 5, 1)
add_layout.addWidget(QLabel("克重 (g/m²):"), 6, 0, Qt.AlignRight)
self.add_gsm = QDoubleSpinBox()
self.add_gsm.setRange(0, 1000)
self.add_gsm.setValue(0)
add_layout.addWidget(self.add_gsm, 6, 1)
add_layout.addWidget(QLabel("单位:"), 7, 0, Qt.AlignRight)
self.add_unit = QComboBox()
self.add_unit.setEditable(True)
self.add_unit.addItems(["", "", "公斤", "一对", "", ""])
add_layout.addWidget(self.add_unit, 7, 1)
# 价格信息
add_layout.addWidget(QLabel("散剪价 (元/单位):"), 8, 0, Qt.AlignRight)
self.add_retail = QDoubleSpinBox()
self.add_retail.setRange(0, 10000)
self.add_retail.setDecimals(2)
add_layout.addWidget(self.add_retail, 8, 1)
add_layout.addWidget(QLabel("大货价 (元/单位):"), 9, 0, Qt.AlignRight)
self.add_bulk = QDoubleSpinBox()
self.add_bulk.setRange(0, 10000)
self.add_bulk.setDecimals(2)
add_layout.addWidget(self.add_bulk, 9, 1)
# 保存按钮
save_btn = QPushButton("保存原料")
save_btn.clicked.connect(self.save_raw_material)
add_layout.addWidget(save_btn, 10, 0, 1, 4)
return add_tab
def create_stock_tab(self):
"""创建库存跟踪标签页"""
stock_tab = QWidget()
stock_layout = QVBoxLayout(stock_tab)
stock_refresh = QPushButton("刷新库存")
stock_refresh.clicked.connect(self.load_stock_table)
stock_layout.addWidget(stock_refresh)
stock_headers = ["型号/名称", "颜色", "单位", "总采购量", "总消耗量", "当前剩余", "操作"]
self.stock_table = QTableWidget()
self.stock_table.setColumnCount(len(stock_headers))
self.stock_table.setHorizontalHeaderLabels(stock_headers)
self.stock_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
stock_layout.addWidget(self.stock_table)
return stock_tab
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def open_stock_in_dialog(self):
"""打开入库对话框"""
dialog = StockInDialog(self.db_path)
dialog.exec_()
self.load_stock_table()
def on_major_changed(self, text):
"""主类目改变事件"""
self.update_full_category()
def on_sub_changed(self, text):
"""子类型改变事件"""
if "胸杯" in text:
self.add_major_category.setCurrentText("辅料")
self.add_unit.setCurrentText("一对")
self.add_unit.setEnabled(False)
else:
self.add_unit.setEnabled(True)
self.update_full_category()
def update_full_category(self):
"""更新完整类目显示"""
major = self.add_major_category.currentText().strip()
sub = self.add_sub_category.text().strip()
if major == "布料" and sub:
full = "布料-" + sub
else:
full = sub or major
self.full_category_label.setText(full)
def refresh_filters_and_table(self):
"""刷新过滤器和表格"""
self.load_major_categories()
self.load_suppliers()
self.load_table()
def load_major_categories(self):
"""加载主类目"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1) ELSE category END FROM fabrics")
majors = set(row[0] for row in cursor.fetchall() if row[0])
majors.update({"布料", "辅料", "其他"})
self.major_combo.blockSignals(True)
self.major_combo.clear()
self.major_combo.addItem("全部类目")
self.major_combo.addItems(sorted(majors))
self.major_combo.blockSignals(False)
except:
pass
self.load_sub_categories()
def load_add_major_categories(self):
"""加载添加界面的主类目"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1) ELSE category END FROM fabrics")
majors = set(row[0] for row in cursor.fetchall() if row[0])
majors.update({"布料", "辅料", "其他"})
self.add_major_category.blockSignals(True)
current_text = self.add_major_category.currentText()
self.add_major_category.clear()
self.add_major_category.addItems(sorted(majors))
if current_text in majors:
self.add_major_category.setCurrentText(current_text)
else:
self.add_major_category.setCurrentText("布料")
self.add_major_category.blockSignals(False)
except:
self.add_major_category.clear()
self.add_major_category.addItems(["布料", "辅料", "其他"])
self.add_major_category.setCurrentText("布料")
def load_sub_categories(self):
"""加载子类型"""
major = self.major_combo.currentText()
self.sub_combo.blockSignals(True)
self.sub_combo.clear()
self.sub_combo.addItem("全部类型")
try:
with self.get_conn() as conn:
if major in ("全部类目", ""):
cursor = conn.execute("SELECT DISTINCT category FROM fabrics WHERE category LIKE '%-%'")
subs = set()
for row in cursor.fetchall():
cat = row[0]
if '-' in cat:
subs.add(cat.split('-', 1)[1])
self.sub_combo.addItems(sorted(subs))
else:
cursor = conn.execute("SELECT category FROM fabrics WHERE category LIKE ? OR category = ?", (major + "-%", major))
subs = set()
for row in cursor.fetchall():
cat = row[0]
if '-' in cat:
subs.add(cat.split('-', 1)[1])
self.sub_combo.addItems(sorted(subs))
except:
pass
self.sub_combo.blockSignals(False)
self.load_table()
def load_suppliers(self):
"""加载供应商列表"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT DISTINCT supplier FROM fabrics WHERE supplier IS NOT NULL AND supplier != '' ORDER BY supplier")
suppliers = [row[0] for row in cursor.fetchall()]
self.supplier_combo.blockSignals(True)
self.supplier_combo.clear()
self.supplier_combo.addItem("全部供应商")
self.supplier_combo.addItems(suppliers)
self.supplier_combo.blockSignals(False)
self.add_supplier.blockSignals(True)
self.add_supplier.clear()
self.add_supplier.addItems(suppliers)
self.add_supplier.blockSignals(False)
except:
pass
def load_table(self):
"""加载原料表格数据"""
try:
with self.get_conn() as conn:
query = "SELECT category, model, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics"
params = []
conditions = []
# 类目过滤
major = self.major_combo.currentText()
sub = self.sub_combo.currentText()
if major != "全部类目" and major:
if sub != "全部类型" and sub:
conditions.append("category = ?")
params.append(major + "-" + sub)
else:
conditions.append("(category LIKE ? OR category = ?)")
params.append(major + "-%")
params.append(major)
# 供应商过滤
supplier = self.supplier_combo.currentText()
if supplier != "全部供应商" and supplier:
conditions.append("supplier = ?")
params.append(supplier)
# 关键词搜索
keyword = self.search_input.text().strip()
if keyword:
conditions.append("(model LIKE ? OR color LIKE ?)")
params.append("%" + keyword + "%")
params.append("%" + keyword + "%")
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY timestamp DESC"
cursor = conn.execute(query, params)
rows = cursor.fetchall()
# 填充表格
self.table.setRowCount(len(rows))
self.table.clearContents()
for row_idx, (category, model, supplier, color, width, gsm, unit, retail, bulk) in enumerate(rows):
major = category.split('-', 1)[0] if '-' in category else category
sub = category.split('-', 1)[1] if '-' in category else ""
self.table.setItem(row_idx, 0, QTableWidgetItem(major))
self.table.setItem(row_idx, 1, QTableWidgetItem(sub))
self.table.setItem(row_idx, 2, QTableWidgetItem(model))
self.table.setItem(row_idx, 3, QTableWidgetItem(supplier or ""))
self.table.setItem(row_idx, 4, QTableWidgetItem(color or ""))
self.table.setItem(row_idx, 5, QTableWidgetItem("{:.1f}".format(width) if width else ""))
self.table.setItem(row_idx, 6, QTableWidgetItem("{:.0f}".format(gsm) if gsm else ""))
self.table.setItem(row_idx, 7, QTableWidgetItem(unit or ""))
self.table.setItem(row_idx, 8, QTableWidgetItem("{:.2f}".format(retail) if retail is not None else ""))
if self.is_admin:
unit_display = unit or ""
bulk_display = "{:.2f} ({})".format(bulk, unit_display) if bulk is not None else ""
self.table.setItem(row_idx, 9, QTableWidgetItem(bulk_display))
# 计算米价和码价
price_per_m = price_per_yard = 0.0
if bulk and width and gsm and width > 0 and gsm > 0:
if unit == "":
price_per_m = bulk
elif unit == "":
price_per_m = bulk / 0.9144
elif unit == "公斤":
price_per_m = bulk * (gsm / 1000.0) * (width / 100.0)
price_per_yard = price_per_m * 0.9144
self.table.setItem(row_idx, 10, QTableWidgetItem("{:.2f}".format(price_per_m)))
self.table.setItem(row_idx, 11, QTableWidgetItem("{:.2f}".format(price_per_yard)))
# 操作按钮
op_widget = QWidget()
op_layout = QHBoxLayout(op_widget)
op_layout.setContentsMargins(5, 2, 5, 2)
op_layout.setSpacing(10)
edit_btn = QPushButton("编辑")
edit_btn.clicked.connect(lambda _, m=model: self.edit_raw_material(m))
op_layout.addWidget(edit_btn)
del_btn = QPushButton("删除")
del_btn.clicked.connect(lambda _, m=model: self.delete_raw(m))
op_layout.addWidget(del_btn)
self.table.setCellWidget(row_idx, self.table.columnCount() - 1, op_widget)
except Exception as e:
QMessageBox.critical(self, "加载失败", str(e))
def edit_raw_material(self, model):
"""编辑原料"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT category, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics WHERE model = ?", (model,))
row = cursor.fetchone()
if not row:
QMessageBox.warning(self, "提示", "原料不存在或已被删除")
return
category, supplier, color, width, gsm, unit, retail, bulk = row
major = category.split('-', 1)[0] if '-' in category else category
sub = category.split('-', 1)[1] if '-' in category else ""
self.add_major_category.setCurrentText(major)
self.add_sub_category.setText(sub)
self.update_full_category()
self.add_supplier.setCurrentText(supplier or "")
self.add_color.setText(color or "")
self.add_model.setText(model)
self.add_width.setValue(width or 0)
self.add_gsm.setValue(gsm or 0)
self.add_unit.setCurrentText(unit or "")
self.add_retail.setValue(retail or 0)
self.add_bulk.setValue(bulk or 0)
if "胸杯" in sub:
self.add_unit.setEnabled(False)
self.current_edit_model = model
# 切换到编辑标签页
tabs = self.findChild(QTabWidget)
tabs.setCurrentIndex(1)
QMessageBox.information(self, "提示", f"已加载 '{model}' 的信息,可修改后点击'保存原料'")
except Exception as e:
QMessageBox.critical(self, "错误", "加载原料信息失败: " + str(e))
def delete_raw(self, model):
"""删除原料"""
reply = QMessageBox.question(self, "确认", f"删除 '{model}'")
if reply == QMessageBox.Yes:
try:
with self.get_conn() as conn:
conn.execute("DELETE FROM fabrics WHERE model=?", (model,))
conn.commit()
self.load_table()
QMessageBox.information(self, "成功", "删除完成")
except Exception as e:
QMessageBox.critical(self, "错误", "删除失败: " + str(e))
def save_raw_material(self):
"""保存原料"""
model = self.add_model.text().strip()
if not model:
QMessageBox.warning(self, "错误", "请输入型号/名称")
return
major = self.add_major_category.currentText().strip()
sub = self.add_sub_category.text().strip()
if "胸杯" in sub:
major = "辅料"
if major and sub:
category = major + "-" + sub
else:
category = sub or major
supplier = self.add_supplier.currentText().strip()
color = self.add_color.text().strip()
unit = self.add_unit.currentText().strip() or ""
try:
with self.get_conn() as conn:
conn.execute('''
INSERT OR REPLACE INTO fabrics
(model, category, supplier, color, width, gsm, retail_price, bulk_price, unit, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (model, category, supplier, color,
self.add_width.value() or None, self.add_gsm.value() or None,
self.add_retail.value() or None, self.add_bulk.value() or None,
unit, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
conn.commit()
action = "更新" if self.current_edit_model else "保存"
QMessageBox.information(self, "成功", f"{action} '{model}'")
self.current_edit_model = None
# 清空表单
self.add_model.clear()
self.add_color.clear()
self.add_width.setValue(0)
self.add_gsm.setValue(0)
self.add_retail.setValue(0)
self.add_bulk.setValue(0)
self.add_sub_category.clear()
self.add_unit.setEnabled(True)
self.update_full_category()
self.refresh_filters_and_table()
self.load_add_major_categories()
except Exception as e:
QMessageBox.critical(self, "错误", str(e))
def load_stock_table(self):
"""加载库存表格"""
try:
with self.get_conn() as conn:
cursor = conn.execute('''
SELECT f.model, f.color, f.unit,
COALESCE(SUM(si.quantity), 0) AS total_in,
COALESCE(SUM(c.consume_quantity), 0) AS total_out
FROM fabrics f
LEFT JOIN fabric_stock_in si ON f.model = si.model
LEFT JOIN fabric_consumption c ON f.model = c.model
GROUP BY f.model
ORDER BY f.timestamp DESC
''')
rows = cursor.fetchall()
self.stock_table.setRowCount(len(rows))
for row_idx, (model, color, unit, total_in, total_out) in enumerate(rows):
remaining = total_in - total_out
self.stock_table.setItem(row_idx, 0, QTableWidgetItem(model))
self.stock_table.setItem(row_idx, 1, QTableWidgetItem(color or ""))
self.stock_table.setItem(row_idx, 2, QTableWidgetItem(unit or ""))
self.stock_table.setItem(row_idx, 3, QTableWidgetItem("{:.3f}".format(total_in)))
self.stock_table.setItem(row_idx, 4, QTableWidgetItem("{:.3f}".format(total_out)))
self.stock_table.setItem(row_idx, 5, QTableWidgetItem("{:.3f}".format(remaining)))
# 操作按钮
op_widget = QWidget()
op_layout = QHBoxLayout(op_widget)
op_layout.setContentsMargins(5, 2, 5, 2)
op_layout.setSpacing(10)
detail_btn = QPushButton("查看明细")
detail_btn.clicked.connect(lambda _, m=model: self.show_stock_detail(m))
op_layout.addWidget(detail_btn)
clear_btn = QPushButton("一键清零剩余")
clear_btn.clicked.connect(lambda _, m=model: self.clear_remaining(m))
op_layout.addWidget(clear_btn)
self.stock_table.setCellWidget(row_idx, 6, op_widget)
except Exception as e:
QMessageBox.critical(self, "错误", str(e))
def show_stock_detail(self, model):
"""显示库存明细"""
try:
with self.get_conn() as conn:
# 查询入库记录
cursor_in = conn.execute("SELECT purchase_date, quantity, unit, note FROM fabric_stock_in WHERE model = ? ORDER BY purchase_date DESC", (model,))
in_rows = cursor_in.fetchall()
# 查询消耗记录
cursor_out = conn.execute('''
SELECT consume_date, style_number, quantity_made, loss_rate, consume_quantity, unit
FROM fabric_consumption WHERE model = ? ORDER BY consume_date DESC
''', (model,))
out_rows = cursor_out.fetchall()
text = f"{model}】库存明细\n\n"
text += "=== 采购入库记录 ===\n"
if in_rows:
for date, qty, unit, note in in_rows:
text += f"{date} +{qty} {unit} {note or ''}\n"
else:
text += "暂无入库记录\n"
text += "\n=== 生产消耗记录 ===\n"
if out_rows:
for date, style, qty_made, loss, consume, unit in out_rows:
text += f"{date} {style} {qty_made}件 (损耗{round(loss * 100, 1)}%) -{round(consume, 3)} {unit}\n"
else:
text += "暂无消耗记录\n"
# 显示明细对话框
dialog = QDialog(self)
dialog.setWindowTitle(model + " 库存明细")
dialog.resize(800, 600)
layout = QVBoxLayout(dialog)
text_edit = QTextEdit()
text_edit.setReadOnly(True)
text_edit.setText(text)
layout.addWidget(text_edit)
close_btn = QPushButton("关闭")
close_btn.clicked.connect(dialog.accept)
layout.addWidget(close_btn)
dialog.exec_()
except Exception as e:
QMessageBox.critical(self, "错误", str(e))
def clear_remaining(self, model):
"""清零剩余库存"""
reply = QMessageBox.question(self, "确认清零", f"确定将 {model} 的剩余量清零?\n(此操作仅逻辑清零,不删除历史记录)")
if reply == QMessageBox.Yes:
QMessageBox.information(self, "完成", f"{model} 剩余量已清零(视为全部用完)")
self.load_stock_table()