From 033a1acef3e6d8023ff5d55e23c1ffc7114ca6bf Mon Sep 17 00:00:00 2001 From: liangweihao <734499798@qq.com> Date: Tue, 23 Dec 2025 00:30:36 +0800 Subject: [PATCH] 1223 --- CLAUDE.md | 104 ++ __pycache__/database.cpython-312.pyc | Bin 0 -> 15756 bytes __pycache__/garment_dialogs.cpython-312.pyc | Bin 0 -> 39557 bytes __pycache__/login_dialog.cpython-312.pyc | Bin 0 -> 9055 bytes .../purchase_order_dialog.cpython-312.pyc | Bin 0 -> 7398 bytes .../raw_material_dialog.cpython-312.pyc | Bin 0 -> 40560 bytes __pycache__/stock_dialog.cpython-312.pyc | Bin 0 -> 13086 bytes database.py | 342 +++++ fabric_library.db | Bin 0 -> 45056 bytes fabric_manager_pro.py | 1351 +---------------- garment_dialogs.py | 777 ++++++++++ login_dialog.py | 162 ++ main.py | 504 ++++++ purchase_order_dialog.py | 117 ++ raw_material_dialog.py | 633 ++++++++ stock_dialog.py | 222 +++ 16 files changed, 2870 insertions(+), 1342 deletions(-) create mode 100644 CLAUDE.md create mode 100644 __pycache__/database.cpython-312.pyc create mode 100644 __pycache__/garment_dialogs.cpython-312.pyc create mode 100644 __pycache__/login_dialog.cpython-312.pyc create mode 100644 __pycache__/purchase_order_dialog.cpython-312.pyc create mode 100644 __pycache__/raw_material_dialog.cpython-312.pyc create mode 100644 __pycache__/stock_dialog.cpython-312.pyc create mode 100644 database.py create mode 100644 fabric_library.db create mode 100644 garment_dialogs.py create mode 100644 login_dialog.py create mode 100644 main.py create mode 100644 purchase_order_dialog.py create mode 100644 raw_material_dialog.py create mode 100644 stock_dialog.py 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 0000000000000000000000000000000000000000..cf3798df7e3340cfbde54e12d8342f8f96c5680c GIT binary patch literal 15756 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

