1223
This commit is contained in:
104
CLAUDE.md
Normal file
104
CLAUDE.md
Normal 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
|
||||||
BIN
__pycache__/database.cpython-312.pyc
Normal file
BIN
__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/garment_dialogs.cpython-312.pyc
Normal file
BIN
__pycache__/garment_dialogs.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/login_dialog.cpython-312.pyc
Normal file
BIN
__pycache__/login_dialog.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/purchase_order_dialog.cpython-312.pyc
Normal file
BIN
__pycache__/purchase_order_dialog.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/raw_material_dialog.cpython-312.pyc
Normal file
BIN
__pycache__/raw_material_dialog.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/stock_dialog.cpython-312.pyc
Normal file
BIN
__pycache__/stock_dialog.cpython-312.pyc
Normal file
Binary file not shown.
342
database.py
Normal file
342
database.py
Normal 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
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
777
garment_dialogs.py
Normal 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
162
login_dialog.py
Normal 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
504
main.py
Normal 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
117
purchase_order_dialog.py
Normal 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
633
raw_material_dialog.py
Normal 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
222
stock_dialog.py
Normal 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)}")
|
||||||
Reference in New Issue
Block a user