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

104
CLAUDE.md Normal file
View File

@@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a standalone Python desktop application for fabric and garment management in the textile/clothing industry. The application is built using PyQt5 and SQLite, providing a comprehensive system for managing fabric inventory, garment specifications, and production calculations.
## Running the Application
```bash
python main.py
```
The application will:
1. Create a SQLite database (`fabric_library.db`) in the same directory if it doesn't exist
2. Show a login dialog with default passwords (123456 for both admin and user modes)
3. Launch the main application window
## Dependencies
The application requires these Python packages:
- PyQt5 (GUI framework)
- sqlite3 (database, built-in)
- PIL/Pillow (image processing)
- datetime, os, sys (built-in modules)
Install dependencies:
```bash
pip install PyQt5 Pillow
```
## Architecture Overview
### Database Schema
The application uses SQLite with these main tables:
- `fabrics` - Raw material library (fabric types, suppliers, pricing)
- `garments` - Garment style definitions with images
- `garment_materials` - Material usage per garment style
- `fabric_stock_in` - Inventory purchase records
- `fabric_consumption` - Production consumption tracking
- `admin_settings` - User passwords and settings
### Main Components
1. **LoginDialog** (`fabric_manager_pro.py:28-142`)
- Handles user authentication (admin vs regular user modes)
- Password management functionality
2. **FabricManager** (Main Window) (`fabric_manager_pro.py:1205-1653`)
- Central application controller
- Batch calculation interface for production planning
- Unit conversion calculator
3. **RawMaterialLibraryDialog** (`fabric_manager_pro.py:239-758`)
- Fabric/material database management
- Multi-level categorization (major/sub categories)
- Supplier and pricing management
- Stock tracking integration
4. **GarmentLibraryDialog** (`fabric_manager_pro.py:760-871`)
- Garment style catalog management
- Image upload and preview functionality
5. **GarmentEditDialog** (`fabric_manager_pro.py:873-1111`)
- Detailed garment specification editor
- Material usage definition per garment
- Integration with fabric library for material selection
6. **StockInDialog** (`fabric_manager_pro.py:144-237`)
- Inventory management for fabric purchases
- Stock level tracking and reporting
7. **PurchaseOrderDialog** (`fabric_manager_pro.py:1113-1203`)
- Automated purchase order generation
- Export functionality (clipboard/file)
### Key Features
- **Multi-user System**: Admin and regular user modes with different permissions
- **Inventory Tracking**: Complete fabric stock management with purchase/consumption tracking
- **Production Planning**: Calculate material requirements for batch production
- **Unit Conversion**: Built-in calculator for meters/yards/kilograms conversion
- **Image Management**: Garment style images with automatic resizing and storage
- **Data Export**: Purchase order generation with multiple export options
### File Structure
- `main.py` - Main application entry point
- `database.py` - Database connection and initialization
- `login_dialog.py` - User login and password management
- `stock_dialog.py` - Inventory management dialogs
- `raw_material_dialog.py` - Raw material library management
- `garment_dialogs.py` - Garment style management
- `purchase_order_dialog.py` - Purchase order generation
- `fabric_library.db` - SQLite database (auto-created)
- `images/` - Directory for garment style images (auto-created)
### Development Notes
- The application uses method binding in `__init__` to avoid AttributeError issues
- Database connections use context managers for proper resource cleanup
- Image processing includes automatic thumbnail generation and format conversion
- All database operations include proper error handling and user feedback

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

342
database.py Normal file
View File

