- Modified load_sub_categories() to show all subcategories when "全部类目" is selected - Fixed category creation logic to support all major categories, not just "布料" - Improved subcategory filtering to work properly with existing data 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1858 lines
78 KiB
Python
1858 lines
78 KiB
Python
"""
|
||
服装布料计算管理器 - 专业版(最终完整修复版)
|
||
- 修复所有 "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, 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"<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, 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_()) |