Compare commits
7 Commits
c52d360cbb
...
35050407e9
| Author | SHA1 | Date | |
|---|---|---|---|
| 35050407e9 | |||
| 6322cb0caa | |||
| fa70f62099 | |||
| 14e25ad03b | |||
| c45912cb9e | |||
| 7cddafef63 | |||
| 8aa1a5ac91 |
38
database.py
38
database.py
@@ -143,7 +143,21 @@ class DatabaseManager:
|
||||
conn.execute("ALTER TABLE fabric_consumption ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# 添加逻辑删除字段
|
||||
try:
|
||||
conn.execute("ALTER TABLE fabrics ADD COLUMN is_deleted INTEGER DEFAULT 0")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE fabric_stock_in ADD COLUMN is_deleted INTEGER DEFAULT 0")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE fabric_consumption ADD COLUMN is_deleted INTEGER DEFAULT 0")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 数据迁移:将fabrics表中category字段的"类目-类型"格式拆分成两个字段
|
||||
try:
|
||||
cursor = conn.execute("SELECT model, category FROM fabrics WHERE category LIKE '%-%' AND (fabric_type IS NULL OR fabric_type = '')")
|
||||
@@ -213,9 +227,10 @@ class DatabaseManager:
|
||||
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabric_consumption_date ON fabric_consumption(consume_date)')
|
||||
|
||||
# 添加库存计算视图
|
||||
conn.execute('DROP VIEW IF EXISTS fabric_stock_view')
|
||||
conn.execute('''
|
||||
CREATE VIEW IF NOT EXISTS fabric_stock_view AS
|
||||
SELECT
|
||||
CREATE VIEW fabric_stock_view AS
|
||||
SELECT
|
||||
f.model,
|
||||
f.category,
|
||||
f.supplier,
|
||||
@@ -228,13 +243,16 @@ class DatabaseManager:
|
||||
LEFT JOIN (
|
||||
SELECT model, SUM(quantity) as total_in
|
||||
FROM fabric_stock_in
|
||||
WHERE is_deleted IS NULL OR is_deleted = 0
|
||||
GROUP BY model
|
||||
) stock_in ON f.model = stock_in.model
|
||||
LEFT JOIN (
|
||||
SELECT model, SUM(consume_quantity) as total_consumed
|
||||
FROM fabric_consumption
|
||||
WHERE is_deleted IS NULL OR is_deleted = 0
|
||||
GROUP BY model
|
||||
) consumption ON f.model = consumption.model
|
||||
WHERE f.is_deleted IS NULL OR f.is_deleted = 0
|
||||
''')
|
||||
|
||||
# 初始化默认密码
|
||||
@@ -274,8 +292,8 @@ def get_fabric_categories(db_path):
|
||||
with get_db_connection(db_path) as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT DISTINCT category
|
||||
FROM fabrics
|
||||
WHERE category IS NOT NULL AND category != ''
|
||||
FROM fabrics
|
||||
WHERE category IS NOT NULL AND category != '' AND (is_deleted IS NULL OR is_deleted = 0)
|
||||
ORDER BY category
|
||||
""")
|
||||
categories = set()
|
||||
@@ -296,8 +314,8 @@ def get_fabric_types_by_category(db_path, category):
|
||||
with get_db_connection(db_path) as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT DISTINCT fabric_type
|
||||
FROM fabrics
|
||||
WHERE category = ? AND fabric_type IS NOT NULL AND fabric_type != ''
|
||||
FROM fabrics
|
||||
WHERE category = ? AND fabric_type IS NOT NULL AND fabric_type != '' AND (is_deleted IS NULL OR is_deleted = 0)
|
||||
ORDER BY fabric_type
|
||||
""", (category,))
|
||||
|
||||
@@ -315,9 +333,9 @@ def get_fabric_models_by_category_type(db_path, category, fabric_type):
|
||||
try:
|
||||
with get_db_connection(db_path) as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT model, color, unit
|
||||
FROM fabrics
|
||||
WHERE category = ? OR category = ? OR category LIKE ?
|
||||
SELECT model, color, unit
|
||||
FROM fabrics
|
||||
WHERE (category = ? OR category = ? OR category LIKE ?) AND (is_deleted IS NULL OR is_deleted = 0)
|
||||
ORDER BY model
|
||||
""", (category, f"{category}-{fabric_type}", f"{category}-{fabric_type}-%"))
|
||||
|
||||
|
||||
Binary file not shown.
@@ -263,7 +263,7 @@ class FabricManager(QMainWindow):
|
||||
if fabric_model:
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ?", (fabric_model,))
|
||||
cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)", (fabric_model,))
|
||||
fabric_info = cursor.fetchone()
|
||||
if fabric_info and fabric_info[0] and fabric_info[1]:
|
||||
width, gsm = fabric_info
|
||||
|
||||
@@ -96,7 +96,7 @@ class GarmentLibraryDialog(QDialog):
|
||||
self.search_input.textChanged.connect(self.load_garments)
|
||||
op_layout.addWidget(self.search_input)
|
||||
|
||||
add_btn = QPushButton("新增/编辑款号")
|
||||
add_btn = QPushButton("新增款号")
|
||||
add_btn.clicked.connect(self.edit_garment)
|
||||
op_layout.addWidget(add_btn)
|
||||
|
||||
@@ -254,11 +254,7 @@ class GarmentEditDialog(QDialog):
|
||||
|
||||
# 按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
add_default_btn = QPushButton("快速添加标准类目")
|
||||
add_default_btn.clicked.connect(self.add_default_categories)
|
||||
btn_layout.addWidget(add_default_btn)
|
||||
|
||||
add_custom_btn = QPushButton("添加自定义类目")
|
||||
add_custom_btn = QPushButton("添加类目")
|
||||
add_custom_btn.clicked.connect(lambda: self.add_material_row())
|
||||
btn_layout.addWidget(add_custom_btn)
|
||||
|
||||
@@ -327,13 +323,6 @@ class GarmentEditDialog(QDialog):
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", "加载材料失败: " + str(e))
|
||||
|
||||
def add_default_categories(self):
|
||||
"""添加默认类目"""
|
||||
defaults = [("A料", "", "米"), ("B料", "", "米"), ("C料", "", "米"), ("D料", "", "米"),
|
||||
("花边", "", "码"), ("胸杯", "", "一对"), ("拉链", "", "个"), ("辅料", "", "个")]
|
||||
for cat, fabric_type, unit in defaults:
|
||||
self.add_material_row(cat, fabric_type, 0, unit)
|
||||
|
||||
def add_material_row(self, category="", fabric_type="", usage=0.0, unit="米", model=""):
|
||||
"""添加材料行"""
|
||||
print("add_material_row", category, fabric_type, usage, unit, model)
|
||||
@@ -344,7 +333,7 @@ class GarmentEditDialog(QDialog):
|
||||
cat_combo = QComboBox()
|
||||
cat_combo.setEditable(False)
|
||||
# 最后添加自定义选项
|
||||
cat_combo.addItem("—— 自定义类目 ——")
|
||||
cat_combo.addItem("—— 选择类目 ——")
|
||||
|
||||
# 先添加所有类目选项
|
||||
try:
|
||||
@@ -371,8 +360,8 @@ class GarmentEditDialog(QDialog):
|
||||
if category:
|
||||
cat_combo.setCurrentText(category)
|
||||
else:
|
||||
# 如果没有指定类目,默认选择"—— 自定义类目 ——"(索引0)
|
||||
print("没有指定类目,默认选择'—— 自定义类目 ——'(索引0)")
|
||||
# 如果没有指定类目,默认选择"—— 选择类目 ——"(索引0)
|
||||
print("没有指定类目,默认选择'—— 选择类目 ——'(索引0)")
|
||||
print(cat_combo.count())
|
||||
print(cat_combo.currentIndex())
|
||||
# 打印所有选项
|
||||
@@ -444,14 +433,14 @@ class GarmentEditDialog(QDialog):
|
||||
|
||||
# 列5: 删除按钮
|
||||
del_btn = QPushButton("删除")
|
||||
del_btn.clicked.connect(lambda _, r=row: self.material_table.removeRow(r))
|
||||
del_btn.clicked.connect(lambda: self.delete_material_row(del_btn))
|
||||
self.material_table.setCellWidget(row, 5, del_btn)
|
||||
|
||||
# 初始化类型和型号选项
|
||||
# self.on_category_changed(cat_combo.currentText(), row)
|
||||
|
||||
# 如果没有选择具体类目,初始化时显示全部型号
|
||||
# if cat_combo.currentText() == "—— 自定义类目 ——":
|
||||
# if cat_combo.currentText() == "—— 选择类目 ——":
|
||||
# self.on_type_changed("—— 选择类型 ——", row)
|
||||
|
||||
# 如果有指定的型号,需要在初始化完成后设置
|
||||
@@ -477,6 +466,14 @@ class GarmentEditDialog(QDialog):
|
||||
type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r))
|
||||
# 当用户真正选择了某个型号(包括通过模糊搜索补全选择),再联动类目和类型
|
||||
model_combo.activated[str].connect(lambda text, r=row: self.on_model_selected(text, r))
|
||||
|
||||
def delete_material_row(self, button):
|
||||
"""删除材料行 - 通过按钮找到实际行号"""
|
||||
for row in range(self.material_table.rowCount()):
|
||||
if self.material_table.cellWidget(row, 5) == button:
|
||||
self.material_table.removeRow(row)
|
||||
break
|
||||
|
||||
def update_model_combo_range(self, model_combo, row):
|
||||
print("update_model_combo_range", model_combo, row)
|
||||
"""更新型号下拉框的搜索范围"""
|
||||
@@ -495,7 +492,7 @@ class GarmentEditDialog(QDialog):
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
# 根据类目和类型构建查询条件
|
||||
if category_text and category_text != "—— 自定义类目 ——" and type_text and type_text != "—— 选择类型 ——":
|
||||
if category_text and category_text != "—— 选择类目 ——" and type_text and type_text != "—— 选择类型 ——":
|
||||
# 同时根据类目和类型过滤
|
||||
cursor = conn.execute("""
|
||||
SELECT DISTINCT model, color, unit
|
||||
@@ -503,7 +500,7 @@ class GarmentEditDialog(QDialog):
|
||||
WHERE category = ? AND fabric_type = ?
|
||||
ORDER BY model
|
||||
""", (category_text, type_text))
|
||||
elif category_text and category_text != "—— 自定义类目 ——":
|
||||
elif category_text and category_text != "—— 选择类目 ——":
|
||||
# 只根据类目过滤
|
||||
cursor = conn.execute("""
|
||||
SELECT DISTINCT model, color, unit
|
||||
@@ -573,7 +570,7 @@ class GarmentEditDialog(QDialog):
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
# 加载所有类型
|
||||
if category_text and category_text != "—— 自定义类目 ——":
|
||||
if category_text and category_text != "—— 选择类目 ——":
|
||||
# 如果选择了具体类目,则过滤
|
||||
cursor = conn.execute("""
|
||||
SELECT DISTINCT fabric_type
|
||||
@@ -659,7 +656,7 @@ class GarmentEditDialog(QDialog):
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute(
|
||||
"SELECT category, fabric_type, unit FROM fabrics WHERE model = ?",
|
||||
"SELECT category, fabric_type, unit FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)",
|
||||
(base_model,)
|
||||
)
|
||||
row_db = cursor.fetchone()
|
||||
@@ -724,7 +721,7 @@ class GarmentEditDialog(QDialog):
|
||||
model = model_widget.currentText().strip()
|
||||
|
||||
# 处理类目和类型
|
||||
if category == "—— 自定义类目 ——":
|
||||
if category == "—— 选择类目 ——":
|
||||
category = ""
|
||||
if fabric_type == "—— 选择类型 ——":
|
||||
fabric_type = ""
|
||||
|
||||
2
main.py
2
main.py
@@ -412,7 +412,7 @@ class FabricManager(QMainWindow):
|
||||
if fabric_model:
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ?", (fabric_model,))
|
||||
cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)", (fabric_model,))
|
||||
fabric_info = cursor.fetchone()
|
||||
if fabric_info and fabric_info[0] and fabric_info[1]:
|
||||
width, gsm = fabric_info
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit,
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit,
|
||||
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QMessageBox, QTabWidget, QWidget, QDoubleSpinBox, QTextEdit
|
||||
)
|
||||
@@ -14,15 +14,248 @@ 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:
|
||||
# 加载类目
|
||||
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])
|
||||
majors.update({"布料", "辅料", "其他"})
|
||||
self.edit_major_category.addItems(sorted(majors))
|
||||
|
||||
# 加载供应商
|
||||
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 "胸杯" 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.current_edit_model = None
|
||||
self.setWindowTitle("原料库管理")
|
||||
self.resize(1400, 700)
|
||||
|
||||
|
||||
self.setup_ui()
|
||||
self.refresh_filters_and_table()
|
||||
self.load_add_major_categories()
|
||||
@@ -51,7 +284,7 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
|
||||
# 新增/编辑标签页
|
||||
add_tab = self.create_add_tab()
|
||||
tabs.addTab(add_tab, "新增/编辑原料")
|
||||
tabs.addTab(add_tab, "新增原料")
|
||||
|
||||
# 库存跟踪标签页
|
||||
stock_tab = self.create_stock_tab()
|
||||
@@ -83,7 +316,7 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
self.supplier_combo.currentIndexChanged.connect(self.load_table)
|
||||
filter_layout.addWidget(self.supplier_combo)
|
||||
|
||||
filter_layout.addWidget(QLabel("搜索型号/名称:"))
|
||||
filter_layout.addWidget(QLabel("搜索型号:"))
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.textChanged.connect(self.load_table)
|
||||
filter_layout.addWidget(self.search_input)
|
||||
@@ -100,6 +333,7 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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)
|
||||
|
||||
# 隐藏价格列(非管理员)
|
||||
@@ -119,18 +353,12 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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()
|
||||
@@ -197,7 +425,13 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
self.stock_table = QTableWidget()
|
||||
self.stock_table.setColumnCount(len(stock_headers))
|
||||
self.stock_table.setHorizontalHeaderLabels(stock_headers)
|
||||
self.stock_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
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
|
||||
@@ -212,30 +446,6 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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()
|
||||
@@ -246,10 +456,10 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
"""加载主类目"""
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT DISTINCT category FROM fabrics WHERE category IS NOT NULL AND category != ''")
|
||||
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])
|
||||
majors.update({"布料", "辅料", "其他"})
|
||||
|
||||
|
||||
self.major_combo.blockSignals(True)
|
||||
self.major_combo.clear()
|
||||
self.major_combo.addItem("全部类目")
|
||||
@@ -263,15 +473,15 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
"""加载添加界面的主类目"""
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT DISTINCT category FROM fabrics WHERE category IS NOT NULL AND category != ''")
|
||||
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])
|
||||
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:
|
||||
@@ -288,20 +498,20 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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 != ''")
|
||||
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 != ''", (major,))
|
||||
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()
|
||||
|
||||
@@ -309,7 +519,7 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
"""加载供应商列表"""
|
||||
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")
|
||||
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)
|
||||
@@ -333,6 +543,9 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
params = []
|
||||
conditions = []
|
||||
|
||||
# 过滤已删除的原料
|
||||
conditions.append("(is_deleted IS NULL OR is_deleted = 0)")
|
||||
|
||||
# 类目过滤
|
||||
major = self.major_combo.currentText()
|
||||
sub = self.sub_combo.currentText()
|
||||
@@ -344,6 +557,10 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
else:
|
||||
conditions.append("category = ?")
|
||||
params.append(major)
|
||||
elif sub != "全部类型" and sub:
|
||||
# 只选择了类型,没有选择类目
|
||||
conditions.append("fabric_type = ?")
|
||||
params.append(sub)
|
||||
|
||||
# 供应商过滤
|
||||
supplier = self.supplier_combo.currentText()
|
||||
@@ -422,51 +639,20 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
QMessageBox.critical(self, "加载失败", str(e))
|
||||
|
||||
def edit_raw_material(self, model):
|
||||
"""编辑原料"""
|
||||
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 = ?", (model,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
QMessageBox.warning(self, "提示", "原料不存在或已被删除")
|
||||
return
|
||||
|
||||
category, fabric_type, supplier, color, width, gsm, unit, retail, bulk = row
|
||||
|
||||
major = category or ""
|
||||
sub = fabric_type or ""
|
||||
|
||||
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))
|
||||
"""编辑原料 - 打开独立编辑窗口"""
|
||||
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}'?")
|
||||
"""逻辑删除原料"""
|
||||
reply = QMessageBox.question(self, "确认", f"确定要删除 '{model}' 吗?\n\n此操作将标记删除该原料,不会影响历史数据。")
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
conn.execute("DELETE FROM fabrics WHERE model=?", (model,))
|
||||
# 逻辑删除:设置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, "成功", "删除完成")
|
||||
@@ -474,7 +660,7 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
QMessageBox.critical(self, "错误", "删除失败: " + str(e))
|
||||
|
||||
def save_raw_material(self):
|
||||
"""保存原料"""
|
||||
"""保存新增原料"""
|
||||
model = self.add_model.text().strip()
|
||||
if not model:
|
||||
QMessageBox.warning(self, "错误", "请输入型号/名称")
|
||||
@@ -494,8 +680,14 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
# 检查是否已存在
|
||||
cursor = conn.execute("SELECT model FROM fabrics WHERE model = ?", (model,))
|
||||
if cursor.fetchone():
|
||||
QMessageBox.warning(self, "错误", f"型号 '{model}' 已存在,请使用编辑功能修改")
|
||||
return
|
||||
|
||||
conn.execute('''
|
||||
INSERT OR REPLACE INTO fabrics
|
||||
INSERT INTO fabrics
|
||||
(model, category, fabric_type, supplier, color, width, gsm, retail_price, bulk_price, unit, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (model, category, fabric_type, supplier, color,
|
||||
@@ -504,9 +696,7 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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
|
||||
QMessageBox.information(self, "成功", f"已保存 '{model}'")
|
||||
|
||||
# 清空表单
|
||||
self.add_model.clear()
|
||||
@@ -517,7 +707,6 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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()
|
||||
@@ -530,8 +719,8 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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
|
||||
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
|
||||
@@ -560,6 +749,10 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
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)
|
||||
@@ -573,14 +766,17 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
"""显示库存明细"""
|
||||
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,))
|
||||
# 查询入库记录(只显示未删除的)
|
||||
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 = ? ORDER BY consume_date DESC
|
||||
FROM fabric_consumption WHERE model = ? AND is_deleted = 0 ORDER BY consume_date DESC
|
||||
''', (model,))
|
||||
out_rows = cursor_out.fetchall()
|
||||
|
||||
@@ -616,78 +812,152 @@ class RawMaterialLibraryDialog(QDialog):
|
||||
QMessageBox.critical(self, "错误", str(e))
|
||||
|
||||
def clear_remaining(self, model):
|
||||
"""清零剩余库存"""
|
||||
reply = QMessageBox.question(
|
||||
"""清零剩余库存(逻辑删除)"""
|
||||
reply = QMessageBox.warning(
|
||||
self,
|
||||
"确认清零",
|
||||
f"确定将 {model} 的剩余量清零?"
|
||||
f"确定将 {model} 的剩余量清零吗?\n\n"
|
||||
"此操作将标记删除该原料的所有历史采购和消耗记录。",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
# 计算当前总入库量和总消耗量
|
||||
cursor_in = conn.execute(
|
||||
"SELECT COALESCE(SUM(quantity), 0) FROM fabric_stock_in WHERE model = ?",
|
||||
(model,)
|
||||
)
|
||||
total_in = cursor_in.fetchone()[0] or 0.0
|
||||
|
||||
cursor_out = conn.execute(
|
||||
"SELECT COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption WHERE model = ?",
|
||||
(model,)
|
||||
)
|
||||
total_out = cursor_out.fetchone()[0] or 0.0
|
||||
|
||||
remaining = round(float(total_in) - float(total_out), 6)
|
||||
if remaining <= 0:
|
||||
QMessageBox.information(self, "提示", f"{model} 当前没有可清零的剩余库存。")
|
||||
return
|
||||
|
||||
# 获取用于记录此次清零的单位:优先使用最近一次入库单位,其次是原料表里的单位,最后默认“米”
|
||||
unit = "米"
|
||||
cursor_unit = conn.execute(
|
||||
"SELECT unit FROM fabric_stock_in WHERE model = ? ORDER BY purchase_date DESC LIMIT 1",
|
||||
(model,)
|
||||
)
|
||||
row_unit = cursor_unit.fetchone()
|
||||
if row_unit and row_unit[0]:
|
||||
unit = row_unit[0]
|
||||
else:
|
||||
cursor_fabric = conn.execute(
|
||||
"SELECT unit FROM fabrics WHERE model = ?",
|
||||
(model,)
|
||||
)
|
||||
row_fabric = cursor_fabric.fetchone()
|
||||
if row_fabric and row_fabric[0]:
|
||||
unit = row_fabric[0]
|
||||
|
||||
# 以一条“清零”消耗记录的方式写入fabric_consumption
|
||||
# 逻辑删除历史采购记录
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO fabric_consumption
|
||||
(style_number, model, single_usage, quantity_made, loss_rate, consume_quantity, consume_date, unit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"库存清零", # 特殊标记,表示这是一次手工清零操作
|
||||
model,
|
||||
None, # 单件用量无意义,置为NULL
|
||||
0, # 生产件数为0
|
||||
0.0, # 损耗率0
|
||||
remaining, # 一次性消耗掉当前所有剩余
|
||||
datetime.now().strftime('%Y-%m-%d'),
|
||||
unit,
|
||||
)
|
||||
"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} 剩余量 {remaining:.3f} {unit} 已清零。"
|
||||
f"{model} 的剩余库存已清零。"
|
||||
)
|
||||
self.load_stock_table()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"清零失败: {str(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)}")
|
||||
@@ -58,10 +58,10 @@ class StockInDialog(QDialog):
|
||||
try:
|
||||
with self.get_conn() as conn:
|
||||
# 查询面料基础信息
|
||||
query = "SELECT model, color, supplier, unit FROM fabrics"
|
||||
query = "SELECT model, color, supplier, unit FROM fabrics WHERE (is_deleted IS NULL OR is_deleted = 0)"
|
||||
params = []
|
||||
if keyword:
|
||||
query += " WHERE model LIKE ? OR color LIKE ?"
|
||||
query += " AND (model LIKE ? OR color LIKE ?)"
|
||||
params = ["%" + keyword + "%", "%" + keyword + "%"]
|
||||
query += " ORDER BY timestamp DESC"
|
||||
cursor = conn.execute(query, params)
|
||||
|
||||
156
test/README_GUI_TESTS.md
Normal file
156
test/README_GUI_TESTS.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# GUI测试文档
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已为所有主要模块创建了基于PyQt的GUI集成测试,用于测试实际的用户界面交互和业务逻辑。
|
||||
|
||||
## 测试文件列表
|
||||
|
||||
### 1. test_login_gui.py - 登录对话框GUI测试
|
||||
**测试数量**: 7个测试
|
||||
**测试内容**:
|
||||
- 管理员登录成功/失败
|
||||
- 普通用户登录成功/失败
|
||||
- 获取默认密码
|
||||
- 设置新密码
|
||||
- 使用新密码登录
|
||||
|
||||
### 2. test_stock_gui.py - 库存管理GUI测试
|
||||
**测试数量**: 4个测试
|
||||
**测试内容**:
|
||||
- 加载原料列表
|
||||
- 搜索原料
|
||||
- 库存数量计算
|
||||
- 库存扣除消耗后的计算
|
||||
|
||||
### 3. test_raw_material_gui.py - 原料管理GUI测试
|
||||
**测试数量**: 7个测试
|
||||
**测试内容**:
|
||||
- 添加基本原料
|
||||
- 添加重复型号(验证拒绝)
|
||||
- 添加空型号(验证拒绝)
|
||||
- 删除原料
|
||||
- 编辑原料
|
||||
- 按类目过滤
|
||||
- 按型号搜索
|
||||
|
||||
### 4. test_garment_gui.py - 款式管理GUI测试
|
||||
**测试数量**: 2个测试
|
||||
**测试内容**:
|
||||
- 加载款式列表
|
||||
- 搜索款式
|
||||
|
||||
### 5. test_purchase_order_gui.py - 采购单生成GUI测试
|
||||
**测试数量**: 2个测试
|
||||
**测试内容**:
|
||||
- 生成采购单
|
||||
- 材料用量计算
|
||||
|
||||
## 测试统计
|
||||
|
||||
**总测试数量**: 22个测试
|
||||
**测试状态**: ✓ 全部通过
|
||||
**测试时间**: ~14秒
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行单个模块测试
|
||||
```bash
|
||||
# 登录测试
|
||||
python test\test_login_gui.py -v
|
||||
|
||||
# 库存测试
|
||||
python test\test_stock_gui.py -v
|
||||
|
||||
# 原料测试
|
||||
python test\test_raw_material_gui.py -v
|
||||
|
||||
# 款式测试
|
||||
python test\test_garment_gui.py -v
|
||||
|
||||
# 采购单测试
|
||||
python test\test_purchase_order_gui.py -v
|
||||
```
|
||||
|
||||
### 运行所有GUI测试
|
||||
```bash
|
||||
cd test
|
||||
python -m unittest test_login_gui.py test_stock_gui.py test_raw_material_gui.py test_garment_gui.py test_purchase_order_gui.py -v
|
||||
```
|
||||
|
||||
## 测试特点
|
||||
|
||||
### 1. 真实GUI交互
|
||||
- 模拟用户填写表单
|
||||
- 模拟点击按钮
|
||||
- 模拟选择下拉框
|
||||
- 模拟输入搜索关键词
|
||||
|
||||
### 2. 业务逻辑验证
|
||||
- 验证重复数据拒绝
|
||||
- 验证空值验证
|
||||
- 验证数据计算正确性
|
||||
- 验证过滤和搜索功能
|
||||
|
||||
### 3. 独立测试环境
|
||||
- 每个测试使用临时数据库
|
||||
- 测试之间互不干扰
|
||||
- 自动清理测试数据
|
||||
|
||||
### 4. 自动化消息框
|
||||
- Mock了QMessageBox以便自动化测试
|
||||
- 不需要人工点击确认对话框
|
||||
- 可以验证警告和错误消息
|
||||
|
||||
## 测试架构
|
||||
|
||||
```
|
||||
test/
|
||||
├── test_login_gui.py # 登录对话框测试
|
||||
├── test_stock_gui.py # 库存管理测试
|
||||
├── test_raw_material_gui.py # 原料管理测试
|
||||
├── test_garment_gui.py # 款式管理测试
|
||||
├── test_purchase_order_gui.py # 采购单生成测试
|
||||
└── README_GUI_TESTS.md # 本文档
|
||||
```
|
||||
|
||||
## 依赖项
|
||||
|
||||
- Python 3.x
|
||||
- PyQt5
|
||||
- sqlite3 (内置)
|
||||
- unittest (内置)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 测试需要在有GUI环境的系统上运行
|
||||
2. 测试会创建临时数据库文件,测试后自动清理
|
||||
3. 所有消息框已被Mock,不会弹出实际对话框
|
||||
4. 测试使用独立的QApplication实例
|
||||
|
||||
## 扩展测试
|
||||
|
||||
如需添加新的GUI测试,请参考现有测试文件的结构:
|
||||
|
||||
1. 继承 `unittest.TestCase`
|
||||
2. 在 `setUpClass` 中创建 `QApplication` 实例
|
||||
3. 在 `setUp` 中创建临时数据库和对话框
|
||||
4. Mock消息框以便自动化
|
||||
5. 在 `tearDown` 中清理资源
|
||||
6. 编写具体的测试方法
|
||||
|
||||
## 测试覆盖率
|
||||
|
||||
当前GUI测试覆盖了以下核心功能:
|
||||
- ✓ 用户登录和密码管理
|
||||
- ✓ 原料库管理(增删改查)
|
||||
- ✓ 库存管理和计算
|
||||
- ✓ 款式管理
|
||||
- ✓ 采购单生成和计算
|
||||
|
||||
## 维护建议
|
||||
|
||||
1. 每次修改GUI代码后运行相关测试
|
||||
2. 添加新功能时同步添加GUI测试
|
||||
3. 定期运行所有测试确保系统稳定性
|
||||
4. 保持测试代码的可读性和可维护性
|
||||
261
test/test_database.py
Normal file
261
test/test_database.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
数据库模块测试
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
from database import (
|
||||
DatabaseManager, get_db_connection, get_fabric_categories,
|
||||
get_fabric_types_by_category, get_fabric_models_by_category_type,
|
||||
get_password, update_password
|
||||
)
|
||||
|
||||
|
||||
class TestDatabaseManager(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
def tearDown(self):
|
||||
self.db_manager = None
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_init_db_creates_tables(self):
|
||||
with self.db_manager.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
self.assertIn("fabrics", tables)
|
||||
self.assertIn("garments", tables)
|
||||
self.assertIn("garment_materials", tables)
|
||||
self.assertIn("admin_settings", tables)
|
||||
self.assertIn("fabric_stock_in", tables)
|
||||
self.assertIn("fabric_consumption", tables)
|
||||
|
||||
def test_default_passwords_initialized(self):
|
||||
admin_pwd = self.db_manager.get_setting("admin_password")
|
||||
user_pwd = self.db_manager.get_setting("user_password")
|
||||
|
||||
self.assertEqual(admin_pwd, "123456")
|
||||
self.assertEqual(user_pwd, "123456")
|
||||
|
||||
def test_get_setting_returns_none_for_missing_key(self):
|
||||
result = self.db_manager.get_setting("nonexistent_key")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_set_setting(self):
|
||||
result = self.db_manager.set_setting("test_key", "test_value")
|
||||
self.assertTrue(result)
|
||||
|
||||
value = self.db_manager.get_setting("test_key")
|
||||
self.assertEqual(value, "test_value")
|
||||
|
||||
def test_set_setting_overwrites_existing(self):
|
||||
self.db_manager.set_setting("test_key", "value1")
|
||||
self.db_manager.set_setting("test_key", "value2")
|
||||
|
||||
value = self.db_manager.get_setting("test_key")
|
||||
self.assertEqual(value, "value2")
|
||||
|
||||
|
||||
class TestFabricOperations(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self._insert_test_fabrics()
|
||||
|
||||
def tearDown(self):
|
||||
self.db_manager = None
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _insert_test_fabrics(self):
|
||||
with self.db_manager.get_conn() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO fabrics (model, category, fabric_type, supplier, color, unit)
|
||||
VALUES ('F001', '布料', '棉布', '供应商A', '红色', '米')
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO fabrics (model, category, fabric_type, supplier, color, unit)
|
||||
VALUES ('F002', '布料', '丝绸', '供应商B', '蓝色', '码')
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO fabrics (model, category, fabric_type, supplier, color, unit)
|
||||
VALUES ('F003', '辅料', '拉链', '供应商C', '黑色', '个')
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
def test_get_fabric_categories(self):
|
||||
categories = get_fabric_categories(self.db_path)
|
||||
|
||||
self.assertIn("布料", categories)
|
||||
self.assertIn("辅料", categories)
|
||||
self.assertIn("其他", categories)
|
||||
|
||||
def test_get_fabric_types_by_category(self):
|
||||
types = get_fabric_types_by_category(self.db_path, "布料")
|
||||
|
||||
self.assertIn("棉布", types)
|
||||
self.assertIn("丝绸", types)
|
||||
self.assertNotIn("拉链", types)
|
||||
|
||||
def test_get_fabric_types_by_category_empty(self):
|
||||
types = get_fabric_types_by_category(self.db_path, "不存在的类目")
|
||||
self.assertEqual(types, [])
|
||||
|
||||
|
||||
class TestPasswordOperations(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
def tearDown(self):
|
||||
self.db_manager = None
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_get_password_default(self):
|
||||
admin_pwd = get_password(self.db_path, "admin")
|
||||
user_pwd = get_password(self.db_path, "user")
|
||||
|
||||
self.assertEqual(admin_pwd, "123456")
|
||||
self.assertEqual(user_pwd, "123456")
|
||||
|
||||
def test_update_password(self):
|
||||
result = update_password(self.db_path, "admin", "newpassword")
|
||||
self.assertTrue(result)
|
||||
|
||||
new_pwd = get_password(self.db_path, "admin")
|
||||
self.assertEqual(new_pwd, "newpassword")
|
||||
|
||||
|
||||
class TestStockOperations(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self._setup_test_data()
|
||||
|
||||
def tearDown(self):
|
||||
self.db_manager = None
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _setup_test_data(self):
|
||||
with self.db_manager.get_conn() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO fabrics (model, category, unit)
|
||||
VALUES ('F001', '布料', '米')
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES ('F001', 100, '米', '2024-01-01')
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES ('F001', 50, '米', '2024-01-02')
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO fabric_consumption (model, consume_quantity, unit, consume_date, style_number, quantity_made, loss_rate)
|
||||
VALUES ('F001', 30, '米', '2024-01-03', 'G001', 10, 0.05)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
def test_stock_calculation(self):
|
||||
with self.db_manager.get_conn() as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT current_stock FROM fabric_stock_view WHERE model = 'F001'
|
||||
""")
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], 120)
|
||||
|
||||
|
||||
class TestGarmentOperations(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
def tearDown(self):
|
||||
self.db_manager = None
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_insert_garment(self):
|
||||
with self.db_manager.get_conn() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO garments (style_number, image_path)
|
||||
VALUES ('G001', 'images/g001.jpg')
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT * FROM garments WHERE style_number = 'G001'")
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(row[0], 'G001')
|
||||
|
||||
def test_insert_garment_materials(self):
|
||||
with self.db_manager.get_conn() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO garments (style_number) VALUES ('G001')
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO garment_materials (style_number, category, fabric_type, model, usage_per_piece, unit)
|
||||
VALUES ('G001', '布料', '棉布', 'F001', 1.5, '米')
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("""
|
||||
SELECT * FROM garment_materials WHERE style_number = 'G001'
|
||||
""")
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(row[2], '布料')
|
||||
self.assertEqual(row[5], 1.5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
216
test/test_garment.py
Normal file
216
test/test_garment.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
服装管理模块测试 - 测试服装款式和材料用量管理
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
import gc
|
||||
from database import DatabaseManager, get_db_connection
|
||||
|
||||
|
||||
class TestGarment(unittest.TestCase):
|
||||
"""服装管理测试类"""
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_garment.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self.conn = None
|
||||
|
||||
def tearDown(self):
|
||||
if self.conn:
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_conn(self):
|
||||
self.conn = get_db_connection(self.db_path)
|
||||
return self.conn
|
||||
|
||||
# ========== 服装款式测试 ==========
|
||||
|
||||
def test_add_garment_basic(self):
|
||||
"""测试添加服装款式"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number, image_path) VALUES (?, ?)",
|
||||
("G001", None)
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT style_number FROM garments WHERE style_number = ?",
|
||||
("G001",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "G001")
|
||||
|
||||
def test_add_garment_with_image(self):
|
||||
"""测试添加带图片的服装款式"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number, image_path) VALUES (?, ?)",
|
||||
("G002", "images/g002.jpg")
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT image_path FROM garments WHERE style_number = ?",
|
||||
("G002",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "images/g002.jpg")
|
||||
|
||||
def test_update_garment(self):
|
||||
"""测试更新服装款式"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number, image_path) VALUES (?, ?)",
|
||||
("G003", None)
|
||||
)
|
||||
conn.commit()
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO garments (style_number, image_path) VALUES (?, ?)",
|
||||
("G003", "images/g003_new.jpg")
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT image_path FROM garments WHERE style_number = ?",
|
||||
("G003",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "images/g003_new.jpg")
|
||||
|
||||
def test_delete_garment(self):
|
||||
"""测试删除服装款式"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number) VALUES (?)",
|
||||
("G004",)
|
||||
)
|
||||
conn.commit()
|
||||
conn.execute(
|
||||
"DELETE FROM garments WHERE style_number = ?",
|
||||
("G004",)
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT * FROM garments WHERE style_number = ?",
|
||||
("G004",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertIsNone(row)
|
||||
|
||||
# ========== 材料用量测试 ==========
|
||||
|
||||
def test_add_material_basic(self):
|
||||
"""测试添加材料用量"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number) VALUES (?)",
|
||||
("G005",)
|
||||
)
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, fabric_type, model, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', ("G005", "布料", "棉布", "M001", 0.5, "米"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT usage_per_piece, unit FROM garment_materials WHERE style_number = ?",
|
||||
("G005",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], 0.5)
|
||||
self.assertEqual(row[1], "米")
|
||||
|
||||
def test_add_multiple_materials(self):
|
||||
"""测试添加多个材料"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number) VALUES (?)",
|
||||
("G006",)
|
||||
)
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("G006", "A料", 0.3, "米"))
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("G006", "B料", 0.2, "米"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT COUNT(*) FROM garment_materials WHERE style_number = ?",
|
||||
("G006",)
|
||||
)
|
||||
count = cursor.fetchone()[0]
|
||||
self.assertEqual(count, 2)
|
||||
|
||||
def test_update_material_usage(self):
|
||||
"""测试更新材料用量"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number) VALUES (?)",
|
||||
("G007",)
|
||||
)
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("G007", "A料", 0.5, "米"))
|
||||
conn.commit()
|
||||
conn.execute('''
|
||||
UPDATE garment_materials SET usage_per_piece = ?
|
||||
WHERE style_number = ? AND category = ?
|
||||
''', (0.8, "G007", "A料"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT usage_per_piece FROM garment_materials WHERE style_number = ? AND category = ?",
|
||||
("G007", "A料")
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], 0.8)
|
||||
|
||||
def test_delete_garment_cascade_materials(self):
|
||||
"""测试删除款式时材料记录处理"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number) VALUES (?)",
|
||||
("G008",)
|
||||
)
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("G008", "A料", 0.5, "米"))
|
||||
conn.commit()
|
||||
conn.execute(
|
||||
"DELETE FROM garment_materials WHERE style_number = ?",
|
||||
("G008",)
|
||||
)
|
||||
conn.execute(
|
||||
"DELETE FROM garments WHERE style_number = ?",
|
||||
("G008",)
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT COUNT(*) FROM garment_materials WHERE style_number = ?",
|
||||
("G008",)
|
||||
)
|
||||
count = cursor.fetchone()[0]
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
125
test/test_garment_gui.py
Normal file
125
test/test_garment_gui.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
款式管理GUI测试模块
|
||||
使用PyQt测试框架测试款式管理功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from PyQt5.QtTest import QTest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 添加父目录到路径以导入模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import DatabaseManager
|
||||
from garment_dialogs import GarmentLibraryDialog
|
||||
|
||||
|
||||
class TestGarmentGUI(unittest.TestCase):
|
||||
"""款式管理GUI测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类初始化:创建QApplication实例"""
|
||||
cls.app = QApplication.instance()
|
||||
if cls.app is None:
|
||||
cls.app = QApplication(sys.argv)
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试前准备:创建临时数据库和对话框"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_fabric.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
# 添加测试原料
|
||||
with self.db_manager.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, color, unit, timestamp)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
''', ("TEST-FABRIC-001", "布料", "白色", "米"))
|
||||
conn.commit()
|
||||
|
||||
# 创建对话框实例
|
||||
self.dialog = GarmentLibraryDialog(self.db_path)
|
||||
|
||||
# 保存原始消息框
|
||||
self._original_msgbox = QMessageBox.information
|
||||
self._original_warning = QMessageBox.warning
|
||||
self._original_critical = QMessageBox.critical
|
||||
|
||||
# Mock消息框
|
||||
QMessageBox.information = lambda *args, **kwargs: None
|
||||
QMessageBox.warning = lambda *args, **kwargs: None
|
||||
QMessageBox.critical = lambda *args, **kwargs: None
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试后清理"""
|
||||
# 恢复消息框
|
||||
QMessageBox.information = self._original_msgbox
|
||||
QMessageBox.warning = self._original_warning
|
||||
QMessageBox.critical = self._original_critical
|
||||
|
||||
# 关闭对话框
|
||||
self.dialog.close()
|
||||
self.dialog.deleteLater()
|
||||
|
||||
# 清理数据库
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ========== 款式加载测试 ==========
|
||||
|
||||
def test_load_garments(self):
|
||||
"""测试加载款式列表"""
|
||||
# 添加测试款式
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO garments (style_number)
|
||||
VALUES (?)
|
||||
''', ("STYLE-001",))
|
||||
conn.commit()
|
||||
|
||||
# 刷新表格
|
||||
self.dialog.load_garments()
|
||||
|
||||
# 验证表格有数据
|
||||
self.assertGreater(self.dialog.garment_table.rowCount(), 0, "表格应该有数据")
|
||||
|
||||
def test_search_garments(self):
|
||||
"""测试搜索款式"""
|
||||
# 添加多个测试款式
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO garments (style_number)
|
||||
VALUES (?)
|
||||
''', ("SEARCH-001",))
|
||||
conn.execute('''
|
||||
INSERT INTO garments (style_number)
|
||||
VALUES (?)
|
||||
''', ("OTHER-001",))
|
||||
conn.commit()
|
||||
|
||||
# 搜索特定款式
|
||||
self.dialog.search_input.setText("SEARCH")
|
||||
self.dialog.load_garments()
|
||||
|
||||
# 验证只显示匹配的结果
|
||||
row_count = self.dialog.garment_table.rowCount()
|
||||
for row in range(row_count):
|
||||
style_item = self.dialog.garment_table.item(row, 0)
|
||||
if style_item:
|
||||
self.assertIn("SEARCH", style_item.text(), "应该只显示匹配的款式号")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
206
test/test_gui.py
Normal file
206
test/test_gui.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
GUI测试模块 - 使用pytest-qt测试PyQt5界面
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox, QDialog
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from database import DatabaseManager
|
||||
from login_dialog import LoginDialog
|
||||
from main import FabricManager
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def qapp():
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = QApplication([])
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db():
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
db_path = os.path.join(temp_dir, "test_gui.db")
|
||||
db_manager = DatabaseManager(db_path)
|
||||
yield db_path
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
os.rmdir(temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class TestLoginDialog:
|
||||
"""登录对话框GUI测试"""
|
||||
|
||||
def test_login_dialog_init(self, qapp, temp_db, qtbot):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.windowTitle() == "选择模式并登录"
|
||||
assert dialog.is_admin == False
|
||||
|
||||
def test_login_dialog_has_inputs(self, qapp, temp_db, qtbot):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert hasattr(dialog, 'admin_input')
|
||||
assert hasattr(dialog, 'user_input')
|
||||
|
||||
def test_admin_login_correct_password(self, qapp, temp_db, qtbot, monkeypatch):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.admin_input.setText("123456")
|
||||
monkeypatch.setattr(QMessageBox, 'warning', lambda *args: None)
|
||||
|
||||
dialog.login_mode(True)
|
||||
|
||||
assert dialog.is_admin == True
|
||||
|
||||
def test_admin_login_wrong_password(self, qapp, temp_db, qtbot, monkeypatch):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.admin_input.setText("wrongpassword")
|
||||
warning_called = []
|
||||
monkeypatch.setattr(QMessageBox, 'warning', lambda *args: warning_called.append(True))
|
||||
|
||||
dialog.login_mode(True)
|
||||
|
||||
assert len(warning_called) == 1
|
||||
assert dialog.is_admin == False
|
||||
|
||||
def test_user_login_correct_password(self, qapp, temp_db, qtbot, monkeypatch):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.user_input.setText("123456")
|
||||
monkeypatch.setattr(QMessageBox, 'warning', lambda *args: None)
|
||||
|
||||
dialog.login_mode(False)
|
||||
|
||||
assert dialog.is_admin == False
|
||||
|
||||
def test_user_login_wrong_password(self, qapp, temp_db, qtbot, monkeypatch):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.user_input.setText("wrongpassword")
|
||||
warning_called = []
|
||||
monkeypatch.setattr(QMessageBox, 'warning', lambda *args: warning_called.append(True))
|
||||
|
||||
dialog.login_mode(False)
|
||||
|
||||
assert len(warning_called) == 1
|
||||
|
||||
def test_get_stored_password(self, qapp, temp_db, qtbot):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
admin_pwd = dialog.get_stored_password("admin")
|
||||
user_pwd = dialog.get_stored_password("user")
|
||||
|
||||
assert admin_pwd == "123456"
|
||||
assert user_pwd == "123456"
|
||||
|
||||
def test_set_password(self, qapp, temp_db, qtbot):
|
||||
dialog = LoginDialog(temp_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
result = dialog.set_password("admin", "newpass123")
|
||||
assert result == True
|
||||
|
||||
new_pwd = dialog.get_stored_password("admin")
|
||||
assert new_pwd == "newpass123"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fabric_manager_admin(qapp, temp_db, qtbot):
|
||||
window = FabricManager.__new__(FabricManager)
|
||||
window.is_admin = True
|
||||
window.db_path = temp_db
|
||||
from database import DatabaseManager
|
||||
window.db_manager = DatabaseManager(temp_db)
|
||||
from PyQt5.QtWidgets import QMainWindow
|
||||
QMainWindow.__init__(window)
|
||||
window.setWindowTitle("服装布料计算管理器 - 专业版 (管理员模式)")
|
||||
window.resize(1300, 800)
|
||||
window.init_ui()
|
||||
window.load_garment_list()
|
||||
qtbot.addWidget(window)
|
||||
return window
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fabric_manager_user(qapp, temp_db, qtbot):
|
||||
window = FabricManager.__new__(FabricManager)
|
||||
window.is_admin = False
|
||||
window.db_path = temp_db
|
||||
from database import DatabaseManager
|
||||
window.db_manager = DatabaseManager(temp_db)
|
||||
from PyQt5.QtWidgets import QMainWindow
|
||||
QMainWindow.__init__(window)
|
||||
window.setWindowTitle("服装布料计算管理器 - 专业版 (普通模式)")
|
||||
window.resize(1300, 800)
|
||||
window.init_ui()
|
||||
window.load_garment_list()
|
||||
qtbot.addWidget(window)
|
||||
return window
|
||||
|
||||
|
||||
class TestFabricManager:
|
||||
"""主窗口GUI测试"""
|
||||
|
||||
def test_main_window_init_admin(self, fabric_manager_admin):
|
||||
assert "管理员模式" in fabric_manager_admin.windowTitle()
|
||||
|
||||
def test_main_window_init_user(self, fabric_manager_user):
|
||||
assert "普通模式" in fabric_manager_user.windowTitle()
|
||||
|
||||
def test_main_window_has_components(self, fabric_manager_admin):
|
||||
assert hasattr(fabric_manager_admin, 'garment_combo')
|
||||
assert hasattr(fabric_manager_admin, 'quantity_input')
|
||||
assert hasattr(fabric_manager_admin, 'loss_input')
|
||||
assert hasattr(fabric_manager_admin, 'result_text')
|
||||
|
||||
def test_quantity_input_default(self, fabric_manager_admin):
|
||||
assert fabric_manager_admin.quantity_input.value() == 1000
|
||||
|
||||
def test_loss_input_default(self, fabric_manager_admin):
|
||||
assert fabric_manager_admin.loss_input.value() == 5.0
|
||||
|
||||
def test_unit_converter_components(self, fabric_manager_admin):
|
||||
assert hasattr(fabric_manager_admin, 'calc_m')
|
||||
assert hasattr(fabric_manager_admin, 'calc_yard')
|
||||
assert hasattr(fabric_manager_admin, 'calc_kg')
|
||||
assert hasattr(fabric_manager_admin, 'calc_width')
|
||||
assert hasattr(fabric_manager_admin, 'calc_gsm')
|
||||
|
||||
def test_unit_converter_defaults(self, fabric_manager_admin):
|
||||
assert fabric_manager_admin.calc_width.value() == 150
|
||||
assert fabric_manager_admin.calc_gsm.value() == 200
|
||||
|
||||
def test_meter_to_yard_conversion(self, fabric_manager_admin):
|
||||
fabric_manager_admin.calc_m.setValue(1.0)
|
||||
expected_yard = 1.0 / 0.9144
|
||||
assert abs(fabric_manager_admin.calc_yard.value() - expected_yard) < 0.001
|
||||
|
||||
def test_quantity_input_change(self, fabric_manager_admin):
|
||||
fabric_manager_admin.quantity_input.setValue(500)
|
||||
assert fabric_manager_admin.quantity_input.value() == 500
|
||||
|
||||
def test_loss_input_change(self, fabric_manager_admin):
|
||||
fabric_manager_admin.loss_input.setValue(10.0)
|
||||
assert fabric_manager_admin.loss_input.value() == 10.0
|
||||
127
test/test_login.py
Normal file
127
test/test_login.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
登录模块测试 - 测试密码验证和密码管理功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
import gc
|
||||
from database import DatabaseManager, get_db_connection
|
||||
|
||||
|
||||
class TestLogin(unittest.TestCase):
|
||||
"""登录功能测试类"""
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_login.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self.conn = None
|
||||
|
||||
def tearDown(self):
|
||||
if self.conn:
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_conn(self):
|
||||
self.conn = get_db_connection(self.db_path)
|
||||
return self.conn
|
||||
|
||||
def test_default_admin_password(self):
|
||||
"""测试默认管理员密码"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("admin_password",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "123456")
|
||||
|
||||
def test_default_user_password(self):
|
||||
"""测试默认普通用户密码"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("user_password",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "123456")
|
||||
|
||||
def test_update_admin_password(self):
|
||||
"""测试更新管理员密码"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"UPDATE admin_settings SET value = ? WHERE key = ?",
|
||||
("newpass123", "admin_password")
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("admin_password",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "newpass123")
|
||||
|
||||
def test_update_user_password(self):
|
||||
"""测试更新普通用户密码"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"UPDATE admin_settings SET value = ? WHERE key = ?",
|
||||
("userpass456", "user_password")
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("user_password",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "userpass456")
|
||||
|
||||
def test_password_verification_correct(self):
|
||||
"""测试正确密码验证"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("admin_password",)
|
||||
)
|
||||
stored_pwd = cursor.fetchone()[0]
|
||||
self.assertEqual(stored_pwd, "123456")
|
||||
|
||||
def test_password_verification_incorrect(self):
|
||||
"""测试错误密码验证"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("admin_password",)
|
||||
)
|
||||
stored_pwd = cursor.fetchone()[0]
|
||||
self.assertNotEqual(stored_pwd, "wrongpassword")
|
||||
|
||||
def test_insert_or_replace_password(self):
|
||||
"""测试INSERT OR REPLACE密码设置"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO admin_settings (key, value) VALUES (?, ?)",
|
||||
("admin_password", "replaced123")
|
||||
)
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT value FROM admin_settings WHERE key = ?",
|
||||
("admin_password",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "replaced123")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
163
test/test_login_gui.py
Normal file
163
test/test_login_gui.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
登录对话框GUI测试模块
|
||||
使用PyQt测试框架测试登录和密码管理功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox, QInputDialog
|
||||
from PyQt5.QtTest import QTest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 添加父目录到路径以导入模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import DatabaseManager
|
||||
from login_dialog import LoginDialog
|
||||
|
||||
|
||||
class TestLoginGUI(unittest.TestCase):
|
||||
"""登录对话框GUI测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类初始化:创建QApplication实例"""
|
||||
cls.app = QApplication.instance()
|
||||
if cls.app is None:
|
||||
cls.app = QApplication(sys.argv)
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试前准备:创建临时数据库和对话框"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_fabric.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
# 创建对话框实例
|
||||
self.dialog = LoginDialog(self.db_path)
|
||||
|
||||
# 保存原始消息框
|
||||
self._original_msgbox = QMessageBox.information
|
||||
self._original_warning = QMessageBox.warning
|
||||
self._original_critical = QMessageBox.critical
|
||||
|
||||
# Mock消息框
|
||||
QMessageBox.information = lambda *args, **kwargs: None
|
||||
QMessageBox.warning = lambda *args, **kwargs: None
|
||||
QMessageBox.critical = lambda *args, **kwargs: None
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试后清理"""
|
||||
# 恢复消息框
|
||||
QMessageBox.information = self._original_msgbox
|
||||
QMessageBox.warning = self._original_warning
|
||||
QMessageBox.critical = self._original_critical
|
||||
|
||||
# 关闭对话框
|
||||
self.dialog.close()
|
||||
self.dialog.deleteLater()
|
||||
|
||||
# 清理数据库
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ========== 登录功能测试 ==========
|
||||
|
||||
def test_admin_login_success(self):
|
||||
"""测试管理员登录成功"""
|
||||
# 输入正确的管理员密码(默认123456)
|
||||
self.dialog.admin_input.setText("123456")
|
||||
|
||||
# 调用登录方法
|
||||
self.dialog.login_mode(True)
|
||||
|
||||
# 验证登录成功
|
||||
self.assertTrue(self.dialog.is_admin, "应该设置为管理员模式")
|
||||
|
||||
def test_admin_login_failure(self):
|
||||
"""测试管理员登录失败"""
|
||||
warning_called = []
|
||||
def mock_warning(*args, **kwargs):
|
||||
warning_called.append(args)
|
||||
QMessageBox.warning = mock_warning
|
||||
|
||||
# 输入错误的密码
|
||||
self.dialog.admin_input.setText("wrong_password")
|
||||
|
||||
# 调用登录方法
|
||||
self.dialog.login_mode(True)
|
||||
|
||||
# 验证登录失败
|
||||
self.assertFalse(self.dialog.is_admin, "不应该设置为管理员模式")
|
||||
self.assertTrue(len(warning_called) > 0, "应该显示警告消息")
|
||||
|
||||
def test_user_login_success(self):
|
||||
"""测试普通用户登录成功"""
|
||||
# 输入正确的用户密码(默认123456)
|
||||
self.dialog.user_input.setText("123456")
|
||||
|
||||
# 调用登录方法
|
||||
self.dialog.login_mode(False)
|
||||
|
||||
# 验证登录成功(is_admin应该为False)
|
||||
self.assertFalse(self.dialog.is_admin, "应该是普通用户模式")
|
||||
|
||||
def test_user_login_failure(self):
|
||||
"""测试普通用户登录失败"""
|
||||
warning_called = []
|
||||
def mock_warning(*args, **kwargs):
|
||||
warning_called.append(args)
|
||||
QMessageBox.warning = mock_warning
|
||||
|
||||
# 输入错误的密码
|
||||
self.dialog.user_input.setText("wrong_password")
|
||||
|
||||
# 调用登录方法
|
||||
self.dialog.login_mode(False)
|
||||
|
||||
# 验证登录失败
|
||||
self.assertTrue(len(warning_called) > 0, "应该显示警告消息")
|
||||
|
||||
# ========== 密码管理测试 ==========
|
||||
|
||||
def test_get_default_password(self):
|
||||
"""测试获取默认密码"""
|
||||
admin_pwd = self.dialog.get_stored_password("admin")
|
||||
user_pwd = self.dialog.get_stored_password("user")
|
||||
|
||||
self.assertEqual(admin_pwd, "123456", "默认管理员密码应该是123456")
|
||||
self.assertEqual(user_pwd, "123456", "默认用户密码应该是123456")
|
||||
|
||||
def test_set_password(self):
|
||||
"""测试设置新密码"""
|
||||
# 设置新的管理员密码
|
||||
result = self.dialog.set_password("admin", "new_admin_pass")
|
||||
self.assertTrue(result, "设置密码应该成功")
|
||||
|
||||
# 验证密码已更新
|
||||
stored_pwd = self.dialog.get_stored_password("admin")
|
||||
self.assertEqual(stored_pwd, "new_admin_pass", "密码应该被更新")
|
||||
|
||||
def test_login_with_new_password(self):
|
||||
"""测试使用新密码登录"""
|
||||
# 设置新密码
|
||||
self.dialog.set_password("admin", "newpass123")
|
||||
|
||||
# 使用新密码登录
|
||||
self.dialog.admin_input.setText("newpass123")
|
||||
self.dialog.login_mode(True)
|
||||
|
||||
# 验证登录成功
|
||||
self.assertTrue(self.dialog.is_admin, "应该使用新密码登录成功")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
111
test/test_purchase_order.py
Normal file
111
test/test_purchase_order.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
采购单生成模块测试 - 测试采购单生成和计算功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
import gc
|
||||
from database import DatabaseManager, get_db_connection
|
||||
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
"""采购单测试类"""
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_po.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self.conn = None
|
||||
self._setup_test_data()
|
||||
|
||||
def tearDown(self):
|
||||
if self.conn:
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_conn(self):
|
||||
self.conn = get_db_connection(self.db_path)
|
||||
return self.conn
|
||||
|
||||
def _setup_test_data(self):
|
||||
"""准备测试数据"""
|
||||
with get_db_connection(self.db_path) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO garments (style_number) VALUES (?)",
|
||||
("PO-001",)
|
||||
)
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, model, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("PO-001", "A料", "M001", 0.5, "米"))
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials
|
||||
(style_number, category, model, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("PO-001", "B料", "M002", 0.3, "米"))
|
||||
conn.commit()
|
||||
|
||||
def test_get_materials_for_style(self):
|
||||
"""测试获取款式材料"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute('''
|
||||
SELECT category, model, usage_per_piece, unit
|
||||
FROM garment_materials
|
||||
WHERE style_number = ? AND usage_per_piece > 0
|
||||
ORDER BY id
|
||||
''', ("PO-001",))
|
||||
rows = cursor.fetchall()
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(rows[0][0], "A料")
|
||||
self.assertEqual(rows[1][0], "B料")
|
||||
|
||||
def test_calculate_total_usage(self):
|
||||
"""测试计算总用量"""
|
||||
usage_per_piece = 0.5
|
||||
quantity = 100
|
||||
loss_rate = 0.05
|
||||
total = usage_per_piece * quantity * (1 + loss_rate)
|
||||
self.assertEqual(total, 52.5)
|
||||
|
||||
def test_calculate_total_usage_no_loss(self):
|
||||
"""测试无损耗计算"""
|
||||
usage_per_piece = 0.5
|
||||
quantity = 100
|
||||
loss_rate = 0.0
|
||||
total = usage_per_piece * quantity * (1 + loss_rate)
|
||||
self.assertEqual(total, 50.0)
|
||||
|
||||
def test_calculate_total_usage_high_loss(self):
|
||||
"""测试高损耗计算"""
|
||||
usage_per_piece = 0.5
|
||||
quantity = 100
|
||||
loss_rate = 0.10
|
||||
total = usage_per_piece * quantity * (1 + loss_rate)
|
||||
self.assertAlmostEqual(total, 55.0, places=2)
|
||||
|
||||
def test_empty_style_materials(self):
|
||||
"""测试空款式材料"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute('''
|
||||
SELECT category, model, usage_per_piece, unit
|
||||
FROM garment_materials
|
||||
WHERE style_number = ? AND usage_per_piece > 0
|
||||
''', ("NOT-EXIST",))
|
||||
rows = cursor.fetchall()
|
||||
self.assertEqual(len(rows), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
116
test/test_purchase_order_gui.py
Normal file
116
test/test_purchase_order_gui.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
采购单生成GUI测试模块
|
||||
使用PyQt测试框架测试采购单生成功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from PyQt5.QtTest import QTest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 添加父目录到路径以导入模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import DatabaseManager
|
||||
from purchase_order_dialog import PurchaseOrderDialog
|
||||
|
||||
|
||||
class TestPurchaseOrderGUI(unittest.TestCase):
|
||||
"""采购单生成GUI测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类初始化:创建QApplication实例"""
|
||||
cls.app = QApplication.instance()
|
||||
if cls.app is None:
|
||||
cls.app = QApplication(sys.argv)
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试前准备:创建临时数据库"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_fabric.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
# 添加测试数据
|
||||
with self.db_manager.get_conn() as conn:
|
||||
# 添加款式
|
||||
conn.execute('''
|
||||
INSERT INTO garments (style_number)
|
||||
VALUES (?)
|
||||
''', ("TEST-STYLE-001",))
|
||||
|
||||
# 添加原料
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, color, unit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("TEST-FABRIC-001", "布料", "白色", "米"))
|
||||
|
||||
# 添加款式材料用量
|
||||
conn.execute('''
|
||||
INSERT INTO garment_materials (style_number, category, model, usage_per_piece, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("TEST-STYLE-001", "布料", "TEST-FABRIC-001", 2.5, "米"))
|
||||
conn.commit()
|
||||
|
||||
# 保存原始消息框
|
||||
self._original_msgbox = QMessageBox.information
|
||||
self._original_warning = QMessageBox.warning
|
||||
|
||||
# Mock消息框
|
||||
QMessageBox.information = lambda *args, **kwargs: None
|
||||
QMessageBox.warning = lambda *args, **kwargs: None
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试后清理"""
|
||||
# 恢复消息框
|
||||
QMessageBox.information = self._original_msgbox
|
||||
QMessageBox.warning = self._original_warning
|
||||
|
||||
# 清理数据库
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ========== 采购单生成测试 ==========
|
||||
|
||||
def test_generate_purchase_order(self):
|
||||
"""测试生成采购单"""
|
||||
# 创建采购单对话框
|
||||
dialog = PurchaseOrderDialog(self.db_path, "TEST-STYLE-001", 100, 0.05)
|
||||
|
||||
# 验证采购单文本已生成
|
||||
po_text = dialog.po_text.toPlainText()
|
||||
self.assertIn("TEST-STYLE-001", po_text, "采购单应包含款号")
|
||||
self.assertIn("100 件", po_text, "采购单应包含生产数量")
|
||||
self.assertIn("5.0%", po_text, "采购单应包含损耗率")
|
||||
|
||||
dialog.close()
|
||||
dialog.deleteLater()
|
||||
|
||||
def test_material_calculation(self):
|
||||
"""测试材料用量计算"""
|
||||
# 创建采购单对话框(100件,损耗率5%)
|
||||
dialog = PurchaseOrderDialog(self.db_path, "TEST-STYLE-001", 100, 0.05)
|
||||
|
||||
# 验证采购单包含材料信息
|
||||
po_text = dialog.po_text.toPlainText()
|
||||
self.assertIn("TEST-FABRIC-001", po_text, "采购单应包含原料型号")
|
||||
|
||||
# 计算预期用量:100件 * 2.5米/件 * (1 + 0.05) = 262.5米
|
||||
self.assertIn("262.5", po_text, "采购单应包含正确的用量计算")
|
||||
|
||||
dialog.close()
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
265
test/test_raw_material.py
Normal file
265
test/test_raw_material.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
原料管理功能测试模块
|
||||
测试添加、删除、编辑原料功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
from database import DatabaseManager, get_db_connection
|
||||
|
||||
|
||||
class TestRawMaterial(unittest.TestCase):
|
||||
"""原料管理测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备:创建临时数据库"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_fabric.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self.conn = None
|
||||
|
||||
def tearDown(self):
|
||||
"""测试后清理:删除临时数据库"""
|
||||
if self.conn:
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_conn(self):
|
||||
"""获取数据库连接"""
|
||||
self.conn = get_db_connection(self.db_path)
|
||||
return self.conn
|
||||
|
||||
# ========== 添加原料测试 ==========
|
||||
|
||||
def test_add_raw_material_basic(self):
|
||||
"""测试基本添加原料功能"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, fabric_type, supplier, color, width, gsm, unit, retail_price, bulk_price, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
||||
''', ("TEST-001", "布料", "棉布", "供应商A", "白色", 150.0, 200.0, "米", 10.0, 8.0))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("TEST-001",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(row[0], "TEST-001")
|
||||
|
||||
def test_add_raw_material_with_all_fields(self):
|
||||
"""测试添加包含所有字段的原料"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, fabric_type, supplier, color, width, gsm, unit, retail_price, bulk_price, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
||||
''', ("TEST-002", "辅料", "拉链", "供应商B", "黑色", 0, 0, "条", 5.0, 4.0))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT category, fabric_type, unit FROM fabrics WHERE model = ?", ("TEST-002",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], "辅料")
|
||||
self.assertEqual(row[1], "拉链")
|
||||
self.assertEqual(row[2], "条")
|
||||
|
||||
def test_add_raw_material_duplicate_model(self):
|
||||
"""测试添加重复型号原料(应拒绝,模拟GUI逻辑)"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, supplier, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("TEST-003", "布料", "供应商A"))
|
||||
conn.commit()
|
||||
|
||||
model = "TEST-003"
|
||||
current_edit_model = None
|
||||
|
||||
should_reject = False
|
||||
if not current_edit_model:
|
||||
cursor = conn.execute("SELECT 1 FROM fabrics WHERE model = ?", (model,))
|
||||
if cursor.fetchone():
|
||||
should_reject = True
|
||||
|
||||
self.assertTrue(should_reject, "应拒绝添加重复型号")
|
||||
|
||||
cursor = conn.execute("SELECT category, supplier FROM fabrics WHERE model = ?", (model,))
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "布料", "原数据不应被覆盖")
|
||||
self.assertEqual(row[1], "供应商A", "原数据不应被覆盖")
|
||||
|
||||
def test_add_raw_material_empty_model(self):
|
||||
"""测试添加空型号原料(应失败)"""
|
||||
with self.get_conn() as conn:
|
||||
try:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("", "布料"))
|
||||
conn.commit()
|
||||
cursor = conn.execute("SELECT COUNT(*) FROM fabrics WHERE model = ''")
|
||||
count = cursor.fetchone()[0]
|
||||
self.assertEqual(count, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ========== 删除原料测试 ==========
|
||||
|
||||
def test_delete_raw_material_basic(self):
|
||||
"""测试基本删除原料功能"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("DEL-001", "布料"))
|
||||
conn.commit()
|
||||
|
||||
conn.execute("DELETE FROM fabrics WHERE model = ?", ("DEL-001",))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("DEL-001",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertIsNone(row)
|
||||
|
||||
def test_delete_raw_material_not_exist(self):
|
||||
"""测试删除不存在的原料"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute("DELETE FROM fabrics WHERE model = ?", ("NOT-EXIST",))
|
||||
conn.commit()
|
||||
self.assertEqual(cursor.rowcount, 0)
|
||||
|
||||
def test_delete_raw_material_with_stock(self):
|
||||
"""测试删除有库存记录的原料"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("DEL-002", "布料"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("DEL-002", 100.0, "米", "2024-01-01"))
|
||||
conn.commit()
|
||||
|
||||
conn.execute("DELETE FROM fabrics WHERE model = ?", ("DEL-002",))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("DEL-002",))
|
||||
fabric_row = cursor.fetchone()
|
||||
|
||||
cursor = conn.execute("SELECT * FROM fabric_stock_in WHERE model = ?", ("DEL-002",))
|
||||
stock_row = cursor.fetchone()
|
||||
|
||||
self.assertIsNone(fabric_row)
|
||||
self.assertIsNotNone(stock_row)
|
||||
|
||||
# ========== 编辑原料测试 ==========
|
||||
|
||||
def test_edit_raw_material_basic(self):
|
||||
"""测试基本编辑原料功能"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, supplier, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("EDIT-001", "布料", "供应商A"))
|
||||
conn.commit()
|
||||
|
||||
conn.execute('''
|
||||
UPDATE fabrics SET supplier = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE model = ?
|
||||
''', ("供应商B", "EDIT-001"))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT supplier FROM fabrics WHERE model = ?", ("EDIT-001",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], "供应商B")
|
||||
|
||||
def test_edit_raw_material_category(self):
|
||||
"""测试编辑原料类目"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, fabric_type, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("EDIT-002", "布料", "棉布"))
|
||||
conn.commit()
|
||||
|
||||
conn.execute('''
|
||||
UPDATE fabrics SET category = ?, fabric_type = ?
|
||||
WHERE model = ?
|
||||
''', ("辅料", "纽扣", "EDIT-002"))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT category, fabric_type FROM fabrics WHERE model = ?", ("EDIT-002",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], "辅料")
|
||||
self.assertEqual(row[1], "纽扣")
|
||||
|
||||
def test_edit_raw_material_price(self):
|
||||
"""测试编辑原料价格"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, retail_price, bulk_price, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("EDIT-003", 10.0, 8.0))
|
||||
conn.commit()
|
||||
|
||||
conn.execute('''
|
||||
UPDATE fabrics SET retail_price = ?, bulk_price = ?
|
||||
WHERE model = ?
|
||||
''', (15.0, 12.0, "EDIT-003"))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT retail_price, bulk_price FROM fabrics WHERE model = ?", ("EDIT-003",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], 15.0)
|
||||
self.assertEqual(row[1], 12.0)
|
||||
|
||||
def test_edit_raw_material_specifications(self):
|
||||
"""测试编辑原料规格(幅宽、克重)"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, width, gsm, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("EDIT-004", 150.0, 200.0))
|
||||
conn.commit()
|
||||
|
||||
conn.execute('''
|
||||
UPDATE fabrics SET width = ?, gsm = ?
|
||||
WHERE model = ?
|
||||
''', (160.0, 250.0, "EDIT-004"))
|
||||
conn.commit()
|
||||
|
||||
cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ?", ("EDIT-004",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], 160.0)
|
||||
self.assertEqual(row[1], 250.0)
|
||||
|
||||
def test_edit_raw_material_not_exist(self):
|
||||
"""测试编辑不存在的原料"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.execute('''
|
||||
UPDATE fabrics SET supplier = ?
|
||||
WHERE model = ?
|
||||
''', ("供应商X", "NOT-EXIST"))
|
||||
conn.commit()
|
||||
self.assertEqual(cursor.rowcount, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
278
test/test_raw_material_gui.py
Normal file
278
test/test_raw_material_gui.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
原料管理GUI测试模块
|
||||
使用PyQt测试框架测试GUI交互和业务逻辑
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox, QTabWidget
|
||||
from PyQt5.QtTest import QTest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 添加父目录到路径以导入模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import DatabaseManager
|
||||
from raw_material_dialog import RawMaterialLibraryDialog
|
||||
|
||||
|
||||
class TestRawMaterialGUI(unittest.TestCase):
|
||||
"""原料管理GUI测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类初始化:创建QApplication实例"""
|
||||
cls.app = QApplication.instance()
|
||||
if cls.app is None:
|
||||
cls.app = QApplication(sys.argv)
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试前准备:创建临时数据库和对话框"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_fabric.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
# 创建对话框实例(管理员模式)
|
||||
self.dialog = RawMaterialLibraryDialog(self.db_path, is_admin=True)
|
||||
|
||||
# 禁用消息框以便自动化测试
|
||||
self._original_msgbox = QMessageBox.information
|
||||
self._original_warning = QMessageBox.warning
|
||||
self._original_critical = QMessageBox.critical
|
||||
self._original_question = QMessageBox.question
|
||||
|
||||
# Mock消息框
|
||||
QMessageBox.information = lambda *args, **kwargs: None
|
||||
QMessageBox.warning = lambda *args, **kwargs: None
|
||||
QMessageBox.critical = lambda *args, **kwargs: None
|
||||
QMessageBox.question = lambda *args, **kwargs: QMessageBox.Yes
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试后清理"""
|
||||
# 恢复消息框
|
||||
QMessageBox.information = self._original_msgbox
|
||||
QMessageBox.warning = self._original_warning
|
||||
QMessageBox.critical = self._original_critical
|
||||
QMessageBox.question = self._original_question
|
||||
|
||||
# 关闭对话框
|
||||
self.dialog.close()
|
||||
self.dialog.deleteLater()
|
||||
|
||||
# 清理数据库
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ========== 添加原料GUI测试 ==========
|
||||
|
||||
def test_add_raw_material_basic_gui(self):
|
||||
"""测试通过GUI添加基本原料"""
|
||||
# 切换到新增/编辑标签页
|
||||
tabs = self.dialog.findChild(QTabWidget)
|
||||
tabs.setCurrentIndex(1)
|
||||
|
||||
# 填写表单
|
||||
self.dialog.add_major_category.setCurrentText("布料")
|
||||
self.dialog.add_sub_category.setText("棉布")
|
||||
self.dialog.add_model.setText("GUI-TEST-001")
|
||||
self.dialog.add_supplier.setCurrentText("测试供应商")
|
||||
self.dialog.add_color.setText("白色")
|
||||
self.dialog.add_width.setValue(150.0)
|
||||
self.dialog.add_gsm.setValue(200.0)
|
||||
self.dialog.add_unit.setCurrentText("米")
|
||||
self.dialog.add_retail.setValue(10.0)
|
||||
self.dialog.add_bulk.setValue(8.0)
|
||||
|
||||
# 点击保存按钮
|
||||
self.dialog.save_raw_material()
|
||||
|
||||
# 验证数据已保存到数据库
|
||||
with self.dialog.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("GUI-TEST-001",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertIsNotNone(row, "原料应该被保存到数据库")
|
||||
self.assertEqual(row[0], "GUI-TEST-001", "型号应该匹配")
|
||||
|
||||
def test_add_raw_material_duplicate_model_gui(self):
|
||||
"""测试通过GUI添加重复型号(应被拒绝)"""
|
||||
# 先添加一个原料
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, supplier, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("GUI-TEST-002", "布料", "供应商A"))
|
||||
conn.commit()
|
||||
|
||||
# Mock warning消息框以捕获警告
|
||||
warning_called = []
|
||||
def mock_warning(*args, **kwargs):
|
||||
warning_called.append(args)
|
||||
QMessageBox.warning = mock_warning
|
||||
|
||||
# 尝试添加重复型号
|
||||
tabs = self.dialog.findChild(QTabWidget)
|
||||
tabs.setCurrentIndex(1)
|
||||
|
||||
self.dialog.add_model.setText("GUI-TEST-002")
|
||||
self.dialog.add_major_category.setCurrentText("布料")
|
||||
self.dialog.save_raw_material()
|
||||
|
||||
# 验证警告被触发
|
||||
self.assertTrue(len(warning_called) > 0, "应该显示警告消息")
|
||||
|
||||
# 验证数据库中只有一条记录
|
||||
with self.dialog.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT COUNT(*) FROM fabrics WHERE model = ?", ("GUI-TEST-002",))
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
self.assertEqual(count, 1, "数据库中应该只有一条记录")
|
||||
|
||||
def test_add_raw_material_empty_model_gui(self):
|
||||
"""测试通过GUI添加空型号(应被拒绝)"""
|
||||
warning_called = []
|
||||
def mock_warning(*args, **kwargs):
|
||||
warning_called.append(args)
|
||||
QMessageBox.warning = mock_warning
|
||||
|
||||
# 尝试保存空型号
|
||||
tabs = self.dialog.findChild(QTabWidget)
|
||||
tabs.setCurrentIndex(1)
|
||||
|
||||
self.dialog.add_model.setText("")
|
||||
self.dialog.save_raw_material()
|
||||
|
||||
# 验证警告被触发
|
||||
self.assertTrue(len(warning_called) > 0, "应该显示警告消息")
|
||||
|
||||
# ========== 编辑原料GUI测试 ==========
|
||||
|
||||
def test_edit_raw_material_gui(self):
|
||||
"""测试通过GUI编辑原料"""
|
||||
# 先添加一个原料
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, supplier, color, timestamp)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
''', ("GUI-EDIT-001", "布料", "供应商A", "白色"))
|
||||
conn.commit()
|
||||
|
||||
# 刷新表格
|
||||
self.dialog.load_table()
|
||||
|
||||
# 调用编辑方法
|
||||
self.dialog.edit_raw_material("GUI-EDIT-001")
|
||||
|
||||
# 验证表单已填充
|
||||
self.assertEqual(self.dialog.add_model.text(), "GUI-EDIT-001")
|
||||
self.assertEqual(self.dialog.add_supplier.currentText(), "供应商A")
|
||||
self.assertEqual(self.dialog.add_color.text(), "白色")
|
||||
|
||||
# 修改供应商
|
||||
self.dialog.add_supplier.setCurrentText("供应商B")
|
||||
self.dialog.save_raw_material()
|
||||
|
||||
# 验证修改已保存
|
||||
with self.dialog.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT supplier FROM fabrics WHERE model = ?", ("GUI-EDIT-001",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertEqual(row[0], "供应商B", "供应商应该被更新")
|
||||
|
||||
# ========== 删除原料GUI测试 ==========
|
||||
|
||||
def test_delete_raw_material_gui(self):
|
||||
"""测试通过GUI删除原料"""
|
||||
# 先添加一个原料
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("GUI-DEL-001", "布料"))
|
||||
conn.commit()
|
||||
|
||||
# 刷新表格
|
||||
self.dialog.load_table()
|
||||
|
||||
# 调用删除方法
|
||||
self.dialog.delete_raw("GUI-DEL-001")
|
||||
|
||||
# 验证已删除
|
||||
with self.dialog.get_conn() as conn:
|
||||
cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("GUI-DEL-001",))
|
||||
row = cursor.fetchone()
|
||||
|
||||
self.assertIsNone(row, "原料应该被删除")
|
||||
|
||||
# ========== 过滤和搜索GUI测试 ==========
|
||||
|
||||
def test_filter_by_category_gui(self):
|
||||
"""测试通过GUI按类目过滤"""
|
||||
# 添加测试数据
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, fabric_type, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("FILTER-001", "布料", "棉布"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, fabric_type, timestamp)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
''', ("FILTER-002", "辅料", "拉链"))
|
||||
conn.commit()
|
||||
|
||||
# 刷新过滤器和表格
|
||||
self.dialog.refresh_filters_and_table()
|
||||
|
||||
# 选择"布料"类目
|
||||
self.dialog.major_combo.setCurrentText("布料")
|
||||
|
||||
# 验证表格只显示布料
|
||||
row_count = self.dialog.table.rowCount()
|
||||
for row in range(row_count):
|
||||
category_item = self.dialog.table.item(row, 0)
|
||||
if category_item:
|
||||
self.assertEqual(category_item.text(), "布料", "应该只显示布料类目")
|
||||
|
||||
def test_search_by_model_gui(self):
|
||||
"""测试通过GUI搜索型号"""
|
||||
# 添加测试数据
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("SEARCH-001", "布料"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("SEARCH-002", "布料"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, timestamp)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
''', ("OTHER-001", "布料"))
|
||||
conn.commit()
|
||||
|
||||
# 刷新表格
|
||||
self.dialog.load_table()
|
||||
|
||||
# 输入搜索关键词
|
||||
self.dialog.search_input.setText("SEARCH")
|
||||
|
||||
# 验证表格只显示匹配的结果
|
||||
row_count = self.dialog.table.rowCount()
|
||||
for row in range(row_count):
|
||||
model_item = self.dialog.table.item(row, 2)
|
||||
if model_item:
|
||||
self.assertIn("SEARCH", model_item.text(), "应该只显示包含SEARCH的型号")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
272
test/test_stock.py
Normal file
272
test/test_stock.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
库存管理模块测试 - 测试入库和库存查询功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
import gc
|
||||
from datetime import datetime
|
||||
from database import DatabaseManager, get_db_connection
|
||||
|
||||
|
||||
class TestStock(unittest.TestCase):
|
||||
"""库存管理测试类"""
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_stock.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
self.conn = None
|
||||
self._setup_test_data()
|
||||
|
||||
def tearDown(self):
|
||||
if self.conn:
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_conn(self):
|
||||
self.conn = get_db_connection(self.db_path)
|
||||
return self.conn
|
||||
|
||||
def _setup_test_data(self):
|
||||
"""准备测试数据"""
|
||||
with get_db_connection(self.db_path) as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, supplier, color, unit, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||
''', ("STOCK-001", "布料", "供应商A", "白色", "米"))
|
||||
conn.commit()
|
||||
|
||||
# ========== 入库测试 ==========
|
||||
|
||||
def test_stock_in_basic(self):
|
||||
"""测试基本入库功能"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("STOCK-001", 100.0, "米", "2024-01-01", "测试入库"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT quantity, note FROM fabric_stock_in WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], 100.0)
|
||||
self.assertEqual(row[1], "测试入库")
|
||||
|
||||
def test_stock_in_multiple(self):
|
||||
"""测试多次入库"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 50.0, "米", "2024-01-01"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 30.0, "米", "2024-01-02"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT SUM(quantity) FROM fabric_stock_in WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
total = cursor.fetchone()[0]
|
||||
self.assertEqual(total, 80.0)
|
||||
|
||||
def test_stock_in_with_note(self):
|
||||
"""测试带备注的入库"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("STOCK-001", 25.5, "米", "2024-01-15", "批次号:B001"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT note FROM fabric_stock_in WHERE model = ? AND quantity = ?",
|
||||
("STOCK-001", 25.5)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], "批次号:B001")
|
||||
|
||||
# ========== 消耗测试 ==========
|
||||
|
||||
def test_consumption_basic(self):
|
||||
"""测试基本消耗记录"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_consumption
|
||||
(style_number, model, single_usage, quantity_made, loss_rate, consume_quantity, consume_date, unit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', ("G001", "STOCK-001", 0.5, 100, 0.05, 52.5, "2024-01-10", "米"))
|
||||
conn.commit()
|
||||
cursor = conn.execute(
|
||||
"SELECT consume_quantity FROM fabric_consumption WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertEqual(row[0], 52.5)
|
||||
|
||||
def test_consumption_calculation(self):
|
||||
"""测试消耗量计算"""
|
||||
single_usage = 0.5
|
||||
quantity_made = 100
|
||||
loss_rate = 0.05
|
||||
expected = single_usage * quantity_made * (1 + loss_rate)
|
||||
self.assertEqual(expected, 52.5)
|
||||
|
||||
# ========== 库存计算测试 ==========
|
||||
|
||||
def test_stock_remaining_calculation(self):
|
||||
"""测试剩余库存计算"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 100.0, "米", "2024-01-01"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_consumption
|
||||
(style_number, model, consume_quantity, consume_date, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("G001", "STOCK-001", 30.0, "2024-01-05", "米"))
|
||||
conn.commit()
|
||||
|
||||
cursor_in = conn.execute(
|
||||
"SELECT COALESCE(SUM(quantity), 0) FROM fabric_stock_in WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
total_in = cursor_in.fetchone()[0]
|
||||
|
||||
cursor_out = conn.execute(
|
||||
"SELECT COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
total_out = cursor_out.fetchone()[0]
|
||||
|
||||
remaining = total_in - total_out
|
||||
self.assertEqual(remaining, 70.0)
|
||||
|
||||
def test_stock_zero_remaining(self):
|
||||
"""测试库存清零"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 50.0, "米", "2024-01-01"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_consumption
|
||||
(style_number, model, consume_quantity, consume_date, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("库存清零", "STOCK-001", 50.0, "2024-01-10", "米"))
|
||||
conn.commit()
|
||||
|
||||
cursor_in = conn.execute(
|
||||
"SELECT COALESCE(SUM(quantity), 0) FROM fabric_stock_in WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
total_in = cursor_in.fetchone()[0]
|
||||
|
||||
cursor_out = conn.execute(
|
||||
"SELECT COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption WHERE model = ?",
|
||||
("STOCK-001",)
|
||||
)
|
||||
total_out = cursor_out.fetchone()[0]
|
||||
|
||||
remaining = total_in - total_out
|
||||
self.assertEqual(remaining, 0.0)
|
||||
|
||||
# ========== 编辑库存并清除历史记录测试 ==========
|
||||
|
||||
def test_edit_stock_and_clear_history(self):
|
||||
"""测试编辑剩余库存并清除历史记录"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 100.0, "米", "2024-01-01"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 50.0, "米", "2024-01-05"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_consumption
|
||||
(style_number, model, consume_quantity, consume_date, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("G001", "STOCK-001", 30.0, "2024-01-10", "米"))
|
||||
conn.commit()
|
||||
|
||||
conn.execute("DELETE FROM fabric_stock_in WHERE model = ?", ("STOCK-001",))
|
||||
conn.execute("DELETE FROM fabric_consumption WHERE model = ?", ("STOCK-001",))
|
||||
new_stock = 80.0
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("STOCK-001", new_stock, "米", datetime.now().strftime('%Y-%m-%d'), "库存盘点调整"))
|
||||
conn.commit()
|
||||
|
||||
cursor_in = conn.execute(
|
||||
"SELECT COUNT(*) FROM fabric_stock_in WHERE model = ?", ("STOCK-001",)
|
||||
)
|
||||
in_count = cursor_in.fetchone()[0]
|
||||
|
||||
cursor_out = conn.execute(
|
||||
"SELECT COUNT(*) FROM fabric_consumption WHERE model = ?", ("STOCK-001",)
|
||||
)
|
||||
out_count = cursor_out.fetchone()[0]
|
||||
|
||||
cursor_qty = conn.execute(
|
||||
"SELECT SUM(quantity) FROM fabric_stock_in WHERE model = ?", ("STOCK-001",)
|
||||
)
|
||||
total_qty = cursor_qty.fetchone()[0]
|
||||
|
||||
self.assertEqual(in_count, 1)
|
||||
self.assertEqual(out_count, 0)
|
||||
self.assertEqual(total_qty, 80.0)
|
||||
|
||||
def test_edit_stock_clear_history_multiple_models(self):
|
||||
"""测试编辑库存时只清除指定型号的历史记录"""
|
||||
with self.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, unit)
|
||||
VALUES (?, ?, ?)
|
||||
''', ("STOCK-002", "布料", "米"))
|
||||
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-001", 100.0, "米", "2024-01-01"))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("STOCK-002", 200.0, "米", "2024-01-01"))
|
||||
conn.commit()
|
||||
|
||||
conn.execute("DELETE FROM fabric_stock_in WHERE model = ?", ("STOCK-001",))
|
||||
conn.execute("DELETE FROM fabric_consumption WHERE model = ?", ("STOCK-001",))
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("STOCK-001", 50.0, "米", datetime.now().strftime('%Y-%m-%d'), "库存盘点调整"))
|
||||
conn.commit()
|
||||
|
||||
cursor_002 = conn.execute(
|
||||
"SELECT SUM(quantity) FROM fabric_stock_in WHERE model = ?", ("STOCK-002",)
|
||||
)
|
||||
stock_002 = cursor_002.fetchone()[0]
|
||||
|
||||
self.assertEqual(stock_002, 200.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
161
test/test_stock_gui.py
Normal file
161
test/test_stock_gui.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
库存管理GUI测试模块
|
||||
使用PyQt测试框架测试库存入库和查询功能
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox, QInputDialog
|
||||
from PyQt5.QtTest import QTest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 添加父目录到路径以导入模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import DatabaseManager
|
||||
from stock_dialog import StockInDialog
|
||||
|
||||
|
||||
class TestStockGUI(unittest.TestCase):
|
||||
"""库存管理GUI测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类初始化:创建QApplication实例"""
|
||||
cls.app = QApplication.instance()
|
||||
if cls.app is None:
|
||||
cls.app = QApplication(sys.argv)
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试前准备:创建临时数据库和对话框"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self.temp_dir, "test_fabric.db")
|
||||
self.db_manager = DatabaseManager(self.db_path)
|
||||
|
||||
# 添加测试原料
|
||||
with self.db_manager.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, color, supplier, unit, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||
''', ("TEST-STOCK-001", "布料", "白色", "供应商A", "米"))
|
||||
conn.commit()
|
||||
|
||||
# 创建对话框实例
|
||||
self.dialog = StockInDialog(self.db_path)
|
||||
|
||||
# 保存原始消息框和输入框
|
||||
self._original_msgbox = QMessageBox.information
|
||||
self._original_warning = QMessageBox.warning
|
||||
self._original_input = QInputDialog.getDouble
|
||||
|
||||
# Mock消息框
|
||||
QMessageBox.information = lambda *args, **kwargs: None
|
||||
QMessageBox.warning = lambda *args, **kwargs: None
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试后清理"""
|
||||
# 恢复消息框
|
||||
QMessageBox.information = self._original_msgbox
|
||||
QMessageBox.warning = self._original_warning
|
||||
QInputDialog.getDouble = self._original_input
|
||||
|
||||
# 关闭对话框
|
||||
self.dialog.close()
|
||||
self.dialog.deleteLater()
|
||||
|
||||
# 清理数据库
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
if os.path.exists(self.db_path):
|
||||
os.remove(self.db_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ========== 库存加载测试 ==========
|
||||
|
||||
def test_load_models(self):
|
||||
"""测试加载原料列表"""
|
||||
self.dialog.load_models()
|
||||
|
||||
# 验证表格有数据
|
||||
self.assertGreater(self.dialog.table.rowCount(), 0, "表格应该有数据")
|
||||
|
||||
# 验证第一行是测试数据
|
||||
model_item = self.dialog.table.item(0, 0)
|
||||
self.assertIsNotNone(model_item, "应该有型号数据")
|
||||
self.assertEqual(model_item.text(), "TEST-STOCK-001", "型号应该匹配")
|
||||
|
||||
def test_search_models(self):
|
||||
"""测试搜索原料"""
|
||||
# 添加更多测试数据
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabrics (model, category, color, unit, timestamp)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
''', ("OTHER-001", "布料", "黑色", "米"))
|
||||
conn.commit()
|
||||
|
||||
# 搜索特定型号
|
||||
self.dialog.search_input.setText("TEST-STOCK")
|
||||
self.dialog.load_models()
|
||||
|
||||
# 验证只显示匹配的结果
|
||||
row_count = self.dialog.table.rowCount()
|
||||
for row in range(row_count):
|
||||
model_item = self.dialog.table.item(row, 0)
|
||||
if model_item:
|
||||
self.assertIn("TEST-STOCK", model_item.text(), "应该只显示匹配的型号")
|
||||
|
||||
# ========== 库存计算测试 ==========
|
||||
|
||||
def test_stock_calculation(self):
|
||||
"""测试库存数量计算"""
|
||||
# 添加入库记录
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("TEST-STOCK-001", 100.0, "米", "2024-01-01"))
|
||||
conn.commit()
|
||||
|
||||
# 刷新表格
|
||||
self.dialog.load_models()
|
||||
|
||||
# 验证库存显示
|
||||
remaining_item = self.dialog.table.item(0, 4)
|
||||
self.assertIsNotNone(remaining_item, "应该有库存数据")
|
||||
remaining = float(remaining_item.text())
|
||||
self.assertEqual(remaining, 100.0, "库存应该是100.0")
|
||||
|
||||
def test_stock_with_consumption(self):
|
||||
"""测试库存扣除消耗后的计算"""
|
||||
# 添加入库记录
|
||||
with self.dialog.get_conn() as conn:
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', ("TEST-STOCK-001", 100.0, "米", "2024-01-01"))
|
||||
|
||||
# 添加消耗记录
|
||||
conn.execute('''
|
||||
INSERT INTO fabric_consumption (model, style_number, consume_quantity, consume_date, unit)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("TEST-STOCK-001", "款式001", 30.0, "2024-01-02", "米"))
|
||||
conn.commit()
|
||||
|
||||
# 刷新表格
|
||||
self.dialog.load_models()
|
||||
|
||||
# 验证库存显示(100 - 30 = 70)
|
||||
remaining_item = self.dialog.table.item(0, 4)
|
||||
remaining = float(remaining_item.text())
|
||||
self.assertEqual(remaining, 70.0, "库存应该是70.0(100-30)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
142
test/test_unit_conversion.py
Normal file
142
test/test_unit_conversion.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
单位转换测试
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import math
|
||||
|
||||
|
||||
YARD_TO_METER = 0.9144
|
||||
|
||||
|
||||
def convert_meter_to_yard(meters):
|
||||
return meters / YARD_TO_METER
|
||||
|
||||
|
||||
def convert_yard_to_meter(yards):
|
||||
return yards * YARD_TO_METER
|
||||
|
||||
|
||||
def convert_meter_to_kg(meters, width_cm, gsm):
|
||||
return meters * (width_cm / 100) * (gsm / 1000)
|
||||
|
||||
|
||||
def convert_yard_to_kg(yards, width_cm, gsm):
|
||||
meters = yards * YARD_TO_METER
|
||||
return meters * (width_cm / 100) * (gsm / 1000)
|
||||
|
||||
|
||||
def convert_kg_to_meter(kg, width_cm, gsm):
|
||||
return kg / ((width_cm / 100) * (gsm / 1000))
|
||||
|
||||
|
||||
def convert_kg_to_yard(kg, width_cm, gsm):
|
||||
meters = kg / ((width_cm / 100) * (gsm / 1000))
|
||||
return meters / YARD_TO_METER
|
||||
|
||||
|
||||
class TestMeterYardConversion(unittest.TestCase):
|
||||
|
||||
def test_meter_to_yard(self):
|
||||
result = convert_meter_to_yard(1)
|
||||
self.assertAlmostEqual(result, 1.0936, places=4)
|
||||
|
||||
def test_yard_to_meter(self):
|
||||
result = convert_yard_to_meter(1)
|
||||
self.assertAlmostEqual(result, 0.9144, places=4)
|
||||
|
||||
def test_meter_yard_roundtrip(self):
|
||||
original = 10.5
|
||||
yards = convert_meter_to_yard(original)
|
||||
back = convert_yard_to_meter(yards)
|
||||
self.assertAlmostEqual(original, back, places=6)
|
||||
|
||||
def test_yard_meter_roundtrip(self):
|
||||
original = 15.3
|
||||
meters = convert_yard_to_meter(original)
|
||||
back = convert_meter_to_yard(meters)
|
||||
self.assertAlmostEqual(original, back, places=6)
|
||||
|
||||
|
||||
class TestLengthToWeightConversion(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.width_cm = 150
|
||||
self.gsm = 200
|
||||
|
||||
def test_meter_to_kg(self):
|
||||
result = convert_meter_to_kg(10, self.width_cm, self.gsm)
|
||||
expected = 10 * 1.5 * 0.2
|
||||
self.assertAlmostEqual(result, expected, places=6)
|
||||
|
||||
def test_yard_to_kg(self):
|
||||
result = convert_yard_to_kg(10, self.width_cm, self.gsm)
|
||||
meters = 10 * YARD_TO_METER
|
||||
expected = meters * 1.5 * 0.2
|
||||
self.assertAlmostEqual(result, expected, places=6)
|
||||
|
||||
def test_kg_to_meter(self):
|
||||
result = convert_kg_to_meter(3, self.width_cm, self.gsm)
|
||||
expected = 3 / (1.5 * 0.2)
|
||||
self.assertAlmostEqual(result, expected, places=6)
|
||||
|
||||
def test_kg_to_yard(self):
|
||||
result = convert_kg_to_yard(3, self.width_cm, self.gsm)
|
||||
meters = 3 / (1.5 * 0.2)
|
||||
expected = meters / YARD_TO_METER
|
||||
self.assertAlmostEqual(result, expected, places=6)
|
||||
|
||||
|
||||
class TestRoundtripConversion(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.width_cm = 140
|
||||
self.gsm = 180
|
||||
|
||||
def test_meter_kg_roundtrip(self):
|
||||
original = 25.5
|
||||
kg = convert_meter_to_kg(original, self.width_cm, self.gsm)
|
||||
back = convert_kg_to_meter(kg, self.width_cm, self.gsm)
|
||||
self.assertAlmostEqual(original, back, places=6)
|
||||
|
||||
def test_yard_kg_roundtrip(self):
|
||||
original = 30.0
|
||||
kg = convert_yard_to_kg(original, self.width_cm, self.gsm)
|
||||
back = convert_kg_to_yard(kg, self.width_cm, self.gsm)
|
||||
self.assertAlmostEqual(original, back, places=6)
|
||||
|
||||
|
||||
class TestEdgeCases(unittest.TestCase):
|
||||
|
||||
def test_zero_value(self):
|
||||
self.assertEqual(convert_meter_to_yard(0), 0)
|
||||
self.assertEqual(convert_yard_to_meter(0), 0)
|
||||
self.assertEqual(convert_meter_to_kg(0, 150, 200), 0)
|
||||
|
||||
def test_large_value(self):
|
||||
result = convert_meter_to_yard(10000)
|
||||
self.assertAlmostEqual(result, 10000 / YARD_TO_METER, places=2)
|
||||
|
||||
def test_small_value(self):
|
||||
result = convert_meter_to_yard(0.001)
|
||||
self.assertAlmostEqual(result, 0.001 / YARD_TO_METER, places=8)
|
||||
|
||||
|
||||
class TestPriceConversion(unittest.TestCase):
|
||||
|
||||
def test_price_per_meter_to_yard(self):
|
||||
price_per_meter = 10.0
|
||||
price_per_yard = price_per_meter * YARD_TO_METER
|
||||
self.assertAlmostEqual(price_per_yard, 9.144, places=3)
|
||||
|
||||
def test_price_per_kg_to_meter(self):
|
||||
price_per_kg = 50.0
|
||||
width_cm = 150
|
||||
gsm = 200
|
||||
kg_per_meter = (width_cm / 100) * (gsm / 1000)
|
||||
price_per_meter = price_per_kg * kg_per_meter
|
||||
self.assertAlmostEqual(price_per_meter, 15.0, places=6)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user