Compare commits

..

16 Commits

Author SHA1 Message Date
2fe86ef332 打包文件 2025-12-30 17:50:43 +08:00
26397a83dd 1 2025-12-29 00:36:40 +08:00
d33216f9b9 添加库存不足检查功能
在记录消耗到库存功能中增加了库存充足性验证,确保只有在所有原料库存充足时才允许记录消耗。

主要改进:
- 在记录消耗前先检查所有原料的当前库存
- 如果任何原料库存不足,显示详细的库存不足信息并中止操作
- 提供清晰的提示信息,显示每种原料所需数量和实际库存数量
- 只有所有原料库存都充足时才执行记录操作
- 优化成功提示信息,显示记录的原料种类数量

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 00:33:51 +08:00
5cf27c8cfe 优化原料管理界面功能
1. 类目选项标准化
   - 新增和编辑界面的类目固定为"面料"、"辅料"、"其他"三个选项
   - 新增原料时默认选中"面料"

2. 必填字段验证
   - 新增必填验证:类目、类型、幅宽、克重
   - 保存时验证所有必填字段,提供明确的错误提示

3. 已删除原料恢复功能
   - 允许新增已被删除的型号,自动恢复并更新原料信息
   - 无需用户确认,直接恢复已删除原料

4. 供应商字段优化
   - 新增原料时供应商默认为空
   - 用户可选择输入新供应商或从列表选择

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 00:41:39 +08:00
803127062e 修复新建数据库缺少is_deleted列的问题
在CREATE TABLE语句中直接添加is_deleted字段,确保新建数据库时就包含该列。
修改了三个表的建表语句:
- fabrics表
- fabric_stock_in表
- fabric_consumption表

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 00:14:31 +08:00
83609aaa20 款号管理列表设置为只读模式
将衣服款号管理界面的表格所有列(款号、类目数量、款式图预览)设置为只读,防止用户直接在表格中编辑内容,保持与原料库列表的一致性。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 00:10:43 +08:00
35050407e9 优化款号管理界面功能
- 修复删除类目时删除错误行的问题
- 移除"快速添加标准类目"功能
- 统一界面文案:"自定义类目"改为"选择类目"
- 简化按钮文案:"新增/编辑款号"改为"新增款号"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 19:19:26 +08:00
6322cb0caa 实现原料逻辑删除功能
- 为fabrics表添加is_deleted字段用于标记删除状态
- 修改delete_raw方法实现逻辑删除而非物理删除
- 更新所有查询语句过滤已删除的原料数据
- 更新库存视图过滤已删除的原料和相关记录
- 保留历史数据,支持数据恢复

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 19:09:16 +08:00
fa70f62099 优化原料库存管理功能
- 库存跟踪表格设置为只读模式,防止误编辑
- 添加"编辑剩余库存"功能,支持直接修改库存数量
- 实现逻辑删除机制,删除操作不再物理删除数据
  - 在 fabric_stock_in 和 fabric_consumption 表添加 is_deleted 字段
  - 所有删除操作改为标记删除,保留历史数据
  - 查询时自动过滤已删除记录
- 原料编辑支持修改型号
  - 型号字段改为可编辑
  - 保存时检查型号重复并提示
  - 型号修改时级联更新所有关联表
- 优化操作列宽度,确保按钮文本完整显示
- 改进警告提示,明确说明操作影响

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 18:59:18 +08:00
14e25ad03b 原料编辑功能改为独立窗口
- 新增 RawMaterialEditDialog 独立编辑对话框类
- 编辑原料时弹出新窗口,不再复用"新增原料"标签页
- 编辑窗口中型号字段设为只读,防止误修改
- 新增原料功能改为纯新增模式,检查重复型号
- 移除 current_edit_model 状态变量,简化代码逻辑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 18:34:28 +08:00
c45912cb9e 原料库列表设置为只读模式
在原料列表标签页的表格中添加 NoEditTriggers 设置,防止用户直接编辑表格内容。用户需要通过"编辑"按钮进入编辑界面进行修改,避免误操作。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 18:24:38 +08:00
7cddafef63 修复原料库类型筛选功能
修复了原料库管理中"类型筛选"下拉框选择后内容不刷新的问题。现在支持独立使用类型筛选,无需同时选择类目。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 18:22:14 +08:00
8aa1a5ac91 添加PyQt GUI自动化测试套件
新增5个GUI测试模块,覆盖所有主要功能:
- test_login_gui.py: 登录和密码管理测试(7个测试)
- test_stock_gui.py: 库存管理测试(4个测试)
- test_raw_material_gui.py: 原料管理测试(7个测试)
- test_garment_gui.py: 款式管理测试(2个测试)
- test_purchase_order_gui.py: 采购单生成测试(2个测试)

测试特点:
- 真实GUI交互测试(填写表单、点击按钮、搜索过滤)
- 业务逻辑验证(重复数据拒绝、空值验证、计算正确性)
- 独立测试环境(临时数据库,自动清理)
- 自动化消息框(Mock QMessageBox)

总计22个GUI测试,全部通过 ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 16:52:30 +08:00
c52d360cbb 一键清零和单位转换 2025-12-23 18:21:44 +08:00
76b16df32c 模糊搜索输入框 2025-12-23 16:09:50 +08:00
58605b1f92 编辑款式下拉框修复 2025-12-23 12:35:07 +08:00
27 changed files with 3900 additions and 555 deletions

74
build_exe.py Normal file
View File

@@ -0,0 +1,74 @@
"""
打包脚本 - 将项目打包为exe文件
使用方法: python build_exe.py
"""
import os
import sys
import subprocess
import shutil
def build_exe():
"""使用PyInstaller打包为exe"""
# 项目根目录
root_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(root_dir)
# 主程序文件
main_script = "main.py"
# 打包命令
cmd = [
"pyinstaller",
"--name=服装布料计算管理器",
"--onefile", # 打包为单个exe文件
"--windowed", # 不显示控制台窗口
"--hidden-import=PyQt5.QtCore",
"--hidden-import=PyQt5.QtGui",
"--hidden-import=PyQt5.QtWidgets",
"--hidden-import=sqlite3",
"--clean", # 清理临时文件
]
# 如果有图标文件,添加图标参数
if os.path.exists("icon.ico"):
cmd.append("--icon=icon.ico")
# 如果数据库文件存在,添加到打包数据中
if os.path.exists("fabric_library.db"):
# Windows使用分号Linux/Mac使用冒号
separator = ";" if sys.platform == "win32" else ":"
cmd.append(f"--add-data=fabric_library.db{separator}.")
cmd.append(main_script)
print("开始打包...")
print(f"执行命令: {' '.join(cmd)}")
try:
# 执行打包
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
print("打包成功!")
print(f"\n输出文件位置: {os.path.join(root_dir, 'dist', '服装布料计算管理器.exe')}")
# 复制数据库文件到dist目录如果存在
if os.path.exists("fabric_library.db"):
dist_dir = os.path.join(root_dir, "dist")
if os.path.exists(dist_dir):
shutil.copy2("fabric_library.db", dist_dir)
print(f"数据库文件已复制到: {os.path.join(dist_dir, 'fabric_library.db')}")
print("\n打包完成!可以在 dist 目录中找到生成的exe文件。")
except subprocess.CalledProcessError as e:
print(f"打包失败: {e}")
print(f"错误输出: {e.stderr}")
sys.exit(1)
except FileNotFoundError:
print("错误: 未找到 pyinstaller请先安装: pip install pyinstaller")
sys.exit(1)
if __name__ == "__main__":
build_exe()

62
build_exe.spec Normal file
View File