@@ -0,0 +1,342 @@
"""
数据库连接和初始化模块
"""
import sqlite3
import os
from datetime import datetime
def get_db_connection(db_path):
"""获取数据库连接"""
return sqlite3.connect(db_path, timeout=30)
class DatabaseManager:
"""数据库管理类"""
def __init__(self, db_path):
self.db_path = db_path
self.init_db()
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def init_db(self):
"""初始化数据库表结构"""
try:
with self.get_conn() as conn:
# 原料表
conn.execute('''
CREATE TABLE IF NOT EXISTS fabrics (
model TEXT PRIMARY KEY,
category TEXT DEFAULT '未分类',
supplier TEXT,
color TEXT,
width REAL,
gsm REAL,
retail_price REAL,
bulk_price REAL,
unit TEXT DEFAULT '',
timestamp TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# 为fabrics表添加索引
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabrics_category ON fabrics(category)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabrics_supplier ON fabrics(supplier)')
# 衣服款号表
conn.execute('''
CREATE TABLE IF NOT EXISTS garments (
style_number TEXT PRIMARY KEY,
image_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# 衣服材料用量表
conn.execute('''
CREATE TABLE IF NOT EXISTS garment_materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
style_number TEXT,
category TEXT,
fabric_type TEXT,
usage_per_piece REAL,
unit TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (style_number) REFERENCES garments(style_number)
)
''')
# 为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)')
# 添加新字段(如果不存在)
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN fabric_type TEXT")
except:
pass
try:
conn.execute("ALTER TABLE fabrics ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE fabrics ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE garments ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE garments ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE admin_settings ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE admin_settings ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE fabric_stock_in ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE fabric_stock_in ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE fabric_consumption ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
try:
conn.execute("ALTER TABLE fabric_consumption ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP")
except:
pass
# 管理员设置表
conn.execute('''
CREATE TABLE IF NOT EXISTS admin_settings (
key TEXT PRIMARY KEY,
value TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# 原料入库表
conn.execute('''
CREATE TABLE IF NOT EXISTS fabric_stock_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model TEXT,
quantity REAL,
unit TEXT,
purchase_date TEXT,
note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (model) REFERENCES fabrics(model)
)
''')
# 为fabric_stock_in表添加索引
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabric_stock_in_model ON fabric_stock_in(model)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabric_stock_in_date ON fabric_stock_in(purchase_date)')
# 原料消耗表
conn.execute('''
CREATE TABLE IF NOT EXISTS fabric_consumption (
id INTEGER PRIMARY KEY AUTOINCREMENT,
style_number TEXT,
model TEXT,
single_usage REAL,
quantity_made INTEGER,
loss_rate REAL,
consume_quantity REAL,
consume_date TEXT,
unit TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (style_number) REFERENCES garments(style_number),
FOREIGN KEY (model) REFERENCES fabrics(model)
)
''')
# 为fabric_consumption表添加索引
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabric_consumption_style ON fabric_consumption(style_number)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabric_consumption_model ON fabric_consumption(model)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_fabric_consumption_date ON fabric_consumption(consume_date)')
# 添加库存计算视图
conn.execute('''
CREATE VIEW IF NOT EXISTS fabric_stock_view AS
SELECT
f.model,
f.category,
f.supplier,
f.color,
f.unit,
COALESCE(stock_in.total_in, 0) as total_stock_in,
COALESCE(consumption.total_consumed, 0) as total_consumed,
COALESCE(stock_in.total_in, 0) - COALESCE(consumption.total_consumed, 0) as current_stock
FROM fabrics f
LEFT JOIN (
SELECT model, SUM(quantity) as total_in
FROM fabric_stock_in
GROUP BY model
) stock_in ON f.model = stock_in.model
LEFT JOIN (
SELECT model, SUM(consume_quantity) as total_consumed
FROM fabric_consumption
GROUP BY model
) consumption ON f.model = consumption.model
''')
# 初始化默认密码
if not self.get_setting("admin_password"):
conn.execute("INSERT INTO admin_settings (key, value) VALUES ('admin_password', ?)", ("123456",))
if not self.get_setting("user_password"):
conn.execute("INSERT INTO admin_settings (key, value) VALUES ('user_password', ?)", ("123456",))
conn.commit()
except Exception as e:
raise Exception(f"无法初始化数据库:{str(e)}")
def get_setting(self, key):
"""获取设置值"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT value FROM admin_settings WHERE key = ?", (key,))
row = cursor.fetchone()
return row[0] if row else None
except:
return None
def set_setting(self, key, value):
"""设置配置值"""
try:
with self.get_conn() as conn:
conn.execute("INSERT OR REPLACE INTO admin_settings (key, value) VALUES (?, ?)", (key, value))
conn.commit()
return True
except Exception:
return False
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
""")
categories = set()
for row in cursor.fetchall():
if row[0] and row[0].strip():
categories.add(row[0])
# 添加默认类目
categories.update(["布料", "辅料", "其他"])
return sorted(categories)
except:
return ["布料", "辅料", "其他"]
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 = ?
ORDER BY fabric_type
""", (f"{category}-%", category))
types = []
for row in cursor.fetchall():
if row[0] and row[0] != '默认类型':
types.append(row[0])
return types
except:
return []
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 ?
ORDER BY model
""", (category, f"{category}-{fabric_type}", f"{category}-{fabric_type}-%"))
models = []
for row in cursor.fetchall():
model, color, unit = row
display_text = model
if color and color.strip():
display_text = f"{model}-{color}"
models.append((display_text, model, unit))
return models
except:
return []
def get_password(db_path, password_type):
"""获取密码设置"""
try:
with get_db_connection(db_path) as conn:
cursor = conn.execute(
"SELECT value FROM admin_settings WHERE key = ?",
(f"{password_type}_password",)
)
row = cursor.fetchone()
return row[0] if row else "123456"
except:
return "123456"
def update_password(db_path, password_type, new_password):
"""更新密码"""
try:
with get_db_connection(db_path) as conn:
conn.execute(
"UPDATE admin_settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?",
(new_password, f"{password_type}_password")
)
conn.commit()
return True
except:
return False

BIN
fabric_library.db Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

777
garment_dialogs.py Normal file
View File

@@ -0,0 +1,777 @@
"""
服装管理模块 - 处理服装款式和材料用量管理
"""
import os
from datetime import datetime
from PIL import Image
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit,
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox, QFileDialog, QDoubleSpinBox, QWidget, QCompleter
)
from PyQt5.QtCore import Qt, QStringListModel, QTimer
from PyQt5.QtGui import QPixmap
from database import get_db_connection
class SearchableComboBox(QComboBox):
"""支持模糊搜索的下拉框"""
def __init__(self, parent=None):
super().__init__(parent)
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()
class GarmentLibraryDialog(QDialog):
"""服装库管理对话框"""
def __init__(self, db_path):
super().__init__()
self.db_path = db_path
self.setWindowTitle("衣服款号管理")
self.resize(1300, 750)
self.setup_ui()
self.load_garments()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
# 操作按钮区域
op_layout = QHBoxLayout()
op_layout.addWidget(QLabel("搜索款号:"))
self.search_input = QLineEdit()
self.search_input.textChanged.connect(self.load_garments)
op_layout.addWidget(self.search_input)
add_btn = QPushButton("新增/编辑款号")
add_btn.clicked.connect(self.edit_garment)
op_layout.addWidget(add_btn)
del_btn = QPushButton("删除选中款号")
del_btn.clicked.connect(self.delete_garment)
op_layout.addWidget(del_btn)
refresh_btn = QPushButton("刷新")
refresh_btn.clicked.connect(self.load_garments)
op_layout.addWidget(refresh_btn)
layout.addLayout(op_layout)
# 服装表格
self.garment_table = QTableWidget()
self.garment_table.setColumnCount(3)
self.garment_table.setHorizontalHeaderLabels(["款号", "类目数量", "款式图预览"])
self.garment_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.garment_table.itemDoubleClicked.connect(self.edit_garment_from_table)
layout.addWidget(self.garment_table)
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def load_garments(self):
"""加载服装列表"""
keyword = self.search_input.text().strip()
try:
with self.get_conn() as conn:
query = "SELECT style_number, image_path FROM garments"
params = []
if keyword:
query += " WHERE style_number LIKE ?"
params = ["%" + keyword + "%"]
query += " ORDER BY style_number"
cursor = conn.execute(query, params)
rows = cursor.fetchall()
self.garment_table.setRowCount(len(rows))
for i in range(len(rows)):
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))
# 查询材料数量
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)))
# 显示图片预览
image_item = QTableWidgetItem()
image_item.setTextAlignment(Qt.AlignCenter)
if image_path and os.path.exists(image_path):
try:
pixmap = QPixmap(image_path).scaled(130, 130, Qt.KeepAspectRatio, Qt.SmoothTransformation)
image_item.setData(Qt.DecorationRole, pixmap)
except:
image_item.setText("加载失败")
else:
image_item.setText("无图片")
self.garment_table.setItem(row_idx, 2, image_item)
except Exception as e:
QMessageBox.critical(self, "加载失败", "错误: " + str(e))
def edit_garment_from_table(self):
"""从表格编辑服装"""
row = self.garment_table.currentRow()
if row >= 0:
style_number = self.garment_table.item(row, 0).text()
self.edit_garment(style_number)
def edit_garment(self, style_number=None):
"""编辑服装"""
dialog = GarmentEditDialog(self.db_path, style_number)
if dialog.exec_():
self.load_garments()
def delete_garment(self):
"""删除服装"""
row = self.garment_table.currentRow()
if row < 0:
QMessageBox.warning(self, "提示", "请先选中一款号")
return
style_number = self.garment_table.item(row, 0).text()
reply = QMessageBox.question(self, "确认", f"删除款号 '{style_number}' 及其所有信息?")
if reply == QMessageBox.Yes:
try:
with self.get_conn() as conn:
conn.execute("DELETE FROM garment_materials WHERE style_number = ?", (style_number,))
conn.execute("DELETE FROM garments WHERE style_number = ?", (style_number,))
conn.commit()
self.load_garments()
QMessageBox.information(self, "成功", "删除完成")
except Exception as e:
QMessageBox.critical(self, "错误", "删除失败: " + str(e))
class GarmentEditDialog(QDialog):
"""服装编辑对话框"""
def __init__(self, db_path, style_number=None):
super().__init__()
self.db_path = db_path
self.style_number = style_number
self.current_image_path = None
self.setWindowTitle("编辑款号" if style_number else "新增款号")
self.resize(1300, 850)
self.setup_ui()
if style_number:
self.load_garment_data()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
# 基本信息区域
basic_layout = QGridLayout()
basic_layout.addWidget(QLabel("款号:"), 0, 0, Qt.AlignRight)
self.style_input = QLineEdit()
if self.style_number:
self.style_input.setText(self.style_number)
self.style_input.setEnabled(not self.style_number)
basic_layout.addWidget(self.style_input, 0, 1)
basic_layout.addWidget(QLabel("款式图:"), 1, 0, Qt.AlignRight)
self.image_label = QLabel("无图片")
self.image_label.setFixedSize(300, 300)
self.image_label.setStyleSheet("border: 1px solid gray;")
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setScaledContents(True)
basic_layout.addWidget(self.image_label, 1, 1, 5, 1)
upload_btn = QPushButton("上传/更换图片")
upload_btn.clicked.connect(self.upload_image)
basic_layout.addWidget(upload_btn, 1, 2)
layout.addLayout(basic_layout)
# 材料用量区域
layout.addWidget(QLabel("材料用量(单件):"))
self.material_table = QTableWidget()
self.material_table.setColumnCount(6)
self.material_table.setHorizontalHeaderLabels(["类目", "类型", "型号", "单件用量", "单位", "删除"])
self.material_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
layout.addWidget(self.material_table)
# 按钮区域
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.clicked.connect(lambda: self.add_material_row())
btn_layout.addWidget(add_custom_btn)
layout.addLayout(btn_layout)
# 保存/取消按钮
buttons = QHBoxLayout()
save_btn = QPushButton("保存")
save_btn.clicked.connect(self.save_garment)
buttons.addWidget(save_btn)
cancel_btn = QPushButton("取消")
cancel_btn.clicked.connect(self.reject)
buttons.addWidget(cancel_btn)
layout.addLayout(buttons)
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def upload_image(self):
"""上传图片"""
file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.png *.jpg *.jpeg *.bmp)")
if file_path:
try:
img = Image.open(file_path).convert("RGB")
img.thumbnail((800, 800))
os.makedirs("images", exist_ok=True)
filename = os.path.basename(file_path)
save_path = os.path.join("images", filename)
img.save(save_path, "JPEG", quality=85)
self.current_image_path = save_path
pixmap = QPixmap(save_path).scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(pixmap)
except Exception as e:
QMessageBox.critical(self, "错误", "上传图片失败: " + str(e))
def load_garment_data(self):
"""加载服装数据"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT image_path FROM garments WHERE style_number = ?", (self.style_number,))
row = cursor.fetchone()
if row and row[0] and os.path.exists(row[0]):
self.current_image_path = row[0]
pixmap = QPixmap(row[0]).scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(pixmap)
self.load_materials()
except Exception as e:
QMessageBox.critical(self, "错误", "加载失败: " + str(e))
def load_materials(self):
"""加载材料列表"""
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
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=""):
"""添加材料行"""
row = self.material_table.rowCount()
self.material_table.insertRow(row)
# 列0: 类目下拉框
cat_combo = QComboBox()
cat_combo.setEditable(True)
# 最后添加自定义选项
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
FROM fabrics
WHERE category IS NOT NULL AND category != ''
ORDER BY major_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:
# 如果查询失败,使用默认类目
cat_combo.addItem("布料")
cat_combo.addItem("辅料")
cat_combo.addItem("其他")
if category:
cat_combo.setCurrentText(category)
else:
# 如果没有指定类目,默认选择第一个实际类目而不是"自定义类目"
if cat_combo.count() > 1:
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)
# 先添加所有类型选项
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
""")
types = cursor.fetchall()
for type_row in types:
if type_row[0] and type_row[0] != '默认类型':
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)
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))
self.material_table.setCellWidget(row, 2, model_combo)
# 列3: 单件用量
usage_spin = QDoubleSpinBox()
usage_spin.setRange(0, 1000)
usage_spin.setValue(usage)
usage_spin.setDecimals(3)
self.material_table.setCellWidget(row, 3, usage_spin)
# 列4: 单位
unit_combo = QComboBox()
unit_combo.setEditable(True)
unit_combo.addItems(["", "", "公斤", "一对", "", ""])
unit_combo.setCurrentText(unit)
self.material_table.setCellWidget(row, 4, unit_combo)
# 列5: 删除按钮
del_btn = QPushButton("删除")
del_btn.clicked.connect(lambda _, r=row: self.material_table.removeRow(r))
self.material_table.setCellWidget(row, 5, del_btn)
# 初始化类型和型号选项
self.on_category_changed(cat_combo.currentText(), 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)
# 然后设置型号 - 使用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
# 如果没找到直接设置文本SearchableComboBox支持
if not found:
model_combo.setCurrentText(model)
def on_category_changed(self, category_text, row):
"""当类目改变时,更新类型下拉框"""
type_combo = self.material_table.cellWidget(row, 1)
model_combo = self.material_table.cellWidget(row, 2)
# 清空类型下拉框
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 != "—— 自定义类目 ——":
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 = ?
ORDER BY fabric_type
""", (f"{category_text}-%", category_text))
types = cursor.fetchall()
for type_row in types:
if type_row[0] and type_row[0] != '默认类型':
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)
except Exception as e:
pass
def on_type_changed(self, 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("—— 选择型号 ——")
# 始终显示所有型号,不进行过滤
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
def on_model_selected(self, model_text, row):
"""当型号选择时,自动设置单位并填充类目和类型"""
if not model_text or model_text == "—— 选择型号 ——":
return
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
def save_garment(self):
"""保存服装"""
style_number = self.style_input.text().strip()
if not style_number:
QMessageBox.warning(self, "错误", "请输入款号")
return
try:
with self.get_conn() as conn:
conn.execute('INSERT OR REPLACE INTO garments (style_number, image_path) VALUES (?, ?)',
(style_number, self.current_image_path))
conn.execute("DELETE FROM garment_materials WHERE style_number = ?", (style_number,))
for row in range(self.material_table.rowCount()):
# 获取各列的值
category_widget = self.material_table.cellWidget(row, 0) # 类目
type_widget = self.material_table.cellWidget(row, 1) # 类型
model_widget = self.material_table.cellWidget(row, 2) # 型号
usage_widget = self.material_table.cellWidget(row, 3) # 单件用量
unit_widget = self.material_table.cellWidget(row, 4) # 单位
category = category_widget.currentText().strip()
fabric_type = type_widget.currentText().strip()
model = model_widget.currentText().strip()
# 处理类目和类型
if category == "—— 自定义类目 ——":
category = ""
if fabric_type == "—— 选择类型 ——":
fabric_type = ""
# 如果选择了具体型号,获取型号的实际值
final_model = ""
if model and model != "—— 选择型号 ——":
model_data = model_widget.itemData(model_widget.currentIndex())
final_model = model_data if model_data else model
# 至少需要有类目或型号
if not category and not final_model:
continue
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.commit()
QMessageBox.information(self, "成功", "保存完成")
self.accept()
except Exception as e:
QMessageBox.critical(self, "错误", "保存失败: " + str(e))

162
login_dialog.py Normal file
View File

@@ -0,0 +1,162 @@
"""
登录对话框模块 - 处理用户登录和密码管理
"""
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QMessageBox, QInputDialog
)
from PyQt5.QtCore import Qt
from database import get_db_connection
class LoginDialog(QDialog):
def __init__(self, db_path):
super().__init__()
self.db_path = db_path
self.is_admin = False
self.setWindowTitle("选择模式并登录")
self.resize(450, 350)
self.setModal(True)
self.setup_ui()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
layout.addWidget(QLabel("<b>请选择登录模式(默认密码均为 123456</b>"))
# 管理员登录区域
admin_layout = QHBoxLayout()
admin_layout.addWidget(QLabel("管理员模式密码:"))
self.admin_input = QLineEdit()
self.admin_input.setEchoMode(QLineEdit.Password)
self.admin_input.setPlaceholderText("默认 123456")
admin_layout.addWidget(self.admin_input)
admin_login = QPushButton("登录管理员模式")
admin_login.clicked.connect(lambda: self.login_mode(True))
admin_layout.addWidget(admin_login)
admin_change = QPushButton("修改管理员密码")
admin_change.clicked.connect(self.change_admin_password)
admin_layout.addWidget(admin_change)
layout.addLayout(admin_layout)
# 普通用户登录区域
user_layout = QHBoxLayout()
user_layout.addWidget(QLabel("普通用户模式密码:"))
self.user_input = QLineEdit()
self.user_input.setEchoMode(QLineEdit.Password)
self.user_input.setPlaceholderText("默认 123456")
user_layout.addWidget(self.user_input)
user_login = QPushButton("登录普通用户模式")
user_login.clicked.connect(lambda: self.login_mode(False))
user_layout.addWidget(user_login)
user_change = QPushButton("修改普通用户密码")
user_change.clicked.connect(self.change_user_password)
user_layout.addWidget(user_change)
layout.addLayout(user_layout)
layout.addStretch()
# 退出按钮
exit_btn = QPushButton("退出程序")
exit_btn.clicked.connect(self.reject)
layout.addWidget(exit_btn)
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def get_stored_password(self, password_type):
"""获取存储的密码"""
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT value FROM admin_settings WHERE key = ?", (f"{password_type}_password",))
row = cursor.fetchone()
return row[0] if row else "123456"
except:
return "123456"
def set_password(self, password_type, new_password):
"""设置新密码"""
try:
with self.get_conn() as conn:
conn.execute("INSERT OR REPLACE INTO admin_settings (key, value) VALUES (?, ?)",
(f"{password_type}_password", new_password))
conn.commit()
return True
except Exception as e:
QMessageBox.critical(self, "错误", f"密码保存失败: {str(e)}")
return False
def change_admin_password(self):
"""修改管理员密码"""
old_pwd = self.get_stored_password("admin")
old_input, ok1 = QInputDialog.getText(
self, "修改管理员密码", "请输入当前管理员密码:", QLineEdit.Password
)
if not ok1 or old_input != old_pwd:
QMessageBox.warning(self, "错误", "当前密码错误!")
return
new_pwd1, ok2 = QInputDialog.getText(
self, "新密码", "请输入新管理员密码:", QLineEdit.Password
)
if not ok2 or len(new_pwd1) < 4:
QMessageBox.warning(self, "错误", "新密码至少4位")
return
new_pwd2, ok3 = QInputDialog.getText(
self, "确认新密码", "请再次输入新管理员密码:", QLineEdit.Password
)
if not ok3 or new_pwd1 != new_pwd2:
QMessageBox.warning(self, "错误", "两次输入的密码不一致!")
return
if self.set_password("admin", new_pwd1):
QMessageBox.information(self, "成功", "管理员密码修改成功!")
def change_user_password(self):
"""修改普通用户密码"""
old_pwd = self.get_stored_password("user")
old_input, ok1 = QInputDialog.getText(
self, "修改普通用户密码", "请输入当前普通用户密码:", QLineEdit.Password
)
if not ok1 or old_input != old_pwd:
QMessageBox.warning(self, "错误", "当前密码错误!")
return
new_pwd1, ok2 = QInputDialog.getText(
self, "新密码", "请输入新普通用户密码:", QLineEdit.Password
)
if not ok2 or len(new_pwd1) < 4:
QMessageBox.warning(self, "错误", "新密码至少4位")
return
new_pwd2, ok3 = QInputDialog.getText(
self, "确认新密码", "请再次输入新普通用户密码:", QLineEdit.Password
)
if not ok3 or new_pwd1 != new_pwd2:
QMessageBox.warning(self, "错误", "两次输入的密码不一致!")
return
if self.set_password("user", new_pwd1):
QMessageBox.information(self, "成功", "普通用户密码修改成功!")
def login_mode(self, is_admin):
"""登录验证"""
password_type = "admin" if is_admin else "user"
input_pwd = self.admin_input.text().strip() if is_admin else self.user_input.text().strip()
correct_pwd = self.get_stored_password(password_type)
if input_pwd == correct_pwd:
self.is_admin = is_admin
self.accept()
else:
QMessageBox.warning(self, "错误", "密码错误,请重试!")

504
main.py Normal file
View File

@@ -0,0 +1,504 @@
"""
服装布料计算管理器 - 专业版(重构版主程序)
- 模块化设计,代码分离
- 所有功能完整可用
"""
import sys
import os
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit, QMessageBox,
QGroupBox, QDoubleSpinBox, QSpinBox, QDialog, QScrollArea
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
# 导入自定义模块
from database import DatabaseManager, get_db_connection
from login_dialog import LoginDialog
from stock_dialog import StockInDialog
from raw_material_dialog import RawMaterialLibraryDialog
from garment_dialogs import GarmentLibraryDialog
from purchase_order_dialog import PurchaseOrderDialog
class FabricManager(QMainWindow):
"""主应用程序窗口"""
def __init__(self, is_admin=False):
super().__init__()
self.is_admin = is_admin
# 设置数据库路径
exe_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
self.db_path = os.path.join(exe_dir, "fabric_library.db")
# 初始化数据库
self.db_manager = DatabaseManager(self.db_path)
# 设置窗口
mode_text = "(管理员模式)" if is_admin else "(普通模式)"
self.setWindowTitle("服装布料计算管理器 - 专业版 " + mode_text)
self.resize(1300, 800)
# 设置样式
self.setStyleSheet("""
QMainWindow { background-color: #f0fff8; }
QGroupBox { font-weight: bold; color: #2e8b57; border: 2px solid #90ee90; border-radius: 10px; margin-top: 10px; padding-top: 8px; background-color: #ffffff; }
QPushButton { background-color: #4caf50; color: white; padding: 10px; border-radius: 8px; font-size: 13px; font-weight: bold; }
QTextEdit { background-color: #e8f5e9; border: 2px solid #a5d6a7; border-radius: 8px; padding: 10px; font-size: 13px; }
QLineEdit, QDoubleSpinBox, QSpinBox, QComboBox { padding: 6px; border: 2px solid #a5d6a7; border-radius: 6px; font-size: 13px; }
QLabel { font-size: 13px; color: #2e8b57; }
""")
self.init_ui()
self.load_garment_list()
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def init_ui(self):
"""初始化用户界面"""
scroll = QScrollArea()
scroll.setWidgetResizable(True)
central_widget = QWidget()
scroll.setWidget(central_widget)
self.setCentralWidget(scroll)
main_layout = QVBoxLayout(central_widget)
# 标题
title = QLabel("服装布料计算管理器 - 专业版")
title.setFont(QFont("Microsoft YaHei", 18, QFont.Bold))
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("color: #228b22; padding: 15px;")
main_layout.addWidget(title)
# 顶部按钮区域
top_buttons = QHBoxLayout()
guide_btn = QPushButton("📖 查看使用说明")
guide_btn.clicked.connect(self.show_guide)
top_buttons.addWidget(guide_btn)
garment_btn = QPushButton("👔 衣服库管理")
garment_btn.clicked.connect(self.open_garment_library)
top_buttons.addWidget(garment_btn)
library_btn = QPushButton("🗄️ 原料库管理")
library_btn.clicked.connect(self.open_library)
top_buttons.addWidget(library_btn)
stock_in_btn = QPushButton("📦 快速原料入库")
stock_in_btn.clicked.connect(self.quick_stock_in)
top_buttons.addWidget(stock_in_btn)
top_buttons.addStretch()
main_layout.addLayout(top_buttons)
# 主要内容区域
content_layout = QHBoxLayout()
# 左侧区域
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
# 批量计算组
calc_group = QGroupBox("批量计算(按衣服款号)")
calc_layout = QGridLayout()
calc_layout.setVerticalSpacing(12)
calc_layout.addWidget(QLabel("选择衣服款号:"), 0, 0, Qt.AlignRight)
self.garment_combo = QComboBox()
self.garment_combo.setEditable(True)
self.garment_combo.currentIndexChanged.connect(self.load_garment_materials)
calc_layout.addWidget(self.garment_combo, 0, 1, 1, 2)
calc_layout.addWidget(QLabel("生产件数:"), 1, 0, Qt.AlignRight)
self.quantity_input = QSpinBox()
self.quantity_input.setRange(1, 1000000)
self.quantity_input.setValue(1000)
calc_layout.addWidget(self.quantity_input, 1, 1)
calc_layout.addWidget(QLabel("损耗率 (%):"), 2, 0, Qt.AlignRight)
self.loss_input = QDoubleSpinBox()
self.loss_input.setRange(0, 50)
self.loss_input.setValue(5)
calc_layout.addWidget(self.loss_input, 2, 1)
calc_btn = QPushButton("计算本次总用量")
calc_btn.clicked.connect(self.load_garment_materials)
calc_layout.addWidget(calc_btn, 3, 0, 1, 3)
calc_group.setLayout(calc_layout)
left_layout.addWidget(calc_group)
# 结果显示组
result_group = QGroupBox("本次生产用量明细")
result_layout = QVBoxLayout()
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setMinimumHeight(400)
result_layout.addWidget(self.result_text)
# 操作按钮
btn_layout = QHBoxLayout()
po_btn = QPushButton("生成采购单")
po_btn.clicked.connect(self.generate_purchase_order)
po_btn.setStyleSheet("background-color: #ff9800; font-weight: bold; padding: 12px;")
btn_layout.addWidget(po_btn)
record_btn = QPushButton("记录本次消耗到库存")
record_btn.clicked.connect(self.record_current_consumption)
record_btn.setStyleSheet("background-color: #e91e63; color: white; font-weight: bold; padding: 12px;")
btn_layout.addWidget(record_btn)
result_layout.addLayout(btn_layout)
result_group.setLayout(result_layout)
left_layout.addWidget(result_group)
content_layout.addWidget(left_widget, stretch=3)
# 右侧单位换算计算器
right_group = QGroupBox("单位换算计算器")
right_layout = QGridLayout()
right_layout.setVerticalSpacing(10)
right_layout.addWidget(QLabel("米数:"), 0, 0, Qt.AlignRight)
self.calc_m = QDoubleSpinBox()
self.calc_m.setRange(0, 100000)
self.calc_m.setDecimals(3)
self.calc_m.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_m, 0, 1)
right_layout.addWidget(QLabel("码数:"), 1, 0, Qt.AlignRight)
self.calc_yard = QDoubleSpinBox()
self.calc_yard.setRange(0, 100000)
self.calc_yard.setDecimals(3)
self.calc_yard.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_yard, 1, 1)
right_layout.addWidget(QLabel("公斤:"), 2, 0, Qt.AlignRight)
self.calc_kg = QDoubleSpinBox()
self.calc_kg.setRange(0, 100000)
self.calc_kg.setDecimals(6)
self.calc_kg.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_kg, 2, 1)
right_layout.addWidget(QLabel("幅宽 (cm):"), 3, 0, Qt.AlignRight)
self.calc_width = QDoubleSpinBox()
self.calc_width.setRange(50, 300)
self.calc_width.setValue(150)
self.calc_width.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_width, 3, 1)
right_layout.addWidget(QLabel("克重 (g/m²):"), 4, 0, Qt.AlignRight)
self.calc_gsm = QDoubleSpinBox()
self.calc_gsm.setRange(50, 1000)
self.calc_gsm.setValue(200)
self.calc_gsm.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_gsm, 4, 1)
right_group.setLayout(right_layout)
content_layout.addWidget(right_group, stretch=1)
main_layout.addLayout(content_layout)
main_layout.addStretch()
def quick_stock_in(self):
"""快速入库"""
dialog = StockInDialog(self.db_path)
dialog.exec_()
def generate_purchase_order(self):
"""生成采购单"""
style_number = self.garment_combo.currentText().strip()
if not style_number:
QMessageBox.warning(self, "提示", "请先选择或计算一款号!")
return
quantity = self.quantity_input.value()
loss_rate = self.loss_input.value() / 100
dialog = PurchaseOrderDialog(self.db_path, style_number, quantity, loss_rate)
dialog.exec_()
def record_current_consumption(self):
"""记录当前消耗"""
style_number = self.garment_combo.currentText().strip()
if not style_number:
QMessageBox.warning(self, "提示", "请先选择一款号并计算用量!")
return
quantity = self.quantity_input.value()
loss_rate = self.loss_input.value() / 100
try:
with self.get_conn() as conn:
cursor = conn.execute('''
SELECT 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:
if usage_per_piece == 0:
continue
# 获取该原料在入库时使用的单位
stock_cursor = conn.execute('''
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]
# 如果单位不同,需要转换
if unit != stock_unit:
consume_qty = self.convert_unit_value(usage_per_piece * quantity * (1 + loss_rate), unit, stock_unit, category)
final_unit = stock_unit
else:
consume_qty = usage_per_piece * quantity * (1 + loss_rate)
final_unit = unit
else:
# 没有入库记录,使用原单位
consume_qty = usage_per_piece * quantity * (1 + loss_rate)
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))
inserted += 1
conn.commit()
if inserted > 0:
QMessageBox.information(self, "成功", "本次生产消耗已记录到库存!")
else:
QMessageBox.information(self, "提示", "本次没有可记录的原料消耗")
except Exception as e:
QMessageBox.critical(self, "错误", str(e))
def open_library(self):
"""打开原料库"""
try:
dialog = RawMaterialLibraryDialog(self.db_path, self.is_admin)
dialog.exec_()
except Exception as e:
QMessageBox.critical(self, "错误", f"打开原料库失败: {str(e)}")
def open_garment_library(self):
"""打开服装库"""
try:
dialog = GarmentLibraryDialog(self.db_path)
dialog.exec_()
self.load_garment_list()
except Exception as e:
QMessageBox.critical(self, "错误", f"打开衣服库失败: {str(e)}")
def load_garment_list(self):
"""加载服装列表"""
current = self.garment_combo.currentText()
self.garment_combo.blockSignals(True)
self.garment_combo.clear()
self.garment_combo.addItem("")
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT style_number FROM garments ORDER BY style_number")
for row in cursor.fetchall():
self.garment_combo.addItem(row[0])
except:
pass
self.garment_combo.blockSignals(False)
if current:
self.garment_combo.setCurrentText(current)
def load_garment_materials(self):
"""加载服装材料"""
style_number = self.garment_combo.currentText().strip()
if not style_number:
self.result_text.clear()
return
qty = self.quantity_input.value()
loss = self.loss_input.value() / 100
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():
if usage:
total = usage * qty * (1 + loss)
# 显示材料名称(如果有类型则显示类目-类型,否则只显示类目)
material_name = f"{category}-{fabric_type}" if fabric_type else category
text += f"{material_name}\n单件: {usage:.3f} {unit}\n总用量: {total:.3f} {unit}\n\n"
except Exception as e:
text += "计算失败: " + str(e)
self.result_text.setText(text)
def show_guide(self):
"""显示使用说明"""
guide_text = """
【服装布料计算管理器 - 专业版 详细使用说明】
• 启动时弹出登录界面,默认密码均为 123456
• 管理员可修改密码
• 原料库支持大类/子类/供应商多条件筛选
• 胸杯自动归类到"辅料",单位固定"一对"
• 衣服编辑支持从原料库选择具体型号(可选),自动填充单位
• 支持材料行上移/下移/删除
• 主界面支持:
- 批量计算用量(含损耗)
- 生成采购单复制或保存TXT
- "记录本次消耗到库存":自动记录生产消耗
• 原料库"库存跟踪"Tab
- 显示每种原料的总采购、总消耗、当前剩余
- 查看明细(入库和消耗历史)
- 一键清零剩余(盘点用完时使用)
• 在"原料入库管理"或原料库可多次记录采购入库
祝使用愉快!
"""
dialog = QDialog(self)
dialog.setWindowTitle("详细使用说明")
dialog.resize(800, 700)
layout = QVBoxLayout(dialog)
text_area = QTextEdit()
text_area.setReadOnly(True)
text_area.setFont(QFont("Microsoft YaHei", 12))
text_area.setText(guide_text)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(text_area)
layout.addWidget(scroll)
close_btn = QPushButton("关闭")
close_btn.clicked.connect(dialog.accept)
layout.addWidget(close_btn)
dialog.exec_()
def convert_unit_value(self, value, from_unit, to_unit, fabric_model=None):
"""单位转换函数:将数值从一个单位转换为另一个单位"""
if from_unit == to_unit:
return value
# 米 <-> 码 转换
if from_unit == "" and to_unit == "":
return value / 0.9144
elif from_unit == "" and to_unit == "":
return value * 0.9144
# 长度单位转换为重量单位(需要面料的幅宽和克重信息)
elif (from_unit in ["", ""] and to_unit == "公斤") or (from_unit == "公斤" and to_unit in ["", ""]):
if fabric_model:
try:
with self.get_conn() as conn:
cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ?", (fabric_model,))
fabric_info = cursor.fetchone()
if fabric_info and fabric_info[0] and fabric_info[1]:
width, gsm = fabric_info
if width > 0 and gsm > 0:
if from_unit == "" and to_unit == "公斤":
# 米转公斤:米数 * 幅宽(m) * 克重(kg/m²)
return value * (width / 100) * (gsm / 1000)
elif from_unit == "" and to_unit == "公斤":
# 码转公斤:先转米,再转公斤
meters = value * 0.9144
return meters * (width / 100) * (gsm / 1000)
elif from_unit == "公斤" and to_unit == "":
# 公斤转米:公斤 / (幅宽(m) * 克重(kg/m²))
return value / ((width / 100) * (gsm / 1000))
elif from_unit == "公斤" and to_unit == "":
# 公斤转码:先转米,再转码
meters = value / ((width / 100) * (gsm / 1000))
return meters / 0.9144
except Exception:
pass
# 如果无法转换,返回原值
return value
def convert_units(self):
"""单位换算"""
sender = self.sender()
try:
if sender == self.calc_m:
m = self.calc_m.value()
self.calc_yard.blockSignals(True)
self.calc_yard.setValue(m / 0.9144)
self.calc_yard.blockSignals(False)
weight = (m * self.calc_width.value() / 100 * self.calc_gsm.value()) / 1000
self.calc_kg.blockSignals(True)
self.calc_kg.setValue(weight)
self.calc_kg.blockSignals(False)
elif sender == self.calc_yard:
yard = self.calc_yard.value()
m = yard * 0.9144
self.calc_m.blockSignals(True)
self.calc_m.setValue(m)
self.calc_m.blockSignals(False)
weight = (m * self.calc_width.value() / 100 * self.calc_gsm.value()) / 1000
self.calc_kg.blockSignals(True)
self.calc_kg.setValue(weight)
self.calc_kg.blockSignals(False)
elif sender == self.calc_kg:
kg = self.calc_kg.value()
if self.calc_width.value() > 0 and self.calc_gsm.value() > 0:
m = (kg * 1000 * 100) / (self.calc_width.value() * self.calc_gsm.value())
self.calc_m.blockSignals(True)
self.calc_m.setValue(m)
self.calc_m.blockSignals(False)
self.calc_yard.blockSignals(True)
self.calc_yard.setValue(m / 0.9144)
self.calc_yard.blockSignals(False)
elif sender == self.calc_width or sender == self.calc_gsm:
# 当幅宽或克重改变时,重新计算公斤数(基于当前米数)
m = self.calc_m.value()
if m > 0:
weight = (m * self.calc_width.value() / 100 * self.calc_gsm.value()) / 1000
self.calc_kg.blockSignals(True)
self.calc_kg.setValue(weight)
self.calc_kg.blockSignals(False)
except Exception:
pass
def main():
"""主函数"""
app = QApplication(sys.argv)
# 设置数据库路径
exe_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
db_path = os.path.join(exe_dir, "fabric_library.db")
# 显示登录对话框
login = LoginDialog(db_path)
if login.exec_() == QDialog.Accepted:
# 创建并显示主窗口
window = FabricManager(login.is_admin)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

117
purchase_order_dialog.py Normal file
View File

@@ -0,0 +1,117 @@
"""
采购单生成模块 - 处理采购单的生成和导出
"""
from datetime import datetime
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTextEdit, QMessageBox, QFileDialog, QApplication
)
from PyQt5.QtGui import QFont
from database import get_db_connection
class PurchaseOrderDialog(QDialog):
"""采购单生成对话框"""
def __init__(self, db_path, style_number, quantity, loss_rate):
super().__init__()
self.db_path = db_path
self.style_number = style_number
self.quantity = quantity
self.loss_rate = loss_rate
self.setWindowTitle(f"生成采购单 - {style_number}")
self.resize(900, 700)
self.setup_ui()
self.generate_po_text()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
# 信息标签
info_label = QLabel(
f"<b>款号:</b>{self.style_number}<br>"
f"<b>生产件数:</b>{self.quantity} 件<br>"
f"<b>损耗率:</b>{self.loss_rate*100:.1f}%"
)
info_label.setStyleSheet("font-size: 14px; padding: 10px; background-color: #e8f5e9; border-radius: 8px;")
layout.addWidget(info_label)
# 采购单内容显示
self.po_text = QTextEdit()
self.po_text.setReadOnly(True)
self.po_text.setFont(QFont("Microsoft YaHei", 12))
layout.addWidget(self.po_text)
# 按钮区域
btn_layout = QHBoxLayout()
copy_btn = QPushButton("复制到剪贴板")
copy_btn.clicked.connect(self.copy_to_clipboard)
copy_btn.setStyleSheet("background-color: #2196f3; color: white; padding: 10px; font-weight: bold;")
btn_layout.addWidget(copy_btn)
save_btn = QPushButton("保存为TXT文件")
save_btn.clicked.connect(self.save_to_file)
save_btn.setStyleSheet("background-color: #ff9800; color: white; padding: 10px; font-weight: bold;")
btn_layout.addWidget(save_btn)
layout.addLayout(btn_layout)
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def generate_po_text(self):
"""生成采购单文本"""
text = f"【采购单】\n"
text += f"款号:{self.style_number}\n"
text += f"生产数量:{self.quantity}\n"
text += f"损耗率:{self.loss_rate*100:.1f}%\n"
text += f"生成日期:{datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
text += "="*50 + "\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 = ? AND usage_per_piece > 0
ORDER BY id
''', (self.style_number,))
rows = cursor.fetchall()
for category, fabric_type, 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
text += f"材料:{material_name}\n"
text += f" 单件用量:{usage_per_piece:.3f} {unit}\n"
text += f" 总需采购:{total_usage:.3f} {unit}\n\n"
if not rows:
text += "该款号暂无材料用量记录。\n"
except Exception as e:
text += f"加载失败:{str(e)}"
self.po_text.setPlainText(text)
def copy_to_clipboard(self):
"""复制到剪贴板"""
QApplication.clipboard().setText(self.po_text.toPlainText())
QMessageBox.information(self, "成功", "采购单内容已复制到剪贴板!")
def save_to_file(self):
"""保存为文件"""
default_name = f"采购单_{self.style_number}_{self.quantity}件_{datetime.now().strftime('%Y%m%d')}.txt"
file_path, _ = QFileDialog.getSaveFileName(self, "保存采购单", default_name, "Text Files (*.txt)")
if file_path:
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(self.po_text.toPlainText())
QMessageBox.information(self, "成功", f"采购单已保存至:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", "保存失败: " + str(e))

633
raw_material_dialog.py Normal file
View File

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

222
stock_dialog.py Normal file
View File

@@ -0,0 +1,222 @@
"""
库存管理模块 - 处理原料入库和库存查询
"""
from datetime import datetime
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox, QInputDialog
)
from database import get_db_connection
class StockInDialog(QDialog):
"""独立原料入库管理"""
def __init__(self, db_path):
super().__init__()
self.db_path = db_path
self.setWindowTitle("原料入库记录")
self.resize(900, 600)
self.setup_ui()
self.load_models()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
# 搜索过滤区域
filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("搜索型号/名称:"))
self.search_input = QLineEdit()
self.search_input.textChanged.connect(self.load_models)
filter_layout.addWidget(self.search_input)
refresh_btn = QPushButton("刷新")
refresh_btn.clicked.connect(self.load_models)
filter_layout.addWidget(refresh_btn)
layout.addLayout(filter_layout)
# 数据表格
headers = ["型号/名称", "颜色", "供应商", "单位", "当前剩余库存", "操作"]
self.table = QTableWidget()
self.table.setColumnCount(len(headers))
self.table.setHorizontalHeaderLabels(headers)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
layout.addWidget(self.table)
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def load_models(self):
"""加载面料型号列表"""
keyword = self.search_input.text().strip()
try:
with self.get_conn() as conn:
# 查询面料基础信息
query = "SELECT model, color, supplier, unit FROM fabrics"
params = []
if keyword:
query += " WHERE model LIKE ? OR color LIKE ?"
params = ["%" + keyword + "%", "%" + keyword + "%"]
query += " ORDER BY timestamp DESC"
cursor = conn.execute(query, params)
rows = cursor.fetchall()
# 计算库存数量
model_stock = {}
# 入库数量
cursor_in = conn.execute("SELECT model, COALESCE(SUM(quantity), 0) FROM fabric_stock_in GROUP BY model")
for model, quantity in cursor_in.fetchall():
model_stock[model] = quantity or 0
# 消耗数量
cursor_out = conn.execute("SELECT model, COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption GROUP BY model")
for model, quantity in cursor_out.fetchall():
model_stock[model] = model_stock.get(model, 0) - (quantity or 0)
# 填充表格
self.table.setRowCount(len(rows))
for i, (model, color, supplier, unit) in enumerate(rows):
self.table.setItem(i, 0, QTableWidgetItem(model or ""))
self.table.setItem(i, 1, QTableWidgetItem(color or ""))
self.table.setItem(i, 2, QTableWidgetItem(supplier or ""))
self.table.setItem(i, 3, QTableWidgetItem(unit or ""))
remaining = model_stock.get(model, 0)
self.table.setItem(i, 4, QTableWidgetItem("{:.3f}".format(remaining)))
# 入库按钮
btn = QPushButton("入库")
btn.clicked.connect(lambda _, m=model, u=unit or "": self.do_stock_in(m, u))
self.table.setCellWidget(i, 5, btn)
except Exception as e:
QMessageBox.critical(self, "错误", f"加载数据失败: {str(e)}")
def do_stock_in(self, model, unit):
"""执行入库操作"""
# 输入入库数量
quantity, ok1 = QInputDialog.getDouble(
self, "入库数量", f"{model}】入库数量(单位:{unit}:",
0, 0, 1000000, 3
)
if not ok1 or quantity <= 0:
return
# 输入备注信息
note, ok2 = QInputDialog.getText(
self, "入库备注", "备注(供应商/批次/发票号等,可选):"
)
if not ok2:
note = ""
try:
with self.get_conn() as conn:
# 插入入库记录
conn.execute('''
INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note)
VALUES (?, ?, ?, ?, ?)
''', (model, quantity, unit, datetime.now().strftime('%Y-%m-%d'), note))
conn.commit()
QMessageBox.information(self, "成功", f"已入库 {model}{quantity} {unit}")
self.load_models()
except Exception as e:
QMessageBox.critical(self, "错误", f"入库失败: {str(e)}")
class StockQueryDialog(QDialog):
"""库存查询对话框"""
def __init__(self, db_path):
super().__init__()
self.db_path = db_path
self.setWindowTitle("库存查询")
self.resize(1000, 700)
self.setup_ui()
self.load_stock_data()
def setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
# 搜索区域
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("搜索:"))
self.search_input = QLineEdit()
self.search_input.textChanged.connect(self.load_stock_data)
search_layout.addWidget(self.search_input)
refresh_btn = QPushButton("刷新")
refresh_btn.clicked.connect(self.load_stock_data)
search_layout.addWidget(refresh_btn)
layout.addLayout(search_layout)
# 库存表格
headers = ["类目", "型号", "颜色", "供应商", "单位", "入库总量", "消耗总量", "剩余库存"]
self.stock_table = QTableWidget()
self.stock_table.setColumnCount(len(headers))
self.stock_table.setHorizontalHeaderLabels(headers)
self.stock_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
layout.addWidget(self.stock_table)
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def load_stock_data(self):
"""加载库存数据"""
keyword = self.search_input.text().strip()
try:
with self.get_conn() as conn:
# 查询库存汇总数据
query = """
SELECT
f.category,
f.model,
f.color,
f.supplier,
f.unit,
COALESCE(SUM(si.quantity), 0) as total_in,
COALESCE(SUM(fc.consume_quantity), 0) as total_out,
COALESCE(SUM(si.quantity), 0) - COALESCE(SUM(fc.consume_quantity), 0) as remaining
FROM fabrics f
LEFT JOIN fabric_stock_in si ON f.model = si.model
LEFT JOIN fabric_consumption fc ON f.model = fc.model
"""
params = []
if keyword:
query += " WHERE f.model LIKE ? OR f.category LIKE ? OR f.color LIKE ?"
params = [f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"]
query += " GROUP BY f.model ORDER BY f.category, f.model"
cursor = conn.execute(query, params)
rows = cursor.fetchall()
# 填充表格
self.stock_table.setRowCount(len(rows))
for i, row in enumerate(rows):
category, model, color, supplier, unit, total_in, total_out, remaining = row
self.stock_table.setItem(i, 0, QTableWidgetItem(category or ""))
self.stock_table.setItem(i, 1, QTableWidgetItem(model or ""))
self.stock_table.setItem(i, 2, QTableWidgetItem(color or ""))
self.stock_table.setItem(i, 3, QTableWidgetItem(supplier or ""))
self.stock_table.setItem(i, 4, QTableWidgetItem(unit or ""))
self.stock_table.setItem(i, 5, QTableWidgetItem(f"{total_in:.3f}"))
self.stock_table.setItem(i, 6, QTableWidgetItem(f"{total_out:.3f}"))
self.stock_table.setItem(i, 7, QTableWidgetItem(f"{remaining:.3f}"))
except Exception as e:
QMessageBox.critical(self, "错误", f"加载库存数据失败: {str(e)}")