""" 服装布料计算管理器 - 专业版(最终完整修复版) - 修复所有 "name not defined" 错误 - 衣服库和原料库正常打开 - 所有功能完整可用 """ 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 ) 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("全部类型") if major in ("全部类目", ""): self.sub_combo.blockSignals(False) self.load_table() return try: with self.get_conn() as conn: 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 = "辅料" category = "布料-" + sub if major == "布料" and sub else (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, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (self.style_number,)) for category, usage, unit in cursor.fetchall(): self.add_material_row(category, 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, unit in defaults: self.add_material_row(cat, 0, unit) def add_material_row(self, category="", 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 category FROM fabrics WHERE category IS NOT NULL AND category != '' ORDER BY category") categories = cursor.fetchall() for cat_row in categories: cat_combo.addItem(cat_row[0]) except: pass 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("—— 选择类型 ——") 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 LIKE ? ORDER BY model """, (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 = f"{model}" if color: display_text += f"-{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 model and model != "—— 选择型号 ——": # 如果选择了具体型号,使用型号作为category存储 model_data = model_widget.itemData(model_widget.currentIndex()) final_category = model_data if model_data else model elif fabric_type and fabric_type != "—— 选择类型 ——": # 如果只选择了类型,组合类目和类型 final_category = f"{category}-{fabric_type}" else: # 只有类目 final_category = category if not final_category or final_category == "—— 自定义类目 ——": continue usage = usage_widget.value() unit = unit_widget.currentText().strip() or "米" conn.execute("INSERT INTO garment_materials (style_number, category, usage_per_piece, unit) VALUES (?, ?, ?, ?)", (style_number, final_category, 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, 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, usage_per_piece, unit in rows: total_usage = usage_per_piece * self.quantity * (1 + self.loss_rate) text += f"材料:{category}\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)) 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.init_db() 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; } """) # 提前绑定方法,避免 AttributeError self.quick_stock_in = self._quick_stock_in self.generate_purchase_order = self._generate_purchase_order self.record_current_consumption = self._record_current_consumption self.open_library = self._open_library self.open_garment_library = self._open_garment_library self.load_garment_list = self._load_garment_list self.load_garment_materials = self._load_garment_materials self.show_guide = self._show_guide self.convert_units = self._convert_units self.init_ui() self.load_garment_list() 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, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ''', (style_number,)) rows = cursor.fetchall() inserted = 0 for category, 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, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (style_number,)) for cat, usage, unit in cursor.fetchall(): if usage: total = usage * qty * (1 + loss) text += f"{cat}\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 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 ) ''') conn.execute(''' CREATE TABLE IF NOT EXISTS garments ( style_number TEXT PRIMARY KEY, image_path TEXT ) ''') conn.execute(''' CREATE TABLE IF NOT EXISTS garment_materials ( id INTEGER PRIMARY KEY AUTOINCREMENT, style_number TEXT, category TEXT, usage_per_piece REAL, unit TEXT DEFAULT '米' ) ''') conn.execute(''' CREATE TABLE IF NOT EXISTS admin_settings ( key TEXT PRIMARY KEY, value TEXT ) ''') 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 ) ''') 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 ) ''') 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: QMessageBox.critical(self, "数据库错误", "无法初始化数据库:" + 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 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() if __name__ == "__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_())