@@ -0,0 +1,62 @@
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller 配置文件
可以直接使用: pyinstaller build_exe.spec
"""
import os
block_cipher = None
# 收集所有需要打包的Python文件
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[
('fabric_library.db', '.') if os.path.exists('fabric_library.db') else None,
],
hiddenimports=[
'PyQt5.QtCore',
'PyQt5.QtGui',
'PyQt5.QtWidgets',
'sqlite3',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
# 过滤掉None值
a.datas = [d for d in a.datas if d is not None]
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='服装布料计算管理器',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # 不显示控制台窗口
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=None, # 可以指定图标文件路径,例如: 'icon.ico'
)

View File

@@ -32,6 +32,7 @@ class DatabaseManager:
CREATE TABLE IF NOT EXISTS fabrics (
model TEXT PRIMARY KEY,
category TEXT DEFAULT '未分类',
fabric_type TEXT,
supplier TEXT,
color TEXT,
width REAL,
@@ -41,12 +42,14 @@ class DatabaseManager:
unit TEXT DEFAULT '',
timestamp TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted INTEGER DEFAULT 0
)
''')
# 为fabrics表添加索引
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabrics_category ON fabrics(category)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabrics_fabric_type ON fabrics(fabric_type)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabrics_supplier ON fabrics(supplier)')
# 衣服款号表
@@ -66,6 +69,7 @@ class DatabaseManager:
style_number TEXT,
category TEXT,
fabric_type TEXT,
model TEXT,
usage_per_piece REAL,
unit TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -77,12 +81,21 @@ class DatabaseManager:
# 为garment_materials表添加索引
conn.execute('CREATE INDEX IF NOT EXISTS idx_garment_materials_style ON garment_materials(style_number)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_garment_materials_category ON garment_materials(category)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_garment_materials_model ON garment_materials(model)')
# 添加新字段(如果不存在)
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN fabric_type TEXT")
except:
pass
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN model TEXT")
except:
pass
try:
conn.execute("ALTER TABLE fabrics ADD COLUMN fabric_type TEXT")
except:
pass
try:
conn.execute("ALTER TABLE fabrics ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
@@ -132,6 +145,35 @@ class DatabaseManager:
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 = '')")
rows = cursor.fetchall()
for model, category in rows:
if '-' in category:
parts = category.split('-', 1)
new_category = parts[0]
new_fabric_type = parts[1]
conn.execute("UPDATE fabrics SET category = ?, fabric_type = ? WHERE model = ?",
(new_category, new_fabric_type, model))
conn.commit()
except:
pass
# 管理员设置表
conn.execute('''
CREATE TABLE IF NOT EXISTS admin_settings (
@@ -153,6 +195,7 @@ class DatabaseManager:
note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted INTEGER DEFAULT 0,
FOREIGN KEY (model) REFERENCES fabrics(model)
)
''')
@@ -175,6 +218,7 @@ class DatabaseManager:
unit TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted INTEGER DEFAULT 0,
FOREIGN KEY (style_number) REFERENCES garments(style_number),
FOREIGN KEY (model) REFERENCES fabrics(model)
)
@@ -186,9 +230,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,
@@ -201,13 +246,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
''')
# 初始化默认密码
@@ -246,14 +294,10 @@ def get_fabric_categories(db_path):
try:
with get_db_connection(db_path) as conn:
cursor = conn.execute("""
SELECT DISTINCT
CASE
WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1)
ELSE category
END as major_category
FROM fabrics
WHERE category IS NOT NULL AND category != ''
ORDER BY major_category
SELECT DISTINCT category
FROM fabrics
WHERE category IS NOT NULL AND category != '' AND (is_deleted IS NULL OR is_deleted = 0)
ORDER BY category
""")
categories = set()
for row in cursor.fetchall():
@@ -272,19 +316,15 @@ def get_fabric_types_by_category(db_path, category):
try:
with get_db_connection(db_path) as conn:
cursor = conn.execute("""
SELECT DISTINCT
CASE
WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1)
ELSE '默认类型'
END as fabric_type
FROM fabrics
WHERE category LIKE ? OR category = ?
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)
ORDER BY fabric_type
""", (f"{category}-%", category))
""", (category,))
types = []
for row in cursor.fetchall():
if row[0] and row[0] != '默认类型':
if row[0] and row[0].strip():
types.append(row[0])
return types
except:
@@ -296,9 +336,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.

View File

