Files
cangku/raw_material_dialog.py
2025-12-29 00:36:40 +08:00

1009 lines
41 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 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 RawMaterialEditDialog(QDialog):
"""原料编辑对话框 - 独立窗口"""
def __init__(self, db_path, model, is_admin=False, parent=None):
super().__init__(parent)
self.db_path = db_path
self.model = model
self.is_admin = is_admin
self.setWindowTitle(f"编辑原料 - {model}")
self.resize(600, 500)
self.setup_ui()
self.load_material_data()
def setup_ui(self):
"""设置用户界面"""
layout = QGridLayout(self)
# 类目选择
layout.addWidget(QLabel("类目:"), 0, 0, Qt.AlignRight)
self.edit_major_category = QComboBox()
self.edit_major_category.setEditable(True)
layout.addWidget(self.edit_major_category, 0, 1)
layout.addWidget(QLabel("类型:"), 0, 2, Qt.AlignRight)
self.edit_sub_category = QLineEdit()
layout.addWidget(self.edit_sub_category, 0, 3)
# 基本信息
layout.addWidget(QLabel("型号:"), 1, 0, Qt.AlignRight)
self.edit_model = QLineEdit()
layout.addWidget(self.edit_model, 1, 1, 1, 3)
layout.addWidget(QLabel("供应商:"), 2, 0, Qt.AlignRight)
self.edit_supplier = QComboBox()
self.edit_supplier.setEditable(True)
layout.addWidget(self.edit_supplier, 2, 1, 1, 3)
layout.addWidget(QLabel("颜色:"), 3, 0, Qt.AlignRight)
self.edit_color = QLineEdit()
layout.addWidget(self.edit_color, 3, 1, 1, 3)
# 规格信息
layout.addWidget(QLabel("幅宽 (cm):"), 4, 0, Qt.AlignRight)
self.edit_width = QDoubleSpinBox()
self.edit_width.setRange(0, 300)
self.edit_width.setValue(0)
layout.addWidget(self.edit_width, 4, 1)
layout.addWidget(QLabel("克重 (g/m²):"), 5, 0, Qt.AlignRight)
self.edit_gsm = QDoubleSpinBox()
self.edit_gsm.setRange(0, 1000)
self.edit_gsm.setValue(0)
layout.addWidget(self.edit_gsm, 5, 1)
layout.addWidget(QLabel("单位:"), 6, 0, Qt.AlignRight)
self.edit_unit = QComboBox()
self.edit_unit.setEditable(True)
self.edit_unit.addItems(["", "", "公斤", "一对", "", ""])
layout.addWidget(self.edit_unit, 6, 1)
# 价格信息
layout.addWidget(QLabel("散剪价 (元/单位):"), 7, 0, Qt.AlignRight)
self.edit_retail = QDoubleSpinBox()
self.edit_retail.setRange(0, 10000)
self.edit_retail.setDecimals(2)
layout.addWidget(self.edit_retail, 7, 1)
layout.addWidget(QLabel("大货价 (元/单位):"), 8, 0, Qt.AlignRight)
self.edit_bulk = QDoubleSpinBox()
self.edit_bulk.setRange(0, 10000)
self.edit_bulk.setDecimals(2)
layout.addWidget(self.edit_bulk, 8, 1)
# 按钮
button_layout = QHBoxLayout()
save_btn = QPushButton("保存修改")
save_btn.clicked.connect(self.save_changes)
save_btn.setStyleSheet("background-color: #4CAF50; color: white; padding: 8px; font-weight: bold;")
button_layout.addWidget(save_btn)
cancel_btn = QPushButton("取消")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout, 9, 0, 1, 4)
# 加载类目和供应商列表
self.load_categories_and_suppliers()
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def load_categories_and_suppliers(self):
"""加载类目和供应商列表"""
try:
with self.get_conn() as conn:
# 加载类目 - 设置默认选项为:面料、辅料、其他
self.edit_major_category.addItems(["面料", "辅料", "其他"])
# 加载供应商
cursor = conn.execute("SELECT DISTINCT supplier FROM fabrics WHERE supplier IS NOT NULL AND supplier != '' AND (is_deleted IS NULL OR is_deleted = 0) ORDER BY supplier")
suppliers = [row[0] for row in cursor.fetchall()]
self.edit_supplier.addItems(suppliers)
except:
pass
def load_material_data(self):
"""加载原料数据"""
try:
with self.get_conn() as conn:
cursor = conn.execute(
"SELECT category, fabric_type, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)",
(self.model,)
)
row = cursor.fetchone()
if not row:
QMessageBox.warning(self, "提示", "原料不存在或已被删除")
self.reject()
return
category, fabric_type, supplier, color, width, gsm, unit, retail, bulk = row
# 填充表单
self.edit_model.setText(self.model)
self.edit_major_category.setCurrentText(category or "")
self.edit_sub_category.setText(fabric_type or "")
self.edit_supplier.setCurrentText(supplier or "")
self.edit_color.setText(color or "")
self.edit_width.setValue(width or 0)
self.edit_gsm.setValue(gsm or 0)
self.edit_unit.setCurrentText(unit or "")
self.edit_retail.setValue(retail or 0)
self.edit_bulk.setValue(bulk or 0)
# 特殊处理胸杯类型
if fabric_type and "胸杯" in fabric_type:
self.edit_unit.setEnabled(False)
except Exception as e:
QMessageBox.critical(self, "错误", f"加载原料信息失败: {str(e)}")
self.reject()
def save_changes(self):
"""保存修改"""
new_model = self.edit_model.text().strip()
# 检查型号是否为空
if not new_model:
QMessageBox.warning(self, "错误", "型号不能为空")
return
major = self.edit_major_category.currentText().strip()
sub = self.edit_sub_category.text().strip()
# 验证必填字段:类目
if not major:
QMessageBox.warning(self, "错误", "请选择类目")
return
# 验证必填字段:类型
if not sub:
QMessageBox.warning(self, "错误", "请输入类型")
return
# 验证必填字段:幅宽
if self.edit_width.value() <= 0:
QMessageBox.warning(self, "错误", "请输入幅宽")
return
# 验证必填字段:克重
if self.edit_gsm.value() <= 0:
QMessageBox.warning(self, "错误", "请输入克重")
return
# 特殊处理胸杯类型
if "胸杯" in sub:
major = "辅料"
category = major or ""
fabric_type = sub or ""
supplier = self.edit_supplier.currentText().strip()
color = self.edit_color.text().strip()
unit = self.edit_unit.currentText().strip() or ""
try:
with self.get_conn() as conn:
# 如果型号被修改了,检查新型号是否已存在
if new_model != self.model:
cursor = conn.execute("SELECT model FROM fabrics WHERE model = ?", (new_model,))
if cursor.fetchone():
QMessageBox.warning(self, "错误", f"型号 '{new_model}' 已存在,请使用其他型号")
return
# 型号被修改,需要更新所有相关表
# 更新原料表
conn.execute('''
UPDATE fabrics
SET model=?, category=?, fabric_type=?, supplier=?, color=?, width=?, gsm=?,
retail_price=?, bulk_price=?, unit=?, timestamp=?
WHERE model=?
''', (
new_model, category, fabric_type, supplier, color,
self.edit_width.value() or None,
self.edit_gsm.value() or None,
self.edit_retail.value() or None,
self.edit_bulk.value() or None,
unit,
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
self.model
))
# 更新入库记录表
conn.execute("UPDATE fabric_stock_in SET model=? WHERE model=?", (new_model, self.model))
# 更新消耗记录表
conn.execute("UPDATE fabric_consumption SET model=? WHERE model=?", (new_model, self.model))
# 更新衣服材料用量表
conn.execute("UPDATE garment_materials SET model=? WHERE model=?", (new_model, self.model))
else:
# 型号未修改,只更新其他字段
conn.execute('''
UPDATE fabrics
SET category=?, fabric_type=?, supplier=?, color=?, width=?, gsm=?,
retail_price=?, bulk_price=?, unit=?, timestamp=?
WHERE model=?
''', (
category, fabric_type, supplier, color,
self.edit_width.value() or None,
self.edit_gsm.value() or None,
self.edit_retail.value() or None,
self.edit_bulk.value() or None,
unit,
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
self.model
))
conn.commit()
if new_model != self.model:
QMessageBox.information(self, "成功", f"已将 '{self.model}' 更新为 '{new_model}'")
else:
QMessageBox.information(self, "成功", f"已更新 '{self.model}'")
self.accept()
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
class RawMaterialLibraryDialog(QDialog):
def __init__(self, db_path, is_admin=False):
super().__init__()
self.db_path = db_path
self.is_admin = is_admin
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)
self.table.setEditTriggers(QTableWidget.NoEditTriggers) # 禁止编辑表格内容
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)
add_layout.addWidget(self.add_major_category, 0, 1)
add_layout.addWidget(QLabel("类型:"), 0, 2, Qt.AlignRight)
self.add_sub_category = QLineEdit()
add_layout.addWidget(self.add_sub_category, 0, 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.Interactive)
# 设置操作列固定宽度,其他列自动拉伸
for i in range(6):
self.stock_table.horizontalHeader().setSectionResizeMode(i, QHeaderView.Stretch)
self.stock_table.horizontalHeader().setSectionResizeMode(6, QHeaderView.Fixed)
self.stock_table.setColumnWidth(6, 400) # 操作列固定宽度400像素
self.stock_table.setEditTriggers(QTableWidget.NoEditTriggers) # 设置为只读模式
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 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 category FROM fabrics WHERE category IS NOT NULL AND category != '' AND (is_deleted IS NULL OR is_deleted = 0)")
majors = set(row[0] for row in cursor.fetchall() if row[0])
self.major_combo.blockSignals(True)
self.major_combo.clear()
self.major_combo.addItem("全部类目")
if majors:
self.major_combo.addItems(sorted(majors))
self.major_combo.blockSignals(False)
except:
pass
self.load_sub_categories()
def load_add_major_categories(self):
"""加载添加界面的主类目"""
# 设置默认类目选项为:面料、辅料、其他
self.add_major_category.blockSignals(True)
self.add_major_category.clear()
self.add_major_category.addItems(["面料", "辅料", "其他"])
self.add_major_category.setCurrentText("面料")
self.add_major_category.blockSignals(False)
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 fabric_type FROM fabrics WHERE fabric_type IS NOT NULL AND fabric_type != '' AND (is_deleted IS NULL OR is_deleted = 0)")
subs = [row[0] for row in cursor.fetchall() if row[0]]
self.sub_combo.addItems(sorted(subs))
else:
cursor = conn.execute("SELECT DISTINCT fabric_type FROM fabrics WHERE category = ? AND fabric_type IS NOT NULL AND fabric_type != '' AND (is_deleted IS NULL OR is_deleted = 0)", (major,))
subs = [row[0] for row in cursor.fetchall() if row[0]]
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 != '' AND (is_deleted IS NULL OR is_deleted = 0) 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.addItem("") # 添加空选项
self.add_supplier.addItems(suppliers)
self.add_supplier.setCurrentIndex(0) # 默认选中空选项
self.add_supplier.blockSignals(False)
except:
pass
def load_table(self):
"""加载原料表格数据"""
try:
with self.get_conn() as conn:
query = "SELECT category, fabric_type, model, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics"
params = []
conditions = []
# 过滤已删除的原料
conditions.append("(is_deleted IS NULL OR is_deleted = 0)")
# 类目过滤
major = self.major_combo.currentText()
sub = self.sub_combo.currentText()
if major != "全部类目" and major:
if sub != "全部类型" and sub:
conditions.append("category = ? AND fabric_type = ?")
params.append(major)
params.append(sub)
else:
conditions.append("category = ?")
params.append(major)
elif sub != "全部类型" and sub:
# 只选择了类型,没有选择类目
conditions.append("fabric_type = ?")
params.append(sub)
# 供应商过滤
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, fabric_type, model, supplier, color, width, gsm, unit, retail, bulk) in enumerate(rows):
major = category or ""
sub = fabric_type or ""
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):
"""编辑原料 - 打开独立编辑窗口"""
dialog = RawMaterialEditDialog(self.db_path, model, self.is_admin, self)
if dialog.exec_() == QDialog.Accepted:
# 编辑成功后刷新列表
self.refresh_filters_and_table()
def delete_raw(self, model):
"""逻辑删除原料"""
reply = QMessageBox.question(self, "确认", f"确定要删除 '{model}' 吗?")
if reply == QMessageBox.Yes:
try:
with self.get_conn() as conn:
# 逻辑删除设置is_deleted=1
conn.execute("UPDATE fabrics SET is_deleted=1, updated_at=CURRENT_TIMESTAMP 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 not major:
QMessageBox.warning(self, "错误", "请选择类目")
return
# 验证必填字段:类型
if not sub:
QMessageBox.warning(self, "错误", "请输入类型")
return
# 验证必填字段:幅宽
if self.add_width.value() <= 0:
QMessageBox.warning(self, "错误", "请输入幅宽")
return
# 验证必填字段:克重
if self.add_gsm.value() <= 0:
QMessageBox.warning(self, "错误", "请输入克重")
return
if "胸杯" in sub:
major = "辅料"
category = major or ""
fabric_type = sub or ""
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:
# 检查是否已存在该型号
cursor = conn.execute("SELECT model, is_deleted FROM fabrics WHERE model = ?", (model,))
existing = cursor.fetchone()
if existing:
_, is_deleted = existing
# 如果型号存在且未被删除,提示使用编辑功能
if not is_deleted or is_deleted == 0:
QMessageBox.warning(self, "错误", f"型号 '{model}' 已存在,请使用编辑功能修改")
return
# 如果型号已被删除,直接恢复并更新
conn.execute('''
UPDATE fabrics
SET category=?, fabric_type=?, supplier=?, color=?, width=?, gsm=?,
retail_price=?, bulk_price=?, unit=?, timestamp=?, is_deleted=0, updated_at=CURRENT_TIMESTAMP
WHERE model=?
''', (category, fabric_type, 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'), model))
conn.commit()
QMessageBox.information(self, "成功", f"已保存 '{model}'")
else:
# 型号不存在,创建新记录
conn.execute('''
INSERT INTO fabrics
(model, category, fabric_type, supplier, color, width, gsm, retail_price, bulk_price, unit, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (model, category, fabric_type, 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()
QMessageBox.information(self, "成功", f"已保存 '{model}'")
# 清空表单
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.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(CASE WHEN si.is_deleted = 0 THEN si.quantity ELSE 0 END), 0) AS total_in,
COALESCE(SUM(CASE WHEN c.is_deleted = 0 THEN c.consume_quantity ELSE 0 END), 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)
edit_btn = QPushButton("编辑剩余库存")
edit_btn.clicked.connect(lambda _, m=model, u=unit, r=remaining: self.edit_remaining_stock(m, u, r))
op_layout.addWidget(edit_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 = ? AND is_deleted = 0 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 = ? AND is_deleted = 0 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.warning(
self,
"确认清零",
f"确定将 {model} 的剩余量清零吗?\n\n"
"此操作将标记删除该原料的所有历史采购和消耗记录。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
try:
with self.get_conn() as conn:
# 逻辑删除历史采购记录
conn.execute(
"UPDATE fabric_stock_in SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
# 逻辑删除历史消耗记录
conn.execute(
"UPDATE fabric_consumption SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
conn.commit()
QMessageBox.information(
self,
"完成",
f"{model} 的剩余库存已清零。"
)
self.load_stock_table()
except Exception as e:
QMessageBox.critical(self, "错误", f"清零失败: {str(e)}")
def edit_remaining_stock(self, model, unit, current_remaining):
"""编辑剩余库存"""
from PyQt5.QtWidgets import QDialogButtonBox
# 创建编辑对话框
dialog = QDialog(self)
dialog.setWindowTitle(f"编辑剩余库存 - {model}")
dialog.resize(400, 200)
layout = QVBoxLayout(dialog)
# 显示当前剩余量
info_label = QLabel(f"当前剩余量: {current_remaining:.3f} {unit}")
info_label.setStyleSheet("font-weight: bold; color: #2196F3; font-size: 14px;")
layout.addWidget(info_label)
# 新剩余量输入
input_layout = QHBoxLayout()
input_layout.addWidget(QLabel("新剩余量:"))
new_remaining_input = QDoubleSpinBox()
new_remaining_input.setRange(0, 999999)
new_remaining_input.setDecimals(3)
new_remaining_input.setValue(current_remaining)
input_layout.addWidget(new_remaining_input)
input_layout.addWidget(QLabel(unit))
layout.addLayout(input_layout)
# 警告提示
warning_label = QLabel(
"⚠️ 警告:保存后将删除该原料的所有历史采购和消耗记录!\n"
"系统将只保留新设置的剩余库存量。"
)
warning_label.setStyleSheet(
"background-color: #fff3cd; color: #856404; "
"padding: 10px; border: 1px solid #ffc107; border-radius: 5px;"
)
warning_label.setWordWrap(True)
layout.addWidget(warning_label)
# 按钮
button_box = QDialogButtonBox(
QDialogButtonBox.Save | QDialogButtonBox.Cancel
)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box)
# 显示对话框
if dialog.exec_() == QDialog.Accepted:
new_remaining = new_remaining_input.value()
self.save_new_remaining_stock(model, unit, new_remaining)
def save_new_remaining_stock(self, model, unit, new_remaining):
"""保存新的剩余库存量(逻辑删除历史记录)"""
# 二次确认
reply = QMessageBox.warning(
self,
"最后确认",
f"确定要将 {model} 的剩余库存设置为 {new_remaining:.3f} {unit} 吗?\n\n"
"此操作将:\n"
"1. 标记删除该原料的所有历史采购记录\n"
"2. 标记删除该原料的所有历史消耗记录\n"
"3. 创建一条新的入库记录作为当前库存",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
try:
with self.get_conn() as conn:
# 逻辑删除历史采购记录
conn.execute(
"UPDATE fabric_stock_in SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
# 逻辑删除历史消耗记录
conn.execute(
"UPDATE fabric_consumption SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
# 如果新剩余量大于0创建一条新的入库记录
if new_remaining > 0:
conn.execute(
"""
INSERT INTO fabric_stock_in
(model, quantity, unit, purchase_date, note, is_deleted)
VALUES (?, ?, ?, ?, ?, 0)
""",
(
model,
new_remaining,
unit,
datetime.now().strftime('%Y-%m-%d'),
"手动编辑库存"
)
)
conn.commit()
QMessageBox.information(
self,
"成功",
f"{model} 的剩余库存已更新为 {new_remaining:.3f} {unit}"
)
self.load_stock_table()
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")