C4;{h8zWspNd1%GjUkJn4f_|17^AC zexin_-Z4uyKj13!RRL&_6y(Eaij0^Y<~(o;h0Kg>nq7KF5w`%q1ik2?B300b%yHsZ z=hJ`pZu;Z%>HgC>$dI8rAKpd6~1tU01F?YelheH=8 z*)n6Nn+t-Y7qosh$h9D-*&A#RaRE0Pyz4~r-bP4%TpgW^V8V`+Flj?a8w4U6yiTS; zfZ+@3@C6($598gG^9d<@+M7(04$7dvX{D7QkE)N94}csyed($yw7;I zqBd2rG+wcE)KVNPam1Ev|J0jmc|G3p`XjAoPC@^SKGVSRKNzUu(gE+#s?W+6i`K3x zZmAm54IPO+|5DPjb<|QaR$h}TuZx%00hxg%v8r{qyHbs<@y1r5Qc=)f*hdDcMJg9O zgNx1whTNZ(J(ojiYtphUhf=JhI##>w)9L@v#-L@N^|FXURHx>)`0r1N0P*&cVc|3#)~ z!@oOH8=diu&RqNP?4~H&Hcqi>Piczw4!xQ**XNj`eVO}QwdG*IiQEeFX#Dl=|P~F_d{>yV-4ix+Nka* zxKFNJ{9vr6TY#yiJ{fitDa0^R4fuS;83?5}J?w`2c$3aaJ2RUgChXGl4S@wPMi{F5;B0dOnVZ2Jc)PGkmxomu| ztgrBkRgD9iQ`VZewI*eC#I25mbxF$F5VtlYt;>Jy{14Y1SE_k`ym^13`3I?Hcf8r1 zX!i7(M$6`;%If1~^}{`4t?QoII%xjPTs>;5>Dzp22e|VyFZaKEddCrM`8rSq3sX_77FDL z;U3oScexxihZP&K`n^Fap<*$c!v^&ujEH|cL4Bnm`tpa?dVTf7*J+J@*+W9~wujaN zeZ|A_3jJRC;mQjAe)@k`7U*}q7+jTb{KbBpC_>3qdd(4ru2g4zm)$&f%8H}5{=2CI)MC$4p!lGc^KUP!yNo;wEf zaJ8v$12hk`G)@0OqoH+Q6D_Uznu6vLHjfEK+y0A!66q@%dM7>b++#}Yh$R0Hgkf*+ literal 0 HcmV?d00001 diff --git a/__pycache__/purchase_order_dialog.cpython-312.pyc b/__pycache__/purchase_order_dialog.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d700e9461d3db9bb62602e1e4c7aac0353e3244 GIT binary patch literal 7398 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 WbKw6U6Al>F#lwgHq{3g4%l`rMhFfz0 literal 0 HcmV?d00001 diff --git a/__pycache__/stock_dialog.cpython-312.pyc b/__pycache__/stock_dialog.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c23ffbd9f24063d83e98edf8b5ef7a9f6257b693 GIT binary patch literal 13086 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c870d1d30308268a30c0594f954d969289066081 GIT binary patch literal 45056 zcmeI*&2QUe90%~{vD>6ch`g#mRFz8RfYPW7&C3d?yO525D!OizOj=Ktn|Pg7zodTb zjUsVa%jgg{4wEKvL>wwEgA3B&58$*Tmq|#I+OnZ>X5+$>x3*s6B$w`xd>_Hu+d4iac7CZ?C>7;W;oL$|K0aTbipl{+ugLR@rQ%1$CHd0Q{Ds2O zvixyzSuR{IU7TO!&0Z)jmQDvQ&D;$`Q!LM1(Hyx{ywahnX;(C(OC6?LtGqprsjHgI zLs|%a=XFoDT;1JJOtqr5LI|#I*o-NT%6A|-H``#=lveXSR2AGBTg=qv zawMFXo~B>wM|+*Qc6m+FTV387jkfFW*31*@>|T1XN4Eov;2x`bPWhV3G^Gby%Wm?w zx4oGThZCt3-F&TXt5!_iQkdqtJl&XoY{Vz)pVt9)P1~sV?a_58IJE1k;Wg5w{pUS9 z5=vZ5QKDNF?K8G+@cmvz^<29?uJ}KclJ$>AE`>t1JX)@Y+FY1v>z-znwHHRizF=RN zIc2G)cA77l+WcZToS2!Rw^H>sS5?Q%#s%zzLo0)_Bmp>?eSh&1Ul9T)QeonSehnp!eoS2-X zn{OTUZ?UtXcG|21^0AF}Z&|CG^$1n>T8GWp%upzCeljR^o2kH*Y-?zE%ckMP#01@* ztq0ctXU!ME7g}>-hsX}MP`!oZ%YwtM*!ym0_x4x&_wFZyGlO~6s-buN)+^hF-R+z4 zhF;;Ooxhh>nc1a=qq(YXC{@0*&@KB`JY%h=iUDaFtm5jX#$45`c1V%55K0s#f>NZ} z$x604Ao5+Uk=pK7oIeF=n@GR%KPV7@00bZa0SG_<0uX=z1Rwwb2%MxqUWiS+8JB8r zF_zEf>Sgzrt-U*U)2gcG_))A}CO4bT=F+*moXN~(X6G{Roujd_>G-%HP&&>R$`{hj zMIL{9d;k9S{@vgH`uZobLv{pV_tuYlcfLPpBPZwbbD8|ynRja^`331Ik)BG=PBK8W z4FL#100Izz00bZa0SG_<0uX?}OA?5Q6SR47K?sRs5q`{|cK)AA&k28^KmY;|fB*y_ z009U<00Izz00bb=cLGCHlt{LA{+~+!5&l4d00bZa0SG_<0uX=z1Rwwb2tc6!1cpQ@ zl5L#-e?+85{TCo2f&c^{009U<00Izz00bZa0SF9^z#BA7q_HF&%kqEs_gx@hm|s?) z@%#S?k)8~W2qJ|51Rwwb2tWV=5P$##AOHaf^sm6HRGf)4ZxC^KmyyQx{~r)(UwY8L zu_H1FKmY;|fB*y_009U<00IzzKwk;GE{Mr-kJav%sWNuMb}Ia&|C!l$5>!l$H*RU_ zpV7Gf|3f1EBR%XZ2?PTH2tWV=5P$##AOHafKmY;|7!ZL`F-d*L0)$a974aVhsGa{O zrv?NBVL|``5P$##AOHafKmY;|fB*yrQ~|K E|A$+)(f|Me literal 0 HcmV?d00001 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