""" 采购计划Tab页面 """ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QDialog, QFormLayout, QComboBox, QDateEdit, QRadioButton, QButtonGroup, QDoubleSpinBox, QFileDialog, QTextEdit ) from PyQt5.QtCore import Qt, QDate, QDateTime, pyqtSignal from PyQt5.QtGui import QFont, QKeySequence from database import Database import pandas as pd from datetime import datetime class PurchasePlanTab(QWidget): """采购计划Tab""" def __init__(self, db: Database, main_window=None): super().__init__() self.db = db self.main_window = main_window # 保存主窗口引用,用于跳转到地磅单 self.init_ui() self.refresh_list() def init_ui(self): """初始化界面""" layout = QVBoxLayout() layout.setSpacing(10) layout.setContentsMargins(15, 15, 15, 15) # 标题 title = QLabel("采购计划管理") title_font = QFont("微软雅黑", 18, QFont.Bold) title.setFont(title_font) layout.addWidget(title) # 按钮区域 button_layout = QHBoxLayout() button_layout.setSpacing(10) self.add_btn = QPushButton("新增") self.add_btn.setMinimumHeight(45) self.add_btn.setMinimumWidth(120) self.add_btn.setFont(QFont("微软雅黑", 16)) self.add_btn.setStyleSheet("background-color: #2196F3; color: white; border-radius: 5px;") self.add_btn.clicked.connect(self.add_plan) self.add_btn.setShortcut(QKeySequence("Ctrl+N")) self.edit_btn = QPushButton("修改") self.edit_btn.setMinimumHeight(45) self.edit_btn.setMinimumWidth(120) self.edit_btn.setFont(QFont("微软雅黑", 16)) self.edit_btn.clicked.connect(self.edit_plan) self.delete_btn = QPushButton("删除") self.delete_btn.setMinimumHeight(45) self.delete_btn.setMinimumWidth(120) self.delete_btn.setFont(QFont("微软雅黑", 16)) self.delete_btn.setStyleSheet("background-color: #f44336; color: white; border-radius: 5px;") self.delete_btn.clicked.connect(self.delete_plan) self.delete_btn.setShortcut(QKeySequence("Delete")) self.import_btn = QPushButton("Excel导入") self.import_btn.setMinimumHeight(45) self.import_btn.setMinimumWidth(120) self.import_btn.setFont(QFont("微软雅黑", 16)) self.import_btn.setStyleSheet("background-color: #4CAF50; color: white; border-radius: 5px;") self.import_btn.clicked.connect(self.import_excel) self.refresh_btn = QPushButton("刷新") self.refresh_btn.setMinimumHeight(45) self.refresh_btn.setMinimumWidth(120) self.refresh_btn.setFont(QFont("微软雅黑", 16)) self.refresh_btn.setStyleSheet(""" QPushButton { background-color: #607D8B; color: white; border-radius: 5px; border: none; } QPushButton:hover { background-color: #455A64; } QPushButton:pressed { background-color: #37474F; } QPushButton:disabled { background-color: #BDBDBD; color: #757575; } """) self.refresh_btn.setEnabled(True) # 确保按钮是启用状态 self.refresh_btn.clicked.connect(self.refresh_list) self.refresh_btn.setShortcut(QKeySequence("F5")) button_layout.addWidget(self.add_btn) button_layout.addWidget(self.edit_btn) button_layout.addWidget(self.delete_btn) button_layout.addWidget(self.import_btn) button_layout.addWidget(self.refresh_btn) button_layout.addStretch() layout.addLayout(button_layout) # 表格 self.table = QTableWidget() self.table.setColumnCount(10) self.table.setHorizontalHeaderLabels(["序号", "日期", "客户名称", "业务类型", "产品", "规格", "计划量(t)", "车牌号", "备注", "操作"]) self.table.horizontalHeader().setFont(QFont("微软雅黑", 15, QFont.Bold)) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table.setFont(QFont("微软雅黑", 15)) self.table.setAlternatingRowColors(True) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionMode(QTableWidget.SingleSelection) layout.addWidget(self.table) self.setLayout(layout) def refresh_list(self): """刷新列表""" plans = self.db.get_all_purchase_plans() self.table.setRowCount(len(plans)) for i, plan in enumerate(plans): # 序号 self.table.setItem(i, 0, QTableWidgetItem(str(i + 1))) # 日期 self.table.setItem(i, 1, QTableWidgetItem(plan['plan_date'])) # 客户名称 self.table.setItem(i, 2, QTableWidgetItem(plan.get('customer_name', ''))) # 业务类型 self.table.setItem(i, 3, QTableWidgetItem(plan['business_type'])) # 产品 self.table.setItem(i, 4, QTableWidgetItem(plan['product'])) # 规格(保持字符串格式,不进行数值转换) spec_value = plan.get('specification', '') self.table.setItem(i, 5, QTableWidgetItem(str(spec_value))) # 计划量(t) self.table.setItem(i, 6, QTableWidgetItem(f"{plan['planned_quantity']:.3f}")) # 车牌号 self.table.setItem(i, 7, QTableWidgetItem(plan['vehicle_no'])) # 备注 remarks = plan.get('remarks', '') # 如果备注太长,只显示前50个字符 remarks_display = remarks[:50] + '...' if len(remarks) > 50 else remarks self.table.setItem(i, 8, QTableWidgetItem(remarks_display)) # 操作按钮 btn_widget = QWidget() btn_layout = QHBoxLayout() btn_layout.setContentsMargins(5, 2, 5, 2) jump_btn = QPushButton("跳转到地磅单") jump_btn.setFont(QFont("微软雅黑", 14)) jump_btn.setStyleSheet("background-color: #FF9800; color: white;") jump_btn.clicked.connect(lambda checked, pid=plan['id']: self.jump_to_weighing(pid)) btn_layout.addWidget(jump_btn) btn_layout.addStretch() btn_widget.setLayout(btn_layout) self.table.setCellWidget(i, 9, btn_widget) self.table.resizeRowsToContents() def get_selected_plan_id(self) -> int: """获取选中的采购计划ID""" current_row = self.table.currentRow() if current_row < 0: return -1 plans = self.db.get_all_purchase_plans() if current_row >= len(plans): return -1 return plans[current_row]['id'] def add_plan(self): """新增采购计划""" dialog = PurchasePlanDialog(self.db, parent=self) if dialog.exec_() == QDialog.Accepted: self.refresh_list() def edit_plan(self): """修改采购计划""" plan_id = self.get_selected_plan_id() if plan_id < 0: QMessageBox.warning(self, "提示", "请先选择要修改的采购计划") return plan = self.db.get_purchase_plan_by_id(plan_id) if not plan: QMessageBox.warning(self, "错误", "未找到该采购计划") return dialog = PurchasePlanDialog(self.db, plan=plan, parent=self) if dialog.exec_() == QDialog.Accepted: self.refresh_list() def delete_plan(self): """删除采购计划""" plan_id = self.get_selected_plan_id() if plan_id < 0: QMessageBox.warning(self, "提示", "请先选择要删除的采购计划") return # 确认删除 reply = QMessageBox.question( self, "确认", "确定要删除该采购计划吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: if self.db.delete_purchase_plan(plan_id): QMessageBox.information(self, "成功", "删除成功!") self.refresh_list() else: QMessageBox.warning(self, "错误", "删除失败") def import_excel(self): """导入Excel文件""" file_path, _ = QFileDialog.getOpenFileName( self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)" ) if not file_path: return try: # 读取Excel文件(默认读取第一个sheet) # 先读取列名,确定规格列的位置 df_temp = pd.read_excel(file_path, sheet_name=0, nrows=0) original_columns = df_temp.columns.tolist() # 构建converters字典,将规格列转换为字符串,保留原始格式 converters = {} for col in original_columns: col_clean = col.strip().replace('\n', '').replace('\r', '') if col_clean == '规格': # 定义转换函数,将规格列转换为字符串 def spec_converter(x): if pd.isna(x): return '' # 如果是数值类型(可能是百分比被转换成了小数),尝试恢复 if isinstance(x, (int, float)): # 检查是否可能是百分比(0-1之间的小数) if 0 <= x <= 1: # 转换为百分比字符串 return f"{x * 100:.0f}%" else: return str(x) return str(x) converters[col] = spec_converter # 读取Excel,使用converters确保规格列为字符串类型 df = pd.read_excel(file_path, sheet_name=0, converters=converters) # 去除列名的前后空格和换行符 df.columns = df.columns.str.strip().str.replace('\n', '').str.replace('\r', '') # 如果存在规格列,确保NaN值被转换为空字符串 if '规格' in df.columns: df['规格'] = df['规格'].fillna('').astype(str) # 检查必需的列 required_columns = ['日期', '业务类型', '产品', '计划量(t)', '车牌号'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: # 显示实际读取到的列名,方便调试 actual_columns = ', '.join(df.columns.tolist()) QMessageBox.warning( self, "错误", f"Excel文件缺少必需的列:{', '.join(missing_columns)}\n\n" f"必需的列:{', '.join(required_columns)}\n" f"规格列为可选\n\n" f"实际读取到的列名:{actual_columns}" ) return # 导入数据 success_count = 0 error_count = 0 errors = [] for index, row in df.iterrows(): try: # 处理日期 plan_date = row['日期'] if pd.isna(plan_date): errors.append(f"第{index + 2}行:日期为空") error_count += 1 continue # 转换为字符串格式 if isinstance(plan_date, pd.Timestamp): plan_date = plan_date.strftime('%Y-%m-%d') elif isinstance(plan_date, datetime): plan_date = plan_date.strftime('%Y-%m-%d') else: plan_date = str(plan_date) # 处理业务类型 business_type = str(row['业务类型']).strip() if business_type not in ['出库', '入库']: errors.append(f"第{index + 2}行:业务类型必须是'出库'或'入库'") error_count += 1 continue # 处理产品 product = str(row['产品']).strip() if not product: errors.append(f"第{index + 2}行:产品为空") error_count += 1 continue # 处理计划量 planned_quantity = float(row['计划量(t)']) if planned_quantity <= 0: errors.append(f"第{index + 2}行:计划量必须大于0") error_count += 1 continue # 处理车牌号 vehicle_no = str(row['车牌号']).strip() if not vehicle_no: errors.append(f"第{index + 2}行:车牌号为空") error_count += 1 continue # 处理规格(可选) specification = '' if '规格' in df.columns and not pd.isna(row.get('规格')): specification = str(row['规格']).strip() # 处理客户名称(可选) customer_name = '' if '客户名称' in df.columns and not pd.isna(row.get('客户名称')): customer_name = str(row['客户名称']).strip() elif '客户' in df.columns and not pd.isna(row.get('客户')): customer_name = str(row['客户']).strip() # 处理备注(可选) remarks = '' if '备注' in df.columns and not pd.isna(row.get('备注')): remarks = str(row['备注']).strip() # 插入数据 data = { 'plan_date': plan_date, 'business_type': business_type, 'product': product, 'specification': specification, 'planned_quantity': planned_quantity, 'vehicle_no': vehicle_no, 'customer_name': customer_name, 'remarks': remarks } self.db.insert_purchase_plan(data) success_count += 1 except Exception as e: errors.append(f"第{index + 2}行:{str(e)}") error_count += 1 # 显示导入结果 message = f"导入完成!\n成功:{success_count}条\n失败:{error_count}条" if errors: message += f"\n\n错误详情:\n" + "\n".join(errors[:10]) # 最多显示10个错误 if len(errors) > 10: message += f"\n...还有{len(errors) - 10}个错误" if error_count > 0: QMessageBox.warning(self, "导入结果", message) else: QMessageBox.information(self, "导入成功", message) self.refresh_list() except Exception as e: QMessageBox.critical(self, "错误", f"导入失败:{str(e)}") def jump_to_weighing(self, plan_id: int): """跳转到地磅单并填充信息""" plan = self.db.get_purchase_plan_by_id(plan_id) if not plan: QMessageBox.warning(self, "错误", "未找到该采购计划") return if not self.main_window: QMessageBox.warning(self, "错误", "无法访问主窗口") return # 切换到地磅单tab(索引1,因为采购计划是0,地磅单是1) self.main_window.tab_widget.setCurrentIndex(1) # 获取地磅单tab weighing_tab = self.main_window.weighing_tab # 填充信息 # 车牌号 weighing_tab.vehicle_no_input.setText(plan['vehicle_no']) # 业务类型 business_type = plan['business_type'] if business_type == '出库': weighing_tab.business_sale_radio.setChecked(True) elif business_type == '入库': weighing_tab.business_purchase_radio.setChecked(True) # 获取客户名称 customer_name = plan.get('customer_name', '').strip() if plan.get('customer_name') else '' # 固定公司名称 fixed_company_name = "安庆市山峡化工有限公司" # 第一步:自动维护数据到对应模块(避免手动添加) # 1. 自动添加产品到货物模块(如果不存在) product_name = plan['product'] if product_name: if not self.db.cargo_exists(product_name): try: self.db.insert_cargo(product_name) # 产品已自动维护到货物模块,无需手动添加 except Exception as e: QMessageBox.warning(self, "警告", f"自动添加货物'{product_name}'失败:{str(e)}") # 2. 确保"安庆市山峡化工有限公司"在公司列表中(如果不存在则自动添加) if not self.db.company_exists(fixed_company_name): try: self.db.insert_company(fixed_company_name) except Exception as e: QMessageBox.warning(self, "警告", f"自动添加公司'{fixed_company_name}'失败:{str(e)}") # 3. 自动添加客户名称到公司模块(如果不存在) # 注意:客户名称会自动维护到公司模块,无需手动添加 if customer_name: if not self.db.company_exists(customer_name): try: self.db.insert_company(customer_name) # 客户名称已自动维护到公司模块,无需手动添加 except Exception as e: QMessageBox.warning(self, "警告", f"自动添加客户'{customer_name}'到公司模块失败:{str(e)}") # 第二步:刷新下拉列表(确保新添加的数据显示) weighing_tab.refresh_dropdowns() # 第三步:填充表单字段 # 货物名称(产品) if product_name: index = weighing_tab.cargo_combo.findText(product_name) if index >= 0: weighing_tab.cargo_combo.setCurrentIndex(index) # 根据业务类型填充供货单位和收货单位 if business_type == '入库': # 入库:客户名称 -> 供货单位,收货单位固定为"安庆市山峡化工有限公司" if customer_name: # 设置供货单位为客户名称 index = weighing_tab.supplier_combo.findText(customer_name) if index >= 0: weighing_tab.supplier_combo.setCurrentIndex(index) # 设置收货单位为"安庆市山峡化工有限公司" index = weighing_tab.consignee_combo.findText(fixed_company_name) if index >= 0: weighing_tab.consignee_combo.setCurrentIndex(index) elif business_type == '出库': # 出库:客户名称 -> 收货单位,供货单位固定为"安庆市山峡化工有限公司" # 设置供货单位为"安庆市山峡化工有限公司" index = weighing_tab.supplier_combo.findText(fixed_company_name) if index >= 0: weighing_tab.supplier_combo.setCurrentIndex(index) if customer_name: # 设置收货单位为客户名称 index = weighing_tab.consignee_combo.findText(customer_name) if index >= 0: weighing_tab.consignee_combo.setCurrentIndex(index) # 计划量(转换为kg,因为计划量是t) planned_quantity_kg = plan['planned_quantity'] * 1000 weighing_tab.planned_quantity_input.setValue(planned_quantity_kg) # 先清空备注,然后设置采购计划的备注 remarks_parts = [] # 规格可以放在备注中 if plan.get('specification'): remarks_parts.append(f"规格:{plan['specification']}") # 采购计划的备注 if plan.get('remarks'): remarks_parts.append(plan['remarks']) # 先清空备注,然后设置新的备注内容 if remarks_parts: new_remarks = "\n".join(remarks_parts) weighing_tab.remarks_input.setPlainText(new_remarks) else: weighing_tab.remarks_input.setPlainText("") # 清空毛重时间和皮重时间 weighing_tab.gross_weight_time.setDateTime(QDateTime()) if weighing_tab.gross_weight_time.lineEdit(): weighing_tab.gross_weight_time.lineEdit().setText("") weighing_tab.tare_weight_time.setDateTime(QDateTime()) if weighing_tab.tare_weight_time.lineEdit(): weighing_tab.tare_weight_time.lineEdit().setText("") class PurchasePlanDialog(QDialog): """采购计划编辑对话框""" def __init__(self, db: Database, plan=None, parent=None): super().__init__(parent) self.db = db self.plan = plan self.init_ui() def init_ui(self): """初始化对话框界面""" if self.plan: self.setWindowTitle("修改采购计划") else: self.setWindowTitle("新增采购计划") self.setMinimumWidth(500) layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(20, 20, 20, 20) # 表单 form_layout = QFormLayout() form_layout.setSpacing(15) # 日期 date_label = QLabel("日期 *:") date_label.setFont(QFont("微软雅黑", 16)) self.date_input = QDateEdit() self.date_input.setMinimumHeight(45) self.date_input.setFont(QFont("微软雅黑", 16)) self.date_input.setCalendarPopup(True) self.date_input.setDate(QDate.currentDate()) self.date_input.setDisplayFormat("yyyy-MM-dd") form_layout.addRow(date_label, self.date_input) # 业务类型 business_label = QLabel("业务类型 *:") business_label.setFont(QFont("微软雅黑", 16)) self.business_type_group = QButtonGroup() self.business_sale_radio = QRadioButton("出库") self.business_purchase_radio = QRadioButton("入库") self.business_sale_radio.setFont(QFont("微软雅黑", 16)) self.business_purchase_radio.setFont(QFont("微软雅黑", 16)) self.business_type_group.addButton(self.business_sale_radio, 0) self.business_type_group.addButton(self.business_purchase_radio, 1) business_layout = QHBoxLayout() business_layout.addWidget(self.business_sale_radio) business_layout.addWidget(self.business_purchase_radio) business_layout.addStretch() business_widget = QWidget() business_widget.setLayout(business_layout) form_layout.addRow(business_label, business_widget) # 产品 product_label = QLabel("产品 *:") product_label.setFont(QFont("微软雅黑", 16)) self.product_input = QLineEdit() self.product_input.setMinimumHeight(45) self.product_input.setFont(QFont("微软雅黑", 16)) form_layout.addRow(product_label, self.product_input) # 规格 spec_label = QLabel("规格:") spec_label.setFont(QFont("微软雅黑", 16)) self.spec_input = QLineEdit() self.spec_input.setMinimumHeight(45) self.spec_input.setFont(QFont("微软雅黑", 16)) form_layout.addRow(spec_label, self.spec_input) # 计划量(t) quantity_label = QLabel("计划量(t) *:") quantity_label.setFont(QFont("微软雅黑", 16)) self.quantity_input = QDoubleSpinBox() self.quantity_input.setMinimumHeight(45) self.quantity_input.setFont(QFont("微软雅黑", 16)) self.quantity_input.setDecimals(3) self.quantity_input.setMinimum(0.001) self.quantity_input.setMaximum(999999) form_layout.addRow(quantity_label, self.quantity_input) # 车牌号 vehicle_label = QLabel("车牌号 *:") vehicle_label.setFont(QFont("微软雅黑", 16)) self.vehicle_input = QLineEdit() self.vehicle_input.setMinimumHeight(45) self.vehicle_input.setFont(QFont("微软雅黑", 16)) form_layout.addRow(vehicle_label, self.vehicle_input) # 客户名称 customer_label = QLabel("客户名称:") customer_label.setFont(QFont("微软雅黑", 16)) self.customer_input = QLineEdit() self.customer_input.setMinimumHeight(45) self.customer_input.setFont(QFont("微软雅黑", 16)) form_layout.addRow(customer_label, self.customer_input) # 备注 remarks_label = QLabel("备注:") remarks_label.setFont(QFont("微软雅黑", 16)) self.remarks_input = QTextEdit() self.remarks_input.setMinimumHeight(80) self.remarks_input.setFont(QFont("微软雅黑", 16)) form_layout.addRow(remarks_label, self.remarks_input) # 如果是在修改模式,填充数据 if self.plan: self.date_input.setDate(QDate.fromString(self.plan['plan_date'], "yyyy-MM-dd")) if self.plan['business_type'] == '出库': self.business_sale_radio.setChecked(True) else: self.business_purchase_radio.setChecked(True) self.product_input.setText(self.plan['product']) self.spec_input.setText(self.plan.get('specification', '')) self.quantity_input.setValue(self.plan['planned_quantity']) self.vehicle_input.setText(self.plan['vehicle_no']) self.customer_input.setText(self.plan.get('customer_name', '')) self.remarks_input.setPlainText(self.plan.get('remarks', '')) layout.addLayout(form_layout) # 按钮 button_layout = QHBoxLayout() button_layout.addStretch() ok_btn = QPushButton("确定") ok_btn.setMinimumHeight(45) ok_btn.setMinimumWidth(100) ok_btn.setFont(QFont("微软雅黑", 16)) ok_btn.clicked.connect(self.save) cancel_btn = QPushButton("取消") cancel_btn.setMinimumHeight(45) cancel_btn.setMinimumWidth(100) cancel_btn.setFont(QFont("微软雅黑", 16)) cancel_btn.clicked.connect(self.reject) button_layout.addWidget(ok_btn) button_layout.addWidget(cancel_btn) layout.addLayout(button_layout) self.setLayout(layout) def save(self): """保存""" # 验证 if not self.date_input.date().isValid(): QMessageBox.warning(self, "验证失败", "请选择日期") return if not self.business_type_group.checkedButton(): QMessageBox.warning(self, "验证失败", "请选择业务类型") return product = self.product_input.text().strip() if not product: QMessageBox.warning(self, "验证失败", "产品不能为空") return if self.quantity_input.value() <= 0: QMessageBox.warning(self, "验证失败", "计划量必须大于0") return vehicle_no = self.vehicle_input.text().strip() if not vehicle_no: QMessageBox.warning(self, "验证失败", "车牌号不能为空") return # 准备数据 data = { 'plan_date': self.date_input.date().toString("yyyy-MM-dd"), 'business_type': self.business_type_group.checkedButton().text(), 'product': product, 'specification': self.spec_input.text().strip(), 'planned_quantity': self.quantity_input.value(), 'vehicle_no': vehicle_no, 'customer_name': self.customer_input.text().strip(), 'remarks': self.remarks_input.toPlainText().strip() } # 保存 if self.plan: # 修改模式 if self.db.update_purchase_plan(self.plan['id'], data): QMessageBox.information(self, "成功", "修改成功!") self.accept() else: QMessageBox.warning(self, "错误", "修改失败") else: # 新增模式 try: self.db.insert_purchase_plan(data) QMessageBox.information(self, "成功", "新增成功!") self.accept() except Exception as e: QMessageBox.critical(self, "错误", f"新增失败:{str(e)}")