Files
cangku/fabric_manager_pro.py
2025-12-22 20:34:38 +08:00

1902 lines
81 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
服装布料计算管理器 - 专业版(最终完整修复版)
- 修复所有 "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("<b>请选择登录模式(默认密码均为 123456</b>"))
admin_layout = QHBoxLayout()
admin_layout.addWidget(QLabel("管理员模式密码:"))
self.admin_input = QLineEdit()
self.admin_input.setEchoMode(QLineEdit.Password)
self.admin_input.setPlaceholderText("默认 123456")
admin_layout.addWidget(self.admin_input)
admin_login = QPushButton("登录管理员模式")
admin_login.clicked.connect(lambda: self.login_mode(True))
admin_layout.addWidget(admin_login)
admin_change = QPushButton("修改管理员密码")
admin_change.clicked.connect(self.change_admin_password)
admin_layout.addWidget(admin_change)
layout.addLayout(admin_layout)
user_layout = QHBoxLayout()
user_layout.addWidget(QLabel("普通用户模式密码:"))
self.user_input = QLineEdit()
self.user_input.setEchoMode(QLineEdit.Password)
self.user_input.setPlaceholderText("默认 123456")
user_layout.addWidget(self.user_input)
user_login = QPushButton("登录普通用户模式")
user_login.clicked.connect(lambda: self.login_mode(False))
user_layout.addWidget(user_login)
user_change = QPushButton("修改普通用户密码")
user_change.clicked.connect(self.change_user_password)
user_layout.addWidget(user_change)
layout.addLayout(user_layout)
layout.addStretch()
exit_btn = QPushButton("退出程序")
exit_btn.clicked.connect(self.reject)
layout.addWidget(exit_btn)
def get_conn(self):
return get_db_connection(self.db_path)
def get_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"<b>款号:</b>{style_number}<br>"
f"<b>生产件数:</b>{quantity} 件<br>"
f"<b>损耗率:</b>{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))
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, 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 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,
fabric_type TEXT,
usage_per_piece REAL,
unit TEXT DEFAULT ''
)
''')
# 添加fabric_type列如果不存在
try:
conn.execute("ALTER TABLE garment_materials ADD COLUMN fabric_type TEXT")
except:
pass # 列已存在
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_())