@@ -86,45 +86,55 @@ class FabricManager(QMainWindow):
try:
with self.get_conn() as conn:
cursor = conn.execute('''
SELECT category, fabric_type, usage_per_piece, unit
SELECT model, category, fabric_type, usage_per_piece, unit
FROM garment_materials
WHERE style_number = ?
''', (style_number,))
rows = cursor.fetchall()
inserted = 0
for category, fabric_type, usage_per_piece, unit in rows:
for model, category, fabric_type, usage_per_piece, unit in rows:
if usage_per_piece == 0:
continue
# 获取该原料在入库时使用的单位
# 优先使用精确原料型号model如无型号则退回到分类名称兼容旧数据
raw_identifier = model if model else category
# 获取该原料在入库时使用的单位(最近一次入库)
stock_cursor = conn.execute('''
SELECT unit FROM fabric_stock_in
WHERE model = ?
ORDER BY purchase_date DESC
LIMIT 1
''', (category,))
''', (raw_identifier,))
stock_unit_row = stock_cursor.fetchone()
# 如果有入库记录,使用入库单位;否则使用原来的单位
total_usage = usage_per_piece * quantity * (1 + loss_rate)
# 如果有入库记录,则按入库单位扣减库存;必要时进行单位换算
if stock_unit_row:
stock_unit = stock_unit_row[0]
# 如果单位不同,需要转换
if unit != stock_unit:
consume_qty = self.convert_unit_value(usage_per_piece * quantity * (1 + loss_rate), unit, stock_unit, category)
# 单位不同,尝试进行单位转换(米/码/公斤互转依赖面料幅宽和克重)
consume_qty = self.convert_unit_value(
total_usage,
unit,
stock_unit,
fabric_model=model if model else None
)
final_unit = stock_unit
else:
consume_qty = usage_per_piece * quantity * (1 + loss_rate)
consume_qty = total_usage
final_unit = unit
else:
# 没有入库记录,使用原单位
consume_qty = usage_per_piece * quantity * (1 + loss_rate)
# 没有入库记录,只能按原单位记录消耗
consume_qty = total_usage
final_unit = unit
conn.execute('''
INSERT INTO fabric_consumption
(style_number, model, single_usage, quantity_made, loss_rate, consume_quantity, consume_date, unit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (style_number, category, usage_per_piece, quantity, loss_rate, consume_qty, datetime.now().strftime('%Y-%m-%d'), final_unit))
''', (style_number, raw_identifier, usage_per_piece, quantity, loss_rate, consume_qty, datetime.now().strftime('%Y-%m-%d'), final_unit))
inserted += 1
conn.commit()
if inserted > 0:
@@ -177,12 +187,17 @@ class FabricManager(QMainWindow):
text = f"款号: {style_number}\n生产件数: {qty}\n损耗率: {self.loss_input.value()}%\n\n"
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT category, fabric_type, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (style_number,))
for category, fabric_type, usage, unit in cursor.fetchall():
cursor = conn.execute("SELECT category, fabric_type, model, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (style_number,))
for category, fabric_type, model, usage, unit in cursor.fetchall():
if usage:
total = usage * qty * (1 + loss)
# 显示材料名称(如果有类型则显示类目-类型,否则只显示类目
material_name = f"{category}-{fabric_type}" if fabric_type else category
# 显示材料名称:优先显示型号,否则显示类目-类型,最后只显示类目
if model:
material_name = model
elif fabric_type:
material_name = f"{category}-{fabric_type}" if category else fabric_type
else:
material_name = category or "未命名材料"
text += f"{material_name}\n单件: {usage:.3f} {unit}\n总用量: {total:.3f} {unit}\n\n"
except Exception as e:
text += "计算失败: " + str(e)
@@ -248,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
@@ -332,6 +347,7 @@ class FabricManager(QMainWindow):
CREATE TABLE IF NOT EXISTS fabrics (
model TEXT PRIMARY KEY,
category TEXT DEFAULT '未分类',
fabric_type TEXT,
supplier TEXT,
color TEXT,
width REAL,
@@ -356,6 +372,7 @@ class FabricManager(QMainWindow):
style_number TEXT,
category TEXT,
fabric_type TEXT,
model TEXT,
usage_per_piece REAL,
unit TEXT DEFAULT ''
)
@@ -364,6 +381,30 @@ class FabricManager(QMainWindow):
# 添加fabric_type列如果不存在
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN fabric_type TEXT")
except:
pass
# 添加model列如果不存在
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN model TEXT")
except:
pass
# 添加fabrics表的fabric_type列如果不存在
try:
conn.execute("ALTER TABLE fabrics ADD COLUMN fabric_type TEXT")
except:
pass
# 数据迁移将fabrics表中category字段的"类目-类型"格式拆分成两个字段
try:
cursor = conn.execute("SELECT model, category FROM fabrics WHERE category LIKE '%-%' AND (fabric_type IS NULL OR fabric_type = '')")
rows = cursor.fetchall()
for model, category in rows:
if '-' in category:
parts = category.split('-', 1)
new_category = parts[0]
new_fabric_type = parts[1]
conn.execute("UPDATE fabrics SET category = ?, fabric_type = ? WHERE model = ?",
(new_category, new_fabric_type, model))
conn.commit()
except:
pass # 列已存在

View File

@@ -10,107 +10,68 @@ from PyQt5.QtWidgets import (
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox, QFileDialog, QDoubleSpinBox, QWidget, QCompleter
)
from PyQt5.QtCore import Qt, QStringListModel, QTimer
from PyQt5.QtCore import Qt, QTimer, QSortFilterProxyModel
from PyQt5.QtGui import QPixmap
from database import get_db_connection
class SearchableComboBox(QComboBox):
"""支持模糊搜索的下拉框"""
"""支持模糊搜索的下拉框 - 基于 QSortFilterProxyModel 和 QCompleter"""
def __init__(self, parent=None):
super().__init__(parent)
super(SearchableComboBox, self).__init__(parent)
self.setFocusPolicy(Qt.StrongFocus)
self.setEditable(True)
self.setInsertPolicy(QComboBox.NoInsert)
# 存储所有选项
self.all_items = []
self.all_data = []
self.is_filtering = False
# 设置自动完成
self.completer = QCompleter(self)
self.completer.setCompletionMode(QCompleter.PopupCompletion)
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.completer.setFilterMode(Qt.MatchContains)
self.setCompleter(self.completer)
# 连接信号
self.lineEdit().textChanged.connect(self.on_text_changed)
def addItem(self, text, userData=None):
"""添加选项"""
# 临时断开信号连接防止textChanged触发on_text_changed
self.lineEdit().textChanged.disconnect()
super().addItem(text, userData)
if text not in self.all_items:
self.all_items.append(text)
self.all_data.append(userData)
self.update_completer()
# 重新连接信号
self.lineEdit().textChanged.connect(self.on_text_changed)
def addItems(self, texts):
"""批量添加选项"""
for text in texts:
self.addItem(text)
def clear(self):
"""清空所有选项"""
if not self.is_filtering:
super().clear()
self.all_items.clear()
self.all_data.clear()
self.update_completer()
def reset_items(self):
"""重置所有选项"""
# 临时断开信号连接防止textChanged触发on_text_changed
self.lineEdit().textChanged.disconnect()
self.is_filtering = True
super().clear()
for i, item in enumerate(self.all_items):
super().addItem(item, self.all_data[i] if i < len(self.all_data) else None)
self.is_filtering = False
# 重新连接信号
self.lineEdit().textChanged.connect(self.on_text_changed)
def update_completer(self):
"""更新自动完成列表"""
model = QStringListModel(self.all_items)
self.completer.setModel(model)
def on_text_changed(self, text):
"""文本改变时的处理"""
if self.is_filtering:
return
if not text or text in ["—— 选择型号 ——"]:
self.reset_items()
# 如果获得焦点且有选项,显示下拉列表
if self.hasFocus() and self.count() > 0:
self.showPopup()
return
# 模糊搜索匹配
filtered_items = []
filtered_data = []
for i, item in enumerate(self.all_items):
if text.lower() in item.lower():
filtered_items.append(item)
filtered_data.append(self.all_data[i] if i < len(self.all_data) else None)
# 更新下拉列表
self.is_filtering = True
super().clear()
for i, item in enumerate(filtered_items):
super().addItem(item, filtered_data[i])
self.is_filtering = False
# 如果有匹配项且获得焦点,显示下拉列表
if filtered_items and self.hasFocus():
self.showPopup()
# 添加过滤模型来过滤匹配项
self.pFilterModel = QSortFilterProxyModel(self)
self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.pFilterModel.setSourceModel(self.model())
# 添加自动完成器,使用过滤模型
self.completer = QCompleter(self.pFilterModel, self)
# 始终显示所有(过滤后的)完成项
self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
self.setCompleter(self.completer)
# 连接信号
self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
self.completer.activated.connect(self.on_completer_activated)
# 当从自动完成器中选择项时,从组合框中选择对应的项
def on_completer_activated(self, text):
if text:
index = self.findText(text)
self.setCurrentIndex(index)
self.activated[str].emit(self.itemText(index))
# 当模型改变时,同时更新过滤器和自动完成器的模型
def setModel(self, model):
super(SearchableComboBox, self).setModel(model)
self.pFilterModel.setSourceModel(model)
self.completer.setModel(self.pFilterModel)
# 当模型列改变时,同时更新过滤器和自动完成器的模型列
def setModelColumn(self, column):
self.completer.setCompletionColumn(column)
self.pFilterModel.setFilterKeyColumn(column)
super(SearchableComboBox, self).setModelColumn(column)
def setDefaultText(self, text):
"""设置默认文本 - 兼容方法"""
line_edit = self.lineEdit()
if line_edit:
line_edit.blockSignals(True)
line_edit.setText(text)
line_edit.blockSignals(False)
# 尝试找到匹配的项
index = self.findText(text)
if index >= 0:
self.setCurrentIndex(index)
else:
self.setCurrentIndex(-1)
class GarmentLibraryDialog(QDialog):
"""服装库管理对话框"""
@@ -135,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)
@@ -181,17 +142,24 @@ class GarmentLibraryDialog(QDialog):
self.garment_table.setRowHeight(i, 140)
for row_idx, (style_number, image_path) in enumerate(rows):
self.garment_table.setItem(row_idx, 0, QTableWidgetItem(style_number))
# 款号列 - 设置为只读
style_item = QTableWidgetItem(style_number)
style_item.setFlags(style_item.flags() & ~Qt.ItemIsEditable)
self.garment_table.setItem(row_idx, 0, style_item)
# 查询材料数量
with self.get_conn() as conn:
cursor2 = conn.execute("SELECT COUNT(*) FROM garment_materials WHERE style_number = ?", (style_number,))
count = cursor2.fetchone()[0]
self.garment_table.setItem(row_idx, 1, QTableWidgetItem(str(count)))
# 类目数量列 - 设置为只读
count_item = QTableWidgetItem(str(count))
count_item.setFlags(count_item.flags() & ~Qt.ItemIsEditable)
self.garment_table.setItem(row_idx, 1, count_item)
# 显示图片预览
image_item = QTableWidgetItem()
image_item.setTextAlignment(Qt.AlignCenter)
image_item.setFlags(image_item.flags() & ~Qt.ItemIsEditable)
if image_path and os.path.exists(image_path):
try:
@@ -293,11 +261,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)
@@ -355,85 +319,43 @@ class GarmentEditDialog(QDialog):
"""加载材料列表"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT category, fabric_type, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (self.style_number,))
for category, fabric_type, usage, unit in cursor.fetchall():
display_category = ""
display_type = ""
display_model = ""
# category字段可能存储型号或类目-类型组合
if category:
# 首先检查是否是型号在fabrics表中查找
fabric_cursor = conn.execute("SELECT category, model FROM fabrics WHERE model = ?", (category,))
fabric_row = fabric_cursor.fetchone()
if fabric_row:
# 是型号从fabrics表获取类目信息
fabric_category, model = fabric_row
display_model = model
if fabric_category and "-" in fabric_category:
parts = fabric_category.split("-", 1)
display_category = parts[0]
display_type = parts[1]
else:
display_category = fabric_category or ""
else:
# 不是型号,按类目-类型格式解析
if "-" in category:
parts = category.split("-", 1)
display_category = parts[0]
display_type = parts[1]
else:
display_category = category
# 如果有单独的fabric_type字段优先使用
if fabric_type:
display_type = fabric_type
cursor = conn.execute("SELECT category, fabric_type, model, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (self.style_number,))
for category, fabric_type, model, usage, unit in cursor.fetchall():
# 直接使用数据库中的三个字段
display_category = category or ""
display_type = fabric_type or ""
display_model = model or ""
self.add_material_row(display_category, display_type, usage or 0, unit or "", display_model)
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)
row = self.material_table.rowCount()
self.material_table.insertRow(row)
# 列0: 类目下拉框
cat_combo = QComboBox()
cat_combo.setEditable(True)
cat_combo.setEditable(False)
# 最后添加自定义选项
cat_combo.addItem("—— 自定义类目 ——")
cat_combo.addItem("—— 选择类目 ——")
# 先添加所有类目选项
try:
with self.get_conn() as conn:
# 只获取纯类目(提取"-"前面的部分)
cursor = conn.execute("""
SELECT DISTINCT
CASE
WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1)
ELSE category
END as major_category
SELECT DISTINCT category
FROM fabrics
WHERE category IS NOT NULL AND category != ''
ORDER BY major_category
ORDER BY category
""")
categories = set()
for cat_row in cursor.fetchall():
if cat_row[0] and cat_row[0].strip():
categories.add(cat_row[0])
# 添加默认类目
categories.update(["布料", "辅料", "其他"])
for cat in sorted(categories):
cat_combo.addItem(cat)
except:
@@ -445,79 +367,62 @@ class GarmentEditDialog(QDialog):
if category:
cat_combo.setCurrentText(category)
else:
# 如果没有指定类目,默认选择第一个实际类目而不是"自定义类目"
if cat_combo.count() > 1:
cat_combo.setCurrentIndex(0)
# 如果没有指定类目,默认选择"—— 选择类目 ——"索引0
print("没有指定类目,默认选择'—— 选择类目 ——'索引0")
print(cat_combo.count())
print(cat_combo.currentIndex())
# 打印所有选项
for i in range(cat_combo.count()):
print(cat_combo.itemText(i))
cat_combo.setCurrentIndex(0)
cat_combo.currentTextChanged.connect(lambda text, r=row: self.on_category_changed(text, r))
self.material_table.setCellWidget(row, 0, cat_combo)
# 列1: 类型下拉框
type_combo = QComboBox()
type_combo.setEditable(True)
type_combo.setEditable(False)
# 最后添加选择提示
type_combo.addItem("—— 选择类型 ——")
# 先添加所有类型选项
try:
with self.get_conn() as conn:
cursor = conn.execute("""
SELECT DISTINCT
CASE
WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1)
ELSE '默认类型'
END as fabric_type
SELECT DISTINCT fabric_type
FROM fabrics
WHERE category IS NOT NULL AND category != ''
WHERE fabric_type IS NOT NULL AND fabric_type != ''
ORDER BY fabric_type
""")
types = cursor.fetchall()
for type_row in types:
if type_row[0] and type_row[0] != '默认类型':
if type_row[0] and type_row[0].strip():
type_combo.addItem(type_row[0])
except:
pass
# 最后添加选择提示
type_combo.addItem("—— 选择类型 ——")
if fabric_type:
type_combo.setCurrentText(fabric_type)
else:
# 如果没有指定类型,默认选择第一个实际类型而不是"选择类型"
if type_combo.count() > 1:
type_combo.setCurrentIndex(0)
# 如果没有指定类型,默认选择"—— 选择类型 ——"索引0
print("没有指定类型,默认选择'—— 选择类型 ——'索引0")
print(type_combo.count())
print(type_combo.currentIndex())
# 打印所有选项
for i in range(type_combo.count()):
print(type_combo.itemText(i))
type_combo.setCurrentIndex(0)
type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r))
self.material_table.setCellWidget(row, 1, type_combo)
# 列2: 型号下拉框(支持模糊搜索)
model_combo = SearchableComboBox()
model_combo.addItem("—— 选择型号 ——")
# 初始化时加载所有型号
try:
with self.get_conn() as conn:
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
ORDER BY model
""")
models = cursor.fetchall()
for model_row in models:
model, color, unit = model_row
# 显示格式:型号-颜色(如果有颜色的话)
display_text = model
if color and color.strip():
display_text = f"{model}-{color}"
model_combo.addItem(display_text, model)
except Exception as e:
pass
# 确保默认选中第一项("—— 选择型号 ——"
model_combo.setCurrentIndex(0)
model_combo.currentTextChanged.connect(lambda text, r=row: self.on_model_selected(text, r))
# 先将下拉框添加到表格中这样update_model_combo_range才能获取到类目和类型
self.material_table.setCellWidget(row, 2, model_combo)
# 初始化时根据类目和类型加载对应的型号
self.update_model_combo_range(model_combo, row)
# 列3: 单件用量
usage_spin = QDoubleSpinBox()
@@ -535,42 +440,133 @@ 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)
# self.on_category_changed(cat_combo.currentText(), row)
# 如果没有选择具体类目,初始化时显示全部型号
if cat_combo.currentText() == "—— 自定义类目 ——":
self.on_type_changed("—— 选择类型 ——", row)
# if cat_combo.currentText() == "—— 选择类目 ——":
# self.on_type_changed("—— 选择类型 ——", row)
# 如果有指定的型号,需要在初始化完成后设置
if model:
# 先设置类型(如果有的话)
if fabric_type:
type_combo.setCurrentText(fabric_type)
self.on_type_changed(fabric_type, row)
# if model:
# # 先设置类型(如果有的话)
# if fabric_type:
# type_combo.setCurrentText(fabric_type)
# self.on_type_changed(fabric_type, row)
# 然后设置型号 - 使用SearchableComboBox的setCurrentText方法
model_combo = self.material_table.cellWidget(row, 2)
if isinstance(model_combo, SearchableComboBox):
# 确保型号在选项列表中
found = False
for i in range(model_combo.count()):
item_data = model_combo.itemData(i)
item_text = model_combo.itemText(i)
if item_data == model or item_text == model:
model_combo.setCurrentIndex(i)
found = True
break
# # 然后设置型号
# model_combo = self.material_table.cellWidget(row, 2)
# if model_combo:
# # 确保型号在选项列表中
# found = False
# for i in range(model_combo.count()):
# item_data = model_combo.itemData(i)
# item_text = model_combo.itemText(i)
# if item_data == model or item_text == model:
# model_combo.setCurrentIndex(i)
# found = True
# break
cat_combo.currentTextChanged.connect(lambda text, r=row: self.on_category_changed(text, r))
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)
"""更新型号下拉框的搜索范围"""
cat_combo = self.material_table.cellWidget(row, 0)
type_combo = self.material_table.cellWidget(row, 1)
category_text = cat_combo.currentText() if cat_combo else ""
type_text = type_combo.currentText() if type_combo else ""
# 阻止信号防止触发on_model_selected
model_combo.blockSignals(True)
line_edit = model_combo.lineEdit()
if line_edit:
line_edit.blockSignals(True)
try:
with self.get_conn() as conn:
# 根据类目和类型构建查询条件
if category_text and category_text != "—— 选择类目 ——" and type_text and type_text != "—— 选择类型 ——":
# 同时根据类目和类型过滤
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
WHERE category = ? AND fabric_type = ?
ORDER BY model
""", (category_text, type_text))
elif category_text and category_text != "—— 选择类目 ——":
# 只根据类目过滤
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
WHERE category = ?
ORDER BY model
""", (category_text,))
elif type_text and type_text != "—— 选择类型 ——":
# 只根据类型过滤
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
WHERE fabric_type = ?
ORDER BY model
""", (type_text,))
else:
# 显示所有型号
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
ORDER BY model
""")
# 如果没找到直接设置文本SearchableComboBox支持
if not found:
model_combo.setCurrentText(model)
# 保存当前用户输入的文本(如果有
current_text = line_edit.text() if line_edit else ""
cursor_position = line_edit.cursorPosition() if line_edit else 0
print("current_text", current_text)
print("cursor_position", cursor_position)
# 清空并重新填充型号下拉框
model_combo.clear()
models = cursor.fetchall()
for model_row in models:
model, color, unit = model_row
# 显示格式:型号-颜色(如果有颜色的话)
display_text = model
if color and color.strip():
display_text = f"{model}-{color}"
model_combo.addItem(display_text, model)
# 恢复用户输入的文本,如果没有输入则设置默认文本
if line_edit:
if current_text and current_text != "—— 选择型号 ——":
line_edit.setText(current_text)
line_edit.setCursorPosition(cursor_position)
else:
if isinstance(model_combo, SearchableComboBox):
model_combo.setDefaultText("—— 选择型号 ——")
else:
model_combo.setCurrentIndex(0)
except Exception as e:
pass
finally:
if line_edit:
line_edit.blockSignals(False)
model_combo.blockSignals(False)
def on_category_changed(self, category_text, row):
"""当类目改变时,更新类型下拉框"""
print("on_category_changed", category_text, row)
type_combo = self.material_table.cellWidget(row, 1)
model_combo = self.material_table.cellWidget(row, 2)
@@ -578,146 +574,132 @@ class GarmentEditDialog(QDialog):
type_combo.clear()
type_combo.addItem("—— 选择类型 ——")
# 重新初始化型号下拉框,显示所有型号
model_combo.clear()
model_combo.addItem("—— 选择型号 ——")
try:
with self.get_conn() as conn:
# 加载所有类型
cursor = conn.execute("""
SELECT DISTINCT
CASE
WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1)
ELSE '默认类型'
END as fabric_type
FROM fabrics
WHERE category IS NOT NULL AND category != ''
ORDER BY fabric_type
""")
# 如果选择了具体类目,则过滤
if category_text and category_text != "—— 自定义类目 ——":
if category_text and category_text != "—— 选择类目 ——":
# 如果选择了具体类目,则过滤
cursor = conn.execute("""
SELECT DISTINCT
CASE
WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1)
ELSE '默认类型'
END as fabric_type
SELECT DISTINCT fabric_type
FROM fabrics
WHERE category LIKE ? OR category = ?
WHERE category = ? AND fabric_type IS NOT NULL AND fabric_type != ''
ORDER BY fabric_type
""", (f"{category_text}-%", category_text))
""", (category_text,))
else:
# 加载所有类型
cursor = conn.execute("""
SELECT DISTINCT fabric_type
FROM fabrics
WHERE fabric_type IS NOT NULL AND fabric_type != ''
ORDER BY fabric_type
""")
types = cursor.fetchall()
for type_row in types:
if type_row[0] and type_row[0] != '默认类型':
if type_row[0] and type_row[0].strip():
type_combo.addItem(type_row[0])
# 连接类型改变事件
type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r))
# 加载所有型号到型号下拉框
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
ORDER BY model
""")
models = cursor.fetchall()
for model_row in models:
model, color, unit = model_row
# 显示格式:型号-颜色(如果有颜色的话)
display_text = model
if color and color.strip():
display_text = f"{model}-{color}"
model_combo.addItem(display_text, model)
# 确保默认选中第一项("—— 选择型号 ——"
model_combo.setCurrentIndex(0)
# 更新型号下拉框的搜索范围
self.update_model_combo_range(model_combo, row)
except Exception as e:
pass
def on_type_changed(self, type_text, row):
"""当类型改变时,更新型号下拉框"""
"""当类型改变时,更新类目和型号下拉框"""
print("on_type_changed", type_text, row)
cat_combo = self.material_table.cellWidget(row, 0)
model_combo = self.material_table.cellWidget(row, 2)
# 重新初始化型号下拉框,显示所有型号
if hasattr(model_combo, 'clear'):
model_combo.clear()
model_combo.addItem("—— 选择型号 ——")
if not model_combo:
return
# 始终显示所有型号,不进行过滤
try:
with self.get_conn() as conn:
cursor = conn.execute("""
SELECT DISTINCT model, color, unit
FROM fabrics
ORDER BY model
""")
models = cursor.fetchall()
for model_row in models:
model, color, unit = model_row
# 显示格式:型号-颜色(如果有颜色的话)
display_text = model
if color and color.strip():
display_text = f"{model}-{color}"
model_combo.addItem(display_text, model)
# 确保默认选中第一项("—— 选择型号 ——"
model_combo.setCurrentIndex(0)
except Exception as e:
pass
# 如果选择了具体类型,自动选中该类型对应的类目
if cat_combo and type_text and type_text != "—— 选择类型 ——":
try:
with self.get_conn() as conn:
# 查询该类型对应的类目
cursor = conn.execute("""
SELECT DISTINCT category
FROM fabrics
WHERE fabric_type = ? AND category IS NOT NULL AND category != ''
ORDER BY category
LIMIT 1
""", (type_text,))
row_db = cursor.fetchone()
if row_db and row_db[0]:
category = row_db[0].strip()
# 在类目下拉框中查找并选中该类目
index = cat_combo.findText(category)
if index >= 0:
cat_combo.blockSignals(True)
cat_combo.setCurrentIndex(index)
cat_combo.blockSignals(False)
except Exception as e:
pass
# 更新型号下拉框的搜索范围
self.update_model_combo_range(model_combo, row)
def on_model_selected(self, model_text, row):
"""当型号选择时,自动设置单位并填充类目和类型"""
if not model_text or model_text == "—— 选择型号 ——":
return
print("on_model_selected", model_text, row)
# 先获取所有需要的控件
cat_combo = self.material_table.cellWidget(row, 0)
type_combo = self.material_table.cellWidget(row, 1)
model_combo = self.material_table.cellWidget(row, 2)
unit_combo = self.material_table.cellWidget(row, 4)
# 获取选中项的数据
current_index = model_combo.currentIndex()
if current_index > 0:
model = model_combo.itemData(current_index)
if model:
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT category, unit FROM fabrics WHERE model = ?", (model,))
row_db = cursor.fetchone()
if row_db:
category, unit = row_db
# 自动填充单位
if unit:
unit_combo.setCurrentText(unit)
# 自动填充类目和类型
if category:
# 解析类目信息,可能是"类目-类型"格式或单独的类目
if '-' in category:
parts = category.split('-', 1)
cat_text = parts[0]
type_text = parts[1] if len(parts) > 1 else ""
# 设置类目
cat_combo.setCurrentText(cat_text)
# 更新类型下拉框选项
self.on_category_changed(cat_text, row)
# 设置类型
if type_text:
type_combo.setCurrentText(type_text)
else:
# 只有类目,没有类型
cat_combo.setCurrentText(category)
self.on_category_changed(category, row)
except:
pass
if not model_text or model_text == "—— 选择型号 ——":
# 空选择,不做联动
return
# 从显示文本中解析真实型号(例如 "M001-红" -> "M001"
display_text = model_text.strip()
base_model = display_text.split("-", 1)[0] if "-" in display_text else display_text
try:
with self.get_conn() as conn:
cursor = conn.execute(
"SELECT category, fabric_type, unit FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)",
(base_model,)
)
row_db = cursor.fetchone()
if row_db:
category, fabric_type, unit = row_db
# 自动填充单位
if unit:
unit_combo.setCurrentText(unit)
# 自动填充类目和类型,阻止信号以避免触发默认更新逻辑
if category:
# 阻止类目、类型和型号的信号,避免触发默认更新逻辑
cat_combo.blockSignals(True)
type_combo.blockSignals(True)
model_combo.blockSignals(True)
# 设置类目
cat_combo.setCurrentText(category)
# 更新类型下拉框选项(直接调用,不会触发信号因为已经阻止了)
self.on_category_changed(category, row)
# 重新阻止信号因为on_category_changed内部可能恢复了信号
model_combo.blockSignals(True)
# 设置类型
if fabric_type:
type_combo.setCurrentText(fabric_type)
# 恢复信号
cat_combo.blockSignals(False)
type_combo.blockSignals(False)
model_combo.blockSignals(False)
except:
pass
def save_garment(self):
"""保存服装"""
@@ -746,7 +728,7 @@ class GarmentEditDialog(QDialog):
model = model_widget.currentText().strip()
# 处理类目和类型
if category == "—— 自定义类目 ——":
if category == "—— 选择类目 ——":
category = ""
if fabric_type == "—— 选择类型 ——":
fabric_type = ""
@@ -764,11 +746,9 @@ class GarmentEditDialog(QDialog):
usage = usage_widget.value()
unit = unit_widget.currentText().strip() or ""
# 分别存储类目、类型和型号信息
material_identifier = final_model if final_model else (f"{category}-{fabric_type}" if fabric_type else category)
conn.execute("INSERT INTO garment_materials (style_number, category, fabric_type, usage_per_piece, unit) VALUES (?, ?, ?, ?, ?)",
(style_number, material_identifier, fabric_type, usage, unit))
# 分别存储类目、类型和型号到对应字段
conn.execute("INSERT INTO garment_materials (style_number, category, fabric_type, model, usage_per_piece, unit) VALUES (?, ?, ?, ?, ?, ?)",
(style_number, category, fabric_type, final_model, usage, unit))
conn.commit()
QMessageBox.information(self, "成功", "保存完成")

84
main.py
View File

@@ -234,7 +234,7 @@ class FabricManager(QMainWindow):
return
quantity = self.quantity_input.value()
loss_rate = self.loss_input.value() / 100
try:
with self.get_conn() as conn:
cursor = conn.execute('''
@@ -243,21 +243,24 @@ class FabricManager(QMainWindow):
WHERE style_number = ?
''', (style_number,))
rows = cursor.fetchall()
inserted = 0
# 第一步:检查所有原料库存是否充足
insufficient_materials = []
materials_to_record = []
for category, fabric_type, usage_per_piece, unit in rows:
if usage_per_piece == 0:
continue
# 获取该原料在入库时使用的单位
stock_cursor = conn.execute('''
SELECT unit FROM fabric_stock_in
WHERE model = ?
ORDER BY purchase_date DESC
SELECT unit FROM fabric_stock_in
WHERE model = ?
ORDER BY purchase_date DESC
LIMIT 1
''', (category,))
stock_unit_row = stock_cursor.fetchone()
# 如果有入库记录,使用入库单位;否则使用原来的单位
if stock_unit_row:
stock_unit = stock_unit_row[0]
@@ -272,17 +275,59 @@ class FabricManager(QMainWindow):
# 没有入库记录,使用原单位
consume_qty = usage_per_piece * quantity * (1 + loss_rate)
final_unit = unit
# 检查当前库存
stock_info = conn.execute('''
SELECT
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
WHERE f.model = ?
GROUP BY f.model
''', (category,)).fetchone()
if stock_info:
total_in, total_out = stock_info
current_stock = total_in - total_out
else:
current_stock = 0
# 如果库存不足,记录下来
if current_stock < consume_qty:
material_name = category or "未命名材料"
insufficient_materials.append(
f"{material_name}: 需要 {consume_qty:.3f} {final_unit},库存仅剩 {current_stock:.3f} {final_unit}"
)
else:
# 库存充足,加入待记录列表
materials_to_record.append((
style_number, category, usage_per_piece, quantity,
loss_rate, consume_qty, final_unit
))
# 如果有库存不足的材料,提示用户并中止操作
if insufficient_materials:
message = "以下原料库存不足,无法记录消耗:\n\n" + "\n".join(insufficient_materials)
QMessageBox.warning(self, "库存不足", message)
return
# 第二步:所有库存都充足,执行记录
inserted = 0
for material_data in materials_to_record:
style_num, model, single_usage, qty, loss, consume_qty, final_unit = material_data
conn.execute('''
INSERT INTO fabric_consumption
(style_number, model, single_usage, quantity_made, loss_rate, consume_quantity, consume_date, unit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (style_number, category, usage_per_piece, quantity, loss_rate, consume_qty, datetime.now().strftime('%Y-%m-%d'), final_unit))
''', (style_num, model, single_usage, qty, loss, consume_qty, datetime.now().strftime('%Y-%m-%d'), final_unit))
inserted += 1
conn.commit()
if inserted > 0:
QMessageBox.information(self, "成功", "本次生产消耗已记录到库存!")
QMessageBox.information(self, "成功", f"本次生产消耗已记录到库存!共记录 {inserted} 种原料。")
else:
QMessageBox.information(self, "提示", "本次没有可记录的原料消耗")
except Exception as e:
@@ -335,12 +380,17 @@ class FabricManager(QMainWindow):
text = f"款号: {style_number}\n生产件数: {qty}\n损耗率: {self.loss_input.value()}%\n\n"
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT category, fabric_type, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (style_number,))
for category, fabric_type, usage, unit in cursor.fetchall():
cursor = conn.execute("SELECT category, fabric_type, model, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (style_number,))
for category, fabric_type, model, usage, unit in cursor.fetchall():
if usage:
total = usage * qty * (1 + loss)
# 显示材料名称(如果有类型则显示类目-类型,否则只显示类目
material_name = f"{category}-{fabric_type}" if fabric_type else category
# 显示材料名称:优先显示型号,否则显示类目-类型,最后只显示类目
if model:
material_name = model
elif fabric_type:
material_name = f"{category}-{fabric_type}" if category else fabric_type
else:
material_name = category or "未命名材料"
text += f"{material_name}\n单件: {usage:.3f} {unit}\n总用量: {total:.3f} {unit}\n\n"
except Exception as e:
text += "计算失败: " + str(e)
@@ -407,7 +457,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

View File

@@ -76,17 +76,22 @@ class PurchaseOrderDialog(QDialog):
try:
with self.get_conn() as conn:
cursor = conn.execute('''
SELECT category, fabric_type, usage_per_piece, unit
SELECT category, fabric_type, model, usage_per_piece, unit
FROM garment_materials
WHERE style_number = ? AND usage_per_piece > 0
ORDER BY id
''', (self.style_number,))
rows = cursor.fetchall()
for category, fabric_type, usage_per_piece, unit in rows:
for category, fabric_type, model, usage_per_piece, unit in rows:
total_usage = usage_per_piece * self.quantity * (1 + self.loss_rate)
# 显示材料名称(如果有类型则显示类目-类型,否则只显示类目
material_name = f"{category}-{fabric_type}" if fabric_type else category
# 显示材料名称:优先显示型号,否则显示类目-类型,最后只显示类目
if model:
material_name = model
elif fabric_type:
material_name = f"{category}-{fabric_type}" if category else fabric_type
else:
material_name = category or "未命名材料"
text += f"材料:{material_name}\n"
text += f" 单件用量:{usage_per_piece:.3f} {unit}\n"
text += f" 总需采购:{total_usage:.3f} {unit}\n\n"

View File

@@ -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,265 @@ from database import get_db_connection
from stock_dialog import StockInDialog
class RawMaterialEditDialog(QDialog):
"""原料编辑对话框 - 独立窗口"""
def __init__(self, db_path, model, is_admin=False, parent=None):
super().__init__(parent)
self.db_path = db_path
self.model = model
self.is_admin = is_admin
self.setWindowTitle(f"编辑原料 - {model}")
self.resize(600, 500)
self.setup_ui()
self.load_material_data()
def setup_ui(self):
"""设置用户界面"""
layout = QGridLayout(self)
# 类目选择
layout.addWidget(QLabel("类目:"), 0, 0, Qt.AlignRight)
self.edit_major_category = QComboBox()
self.edit_major_category.setEditable(True)
layout.addWidget(self.edit_major_category, 0, 1)
layout.addWidget(QLabel("类型:"), 0, 2, Qt.AlignRight)
self.edit_sub_category = QLineEdit()
layout.addWidget(self.edit_sub_category, 0, 3)
# 基本信息
layout.addWidget(QLabel("型号:"), 1, 0, Qt.AlignRight)
self.edit_model = QLineEdit()
layout.addWidget(self.edit_model, 1, 1, 1, 3)
layout.addWidget(QLabel("供应商:"), 2, 0, Qt.AlignRight)
self.edit_supplier = QComboBox()
self.edit_supplier.setEditable(True)
layout.addWidget(self.edit_supplier, 2, 1, 1, 3)
layout.addWidget(QLabel("颜色:"), 3, 0, Qt.AlignRight)
self.edit_color = QLineEdit()
layout.addWidget(self.edit_color, 3, 1, 1, 3)
# 规格信息
layout.addWidget(QLabel("幅宽 (cm):"), 4, 0, Qt.AlignRight)
self.edit_width = QDoubleSpinBox()
self.edit_width.setRange(0, 300)
self.edit_width.setValue(0)
layout.addWidget(self.edit_width, 4, 1)
layout.addWidget(QLabel("克重 (g/m²):"), 5, 0, Qt.AlignRight)
self.edit_gsm = QDoubleSpinBox()
self.edit_gsm.setRange(0, 1000)
self.edit_gsm.setValue(0)
layout.addWidget(self.edit_gsm, 5, 1)
layout.addWidget(QLabel("单位:"), 6, 0, Qt.AlignRight)
self.edit_unit = QComboBox()
self.edit_unit.setEditable(True)
self.edit_unit.addItems(["", "", "公斤", "一对", "", ""])
layout.addWidget(self.edit_unit, 6, 1)
# 价格信息
layout.addWidget(QLabel("散剪价 (元/单位):"), 7, 0, Qt.AlignRight)
self.edit_retail = QDoubleSpinBox()
self.edit_retail.setRange(0, 10000)
self.edit_retail.setDecimals(2)
layout.addWidget(self.edit_retail, 7, 1)
layout.addWidget(QLabel("大货价 (元/单位):"), 8, 0, Qt.AlignRight)
self.edit_bulk = QDoubleSpinBox()
self.edit_bulk.setRange(0, 10000)
self.edit_bulk.setDecimals(2)
layout.addWidget(self.edit_bulk, 8, 1)
# 按钮
button_layout = QHBoxLayout()
save_btn = QPushButton("保存修改")
save_btn.clicked.connect(self.save_changes)
save_btn.setStyleSheet("background-color: #4CAF50; color: white; padding: 8px; font-weight: bold;")
button_layout.addWidget(save_btn)
cancel_btn = QPushButton("取消")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout, 9, 0, 1, 4)
# 加载类目和供应商列表
self.load_categories_and_suppliers()
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def load_categories_and_suppliers(self):
"""加载类目和供应商列表"""
try:
with self.get_conn() as conn:
# 加载类目 - 设置默认选项为:面料、辅料、其他
self.edit_major_category.addItems(["面料", "辅料", "其他"])
# 加载供应商
cursor = conn.execute("SELECT DISTINCT supplier FROM fabrics WHERE supplier IS NOT NULL AND supplier != '' AND (is_deleted IS NULL OR is_deleted = 0) ORDER BY supplier")
suppliers = [row[0] for row in cursor.fetchall()]
self.edit_supplier.addItems(suppliers)
except:
pass
def load_material_data(self):
"""加载原料数据"""
try:
with self.get_conn() as conn:
cursor = conn.execute(
"SELECT category, fabric_type, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)",
(self.model,)
)
row = cursor.fetchone()
if not row:
QMessageBox.warning(self, "提示", "原料不存在或已被删除")
self.reject()
return
category, fabric_type, supplier, color, width, gsm, unit, retail, bulk = row
# 填充表单
self.edit_model.setText(self.model)
self.edit_major_category.setCurrentText(category or "")
self.edit_sub_category.setText(fabric_type or "")
self.edit_supplier.setCurrentText(supplier or "")
self.edit_color.setText(color or "")
self.edit_width.setValue(width or 0)
self.edit_gsm.setValue(gsm or 0)
self.edit_unit.setCurrentText(unit or "")
self.edit_retail.setValue(retail or 0)
self.edit_bulk.setValue(bulk or 0)
# 特殊处理胸杯类型
if fabric_type and "胸杯" in fabric_type:
self.edit_unit.setEnabled(False)
except Exception as e:
QMessageBox.critical(self, "错误", f"加载原料信息失败: {str(e)}")
self.reject()
def save_changes(self):
"""保存修改"""
new_model = self.edit_model.text().strip()
# 检查型号是否为空
if not new_model:
QMessageBox.warning(self, "错误", "型号不能为空")
return
major = self.edit_major_category.currentText().strip()
sub = self.edit_sub_category.text().strip()
# 验证必填字段:类目
if not major:
QMessageBox.warning(self, "错误", "请选择类目")
return
# 验证必填字段:类型
if not sub:
QMessageBox.warning(self, "错误", "请输入类型")
return
# 验证必填字段:幅宽
if self.edit_width.value() <= 0:
QMessageBox.warning(self, "错误", "请输入幅宽")
return
# 验证必填字段:克重
if self.edit_gsm.value() <= 0:
QMessageBox.warning(self, "错误", "请输入克重")
return
# 特殊处理胸杯类型
if "胸杯" in sub:
major = "辅料"
category = major or ""
fabric_type = sub or ""
supplier = self.edit_supplier.currentText().strip()
color = self.edit_color.text().strip()
unit = self.edit_unit.currentText().strip() or ""
try:
with self.get_conn() as conn:
# 如果型号被修改了,检查新型号是否已存在
if new_model != self.model:
cursor = conn.execute("SELECT model FROM fabrics WHERE model = ?", (new_model,))
if cursor.fetchone():
QMessageBox.warning(self, "错误", f"型号 '{new_model}' 已存在,请使用其他型号")
return
# 型号被修改,需要更新所有相关表
# 更新原料表
conn.execute('''
UPDATE fabrics
SET model=?, category=?, fabric_type=?, supplier=?, color=?, width=?, gsm=?,
retail_price=?, bulk_price=?, unit=?, timestamp=?
WHERE model=?
''', (
new_model, category, fabric_type, supplier, color,
self.edit_width.value() or None,
self.edit_gsm.value() or None,
self.edit_retail.value() or None,
self.edit_bulk.value() or None,
unit,
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
self.model
))
# 更新入库记录表
conn.execute("UPDATE fabric_stock_in SET model=? WHERE model=?", (new_model, self.model))
# 更新消耗记录表
conn.execute("UPDATE fabric_consumption SET model=? WHERE model=?", (new_model, self.model))
# 更新衣服材料用量表
conn.execute("UPDATE garment_materials SET model=? WHERE model=?", (new_model, self.model))
else:
# 型号未修改,只更新其他字段
conn.execute('''
UPDATE fabrics
SET category=?, fabric_type=?, supplier=?, color=?, width=?, gsm=?,
retail_price=?, bulk_price=?, unit=?, timestamp=?
WHERE model=?
''', (
category, fabric_type, supplier, color,
self.edit_width.value() or None,
self.edit_gsm.value() or None,
self.edit_retail.value() or None,
self.edit_bulk.value() or None,
unit,
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
self.model
))
conn.commit()
if new_model != self.model:
QMessageBox.information(self, "成功", f"已将 '{self.model}' 更新为 '{new_model}'")
else:
QMessageBox.information(self, "成功", f"已更新 '{self.model}'")
self.accept()
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
class RawMaterialLibraryDialog(QDialog):
def __init__(self, db_path, is_admin=False):
super().__init__()
self.db_path = db_path
self.is_admin = is_admin
self.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 +301,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 +333,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 +350,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 +370,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 +442,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 +463,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()
@@ -243,17 +470,17 @@ class RawMaterialLibraryDialog(QDialog):
self.load_table()
def load_major_categories(self):
"""加载主类目"""
"""加载主类目 - 只显示数据库中实际存在的类目"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1) ELSE category END FROM fabrics")
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("全部类目")
self.major_combo.addItems(sorted(majors))
if majors:
self.major_combo.addItems(sorted(majors))
self.major_combo.blockSignals(False)
except:
pass
@@ -261,26 +488,12 @@ class RawMaterialLibraryDialog(QDialog):
def load_add_major_categories(self):
"""加载添加界面的主类目"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1) ELSE category END FROM fabrics")
majors = set(row[0] for row in cursor.fetchall() if row[0])
majors.update({"布料", "", "其他"})
self.add_major_category.blockSignals(True)
current_text = self.add_major_category.currentText()
self.add_major_category.clear()
self.add_major_category.addItems(sorted(majors))
if current_text in majors:
self.add_major_category.setCurrentText(current_text)
else:
self.add_major_category.setCurrentText("布料")
self.add_major_category.blockSignals(False)
except:
self.add_major_category.clear()
self.add_major_category.addItems(["布料", "辅料", "其他"])
self.add_major_category.setCurrentText("布料")
# 设置默认类目选项为:面料、辅料、其他
self.add_major_category.blockSignals(True)
self.add_major_category.clear()
self.add_major_category.addItems(["面料", "辅料", "其他"])
self.add_major_category.setCurrentText("")
self.add_major_category.blockSignals(False)
def load_sub_categories(self):
"""加载子类型"""
@@ -288,28 +501,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 category FROM fabrics WHERE category LIKE '%-%'")
subs = set()
for row in cursor.fetchall():
cat = row[0]
if '-' in cat:
subs.add(cat.split('-', 1)[1])
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 category FROM fabrics WHERE category LIKE ? OR category = ?", (major + "-%", major))
subs = set()
for row in cursor.fetchall():
cat = row[0]
if '-' in cat:
subs.add(cat.split('-', 1)[1])
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()
@@ -317,18 +522,21 @@ 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)
self.supplier_combo.clear()
self.supplier_combo.addItem("全部供应商")
self.supplier_combo.addItems(suppliers)
self.supplier_combo.blockSignals(False)
# 新增原料界面的供应商下拉框,添加空选项作为默认值
self.add_supplier.blockSignals(True)
self.add_supplier.clear()
self.add_supplier.addItem("") # 添加空选项
self.add_supplier.addItems(suppliers)
self.add_supplier.setCurrentIndex(0) # 默认选中空选项
self.add_supplier.blockSignals(False)
except:
pass
@@ -337,21 +545,28 @@ class RawMaterialLibraryDialog(QDialog):
"""加载原料表格数据"""
try:
with self.get_conn() as conn:
query = "SELECT category, model, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics"
query = "SELECT category, fabric_type, model, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics"
params = []
conditions = []
# 过滤已删除的原料
conditions.append("(is_deleted IS NULL OR is_deleted = 0)")
# 类目过滤
major = self.major_combo.currentText()
sub = self.sub_combo.currentText()
if major != "全部类目" and major:
if sub != "全部类型" and sub:
conditions.append("category = ?")
params.append(major + "-" + sub)
else:
conditions.append("(category LIKE ? OR category = ?)")
params.append(major + "-%")
conditions.append("category = ? AND fabric_type = ?")
params.append(major)
params.append(sub)
else:
conditions.append("category = ?")
params.append(major)
elif sub != "全部类型" and sub:
# 只选择了类型,没有选择类目
conditions.append("fabric_type = ?")
params.append(sub)
# 供应商过滤
supplier = self.supplier_combo.currentText()
@@ -377,9 +592,9 @@ class RawMaterialLibraryDialog(QDialog):
self.table.setRowCount(len(rows))
self.table.clearContents()
for row_idx, (category, model, supplier, color, width, gsm, unit, retail, bulk) in enumerate(rows):
major = category.split('-', 1)[0] if '-' in category else category
sub = category.split('-', 1)[1] if '-' in category else ""
for row_idx, (category, fabric_type, model, supplier, color, width, gsm, unit, retail, bulk) in enumerate(rows):
major = category or ""
sub = fabric_type or ""
self.table.setItem(row_idx, 0, QTableWidgetItem(major))
self.table.setItem(row_idx, 1, QTableWidgetItem(sub))
@@ -430,51 +645,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, supplier, color, width, gsm, unit, retail_price, bulk_price FROM fabrics WHERE model = ?", (model,))
row = cursor.fetchone()
if not row:
QMessageBox.warning(self, "提示", "原料不存在或已被删除")
return
category, supplier, color, width, gsm, unit, retail, bulk = row
major = category.split('-', 1)[0] if '-' in category else category
sub = category.split('-', 1)[1] if '-' in category else ""
self.add_major_category.setCurrentText(major)
self.add_sub_category.setText(sub)
self.update_full_category()
self.add_supplier.setCurrentText(supplier or "")
self.add_color.setText(color or "")
self.add_model.setText(model)
self.add_width.setValue(width or 0)
self.add_gsm.setValue(gsm or 0)
self.add_unit.setCurrentText(unit or "")
self.add_retail.setValue(retail or 0)
self.add_bulk.setValue(bulk or 0)
if "胸杯" in sub:
self.add_unit.setEnabled(False)
self.current_edit_model = model
# 切换到编辑标签页
tabs = self.findChild(QTabWidget)
tabs.setCurrentIndex(1)
QMessageBox.information(self, "提示", f"已加载 '{model}' 的信息,可修改后点击'保存原料'")
except Exception as e:
QMessageBox.critical(self, "错误", "加载原料信息失败: " + str(e))
"""编辑原料 - 打开独立编辑窗口"""
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}'")
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, "成功", "删除完成")
@@ -482,7 +666,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, "错误", "请输入型号/名称")
@@ -490,13 +674,32 @@ class RawMaterialLibraryDialog(QDialog):
major = self.add_major_category.currentText().strip()
sub = self.add_sub_category.text().strip()
# 验证必填字段:类目
if not major:
QMessageBox.warning(self, "错误", "请选择类目")
return
# 验证必填字段:类型
if not sub:
QMessageBox.warning(self, "错误", "请输入类型")
return
# 验证必填字段:幅宽
if self.add_width.value() <= 0:
QMessageBox.warning(self, "错误", "请输入幅宽")
return
# 验证必填字段:克重
if self.add_gsm.value() <= 0:
QMessageBox.warning(self, "错误", "请输入克重")
return
if "胸杯" in sub:
major = "辅料"
if major and sub:
category = major + "-" + sub
else:
category = sub or major
category = major or ""
fabric_type = sub or ""
supplier = self.add_supplier.currentText().strip()
color = self.add_color.text().strip()
@@ -504,19 +707,42 @@ class RawMaterialLibraryDialog(QDialog):
try:
with self.get_conn() as conn:
conn.execute('''
INSERT OR REPLACE INTO fabrics
(model, category, supplier, color, width, gsm, retail_price, bulk_price, unit, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (model, category, supplier, color,
self.add_width.value() or None, self.add_gsm.value() or None,
self.add_retail.value() or None, self.add_bulk.value() or None,
unit, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
conn.commit()
# 检查是否已存在该型号
cursor = conn.execute("SELECT model, is_deleted FROM fabrics WHERE model = ?", (model,))
existing = cursor.fetchone()
action = "更新" if self.current_edit_model else "保存"
QMessageBox.information(self, "成功", f"{action} '{model}'")
self.current_edit_model = None
if existing:
_, is_deleted = existing
# 如果型号存在且未被删除,提示使用编辑功能
if not is_deleted or is_deleted == 0:
QMessageBox.warning(self, "错误", f"型号 '{model}' 已存在,请使用编辑功能修改")
return
# 如果型号已被删除,直接恢复并更新
conn.execute('''
UPDATE fabrics
SET category=?, fabric_type=?, supplier=?, color=?, width=?, gsm=?,
retail_price=?, bulk_price=?, unit=?, timestamp=?, is_deleted=0, updated_at=CURRENT_TIMESTAMP
WHERE model=?
''', (category, fabric_type, supplier, color,
self.add_width.value() or None, self.add_gsm.value() or None,
self.add_retail.value() or None, self.add_bulk.value() or None,
unit, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), model))
conn.commit()
QMessageBox.information(self, "成功", f"已保存 '{model}'")
else:
# 型号不存在,创建新记录
conn.execute('''
INSERT INTO fabrics
(model, category, fabric_type, supplier, color, width, gsm, retail_price, bulk_price, unit, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (model, category, fabric_type, supplier, color,
self.add_width.value() or None, self.add_gsm.value() or None,
self.add_retail.value() or None, self.add_bulk.value() or None,
unit, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
conn.commit()
QMessageBox.information(self, "成功", f"已保存 '{model}'")
# 清空表单
self.add_model.clear()
@@ -527,7 +753,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()
@@ -540,8 +765,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
@@ -570,6 +795,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)
@@ -583,14 +812,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()
@@ -626,8 +858,152 @@ class RawMaterialLibraryDialog(QDialog):
QMessageBox.critical(self, "错误", str(e))
def clear_remaining(self, model):
"""清零剩余库存"""
reply = QMessageBox.question(self, "确认清零", f"确定将 {model} 的剩余量清零?\n(此操作仅逻辑清零,不删除历史记录)")
if reply == QMessageBox.Yes:
QMessageBox.information(self, "完成", f"{model} 剩余量已清零(视为全部用完)")
self.load_stock_table()
"""清零剩余库存(逻辑删除)"""
reply = QMessageBox.warning(
self,
"确认清零",
f"确定将 {model} 的剩余量清零吗?\n\n"
"此操作将标记删除该原料的所有历史采购和消耗记录。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
try:
with self.get_conn() as conn:
# 逻辑删除历史采购记录
conn.execute(
"UPDATE fabric_stock_in SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
# 逻辑删除历史消耗记录
conn.execute(
"UPDATE fabric_consumption SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
conn.commit()
QMessageBox.information(
self,
"完成",
f"{model} 的剩余库存已清零。"
)
self.load_stock_table()
except Exception as e:
QMessageBox.critical(self, "错误", f"清零失败: {str(e)}")
def edit_remaining_stock(self, model, unit, current_remaining):
"""编辑剩余库存"""
from PyQt5.QtWidgets import QDialogButtonBox
# 创建编辑对话框
dialog = QDialog(self)
dialog.setWindowTitle(f"编辑剩余库存 - {model}")
dialog.resize(400, 200)
layout = QVBoxLayout(dialog)
# 显示当前剩余量
info_label = QLabel(f"当前剩余量: {current_remaining:.3f} {unit}")
info_label.setStyleSheet("font-weight: bold; color: #2196F3; font-size: 14px;")
layout.addWidget(info_label)
# 新剩余量输入
input_layout = QHBoxLayout()
input_layout.addWidget(QLabel("新剩余量:"))
new_remaining_input = QDoubleSpinBox()
new_remaining_input.setRange(0, 999999)
new_remaining_input.setDecimals(3)
new_remaining_input.setValue(current_remaining)
input_layout.addWidget(new_remaining_input)
input_layout.addWidget(QLabel(unit))
layout.addLayout(input_layout)
# 警告提示
warning_label = QLabel(
"⚠️ 警告:保存后将删除该原料的所有历史采购和消耗记录!\n"
"系统将只保留新设置的剩余库存量。"
)
warning_label.setStyleSheet(
"background-color: #fff3cd; color: #856404; "
"padding: 10px; border: 1px solid #ffc107; border-radius: 5px;"
)
warning_label.setWordWrap(True)
layout.addWidget(warning_label)
# 按钮
button_box = QDialogButtonBox(
QDialogButtonBox.Save | QDialogButtonBox.Cancel
)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box)
# 显示对话框
if dialog.exec_() == QDialog.Accepted:
new_remaining = new_remaining_input.value()
self.save_new_remaining_stock(model, unit, new_remaining)
def save_new_remaining_stock(self, model, unit, new_remaining):
"""保存新的剩余库存量(逻辑删除历史记录)"""
# 二次确认
reply = QMessageBox.warning(
self,
"最后确认",
f"确定要将 {model} 的剩余库存设置为 {new_remaining:.3f} {unit} 吗?\n\n"
"此操作将:\n"
"1. 标记删除该原料的所有历史采购记录\n"
"2. 标记删除该原料的所有历史消耗记录\n"
"3. 创建一条新的入库记录作为当前库存",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
try:
with self.get_conn() as conn:
# 逻辑删除历史采购记录
conn.execute(
"UPDATE fabric_stock_in SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
# 逻辑删除历史消耗记录
conn.execute(
"UPDATE fabric_consumption SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE model = ? AND is_deleted = 0",
(model,)
)
# 如果新剩余量大于0创建一条新的入库记录
if new_remaining > 0:
conn.execute(
"""
INSERT INTO fabric_stock_in
(model, quantity, unit, purchase_date, note, is_deleted)
VALUES (?, ?, ?, ?, ?, 0)
""",
(
model,
new_remaining,
unit,
datetime.now().strftime('%Y-%m-%d'),
"手动编辑库存"
)
)
conn.commit()
QMessageBox.information(
self,
"成功",
f"{model} 的剩余库存已更新为 {new_remaining:.3f} {unit}"
)
self.load_stock_table()
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
PyQt5>=5.15.0
pyinstaller>=5.0.0

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View 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
View 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()

View 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
View 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
View 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.0100-30")
if __name__ == "__main__":
unittest.main()

View 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()

30
打包.bat Normal file
View File

@@ -0,0 +1,30 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 服装布料计算管理器 - 打包脚本
echo ========================================
echo.
echo 正在检查依赖...
python -c "import PyQt5" 2>nul
if errorlevel 1 (
echo 错误: 未安装PyQt5正在安装...
pip install -r requirements.txt
if errorlevel 1 (
echo 安装失败,请手动执行: pip install -r requirements.txt
pause
exit /b 1
)
)
echo.
echo 开始打包...
python build_exe.py
echo.
echo ========================================
echo 打包完成!
echo exe文件位置: dist\服装布料计算管理器.exe
echo ========================================
pause

85
打包说明.md Normal file
View File

@@ -0,0 +1,85 @@
# 打包为EXE文件说明
## 前置要求
1. 确保已安装Python 3.7或更高版本
2. 安装项目依赖:
```bash
pip install -r requirements.txt
```
## 打包方法
### 方法一:使用打包脚本(推荐)
直接运行打包脚本:
```bash
python build_exe.py
```
### 方法二使用PyInstaller命令
使用spec文件打包
```bash
pyinstaller build_exe.spec
```
或者直接使用命令行:
```bash
pyinstaller --name=服装布料计算管理器 --onefile --windowed --add-data=fabric_library.db;. main.py
```
### 方法三使用spec文件推荐用于自定义配置
```bash
pyinstaller build_exe.spec
```
## 打包输出
打包完成后生成的exe文件位于 `dist` 目录中:
- `dist/服装布料计算管理器.exe`
## 注意事项
1. **数据库文件**:如果项目中有 `fabric_library.db` 文件打包脚本会自动将其包含在exe同目录下。首次运行exe时如果数据库不存在程序会自动创建。
2. **文件大小**打包后的exe文件可能较大通常50-100MB这是因为包含了Python解释器和所有依赖库。
3. **杀毒软件**某些杀毒软件可能会误报这是正常现象。PyInstaller打包的exe文件需要添加白名单。
4. **依赖库**确保所有依赖都已正确安装特别是PyQt5。
5. **测试**打包完成后建议在干净的Windows系统上测试exe文件是否能正常运行。
## 常见问题
### 问题1打包失败提示找不到模块
**解决方案**:在 `build_exe.spec` 的 `hiddenimports` 中添加缺失的模块。
### 问题2exe运行时缺少DLL文件
**解决方案**确保PyQt5已正确安装可以尝试重新安装
```bash
pip uninstall PyQt5
pip install PyQt5
```
### 问题3exe文件太大
**解决方案**
- 使用 `--exclude-module` 排除不需要的模块
- 使用 `--onedir` 模式代替 `--onefile`会生成一个文件夹而不是单个exe
### 问题4数据库路径问题
**解决方案**程序已自动处理数据库路径exe运行时会在exe同目录下查找或创建数据库文件。
## 优化建议
1. **添加图标**:在 `build_exe.spec` 中设置 `icon='icon.ico'`需要准备一个ico格式的图标文件。
2. **减小体积**:如果不需要某些功能,可以排除相关模块:
```python
excludes=['matplotlib', 'numpy', 'pandas'] # 示例
```
3. **版本信息**:可以创建版本信息文件(.rc文件并添加到spec配置中。