Files
cangku/main.py
liangweihao d33216f9b9 添加库存不足检查功能
在记录消耗到库存功能中增加了库存充足性验证,确保只有在所有原料库存充足时才允许记录消耗。

主要改进:
- 在记录消耗前先检查所有原料的当前库存
- 如果任何原料库存不足,显示详细的库存不足信息并中止操作
- 提供清晰的提示信息,显示每种原料所需数量和实际库存数量
- 只有所有原料库存都充足时才执行记录操作
- 优化成功提示信息,显示记录的原料种类数量

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 00:33:51 +08:00

554 lines
23 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.

"""
服装布料计算管理器 - 专业版(重构版主程序)
- 模块化设计,代码分离
- 所有功能完整可用
"""
import sys
import os
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit, QMessageBox,
QGroupBox, QDoubleSpinBox, QSpinBox, QDialog, QScrollArea
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
# 导入自定义模块
from database import DatabaseManager, get_db_connection
from login_dialog import LoginDialog
from stock_dialog import StockInDialog
from raw_material_dialog import RawMaterialLibraryDialog
from garment_dialogs import GarmentLibraryDialog
from purchase_order_dialog import PurchaseOrderDialog
class FabricManager(QMainWindow):
"""主应用程序窗口"""
def __init__(self, is_admin=False):
super().__init__()
self.is_admin = is_admin
# 设置数据库路径
exe_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
self.db_path = os.path.join(exe_dir, "fabric_library.db")
# 初始化数据库
self.db_manager = DatabaseManager(self.db_path)
# 设置窗口
mode_text = "(管理员模式)" if is_admin else "(普通模式)"
self.setWindowTitle("服装布料计算管理器 - 专业版 " + mode_text)
self.resize(1300, 800)
# 设置样式
self.setStyleSheet("""
QMainWindow { background-color: #f0fff8; }
QGroupBox { font-weight: bold; color: #2e8b57; border: 2px solid #90ee90; border-radius: 10px; margin-top: 10px; padding-top: 8px; background-color: #ffffff; }
QPushButton { background-color: #4caf50; color: white; padding: 10px; border-radius: 8px; font-size: 13px; font-weight: bold; }
QTextEdit { background-color: #e8f5e9; border: 2px solid #a5d6a7; border-radius: 8px; padding: 10px; font-size: 13px; }
QLineEdit, QDoubleSpinBox, QSpinBox, QComboBox { padding: 6px; border: 2px solid #a5d6a7; border-radius: 6px; font-size: 13px; }
QLabel { font-size: 13px; color: #2e8b57; }
""")
self.init_ui()
self.load_garment_list()
def get_conn(self):
"""获取数据库连接"""
return get_db_connection(self.db_path)
def init_ui(self):
"""初始化用户界面"""
scroll = QScrollArea()
scroll.setWidgetResizable(True)
central_widget = QWidget()
scroll.setWidget(central_widget)
self.setCentralWidget(scroll)
main_layout = QVBoxLayout(central_widget)
# 标题
title = QLabel("服装布料计算管理器 - 专业版")
title.setFont(QFont("Microsoft YaHei", 18, QFont.Bold))
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("color: #228b22; padding: 15px;")
main_layout.addWidget(title)
# 顶部按钮区域
top_buttons = QHBoxLayout()
guide_btn = QPushButton("📖 查看使用说明")
guide_btn.clicked.connect(self.show_guide)
top_buttons.addWidget(guide_btn)
garment_btn = QPushButton("👔 衣服库管理")
garment_btn.clicked.connect(self.open_garment_library)
top_buttons.addWidget(garment_btn)
library_btn = QPushButton("🗄️ 原料库管理")
library_btn.clicked.connect(self.open_library)
top_buttons.addWidget(library_btn)
stock_in_btn = QPushButton("📦 快速原料入库")
stock_in_btn.clicked.connect(self.quick_stock_in)
top_buttons.addWidget(stock_in_btn)
top_buttons.addStretch()
main_layout.addLayout(top_buttons)
# 主要内容区域
content_layout = QHBoxLayout()
# 左侧区域
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
# 批量计算组
calc_group = QGroupBox("批量计算(按衣服款号)")
calc_layout = QGridLayout()
calc_layout.setVerticalSpacing(12)
calc_layout.addWidget(QLabel("选择衣服款号:"), 0, 0, Qt.AlignRight)
self.garment_combo = QComboBox()
self.garment_combo.setEditable(True)
self.garment_combo.currentIndexChanged.connect(self.load_garment_materials)
calc_layout.addWidget(self.garment_combo, 0, 1, 1, 2)
calc_layout.addWidget(QLabel("生产件数:"), 1, 0, Qt.AlignRight)
self.quantity_input = QSpinBox()
self.quantity_input.setRange(1, 1000000)
self.quantity_input.setValue(1000)
calc_layout.addWidget(self.quantity_input, 1, 1)
calc_layout.addWidget(QLabel("损耗率 (%):"), 2, 0, Qt.AlignRight)
self.loss_input = QDoubleSpinBox()
self.loss_input.setRange(0, 50)
self.loss_input.setValue(5)
calc_layout.addWidget(self.loss_input, 2, 1)
calc_btn = QPushButton("计算本次总用量")
calc_btn.clicked.connect(self.load_garment_materials)
calc_layout.addWidget(calc_btn, 3, 0, 1, 3)
calc_group.setLayout(calc_layout)
left_layout.addWidget(calc_group)
# 结果显示组
result_group = QGroupBox("本次生产用量明细")
result_layout = QVBoxLayout()
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setMinimumHeight(400)
result_layout.addWidget(self.result_text)
# 操作按钮
btn_layout = QHBoxLayout()
po_btn = QPushButton("生成采购单")
po_btn.clicked.connect(self.generate_purchase_order)
po_btn.setStyleSheet("background-color: #ff9800; font-weight: bold; padding: 12px;")
btn_layout.addWidget(po_btn)
record_btn = QPushButton("记录本次消耗到库存")
record_btn.clicked.connect(self.record_current_consumption)
record_btn.setStyleSheet("background-color: #e91e63; color: white; font-weight: bold; padding: 12px;")
btn_layout.addWidget(record_btn)
result_layout.addLayout(btn_layout)
result_group.setLayout(result_layout)
left_layout.addWidget(result_group)
content_layout.addWidget(left_widget, stretch=3)
# 右侧单位换算计算器
right_group = QGroupBox("单位换算计算器")
right_layout = QGridLayout()
right_layout.setVerticalSpacing(10)
right_layout.addWidget(QLabel("米数:"), 0, 0, Qt.AlignRight)
self.calc_m = QDoubleSpinBox()
self.calc_m.setRange(0, 100000)
self.calc_m.setDecimals(3)
self.calc_m.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_m, 0, 1)
right_layout.addWidget(QLabel("码数:"), 1, 0, Qt.AlignRight)
self.calc_yard = QDoubleSpinBox()
self.calc_yard.setRange(0, 100000)
self.calc_yard.setDecimals(3)
self.calc_yard.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_yard, 1, 1)
right_layout.addWidget(QLabel("公斤:"), 2, 0, Qt.AlignRight)
self.calc_kg = QDoubleSpinBox()
self.calc_kg.setRange(0, 100000)
self.calc_kg.setDecimals(6)
self.calc_kg.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_kg, 2, 1)
right_layout.addWidget(QLabel("幅宽 (cm):"), 3, 0, Qt.AlignRight)
self.calc_width = QDoubleSpinBox()
self.calc_width.setRange(50, 300)
self.calc_width.setValue(150)
self.calc_width.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_width, 3, 1)
right_layout.addWidget(QLabel("克重 (g/m²):"), 4, 0, Qt.AlignRight)
self.calc_gsm = QDoubleSpinBox()
self.calc_gsm.setRange(50, 1000)
self.calc_gsm.setValue(200)
self.calc_gsm.valueChanged.connect(self.convert_units)
right_layout.addWidget(self.calc_gsm, 4, 1)
right_group.setLayout(right_layout)
content_layout.addWidget(right_group, stretch=1)
main_layout.addLayout(content_layout)
main_layout.addStretch()
def quick_stock_in(self):
"""快速入库"""
dialog = StockInDialog(self.db_path)
dialog.exec_()
def generate_purchase_order(self):
"""生成采购单"""
style_number = self.garment_combo.currentText().strip()
if not style_number:
QMessageBox.warning(self, "提示", "请先选择或计算一款号!")
return
quantity = self.quantity_input.value()
loss_rate = self.loss_input.value() / 100
dialog = PurchaseOrderDialog(self.db_path, style_number, quantity, loss_rate)
dialog.exec_()
def record_current_consumption(self):
"""记录当前消耗"""
style_number = self.garment_combo.currentText().strip()
if not style_number:
QMessageBox.warning(self, "提示", "请先选择一款号并计算用量!")
return
quantity = self.quantity_input.value()
loss_rate = self.loss_input.value() / 100
try:
with self.get_conn() as conn:
cursor = conn.execute('''
SELECT category, fabric_type, usage_per_piece, unit
FROM garment_materials
WHERE style_number = ?
''', (style_number,))
rows = cursor.fetchall()
# 第一步:检查所有原料库存是否充足
insufficient_materials = []
materials_to_record = []
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
# 检查当前库存
stock_info = conn.execute('''
SELECT
COALESCE(SUM(CASE WHEN si.is_deleted = 0 THEN si.quantity ELSE 0 END), 0) AS total_in,
COALESCE(SUM(CASE WHEN c.is_deleted = 0 THEN c.consume_quantity ELSE 0 END), 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
WHERE f.model = ?
GROUP BY f.model
''', (category,)).fetchone()
if stock_info:
total_in, total_out = stock_info
current_stock = total_in - total_out
else:
current_stock = 0
# 如果库存不足,记录下来
if current_stock < consume_qty:
material_name = category or "未命名材料"
insufficient_materials.append(
f"{material_name}: 需要 {consume_qty:.3f} {final_unit},库存仅剩 {current_stock:.3f} {final_unit}"
)
else:
# 库存充足,加入待记录列表
materials_to_record.append((
style_number, category, usage_per_piece, quantity,
loss_rate, consume_qty, final_unit
))
# 如果有库存不足的材料,提示用户并中止操作
if insufficient_materials:
message = "以下原料库存不足,无法记录消耗:\n\n" + "\n".join(insufficient_materials)
QMessageBox.warning(self, "库存不足", message)
return
# 第二步:所有库存都充足,执行记录
inserted = 0
for material_data in materials_to_record:
style_num, model, single_usage, qty, loss, consume_qty, final_unit = material_data
conn.execute('''
INSERT INTO fabric_consumption
(style_number, model, single_usage, quantity_made, loss_rate, consume_quantity, consume_date, unit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (style_num, model, single_usage, qty, loss, consume_qty, datetime.now().strftime('%Y-%m-%d'), final_unit))
inserted += 1
conn.commit()
if inserted > 0:
QMessageBox.information(self, "成功", f"本次生产消耗已记录到库存!共记录 {inserted} 种原料。")
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, model, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (style_number,))
for category, fabric_type, model, usage, unit in cursor.fetchall():
if usage:
total = usage * qty * (1 + loss)
# 显示材料名称:优先显示型号,否则显示类目-类型,最后只显示类目
if model:
material_name = model
elif fabric_type:
material_name = f"{category}-{fabric_type}" if category else fabric_type
else:
material_name = category or "未命名材料"
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 = ? AND (is_deleted IS NULL OR is_deleted = 0)", (fabric_model,))
fabric_info = cursor.fetchone()
if fabric_info and fabric_info[0] and fabric_info[1]:
width, gsm = fabric_info
if width > 0 and gsm > 0:
if from_unit == "" and to_unit == "公斤":
# 米转公斤:米数 * 幅宽(m) * 克重(kg/m²)
return value * (width / 100) * (gsm / 1000)
elif from_unit == "" and to_unit == "公斤":
# 码转公斤:先转米,再转公斤
meters = value * 0.9144
return meters * (width / 100) * (gsm / 1000)
elif from_unit == "公斤" and to_unit == "":
# 公斤转米:公斤 / (幅宽(m) * 克重(kg/m²))
return value / ((width / 100) * (gsm / 1000))
elif from_unit == "公斤" and to_unit == "":
# 公斤转码:先转米,再转码
meters = value / ((width / 100) * (gsm / 1000))
return meters / 0.9144
except Exception:
pass
# 如果无法转换,返回原值
return value
def convert_units(self):
"""单位换算"""
sender = self.sender()
try:
if sender == self.calc_m:
m = self.calc_m.value()
self.calc_yard.blockSignals(True)
self.calc_yard.setValue(m / 0.9144)
self.calc_yard.blockSignals(False)
weight = (m * self.calc_width.value() / 100 * self.calc_gsm.value()) / 1000
self.calc_kg.blockSignals(True)
self.calc_kg.setValue(weight)
self.calc_kg.blockSignals(False)
elif sender == self.calc_yard:
yard = self.calc_yard.value()
m = yard * 0.9144
self.calc_m.blockSignals(True)
self.calc_m.setValue(m)
self.calc_m.blockSignals(False)
weight = (m * self.calc_width.value() / 100 * self.calc_gsm.value()) / 1000
self.calc_kg.blockSignals(True)
self.calc_kg.setValue(weight)
self.calc_kg.blockSignals(False)
elif sender == self.calc_kg:
kg = self.calc_kg.value()
if self.calc_width.value() > 0 and self.calc_gsm.value() > 0:
m = (kg * 1000 * 100) / (self.calc_width.value() * self.calc_gsm.value())
self.calc_m.blockSignals(True)
self.calc_m.setValue(m)
self.calc_m.blockSignals(False)
self.calc_yard.blockSignals(True)
self.calc_yard.setValue(m / 0.9144)
self.calc_yard.blockSignals(False)
elif sender == self.calc_width or sender == self.calc_gsm:
# 当幅宽或克重改变时,重新计算公斤数(基于当前米数)
m = self.calc_m.value()
if m > 0:
weight = (m * self.calc_width.value() / 100 * self.calc_gsm.value()) / 1000
self.calc_kg.blockSignals(True)
self.calc_kg.setValue(weight)
self.calc_kg.blockSignals(False)
except Exception:
pass
def main():
"""主函数"""
app = QApplication(sys.argv)
# 设置数据库路径
exe_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
db_path = os.path.join(exe_dir, "fabric_library.db")
# 显示登录对话框
login = LoginDialog(db_path)
if login.exec_() == QDialog.Accepted:
# 创建并显示主窗口
window = FabricManager(login.is_admin)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()