diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..7a21889
--- /dev/null
+++ b/CLAUDE.md
@@ -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
\ No newline at end of file
diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc
new file mode 100644
index 0000000..cf3798d
Binary files /dev/null and b/__pycache__/database.cpython-312.pyc differ
diff --git a/__pycache__/garment_dialogs.cpython-312.pyc b/__pycache__/garment_dialogs.cpython-312.pyc
new file mode 100644
index 0000000..eb5f96f
Binary files /dev/null and b/__pycache__/garment_dialogs.cpython-312.pyc differ
diff --git a/__pycache__/login_dialog.cpython-312.pyc b/__pycache__/login_dialog.cpython-312.pyc
new file mode 100644
index 0000000..36eb806
Binary files /dev/null and b/__pycache__/login_dialog.cpython-312.pyc differ
diff --git a/__pycache__/purchase_order_dialog.cpython-312.pyc b/__pycache__/purchase_order_dialog.cpython-312.pyc
new file mode 100644
index 0000000..8d700e9
Binary files /dev/null and b/__pycache__/purchase_order_dialog.cpython-312.pyc differ
diff --git a/__pycache__/raw_material_dialog.cpython-312.pyc b/__pycache__/raw_material_dialog.cpython-312.pyc
new file mode 100644
index 0000000..ee6bedb
Binary files /dev/null and b/__pycache__/raw_material_dialog.cpython-312.pyc differ
diff --git a/__pycache__/stock_dialog.cpython-312.pyc b/__pycache__/stock_dialog.cpython-312.pyc
new file mode 100644
index 0000000..c23ffbd
Binary files /dev/null and b/__pycache__/stock_dialog.cpython-312.pyc differ
diff --git a/database.py b/database.py
new file mode 100644
index 0000000..135a587
--- /dev/null
+++ b/database.py
@@ -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
\ No newline at end of file
diff --git a/fabric_library.db b/fabric_library.db
new file mode 100644
index 0000000..c870d1d
Binary files /dev/null and b/fabric_library.db differ
diff --git a/fabric_manager_pro.py b/fabric_manager_pro.py
index abb68cf..9022fdc 100644
--- a/fabric_manager_pro.py
+++ b/fabric_manager_pro.py
@@ -6,1356 +6,23 @@
"""
import sys
-import sqlite3
import os
from datetime import datetime
-from PIL import Image
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit, QMessageBox,
- QGroupBox, QTableWidget, QTableWidgetItem, QHeaderView,
- QDoubleSpinBox, QSpinBox, QDialog, QFileDialog, QTabWidget, QScrollArea, QInputDialog
+ QGroupBox, QDoubleSpinBox, QSpinBox, QDialog, QScrollArea
)
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QFont, QPixmap
-
-
-def get_db_connection(db_path):
- return sqlite3.connect(db_path, timeout=30)
-
-
-class LoginDialog(QDialog):
- def __init__(self, db_path):
- super().__init__()
- self.db_path = db_path
- self.setWindowTitle("选择模式并登录")
- self.resize(450, 350)
- self.setModal(True)
-
- layout = QVBoxLayout(self)
- layout.addWidget(QLabel("请选择登录模式(默认密码均为 123456)"))
-
- 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_password(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 "123456"
- except:
- return "123456"
-
- def set_password(self, key, new_pwd):
- try:
- with self.get_conn() as conn:
- conn.execute("INSERT OR REPLACE INTO admin_settings (key, value) VALUES (?, ?)", (key, new_pwd))
- conn.commit()
- except Exception as e:
- QMessageBox.critical(self, "错误", "密码保存失败: " + str(e))
-
- def change_admin_password(self):
- old_pwd = self.get_password("admin_password")
- 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
- self.set_password("admin_password", new_pwd1)
- QMessageBox.information(self, "成功", "管理员密码修改成功!")
-
- def change_user_password(self):
- old_pwd = self.get_password("user_password")
- 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
- self.set_password("user_password", new_pwd1)
- QMessageBox.information(self, "成功", "普通用户密码修改成功!")
-
- def login_mode(self, is_admin):
- key = "admin_password" if is_admin else "user_password"
- input_pwd = self.admin_input.text().strip() if is_admin else self.user_input.text().strip()
- correct_pwd = self.get_password(key)
- if input_pwd == correct_pwd:
- self.is_admin = is_admin
- self.accept()
- else:
- QMessageBox.warning(self, "错误", "密码错误,请重试!")
-
-
-class StockInDialog(QDialog):
- """独立原料入库管理"""
- def __init__(self, db_path):
- super().__init__()
- self.db_path = db_path
- self.setWindowTitle("原料入库记录")
- self.resize(900, 600)
-
- 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)
-
- self.load_models()
-
- 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 m, q in cursor_in.fetchall():
- model_stock[m] = q or 0
-
- cursor_out = conn.execute("SELECT model, COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption GROUP BY model")
- for m, q in cursor_out.fetchall():
- model_stock[m] = model_stock.get(m, 0) - (q or 0)
-
- self.table.setRowCount(len(rows))
- for i, (model, color, supplier, unit) in enumerate(rows):
- self.table.setItem(i, 0, QTableWidgetItem(model))
- 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, "错误", 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:
- return
-
- 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 or ""))
- conn.commit()
- QMessageBox.information(self, "成功", f"已入库 {model}:{quantity} {unit}")
- self.load_models()
- except Exception as e:
- QMessageBox.critical(self, "错误", str(e))
-
-
-class RawMaterialLibraryDialog(QDialog):
- def __init__(self, db_path, is_admin=False):
- super().__init__()
- self.db_path = db_path
- self.is_admin = is_admin
- self.current_edit_model = None
- self.setWindowTitle("原料库管理")
- self.resize(1400, 700)
-
- 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 = 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)
-
- tabs.addTab(list_tab, "原料列表")
-
- 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)
-
- tabs.addTab(add_tab, "新增/编辑原料")
-
- 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)
-
- tabs.addTab(stock_tab, "库存跟踪")
-
- self.refresh_filters_and_table()
- self.load_add_major_categories()
- self.load_stock_table()
-
- 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])
- 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])
-
- # 添加默认类目
- default_majors = {"布料", "辅料", "其他"}
- majors.update(default_majors)
-
- 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() # 清理所有单元格内容,包括widget
- 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()
-
-
-class GarmentLibraryDialog(QDialog):
- def __init__(self, db_path):
- super().__init__()
- self.db_path = db_path
- self.setWindowTitle("衣服款号管理")
- self.resize(1300, 750)
-
- 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)
-
- self.load_garments()
-
- 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))
-
- 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)
-
- layout = QVBoxLayout(self)
-
- basic_layout = QGridLayout()
- basic_layout.addWidget(QLabel("款号:"), 0, 0, Qt.AlignRight)
- self.style_input = QLineEdit()
- if style_number:
- self.style_input.setText(style_number)
- self.style_input.setEnabled(not 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)
-
- if style_number:
- self.load_garment_data()
-
- 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():
- # 如果是旧数据(category包含"-"),需要拆分
- if category and "-" in category and not fabric_type:
- parts = category.split("-", 1)
- category = parts[0]
- fabric_type = parts[1] if len(parts) > 1 else ""
- self.add_material_row(category or "", fabric_type or "", usage or 0, unit or "米")
- 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="米"):
- 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)
-
- cat_combo.currentTextChanged.connect(lambda text, r=row: self.on_category_changed(text, r))
- self.material_table.setCellWidget(row, 0, cat_combo)
-
- # 列1: 类型下拉框
- type_combo = QComboBox()
- type_combo.setEditable(True)
- type_combo.addItem("—— 选择类型 ——")
- if fabric_type:
- type_combo.setCurrentText(fabric_type)
- 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 = QComboBox()
- model_combo.setEditable(True)
- model_combo.addItem("—— 选择型号 ——")
- 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)
-
- 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("—— 选择型号 ——")
-
- if not category_text or category_text == "—— 自定义类目 ——":
- return
-
- try:
- with self.get_conn() as conn:
- # 根据类目获取对应的类型(从category字段中提取)
- 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))
- 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)
-
- # 清空型号下拉框
- model_combo.clear()
- model_combo.addItem("—— 选择型号 ——")
-
- category_text = cat_combo.currentText()
- if not category_text or category_text == "—— 自定义类目 ——" or not type_text or type_text == "—— 选择类型 ——":
- return
-
- try:
- with self.get_conn() as conn:
- # 根据类目和类型获取对应的型号,现在需要匹配分开的字段或组合的旧格式
- cursor = conn.execute("""
- SELECT model, color, unit
- FROM fabrics
- WHERE category = ? OR category = ? OR category LIKE ?
- ORDER BY model
- """, (category_text, f"{category_text}-{type_text}", f"{category_text}-{type_text}-%"))
-
- 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
-
- def on_model_selected(self, model_text, row):
- """当型号选择时,自动设置单位"""
- if not model_text or model_text == "—— 选择型号 ——":
- return
-
- unit_combo = self.material_table.cellWidget(row, 4)
- model_combo = self.material_table.cellWidget(row, 2)
-
- # 获取选中项的数据
- 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 unit FROM fabrics WHERE model = ?", (model,))
- row_db = cursor.fetchone()
- if row_db and row_db[0]:
- unit_combo.setCurrentText(row_db[0])
- 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))
-
-
-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)
-
- layout = QVBoxLayout(self)
-
- info_label = QLabel(
- f"款号:{style_number}
"
- f"生产件数:{quantity} 件
"
- f"损耗率:{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)
-
- self.generate_po_text()
-
- 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))
+from PyQt5.QtGui import QFont
+
+from database import 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):
diff --git a/garment_dialogs.py b/garment_dialogs.py
new file mode 100644
index 0000000..e782b4d
--- /dev/null
+++ b/garment_dialogs.py
@@ -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))
\ No newline at end of file
diff --git a/login_dialog.py b/login_dialog.py
new file mode 100644
index 0000000..746efd3
--- /dev/null
+++ b/login_dialog.py
@@ -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("请选择登录模式(默认密码均为 123456)"))
+
+ # 管理员登录区域
+ 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, "错误", "密码错误,请重试!")
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..1b1234e
--- /dev/null
+++ b/main.py
@@ -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()
\ No newline at end of file
diff --git a/purchase_order_dialog.py b/purchase_order_dialog.py
new file mode 100644
index 0000000..e9f69ff
--- /dev/null
+++ b/purchase_order_dialog.py
@@ -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"款号:{self.style_number}
"
+ f"生产件数:{self.quantity} 件
"
+ f"损耗率:{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))
\ No newline at end of file
diff --git a/raw_material_dialog.py b/raw_material_dialog.py
new file mode 100644
index 0000000..7dc96d4
--- /dev/null
+++ b/raw_material_dialog.py
@@ -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()
\ No newline at end of file
diff --git a/stock_dialog.py b/stock_dialog.py
new file mode 100644
index 0000000..3980d11
--- /dev/null
+++ b/stock_dialog.py
@@ -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)}")
\ No newline at end of file