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