From 8b8df295f1bdbfe35baa84c2a6fa867641c7284c Mon Sep 17 00:00:00 2001 From: zhu-mengmeng <15588200382@163.com> Date: Sat, 19 Jul 2025 02:00:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=90=97=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/app_config.json | 2 +- dao/inspection_dao.py | 71 +- dao/inspection_dao.py.bak | 972 ++++++++ dao/pallet_type_dao.py | 4 +- db/jtDB.db | Bin 172032 -> 172032 bytes fix_status_management.py | 367 +++ from pymodbus.py | 4 +- status_management_patch.py | 378 +++ temp_fix.py | 7 + temp_save_inspection_data.py | 39 + ui/main_window_ui.py | 24 +- widgets/loading_dialog_widget.py | 14 +- widgets/main_window.py | 347 ++- widgets/main_window.py.bak | 4019 ++++++++++++++++++++++++++++++ widgets/order_query_dialog.py | 2 +- 15 files changed, 6133 insertions(+), 117 deletions(-) create mode 100644 dao/inspection_dao.py.bak create mode 100644 fix_status_management.py create mode 100755 status_management_patch.py create mode 100644 temp_fix.py create mode 100644 temp_save_inspection_data.py create mode 100644 widgets/main_window.py.bak diff --git a/config/app_config.json b/config/app_config.json index 921f301..e4f95e2 100644 --- a/config/app_config.json +++ b/config/app_config.json @@ -8,7 +8,7 @@ "enable_camera": false }, "base_url": "http://localhost:8084", - "mode": "api" + "mode": "standalone" }, "apis": { "get_tray_info": "/apjt/xcsc/tpda/getByTp_note/", diff --git a/dao/inspection_dao.py b/dao/inspection_dao.py index af5d76e..bd6b24c 100644 --- a/dao/inspection_dao.py +++ b/dao/inspection_dao.py @@ -453,7 +453,7 @@ class InspectionDAO: SELECT DISTINCT d.gc_note FROM wsbz_inspection_data d WHERE d.is_deleted = FALSE AND d.tray_id = ? - AND d.position = 11 AND COALESCE(d.value,'') = '' + AND status != 'labeled' """ params = (tray_id,) with SQLUtils('sqlite', database='db/jtDB.db') as db: @@ -583,15 +583,20 @@ class InspectionDAO: except Exception as e: logging.error(f"获取包装记录失败: {str(e)}") return [] - def save_package_record(self, order_id, tray_id, label_value, weight_value,net_weight_value, finish_time,gc_note): + def save_package_record(self, order_id, tray_id, label_value, weight_value, net_weight_value, finish_time, gc_note=None): """保存包装记录 Args: - order_id: 工程号 + order_id: 订单号 tray_id: 托盘号 - label_value: 标签值 - weight_value: 重量值 + label_value: 贴标值 + weight_value: 称重值 + net_weight_value: 净重值 finish_time: 完成时间 + gc_note: 工程号 + + Returns: + bool: 保存是否成功 """ # TODO:调用接口,获取到工程号对应的其他信息,比如材质,规格,后续完成 try: @@ -610,6 +615,60 @@ class InspectionDAO: except Exception as e: logging.error(f"保存包装记录失败: {str(e)}") return False + + def get_product_status(self, order_id, gc_note, tray_id): + """获取产品的当前状态 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + + Returns: + str: 产品状态,如果没有找到则返回'init' + """ + try: + with SQLUtils('sqlite', database='db/jtDB.db') as db: + sql = """ + SELECT status FROM wsbz_inspection_data + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + ORDER BY id ASC LIMIT 1 + """ + params = (order_id, gc_note, tray_id) + db.cursor.execute(sql, params) + result = db.cursor.fetchone() + return result[0] if result and result[0] else 'init' # 默认为init状态 + except Exception as e: + logging.error(f"获取产品状态失败: {str(e)}") + return 'init' # 出错时返回默认状态 + + def update_product_status(self, order_id, gc_note, tray_id, new_status): + """更新产品的状态 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + new_status: 新状态 + + Returns: + bool: 更新是否成功 + """ + try: + with SQLUtils('sqlite', database='db/jtDB.db') as db: + # 更新该产品所有记录的状态字段 + update_sql = """ + UPDATE wsbz_inspection_data SET status = ?, update_time = ? + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + """ + update_params = (new_status, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + order_id, gc_note, tray_id) + db.execute_update(update_sql, update_params) + logging.info(f"已更新产品状态: 订单号={order_id}, 工程号={gc_note}, 托盘号={tray_id}, 新状态={new_status}") + return True + except Exception as e: + logging.error(f"更新产品状态失败: {str(e)}") + return False def delete_inspection_data(self, order_id, gc_note, tray_id): """删除检验数据 @@ -689,7 +748,7 @@ class InspectionDAO: with SQLUtils('sqlite', database='db/jtDB.db') as db: db.cursor.execute(sql, params) result = db.cursor.fetchone() - return result[0],result[1] if result else None,None + return (result[0],result[1]) if result else (None,None) except Exception as e: logging.error(f"获取线径范围失败: {str(e)}") return None,None diff --git a/dao/inspection_dao.py.bak b/dao/inspection_dao.py.bak new file mode 100644 index 0000000..cb07ece --- /dev/null +++ b/dao/inspection_dao.py.bak @@ -0,0 +1,972 @@ +import json +import logging +from datetime import datetime +from utils.sql_utils import SQLUtils + +class InspectionDAO: + """检验项目配置和数据访问对象""" + + def __init__(self): + """初始化数据访问对象""" + # 不再在初始化时创建数据库连接,而是在需要时创建 + pass + + def __del__(self): + """析构函数,确保数据库连接关闭""" + # 不再需要在这里关闭连接,由上下文管理器处理 + pass + + def get_all_inspection_configs(self, include_disabled=False): + """获取所有检验项目配置 + + Args: + include_disabled: 是否包含禁用的项目 + + Returns: + list: 检验项目配置列表 + """ + try: + if include_disabled: + sql = """ + SELECT id, position, name, display_name, enabled, required, + data_type, min_value, max_value, enum_values, unit, sort_order + FROM wsbz_inspection_config + WHERE is_deleted = FALSE + ORDER BY sort_order, position + """ + params = () + else: + sql = """ + SELECT id, position, name, display_name, enabled, required, + data_type, min_value, max_value, enum_values, unit, sort_order + FROM wsbz_inspection_config + WHERE is_deleted = FALSE AND enabled = TRUE + ORDER BY sort_order, position + """ + params = () + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + results = db.cursor.fetchall() + + configs = [] + for row in results: + config = { + 'id': row[0], + 'position': row[1], + 'name': row[2], + 'display_name': row[3], + 'enabled': bool(row[4]), + 'required': bool(row[5]), + 'data_type': row[6], + 'min_value': row[7], + 'max_value': row[8], + 'enum_values': json.loads(row[9]) if row[9] else None, + 'unit': row[10], + 'sort_order': row[11] + } + configs.append(config) + + return configs + except Exception as e: + logging.error(f"获取检验项目配置失败: {str(e)}") + return [] + + def get_enabled_inspection_configs(self): + """获取已启用的检验项目配置 + + Returns: + list: 已启用的检验项目配置列表 + """ + return self.get_all_inspection_configs(include_disabled=False) + + def get_inspection_config_by_position(self, position): + """根据位置获取检验项目配置 + + Args: + position: 位置序号 (1-6) + + Returns: + dict: 检验项目配置, 未找到则返回None + """ + try: + sql = """ + SELECT id, position, name, display_name, enabled, required, + data_type, min_value, max_value, enum_values, unit, sort_order + FROM wsbz_inspection_config + WHERE position = ? AND is_deleted = FALSE + """ + params = (position,) + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + row = db.cursor.fetchone() + + if row: + config = { + 'id': row[0], + 'position': row[1], + 'name': row[2], + 'display_name': row[3], + 'enabled': bool(row[4]), + 'required': bool(row[5]), + 'data_type': row[6], + 'min_value': row[7], + 'max_value': row[8], + 'enum_values': json.loads(row[9]) if row[9] else None, + 'unit': row[10], + 'sort_order': row[11] + } + return config + else: + return None + except Exception as e: + logging.error(f"获取检验项目配置失败: {str(e)}") + return None + + def update_inspection_config(self, config_id, data, username='system'): + """更新检验项目配置 + + Args: + config_id: 配置ID + data: 更新数据 + username: 操作用户 + + Returns: + bool: 更新是否成功 + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # 构建更新SQL + update_fields = [] + params = [] + + # 可更新的字段 + allowed_fields = [ + 'name', 'display_name', 'enabled', 'required', + 'data_type', 'min_value', 'max_value', 'unit', + 'sort_order', 'enum_values' + ] + + for field in allowed_fields: + if field in data: + # 特殊处理enum_values字段,确保存储为JSON字符串 + if field == 'enum_values' and data[field] is not None: + if isinstance(data[field], list): + update_fields.append(f"{field} = ?") + params.append(json.dumps(data[field])) + elif isinstance(data[field], str): + # 如果已经是字符串,检查是否有效的JSON + try: + json.loads(data[field]) + update_fields.append(f"{field} = ?") + params.append(data[field]) + except: + logging.warning(f"无效的JSON: {data[field]}") + continue + else: + update_fields.append(f"{field} = ?") + params.append(data[field]) + + # 添加更新时间和更新人 + update_fields.append("update_time = ?") + params.append(current_time) + update_fields.append("update_by = ?") + params.append(username) + + # 添加配置ID到参数列表 + params.append(config_id) + + # 构建SQL + sql = f""" + UPDATE inspection_config + SET {', '.join(update_fields)} + WHERE id = ? + """ + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.execute_update(sql, params) + return True + except Exception as e: + logging.error(f"更新检验项目配置失败: {str(e)}") + return False + + def toggle_inspection_config(self, position, enabled, username='system'): + """启用或禁用检验项目配置 + + Args: + position: 位置序号 (1-6) + enabled: 是否启用 + username: 操作用户 + + Returns: + bool: 操作是否成功 + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + sql = """ + UPDATE wsbz_inspection_config + SET enabled = ?, update_time = ?, update_by = ? + WHERE position = ? AND is_deleted = FALSE + """ + params = (enabled, current_time, username, position) + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.execute_update(sql, params) + return True + except Exception as e: + logging.error(f"更新检验项目启用状态失败: {str(e)}") + return False + def save_order_info(self, order_id,data): + """保存订单信息到 wsbz_order_info 表 + + Args: + data: 订单信息字典 + + Returns: + bool: 操作是否成功 + """ + try: + if not data: + return False + + # 使用单一连接实例处理整个操作 + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.begin_transaction() # 开始事务 + + # 先检查是否存在记录 + check_cursor = db.get_new_cursor() + check_sql = "SELECT ddmo FROM wsbz_order_info WHERE ddmo = ?" + check_cursor.execute(check_sql, (data.get("mo", ""),)) + existing_record = check_cursor.fetchone() + check_cursor.close() # 使用后关闭新游标 + + if existing_record: + # 如果记录存在,执行更新 + sql = """ + UPDATE wsbz_order_info SET + data_corp = ?, user_id = ?, user_name = ?, gzl_zl = ?, + xpack = ?, qd = ?, spack_type = ?, mxzs = ?, jt = ?, + ddnote = ?, code = ?, type = ?, lable = ?, lib = ?, + gzl = ?, maxsl = ?, cz = ?, size = ?, cd = ?, luno = ?, + qfqd = ?, pono = ?, xj = ?, ysl = ?, dycz = ?, + zx_code = ?, edit_id = ?, remarks = ?, zx_name = ?, + bccd = ? ,tccd = ?, zzyq = ?, customer = ?,customerexp = ?, + bz_bqd = ?,bz_tqd = ?,type_name = ?,remarks_hb=?,khno=? + WHERE ddmo = ? + """ + params = ( + data.get("data_corp", "JT"), + data.get("user_id", ""), + data.get("user_name", ""), + data.get("zx_zl", ""), + data.get("xpack", ""), + data.get("qd", ""), + data.get("spack_type", ""), + data.get("mxzs", ""), + data.get("jt", ""), + data.get("note", ""), + data.get("code", ""), + data.get("type", ""), + data.get("template_name", ""), + data.get("lib", ""), + data.get("zx_code", ""), + data.get("maxsl", ""), + data.get("cz", ""), + data.get("size", ""), + data.get("cd", ""), + data.get("luno", ""), + data.get("qfqd", ""), + data.get("khno", ""), + data.get("size", ""), + data.get("ysl", ""), + data.get("dycz", ""), + data.get("zx_code", ""), + data.get("edit_id", ""), + data.get("remarks", ""), + data.get("zx_name", ""), + data.get("bccd", ""), + data.get("tccd", ""), + data.get("zzyq", ""), + data.get("customer", ""), + data.get("customerexp", ""), + data.get("bz_bqd", ""), + data.get("bz_tqd", ""), + data.get("type_name", ""), + data.get("remarks_hb", ""), + data.get("khno", ""), + data.get("mo", "") # WHERE 条件参数 + ) + logging.info(f"更新订单信息: ddmo={data.get('mo', '')}") + else: + # 如果记录不存在,执行插入 + sql = """ + INSERT INTO wsbz_order_info ( + data_corp, user_id, user_name, gzl_zl, ddmo, xpack, + qd, spack_type, mxzs, jt, ddnote, code, type, + lable, lib, gzl, maxsl, cz, size, cd, luno, qfqd, + pono, xj, ysl, dycz, zx_code, edit_id, remarks,zx_name, + bccd,tccd,zzyq,customer,customerexp,bz_bqd,bz_tqd,type_name, + remarks_hb,khno + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + """ + params = ( + data.get("data_corp", "JT"), + data.get("user_id", ""), + data.get("user_name", ""), + data.get("zx_zl", ""), + data.get("mo", ""), + data.get("xpack", ""), + data.get("qd", ""), + data.get("spack_type", ""), + data.get("mxzs", ""), + data.get("jt", ""), + data.get("note", ""), + data.get("code", ""), + data.get("type", ""), + data.get("template_name", ""), + data.get("lib", ""), + data.get("zx_code", ""), + data.get("maxsl", ""), + data.get("cz", ""), + data.get("size", ""), + data.get("cd", ""), + data.get("luno", ""), + data.get("qfqd", ""), + data.get("khno", ""), + data.get("size", ""), + data.get("ysl", ""), + data.get("dycz", ""), + data.get("zx_code", ""), + data.get("edit_id", ""), + data.get("remarks", ""), + data.get("zx_name", ""), + data.get("bccd", ""), + data.get("tccd", ""), + data.get("zzyq", ""), + data.get("customer", ""), + data.get("customerexp", ""), + data.get("bz_bqd", ""), + data.get("bz_tqd", ""), + data.get("type_name", ""), + data.get("remarks_hb", ""), + data.get("khno", "") + ) + logging.info(f"插入新订单信息: ddmo={data.get('mo', '')}") + + # 执行SQL + db.execute_update(sql, params) + db.commit_transaction() + + return True + + except Exception as e: + logging.error(f"保存订单信息失败: {str(e)}") + return False + def save_inspection_data(self, order_id, gc_note, data, username='system'): + """保存检验数据 + + Args: + order_id: 订单号 + gc_note: 工程号 + data: 检验数据列表,每项包含position, config_id, value, status, remark + username: 操作用户 + + Returns: + bool: 保存是否成功 + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # 使用上下文管理器自动处理连接和游标 + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.begin_transaction() # 开始事务 + + for item in data: + position = item.get('position') + config_id = item.get('config_id') + value = item.get('value', '') + status = item.get('status', '') + remark = item.get('remark', '') + tray_id = item.get('tray_id', '') + + # 获取新游标执行查询,避免递归使用 + check_cursor = db.get_new_cursor() + check_sql = """ + SELECT id FROM wsbz_inspection_data + WHERE order_id = ? AND gc_note = ? AND position = ? AND tray_id = ? + """ + check_params = (order_id, gc_note, position, tray_id) + + check_cursor.execute(check_sql, check_params) + existing_record = check_cursor.fetchone() + check_cursor.close() # 使用后关闭新游标 + + if existing_record: + # 更新现有记录 + update_sql = """ + UPDATE wsbz_inspection_data + SET config_id = ?, value = ?, status = ?, remark = ?, + update_time = ?, update_by = ? + WHERE order_id = ? AND gc_note = ? AND position = ? AND tray_id = ? + """ + update_params = ( + config_id, value, status, remark, + current_time, username, + order_id, gc_note, position, tray_id + ) + db.execute_update(update_sql, update_params) + else: + # 插入新记录 + insert_sql = """ + INSERT INTO wsbz_inspection_data ( + order_id, gc_note, position, config_id, value, status, remark, + create_time, create_by, update_time, update_by, tray_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + insert_params = ( + order_id, gc_note, position, config_id, value, status, remark, + current_time, username, current_time, username, tray_id + ) + db.execute_update(insert_sql, insert_params) + + db.commit_transaction() # 提交事务 + + return True + except Exception as e: + logging.error(f"保存检验数据失败: {str(e)}") + return False + def get_inspection_data_unfinished(self, tray_id): + """获取未完成的检验数据,通过是否贴标来判断 + + Returns: + list: 未完成的检验数据列表 + """ + try: + # 先获取所有没有贴标的工程号 + sql_orders = """ + SELECT DISTINCT d.gc_note + FROM wsbz_inspection_data d + WHERE d.is_deleted = FALSE AND d.tray_id = ? + AND d.position = 11 AND COALESCE(d.value,'') = '' + """ + params = (tray_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql_orders, params) + gc_notes = db.cursor.fetchall() + + if not gc_notes: + return [] + + # 构建IN子句的参数 + gc_notes = [gc_note[0] for gc_note in gc_notes] + placeholders = ','.join(['?' for _ in gc_notes]) + + # 获取这些工程号的所有检验数据 + sql = f""" + SELECT d.id, d.gc_note, d.position, d.config_id, d.value, d.status, d.remark, + c.name, c.display_name, c.data_type, c.unit + FROM wsbz_inspection_data d + LEFT JOIN wsbz_inspection_config c ON d.config_id = c.id + WHERE d.is_deleted = FALSE AND d.tray_id = ? + AND d.gc_note IN ({placeholders}) + ORDER BY d.create_time + """ + + params = [tray_id] + gc_notes + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + results = db.cursor.fetchall() + + data_list = [] + for row in results: + data = { + 'id': row[0], + 'gc_note': row[1], + 'position': row[2], + 'config_id': row[3], + 'value': row[4], + 'status': row[5], + 'remark': row[6], + 'name': row[7], + 'display_name': row[8], + 'data_type': row[9], + 'unit': row[10] + } + data_list.append(data) + + return data_list + except Exception as e: + logging.error(f"获取未完成的检验数据失败: {str(e)}") + return [] + def get_inspection_data_by_order(self, order_id,gc_note, tray_id): + """根据工程号获取检验数据 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + Returns: + list: 检验数据列表 + """ + try: + sql = """ + SELECT d.id, d.position, d.config_id, d.value, d.status, d.remark, + c.name, c.display_name, c.data_type, c.unit + FROM wsbz_inspection_data d + LEFT JOIN wsbz_inspection_config c ON d.config_id = c.id + WHERE d.order_id = ? AND d.gc_note = ? AND d.is_deleted = FALSE AND d.tray_id = ? + ORDER BY d.create_time, d.order_id, d.position + """ + params = (order_id, gc_note, tray_id) + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + results = db.cursor.fetchall() + + data_list = [] + for row in results: + data = { + 'id': row[0], + 'position': row[1], + 'config_id': row[2], + 'value': row[3], + 'status': row[4], + 'remark': row[5], + 'name': row[6], + 'display_name': row[7], + 'data_type': row[8], + 'unit': row[9] + } + data_list.append(data) + + return data_list + except Exception as e: + logging.error(f"获取检验数据失败: {str(e)}") + return [] + + def get_package_record(self, tray_id): + """根据托盘号获取包装记录 + + Args: + tray_id: 托盘号 + Returns: + list: 包装记录列表 + """ + try: + sql = """ + SELECT DISTINCT order_id, + gc_note, + COALESCE(orders.size, '') as material, + COALESCE(orders.cz, '') as spec, + tray_id, + COALESCE(axis_package_id, '') as axis_package_id, + COALESCE(weight, 0) as weight, + COALESCE(net_weight, 0) as net_weight, + STRFTIME('%Y-%m-%d %H:%M:%S', pack_time) as pack_time + FROM wsbz_inspection_pack_data t1 + LEFT JOIN wsbz_order_info orders on t1.order_id = orders.ddmo + WHERE tray_id = ? + AND is_deleted = FALSE + ORDER BY pack_time DESC + """ + params = (tray_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + results = db.cursor.fetchall() + return results + except Exception as e: + logging.error(f"获取包装记录失败: {str(e)}") + return [] + def save_package_record(self, order_id, tray_id, label_value, weight_value, net_weight_value, finish_time, gc_note=None): + """保存包装记录 + + Args: + order_id: 订单号 + tray_id: 托盘号 + label_value: 贴标值 + weight_value: 称重值 + net_weight_value: 净重值 + finish_time: 完成时间 + gc_note: 工程号 + + Returns: + bool: 保存是否成功 + """ + # TODO:调用接口,获取到工程号对应的其他信息,比如材质,规格,后续完成 + try: + sql = """ + INSERT INTO wsbz_inspection_pack_data (order_id, tray_id, axis_package_id, weight, net_weight, pack_time, create_time, create_by, update_time, update_by, is_deleted,gc_note) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?) + """ + params = (order_id, tray_id, label_value, weight_value, net_weight_value, finish_time, datetime.now(), 'system', datetime.now(), 'system', False,gc_note) + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.begin_transaction() + db.execute_update(sql, params) + db.commit_transaction() + + return True + except Exception as e: + logging.error(f"保存包装记录失败: {str(e)}") + return False + + def get_product_status(self, order_id, gc_note, tray_id): + """获取产品的当前状态 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + + Returns: + str: 产品状态,如果没有找到则返回'init' + """ + try: + with SQLUtils('sqlite', database='db/jtDB.db') as db: + sql = """ + SELECT status FROM wsbz_inspection_data + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + ORDER BY id ASC LIMIT 1 + """ + params = (order_id, gc_note, tray_id) + result = db.query_one(sql, params) + return result[0] if result and result[0] else 'init' # 默认为init状态 + except Exception as e: + logging.error(f"获取产品状态失败: {str(e)}") + return 'init' # 出错时返回默认状态 + + def update_product_status(self, order_id, gc_note, tray_id, new_status): + """更新产品的状态 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + new_status: 新状态 + + Returns: + bool: 更新是否成功 + """ + try: + with SQLUtils('sqlite', database='db/jtDB.db') as db: + # 更新该产品所有记录的状态字段 + update_sql = """ + UPDATE wsbz_inspection_data SET status = ?, update_time = ? + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + """ + update_params = (new_status, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + order_id, gc_note, tray_id) + db.execute(update_sql, update_params) + logging.info(f"已更新产品状态: 订单号={order_id}, 工程号={gc_note}, 托盘号={tray_id}, 新状态={new_status}") + return True + except Exception as e: + logging.error(f"更新产品状态失败: {str(e)}") + return False + def delete_inspection_data(self, order_id, gc_note, tray_id): + """删除检验数据 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + """ + try: + sql = """ + UPDATE wsbz_inspection_data + SET is_deleted = TRUE + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + """ + params = (order_id, gc_note, tray_id) + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.begin_transaction() + db.execute_update(sql, params) + db.commit_transaction() + + return True + except Exception as e: + logging.error(f"删除检验数据失败: {str(e)}") + return False + def get_axios_num_by_order_id(self, order_id): + """获取托盘号对应的轴号""" + try: + sql = """ + SELECT max(axis_package_id) as axios_num FROM wsbz_inspection_pack_data WHERE order_id = ? + AND is_deleted = FALSE + """ + params = (order_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + result = db.cursor.fetchone() + return int(result[0]) if result[0] else 0 + except Exception as e: + logging.error(f"获取轴号失败: {str(e)}") + return 0 + def get_axios_num(self, tray_id): + """获取托盘号对应的轴号""" + try: + sql = """ + SELECT max(cast(axis_package_id as int)) as axios_num FROM wsbz_inspection_pack_data WHERE tray_id = ? + AND is_deleted = FALSE + """ + params = (tray_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + result = db.cursor.fetchone() + return int(result[0]) if result else 0 + except Exception as e: + logging.error(f"获取轴号失败: {str(e)}") + return 0 + def get_gzl_zl(self,order_id): + """获取工字轮重量""" + try: + sql = """ + SELECT gzl_zl FROM wsbz_order_info WHERE ddmo = ? + """ + params = (order_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + result = db.cursor.fetchone() + return result[0] if result else 0 + except Exception as e: + logging.error(f"获取工字轮重量失败: {str(e)}") + return 0 + def get_xj_range(self,order_id): + """获取线径范围""" + try: + sql = """ + SELECT bccd, tccd FROM wsbz_order_info WHERE ddmo = ? + """ + params = (order_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + result = db.cursor.fetchone() + return (result[0],result[1]) if result else (None,None) + except Exception as e: + logging.error(f"获取线径范围失败: {str(e)}") + return None,None + def get_order_create_time(self, order_id): + """获取工程号的最早创建时间 + + Args: + order_id: 工程号 + + Returns: + str: 创建时间,格式为'YYYY-MM-DD HH:MM:SS',如果未找到则返回None + """ + try: + sql = """ + SELECT MIN(create_time) FROM wsbz_inspection_data + WHERE order_id = ? AND is_deleted = FALSE + """ + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, (order_id,)) + result = db.cursor.fetchone() + return result[0] if result and result[0] else None + except Exception as e: + logging.error(f"获取工程号创建时间失败: {str(e)}") + return None + + def get_orders_by_create_time(self, order_ids): + """按创建时间排序工程号 + + Args: + order_ids: 工程号列表 + + Returns: + list: 按创建时间排序的工程号列表 + """ + try: + if not order_ids: + return [] + + # 构建IN子句 + placeholders = ','.join(['?' for _ in order_ids]) + + # 查询每个工程号的最早创建时间并排序 + sql = f""" + SELECT gc_note, MIN(create_time) as first_create_time + FROM wsbz_inspection_data + WHERE gc_note IN ({placeholders}) AND is_deleted = FALSE + GROUP BY gc_note + ORDER BY first_create_time + """ + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, order_ids) + results = db.cursor.fetchall() + + # 提取排序后的工程号 + sorted_order_ids = [row[0] for row in results] + + # 确保所有传入的工程号都在结果中 + for order_id in order_ids: + if order_id not in sorted_order_ids: + sorted_order_ids.append(order_id) + + return sorted_order_ids + except Exception as e: + logging.error(f"按创建时间排序工程号失败: {str(e)}") + return order_ids # 出错时返回原始顺序 + + def get_order_info(self, order_id): + """获取订单信息 + + Args: + order_id: 工程号 + + Returns: + dict: 订单信息字典 + """ + try: + sql = """ + SELECT DISTINCT data_corp, user_id, user_name, gzl_zl, mzl, ddmo, xpack, qd, spack_type, mxzs, jt, ddnote, code, type, lable, lib, gzl, maxsl, cz, size, cd, luno, + qfqd, pono, xj, ysl, dycz, zx_code, edit_id, remarks, zx_name, bccd, tccd, zzyq, customer, customerexp, bz_bqd as bqd, bz_tqd as tqd, type_name, remarks_hb + FROM wsbz_order_info WHERE ddmo = ? + """ + params = (order_id,) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + result = db.cursor.fetchone() + + if not result: + return {} + + # 获取列名 + column_names = [desc[0] for desc in db.cursor.description] + + # 转换为字典 + result_dict = {} + for i, value in enumerate(result): + if i < len(column_names): + result_dict[column_names[i]] = value + + return result_dict + except Exception as e: + logging.error(f"获取订单信息失败: {str(e)}") + return {} + def get_order_others_info(self, gc_note, order_id, tray_id): + """获取订单其他信息 + + Args: + order_id: 工程号 + tray_id: 托盘号 + + Returns: + dict: 订单其他信息字典,以name为key,value为值 + """ + try: + sql = """ + SELECT t1.order_id, CASE WHEN t1.position = 12 THEN 'mzl' ELSE name END AS name, value + FROM wsbz_inspection_data t1 + LEFT JOIN main.wsbz_inspection_config wic ON t1.config_id = wic.id + WHERE gc_note = ? AND t1.order_id = ? + AND tray_id = ? + AND CASE WHEN t1.position = 12 THEN 'mzl' ELSE name END IS NOT NULL + AND COALESCE(value, '') != '' + """ + params = (gc_note, order_id, tray_id) + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql, params) + results = db.cursor.fetchall() + + if not results: + return {} + + # 将结果转换为字典,以name为key,value为值 + result_dict = {} + for row in results: + if len(row) >= 3: # 确保行至少有3个元素 + name = row[1] # name在第二列 + value = row[2] # value在第三列 + result_dict[name] = value + + return result_dict + except Exception as e: + logging.error(f"获取订单其他信息失败: {str(e)}") + return {} + def get_order_statistics(self): + """获取订单数量和产量统计数据(日/月/年/累计) + + Returns: + dict: 包含日、月、年、累计订单数量和产量的字典 + """ + try: + # 使用提供的SQL查询 + sql = """ + SELECT CASE + WHEN create_time >= DATE('now') AND create_time < DATE('now', '+1 day') + THEN COUNT(DISTINCT order_id) + ELSE 0 END AS order_cnt_day, + CASE + WHEN create_time >= DATE('now', 'start of month') AND create_time < DATE('now', 'start of month', '+1 month') + THEN COUNT(DISTINCT order_id) + ELSE 0 END AS order_cnt_month, + CASE + WHEN create_time >= DATE('now', 'start of year') AND create_time < DATE('now', 'start of year', '+1 year') + THEN COUNT(DISTINCT order_id) + ELSE 0 END AS order_cnt_year, + COUNT(DISTINCT order_id) AS order_cnt_all, + CASE + WHEN create_time >= DATE('now') AND create_time < DATE('now', '+1 day') + THEN SUM(value) + ELSE 0 END AS order_num_day, + CASE + WHEN create_time >= DATE('now', 'start of month') AND + create_time < DATE('now', 'start of month', '+1 month') + THEN SUM(value) + ELSE 0 END AS order_num_month, + CASE + WHEN create_time >= DATE('now', 'start of year') AND + create_time < DATE('now', 'start of year', '+1 year') + THEN SUM(value) + ELSE 0 END AS order_num_year, + CASE WHEN position = 12 THEN SUM(value) ELSE 0 END AS order_num_all + FROM wsbz_inspection_data WHERE position = 12 + """ + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + db.cursor.execute(sql) + row = db.cursor.fetchone() + + if row: + data = { + 'order_cnt_day': row[0] if row[0] is not None else 0, + 'order_cnt_month': row[1] if row[1] is not None else 0, + 'order_cnt_year': row[2] if row[2] is not None else 0, + 'order_cnt_all': row[3] if row[3] is not None else 0, + 'order_num_day': float(row[4]) if row[4] is not None else 0, + 'order_num_month': float(row[5]) if row[5] is not None else 0, + 'order_num_year': float(row[6]) if row[6] is not None else 0, + 'order_num_all': float(row[7]) if row[7] is not None else 0 + } + return data + else: + return { + 'order_cnt_day': 0, + 'order_cnt_month': 0, + 'order_cnt_year': 0, + 'order_cnt_all': 0, + 'order_num_day': 0, + 'order_num_month': 0, + 'order_num_year': 0, + 'order_num_all': 0 + } + except Exception as e: + logging.error(f"获取订单数量和产量统计数据失败: {str(e)}") + return { + 'order_cnt_day': 0, + 'order_cnt_month': 0, + 'order_cnt_year': 0, + 'order_cnt_all': 0, + 'order_num_day': 0, + 'order_num_month': 0, + 'order_num_year': 0, + 'order_num_all': 0 + } \ No newline at end of file diff --git a/dao/pallet_type_dao.py b/dao/pallet_type_dao.py index 2d9fe18..c50278d 100644 --- a/dao/pallet_type_dao.py +++ b/dao/pallet_type_dao.py @@ -367,8 +367,8 @@ class PalletTypeDAO: update_time, update_by, enabled, is_deleted ) VALUES (?, ?, ?, ?, ?, ?, TRUE, FALSE) """ - params = (pallet_code, tier, current_time, user_id, - current_time, user_id) + params = (pallet_code, tier, current_time, user_id[0], + current_time, user_id[0]) self.db.execute_update(insert_sql, params) logging.info(f"创建新托盘档案记录:包装号={pallet_code}, 层数={tier}") diff --git a/db/jtDB.db b/db/jtDB.db index ffa81d76525b699cf0df3bec3128c773da49375a..8233ffc651ee53d7ac714fa254fc3c69964ff7fc 100644 GIT binary patch delta 8849 zcmb`MTWl298OP6@9q)a2?AT_rp-JpW2}u>)x$b&5fN_#?u?aTD2I3kg4w#Dz7Oe>m zZD61(M=dHTt4*>JZIMV7)Jl~avPG6CQls>xPYFo9gtTe%(5ep=A`+ESnFsovnc1D& znPr`p)yj+iKEM5+?|kPwmpSoX{KR|lvk#;@77;>UhIco-L#4-axMT6siyyDygz{5; zk8sz^lYJ!IpG}1{Ts|A_z@gc9*YaI)PN413L&D-ho5mQue@9Zx-${2+NGnN3_x%p{h@-;dA6J7ZU4GqGjS z_oD}+9g$y0rX#8FPzafyPPRPId&4F|Lz! zSLm8WkCE+wj>Y_@>)$(OpZ_4|2vejVwV;%}I(M^$cI-|CekpMKtUmQ)z zV`K}ES&3qCb$@a<6^#+{fPgw-+H{)JCM(bikxK2K-GA7Pl=A@rHU! zX9?JCByHHuSP7T0ii6yfljTEGb8~hAw9qYsRPO#~`#NLyAQg(TGZ&<7B*kFSRPeFt zsHbZvvEWE6X@n#Vl;E-j8_G4*-Xz`=TK<%1!xNk${bi5>Is1@SWMn# z(pqCVWsbAmwlQ>wgaB#}y2WISx|^umn{;pA#|akDaytjwJHfsrs}kMzN#04@C5lBA z23s))+v!#;YWbpSuRq_IpO(i7XTbGZhSljb6aH8F7(oWTmJ4;bFB|kmwjKR}u2b7% zr*t;hpAikh;o$0Z@NqWSpB2r4E$-G+$AD|+MLiN$X6W@bW=YubQ?D9!eWTYfHyFD} zT`JOIZ0SJ-r#B@Ft#;W|BXTi$n^ng=@MiV+!y_b6_^@>6NJuQgf+nvYdTKOU9%4kN7lR&E4Y8SKC^j<@BNcR=9(Rp?(1_ z9}ui&$Qc@`sAxq+?a8TmRqb``9O#MzcMXzAHv+0;t@QNdy=iS7C;%|)1;S27QhPL6 zR77P?_M8Bm8pH}tC4+q~T*ix&U{MBtk|Gug9)B*$pbe~X!JxDLRY~sc$!mpNLH5#Q z*g@AMO53>F1#5RZGZI40_h@;gSI)awPALo;z^dKVzlVpz2o0`sL7m<0?Sjy$`JzI1 z`AQzPMfH(lNQ$J$)v+tT)Rx7Pw~_F3VI+MgeK9?h`cLXYY9M(VM*l`qA0@-)+1J{X z!D41zHn9$q8mwskLj=tPTc9bRjFO=Mtot;nTbEXL^Lm$z-IAZXIe_b1ZrrW{u=?~t z%kOgmT>on0>TLkGj@=Z1wbmMU_B(ppm^(rSfom@{_Ne)HAq}CrHJVls1_Im$<$|>_ z_nWl@ij_yn#sF+Gsk8ky$=wjZ4ax;Sw%y7Sr{7<3+%+j#L7mZ~D7h-}iEzO=ZIb7O zfdol>CH_(TWcWz_O=Pv)mD~AGl3Ur$$iSu?&LSeF1Kn zxd7MKj19fH$Xe$;nhV+}*&BdmPp9U&djh!3Twr49brRhaxcyECaGANlg(L{g{pwJ42Qml#cJeQ8wWd=`#C*R+QK6x>dDexcr=hH4Ko!rmnjYT z6(tAf#Y}0lUy*hMHb;ynklXIDl&|mq4fC`tIZmRqOX*|H6 rT=Oe%u^~dVRYO@1(T9iN#U?>#$C;?zmVVLOKd#9`jd?%F$PNr)GRZqi`lrO*&UIKr{m#w8FE z0=$HMizosT>Y_zR(834QR;ef$h)6#a2~?0;S}H`j6xt6!Ddnh?s+2&F|Ge4Rd9yoj zyoRilXuNOU@Atpo@!$Q=#rHoKKeQk<=eQsUr@=q^xo>4wnsfY#9dDf}DwAv0_J~hR zZeM#xc+Gqzl#wP6g*znajVIU1UG1{^fi*!@PpJP?KT$uJn$`8XI=8!$g)bLZ;_!bv zDZ>8=vh2wTxqYD${X$T$NWGNWs(hr}rnDy?O)81&}YCo*4_V;N5d&$vg4kTJlX~iw_6V~Jp99Y2&IIn?e~Vo(1H2l z7%(x#DHokm&1K-`hszD#(ZOb zw0CJy&uCdqLwzWb=%2WB*RHyZIMW5PrwvorcJ1AD!_Kk#h%=BLum_5R!^Pnm&OmWE z505%P_K%Zgl&#Z;O;PmK+Cav`=-}Ff*oVpDA>EX5 zj-{g@cq}SO+G;&O!<}P%%XO*PaunZmSs!3z>KR{O>84h5B8M?MPL?!S4!faYo9-kt zmdIkt=pT#O3Csqg9U1}pq5i+DsU|gxD{qGbOShz-vC)#nlf6GC@wm8p@22`7`vI$?$QU z23G6fTFtIYIY=(-mO|<%eZb7r3ZAzT>N#_o=u3~2Ib23NxhzI6AvV7bV~NZT@Kd^p zt(gqN3T7*wGc&<)m>wasX6h$KIe$N4l-^gqfx>>(>@rN0Gj-IQU9hMj7z<1;Y>bz8 zP2;tu?3#u#DBd+>l*-q;PQy*>Givl2WpmVEgVNS78>pW#QvQCB({S_JW%C8E`e3yQ zuGQS^^29oV%xbEF*or$r@Z?DA!F2`xWi*z)*CCelmgao17P`^jdj z#Yp*D zDqPDOAR9G!I|44))JlZDaybHV|6uf z!=WOLkfv$5vGPS+LfcJWm9T*i*9c4HtwC-&No+N;(en2kFG2lps4Twc39dO|0J7J= zw&e9X--QleBOOM5e44cxs8?ifmZfzw6P(xRn(_sdPw@>Gk1gT+StIi~e zsqU$F!{W%~toC!K&OiEi25Z6|LA_61mYPasl$Vr?k{>4bCYL2%P28S9@niA3<7;9c z#_o^hqpwA;jJ8LPMs`Lz!q11V53g!_zwIY&TiZJ1-^+K(>qB3Lo(WBaPLtl19+obY zW{bZQ?-17sCxoYfpy87vMOEk&MQ|MZ6FT$vb1Q5vQr&AL^o7xZE4CEdv95$Yc#TPO;q0R|$tFkX*?6gG_J z1d$i!JdQ9;x>zWNsX3dX8VasSGt7bU)d{TPEK-ax*+FtH2gO$F%sDQM2Zg7S9||5M8#pjLoqY1Z7$id+6rP)EC~$e;W+lL!&4Jhag#5~W^$$~V7R+utBJ!WWYtWE-zRU*wPL(+W@3> zuGs6FNxN^Nn?|h)_-dh#l75%3fi8*ZW&cU#+f1^Q&o8W-Vg^I!hf&R6gDoUO5uKo6 z4aSR^X+bxWG=O5?^lR8kSy9+;o$(zH_=<@(6N9HXYEC~J5Z5*`JVc#~*ksC9L_Cim z3g}!yEXQum@j1q{s<-BmvUf>P9qWffQ9WXdm}!aSoJt0kJcEnqXGs>31Gw5B5Ticb z&yv$3ZXtaX(TPb`Y5$n?Dqa&4TRlZr@o}<>qR!wpCu5>){fJO64R9KR0f@2SE5RHd zC#Q26Ik!1qkFi8fqlEVP4>$H!o9_36*r0as&h2(R9 z8Pmwk?ISBG8~+|&XPR6&SUqq<)~R-f4Vkcor52 z603RpH5f}|1=qZZgm+m1wq`PH_5bMz=%+h}Lx1xQjFRP)jaIK#?tGg0>U5Cc)*Z~k zfXjn{r$q4;oQMcnVXvURpx&ttrv96HHg#hPDIY73DHkgp(MysyC;JlbCmu=^!we6-V#i`XifxFVi2fq^a`YRKmm~WlM)>`N5n0fp4s4LUeXRck-(=tIumV#tJkxGz zoF++j@-|I7!)42peK4(V98lB8IFw6qCl$!emS=lT*USvu4%bfUESYBjWeQ}k%E?4eQ6-7>`xxyg!+#WR?&+lrXQaQJLZiH+n+YGLQj5KNUwFg3YzTY44`y@K=acRgr3b_5Lgf705t)N2Axd? z&5G2yww;`p0%RE?(5bX$^8(nz76=5r$vP0WauBwM+zH$Pl-@{oP$ZA*n6|oOA8;M= ztb5J?xK45I9uX>#aVP#^-9ot z6aTGE$dhZpi1kv@SVuK$4R6U7SKD5#H5IDQDAHWWT5j4f16JBxMJ)-@$b9d$3p0T9 z(Bh@p&)C}G3J_w5Y-eoU!h}45UaKo_ zb6i_S@%&QHsS3;hLT3@^t=jz*+7iK?OK3lZE$Uh8R@&;P447%>%wY9XHVlG720WZa zpkK;>)G)lqO&7!2iZ@_D3$#E_xFkxhIN33 z>tvkHWY-gDST>{hssoFob4+AVC~5E@cc5M6bRH}a zlq1kBY@X|@2K=bO-Wo>6igfa0e-XR3E07wigsTK~O1)BDlKNBXp42+!INiR_PQIAj zlRP!?R^q5!Dw&f*O6`E&%!?puWUQown_e*OyJkTPlq;0 zZ%VtRS>j=FP= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为inspected的行或没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"找到没有称重数据的行: {data_row}") + else: + logging.info(f"将使用状态为inspected的行: {data_row}") +""" + +# 5. 添加称重完成后的状态更新代码 +PROCESS_STABLE_WEIGHT_UPDATE_STATUS = """ + # 更新产品状态为weighed + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed') + logging.info(f"工程号 {gc_note} 的称重已完成,状态更新为weighed") +""" + +# 6. 修改handle_label_signal方法中的查找行逻辑 +HANDLE_LABEL_SIGNAL_FIND_ROW = """ + # 基于状态查找行:优先查找状态为weighed的行 + data_row = None + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 首先查找状态为weighed的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'weighed': + data_row = row + logging.info(f"找到状态为weighed的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到weighed状态的行,回退到原有逻辑 + if data_row is None: + # 使用当前选中的行或第一个数据行 + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为weighed的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"将使用状态为weighed的行: {data_row}") +""" + +# 7. 添加贴标完成后的状态更新代码 +HANDLE_LABEL_SIGNAL_UPDATE_STATUS = """ + # 更新产品状态为labeled + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'labeled') + logging.info(f"工程号 {gc_note} 的贴标已完成,状态更新为labeled") +""" + +# 8. 在handle_inspection_cell_changed方法末尾添加调用check_inspection_completed +HANDLE_INSPECTION_CELL_CHANGED_CALL = """ + # 检查是否完成检验并更新状态 + self.check_inspection_completed(row) +""" + +# 9. 在add_new_inspection_row方法末尾添加初始化状态代码 +ADD_NEW_INSPECTION_ROW_INIT_STATUS = """ + # 初始化产品状态为init + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'init') + logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条,初始状态为init") +""" + +# 10. 移除数据校验逻辑,替换为简单的单元格颜色设置 +REMOVE_VALIDATION_LOGIC = """ + # 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) +""" + +# 主函数:应用所有修改 +def apply_fixes(): + print("开始应用状态管理功能修复...") + + # 备份文件 + os.system("cp dao/inspection_dao.py dao/inspection_dao.py.bak") + os.system("cp widgets/main_window.py widgets/main_window.py.bak") + + # 1. 添加DAO方法 + with open("dao/inspection_dao.py", "r") as f: + dao_content = f.read() + + # 检查方法是否已存在 + if "def get_product_status" not in dao_content: + # 找到合适的位置插入新方法 + pattern = r'def save_package_record.*?return False' + match = re.search(pattern, dao_content, re.DOTALL) + if match: + insert_pos = match.end() + new_content = dao_content[:insert_pos] + DAO_METHODS + dao_content[insert_pos:] + with open("dao/inspection_dao.py", "w") as f: + f.write(new_content) + print("1. 已成功添加状态管理方法到 dao/inspection_dao.py") + else: + print("无法找到合适的位置插入DAO方法") + else: + print("1. 状态管理方法已存在,跳过添加") + + # 读取main_window.py + with open("widgets/main_window.py", "r") as f: + main_window_content = f.read() + + # 2. 添加check_inspection_completed方法 + if "def check_inspection_completed" not in main_window_content: + # 找到合适的位置插入新方法 + pattern = r'def validate_inspection_value.*?return False' + match = re.search(pattern, main_window_content, re.DOTALL) + if match: + insert_pos = match.end() + new_content = main_window_content[:insert_pos] + "\n" + CHECK_INSPECTION_METHOD + main_window_content[insert_pos:] + main_window_content = new_content + print("2. 已成功添加check_inspection_completed方法") + else: + print("无法找到合适的位置插入check_inspection_completed方法") + else: + print("2. check_inspection_completed方法已存在,跳过添加") + + # 3. 修改save_inspection_data方法 + pattern = r'def save_inspection_data.*?inspection_dao\.save_inspection_data\(order_id, gc_note, data\)' + replacement = SAVE_INSPECTION_DATA_METHOD + main_window_content = re.sub(pattern, replacement, main_window_content, flags=re.DOTALL) + print("3. 已成功修改save_inspection_data方法") + + # 4. 修改_process_stable_weight方法中的查找行逻辑 + pattern = r'# 查找第一个没有称重数据的行\s*data_row = None\s*for row in range.*?if data_row is None:.*?else:\s*logging\.info\(f"找到没有称重数据的行: \{data_row\}"\)' + replacement = PROCESS_STABLE_WEIGHT_FIND_ROW + main_window_content = re.sub(pattern, replacement, main_window_content, flags=re.DOTALL) + print("4. 已成功修改_process_stable_weight方法中的查找行逻辑") + + # 5. 添加称重完成后的状态更新代码 + pattern = r'(logging\.info\(f"已将稳定的称重数据 \{weight_kg\}kg 写入行 \{data_row\}, 列 \{weight_col\}"\))\s*\n\s*except' + replacement = r'\1\n\n' + PROCESS_STABLE_WEIGHT_UPDATE_STATUS + r'\n except' + main_window_content = re.sub(pattern, replacement, main_window_content) + print("5. 已成功添加称重完成后的状态更新代码") + + # 6. 修改handle_label_signal方法中的查找行逻辑 + pattern = r'# 获取当前选中的行或第一个数据行\s*current_row = self\.process_table\.currentRow\(\)\s*data_row = current_row if current_row >= 2 else 2' + replacement = HANDLE_LABEL_SIGNAL_FIND_ROW + main_window_content = re.sub(pattern, replacement, main_window_content) + print("6. 已成功修改handle_label_signal方法中的查找行逻辑") + + # 7. 添加贴标完成后的状态更新代码 + pattern = r'(logging\.info\(f"已将贴标数据 \{axios_num\} 保存到数据库"\))\s*\n\s*# 调用加载到包装记录的方法' + replacement = r'\1\n\n' + HANDLE_LABEL_SIGNAL_UPDATE_STATUS + r'\n \n # 调用加载到包装记录的方法' + main_window_content = re.sub(pattern, replacement, main_window_content) + print("7. 已成功添加贴标完成后的状态更新代码") + + # 8. 在handle_inspection_cell_changed方法末尾添加调用check_inspection_completed + pattern = r'(logging\.info\(f"处理单元格变更: 行=\{row\}, 列=\{column\}, 类型=\{data_type\}, 工程号=\{gc_note\}, 值=\{value\}, 状态=\{status\}"\))\s*\n\s*except' + replacement = r'\1\n\n' + HANDLE_INSPECTION_CELL_CHANGED_CALL + r'\n except' + main_window_content = re.sub(pattern, replacement, main_window_content) + print("8. 已成功在handle_inspection_cell_changed方法末尾添加调用check_inspection_completed") + + # 9. 修改add_new_inspection_row方法,设置初始状态 + # 9.1 修改检验项的status为init + pattern = r"'status': '',\s*# 默认设置为通过状态" + replacement = "'status': 'init', # 设置初始状态" + main_window_content = re.sub(pattern, replacement, main_window_content) + + # 9.2 修改贴标和称重项的status为init + pattern = r"'status': 'pass',\s*# 默认设置为通过状态" + replacement = "'status': 'init', # 设置初始状态" + main_window_content = re.sub(pattern, replacement, main_window_content) + + # 9.3 添加状态初始化代码 + pattern = r'(logging\.info\(f"已添加工程号 \{gc_note\} 的新记录,显示在第\{new_seq\}条"\))\s*\n\s*except' + replacement = ADD_NEW_INSPECTION_ROW_INIT_STATUS + r'\n except' + main_window_content = re.sub(pattern, replacement, main_window_content) + print("9. 已成功修改add_new_inspection_row方法,设置初始状态") + + # 10. 移除数据校验逻辑 + pattern = r'# 验证数据有效性\s*if self\.validate_inspection_value\(config, value\):.*?status = \'warning\'' + replacement = REMOVE_VALIDATION_LOGIC + main_window_content = re.sub(pattern, replacement, main_window_content, flags=re.DOTALL) + print("10. 已成功移除数据校验逻辑") + + # 保存修改后的main_window.py + with open("widgets/main_window.py", "w") as f: + f.write(main_window_content) + + print("状态管理功能修复完成!") + +if __name__ == "__main__": + apply_fixes() diff --git a/from pymodbus.py b/from pymodbus.py index e1c7797..9fe4c49 100644 --- a/from pymodbus.py +++ b/from pymodbus.py @@ -12,9 +12,9 @@ client.write_registers(address=11, values=[2122]) # client.write_registers(address=5, values=[16]) # 贴标完成 # client.write_registers(address=24, values=[1]) -client.write_registers(address=3, values=[1]) +client.write_registers(address=13, values=[1]) -result = client.read_holding_registers(address=3, count=1) +result = client.read_holding_registers(address=13, count=1) print(result.registers[0],"123===") client.close() \ No newline at end of file diff --git a/status_management_patch.py b/status_management_patch.py new file mode 100755 index 0000000..d6244e7 --- /dev/null +++ b/status_management_patch.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +""" +状态管理功能补丁文件 +用于修复产品状态管理相关的代码 +""" + +import os +import re + +# 定义要修改的文件 +DAO_FILE = "dao/inspection_dao.py" +MAIN_WINDOW_FILE = "widgets/main_window.py" + +# 1. 在InspectionDAO类中添加状态管理方法 +def add_dao_methods(): + with open(DAO_FILE, "r") as f: + content = f.read() + + # 检查方法是否已存在 + if "def get_product_status" in content: + print("状态管理方法已存在,跳过添加") + return + + # 找到合适的位置插入新方法 + pattern = r'(def save_package_record.*?\n\s*return False\n)' + + new_methods = r"""\1 + def get_product_status(self, order_id, gc_note, tray_id): + """获取产品的当前状态 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + + Returns: + str: 产品状态,如果没有找到则返回'init' + """ + try: + with SQLUtils('sqlite', database='db/jtDB.db') as db: + sql = """ + SELECT status FROM wsbz_inspection_data + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + ORDER BY id ASC LIMIT 1 + """ + params = (order_id, gc_note, tray_id) + result = db.query_one(sql, params) + return result[0] if result and result[0] else 'init' # 默认为init状态 + except Exception as e: + logging.error(f"获取产品状态失败: {str(e)}") + return 'init' # 出错时返回默认状态 + + def update_product_status(self, order_id, gc_note, tray_id, new_status): + """更新产品的状态 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + new_status: 新状态 + + Returns: + bool: 更新是否成功 + """ + try: + with SQLUtils('sqlite', database='db/jtDB.db') as db: + # 更新该产品所有记录的状态字段 + update_sql = """ + UPDATE wsbz_inspection_data SET status = ?, update_time = ? + WHERE order_id = ? AND gc_note = ? AND tray_id = ? + """ + update_params = (new_status, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + order_id, gc_note, tray_id) + db.execute(update_sql, update_params) + logging.info(f"已更新产品状态: 订单号={order_id}, 工程号={gc_note}, 托盘号={tray_id}, 新状态={new_status}") + return True + except Exception as e: + logging.error(f"更新产品状态失败: {str(e)}") + return False""" + + modified_content = re.sub(pattern, new_methods, content, flags=re.DOTALL) + + with open(DAO_FILE, "w") as f: + f.write(modified_content) + + print(f"已成功添加状态管理方法到 {DAO_FILE}") + +# 2. 修改add_new_inspection_row方法,设置初始状态 +def update_add_new_inspection_row(): + with open(MAIN_WINDOW_FILE, "r") as f: + content = f.read() + + # 修改检验项的status为init + pattern1 = r"'status': '',\s*# 默认设置为通过状态" + replacement1 = "'status': 'init', # 设置初始状态" + content = re.sub(pattern1, replacement1, content) + + # 修改贴标和称重项的status为init + pattern2 = r"'status': 'pass',\s*# 默认设置为通过状态" + replacement2 = "'status': 'init', # 设置初始状态" + content = re.sub(pattern2, replacement2, content) + + # 添加状态初始化代码 + pattern3 = r"(logging\.info\(f\"已添加工程号 \{gc_note\} 的新记录,显示在第\{new_seq\}条\"\))" + replacement3 = r"""# 初始化产品状态为init + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'init') + logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条,初始状态为init")""" + content = re.sub(pattern3, replacement3, content) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功更新add_new_inspection_row方法") + +# 3. 添加check_inspection_completed方法 +def add_check_inspection_completed(): + with open(MAIN_WINDOW_FILE, "r") as f: + content = f.read() + + # 检查方法是否已存在 + if "def check_inspection_completed" in content: + print("check_inspection_completed方法已存在,跳过添加") + else: + # 找到合适的位置插入新方法 + pattern = r'(def validate_inspection_value.*?)(\n\s*def )' + + new_method = r"""\1 + + def check_inspection_completed(self, row): + """检查行是否有至少一个检验项已完成,如果是则更新状态为inspected + + Args: + row: 行索引 + + Returns: + bool: 是否有至少一个检验项已完成 + """ + try: + # 获取工程号 + gc_note_item = self.process_table.item(row, 1) + if not gc_note_item: + return False + + gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 检查是否有至少一个检验项有值 + has_any_value = False + for i, config in enumerate(enabled_configs): + col_index = 2 + i + item = self.process_table.item(row, col_index) + if item and item.text().strip(): + has_any_value = True + break + + # 如果有至少一个检验项有值,更新状态为inspected + if has_any_value: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'inspected') + logging.info(f"工程号 {gc_note} 的检验已完成,状态更新为inspected") + + return has_any_value + except Exception as e: + logging.error(f"检查检验完成状态失败: {str(e)}") + return False\2""" + + content = re.sub(pattern, new_method, content, flags=re.DOTALL) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功添加check_inspection_completed方法") + + # 在handle_inspection_cell_changed方法末尾添加调用 + pattern = r'(logging\.info\(f"处理单元格变更: 行=\{row\}, 列=\{column\}, 类型=\{data_type\}, 工程号=\{gc_note\}, 值=\{value\}, 状态=\{status\}"\))\n(\s*except)' + replacement = r"""\1 + + # 检查是否完成检验并更新状态 + self.check_inspection_completed(row) + \2""" + + content = re.sub(pattern, replacement, content) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功在handle_inspection_cell_changed方法末尾添加调用check_inspection_completed") + +# 4. 修改_process_stable_weight方法 +def update_process_stable_weight(): + with open(MAIN_WINDOW_FILE, "r") as f: + content = f.read() + + # 修改查找行的逻辑 + pattern1 = r'# 查找第一个没有称重数据的行\s*data_row = None\s*for row in range.*?if data_row is None:.*?else:\s*logging\.info\(f"找到没有称重数据的行: \{data_row\}"\)' + replacement1 = r"""# 基于状态查找行:优先查找状态为inspected的行 + data_row = None + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 首先查找状态为inspected的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'inspected': + data_row = row + logging.info(f"找到状态为inspected的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到inspected状态的行,回退到原有逻辑 + if data_row is None: + # 查找第一个没有称重数据的行 + for row in range(2, self.process_table.rowCount()): + weight_item = self.process_table.item(row, weight_col) + if not weight_item or not weight_item.text().strip(): + data_row = row + break + + # 如果仍然没有找到,使用当前选中行或第一个数据行 + if data_row is None: + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为inspected的行或没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"找到没有称重数据的行: {data_row}") + else: + logging.info(f"将使用状态为inspected的行: {data_row}")""" + + content = re.sub(pattern1, replacement1, content, flags=re.DOTALL) + + # 添加状态更新代码 + pattern2 = r'(logging\.info\(f"已将稳定的称重数据 \{weight_kg\}kg 写入行 \{data_row\}, 列 \{weight_col\}"\))\n\s*except' + replacement2 = r"""\1 + + # 更新产品状态为weighed + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed') + logging.info(f"工程号 {gc_note} 的称重已完成,状态更新为weighed") + except""" + + content = re.sub(pattern2, replacement2, content) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功更新_process_stable_weight方法") + +# 5. 修改handle_label_signal方法 +def update_handle_label_signal(): + with open(MAIN_WINDOW_FILE, "r") as f: + content = f.read() + + # 修改查找行的逻辑 + pattern1 = r'# 获取当前选中的行或第一个数据行\s*current_row = self\.process_table\.currentRow\(\)\s*data_row = current_row if current_row >= 2 else 2' + replacement1 = r"""# 基于状态查找行:优先查找状态为weighed的行 + data_row = None + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 首先查找状态为weighed的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'weighed': + data_row = row + logging.info(f"找到状态为weighed的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到weighed状态的行,回退到原有逻辑 + if data_row is None: + # 使用当前选中的行或第一个数据行 + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为weighed的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"将使用状态为weighed的行: {data_row}")""" + + content = re.sub(pattern1, replacement1, content) + + # 添加状态更新代码 + pattern2 = r'(logging\.info\(f"已将贴标数据 \{axios_num\} 保存到数据库"\))\n\s*# 调用加载到包装记录的方法' + replacement2 = r"""\1 + + # 更新产品状态为labeled + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'labeled') + logging.info(f"工程号 {gc_note} 的贴标已完成,状态更新为labeled") + + # 调用加载到包装记录的方法""" + + content = re.sub(pattern2, replacement2, content) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功更新handle_label_signal方法") + +# 6. 修改save_inspection_data方法 +def update_save_inspection_data(): + with open(MAIN_WINDOW_FILE, "r") as f: + content = f.read() + + # 查找save_inspection_data方法 + pattern = r'def save_inspection_data.*?try:.*?inspection_dao = InspectionDAO\(\).*?# 记录保存前的详细日志' + + # 修改方法,添加状态获取逻辑 + replacement = r"""def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status): + """保存检验数据到数据库 + + Args: + order_id: 订单号 + gc_note: 工程号 + position: 位置序号 + config_id: 配置ID + value: 检验值 + status: 状态 + """ + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + modbus = ModbusUtils() + client = modbus.get_client() + + # 获取当前产品状态,优先使用产品状态管理中的状态 + current_status = inspection_dao.get_product_status(order_id, gc_note, tray_id) + + # 如果当前状态不是初始状态,则使用当前状态而不是传入的status + if current_status not in ['', 'init']: + status = current_status + + # 记录保存前的详细日志""" + + content = re.sub(pattern, replacement, content, flags=re.DOTALL) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功更新save_inspection_data方法") + +# 7. 移除数据校验逻辑 +def remove_validation_logic(): + with open(MAIN_WINDOW_FILE, "r") as f: + content = f.read() + + # 第一处:移除handle_inspection_cell_changed中的数据校验逻辑 + pattern1 = r'# 验证数据有效性\s*if self\.validate_inspection_value\(config, value\):.*?status = \'warning\'' + replacement1 = r"""# 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9")))""" + + content = re.sub(pattern1, replacement1, content, flags=re.DOTALL) + + with open(MAIN_WINDOW_FILE, "w") as f: + f.write(content) + + print(f"已成功移除数据校验逻辑") + +# 执行所有修改 +def apply_all_changes(): + print("开始应用状态管理功能补丁...") + add_dao_methods() + update_add_new_inspection_row() + add_check_inspection_completed() + update_process_stable_weight() + update_handle_label_signal() + update_save_inspection_data() + remove_validation_logic() + print("状态管理功能补丁应用完成!") + +if __name__ == "__main__": + apply_all_changes() diff --git a/temp_fix.py b/temp_fix.py new file mode 100644 index 0000000..dc6c42d --- /dev/null +++ b/temp_fix.py @@ -0,0 +1,7 @@ + # 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) + + # 保持当前状态不变,由状态管理逻辑处理 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) diff --git a/temp_save_inspection_data.py b/temp_save_inspection_data.py new file mode 100644 index 0000000..8e1d31e --- /dev/null +++ b/temp_save_inspection_data.py @@ -0,0 +1,39 @@ + def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status): + """保存检验数据到数据库 + + Args: + order_id: 订单号 + gc_note: 工程号 + position: 位置序号 + config_id: 配置ID + value: 检验值 + status: 状态 + """ + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + modbus = ModbusUtils() + client = modbus.get_client() + + # 获取当前产品状态,优先使用产品状态管理中的状态 + current_status = inspection_dao.get_product_status(order_id, gc_note, tray_id) + + # 如果当前状态不是初始状态,则使用当前状态而不是传入的status + if current_status not in ['', 'init']: + status = current_status + + # 记录保存前的详细日志 + logging.info(f"正在保存检验数据: 工程号={gc_note}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}") + + # 构建数据 + data = [{ + 'position': position, + 'config_id': config_id, + 'value': value, + 'status': status, + 'remark': '', + 'tray_id': tray_id + }] + + # 保存到数据库 + inspection_dao.save_inspection_data(order_id, gc_note, data) diff --git a/ui/main_window_ui.py b/ui/main_window_ui.py index 53c09d2..38a2501 100644 --- a/ui/main_window_ui.py +++ b/ui/main_window_ui.py @@ -1,7 +1,7 @@ from PySide6.QtWidgets import ( QMainWindow, QWidget, QLabel, QGridLayout, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView, QFrame, QSplitter, - QPushButton, QLineEdit, QAbstractItemView, QComboBox, QSizePolicy + QPushButton, QLineEdit, QAbstractItemView, QComboBox, QSizePolicy, QTextEdit ) from PySide6.QtGui import QFont, QAction, QBrush, QColor from PySide6.QtCore import Qt, QDateTime, QTimer @@ -320,11 +320,18 @@ class MainWindowUI(QMainWindow): label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) label.setStyleSheet("background-color: #FAFAFA; padding: 5px;") - # 创建值 - value = QLabel("") + # 创建值(改为QTextEdit) + value = QTextEdit("") value.setFont(self.normal_font) - value.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) value.setStyleSheet("background-color: white; padding: 5px;") + value.setFixedHeight(35) + value.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + value.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + value.setFrameStyle(QFrame.NoFrame) + value.setLineWrapMode(QTextEdit.NoWrap) + value.setContentsMargins(0, 0, 0, 0) + value.setAcceptRichText(False) + value.setTabChangesFocus(True) # 保存引用 self.info_labels["备注"] = label @@ -341,11 +348,14 @@ class MainWindowUI(QMainWindow): label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) label.setStyleSheet("background-color: #FAFAFA; padding: 5px;") - # 创建值 - value = QLabel("") + # 创建值(改为QLineEdit) + value = QLineEdit("") value.setFont(self.normal_font) value.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - value.setStyleSheet("background-color: white; padding: 5px;") + value.setStyleSheet("background-color: white; padding: 5px; border: none;") + value.setFrame(False) + value.setContentsMargins(0, 0, 0, 0) + value.setFixedHeight(35) # 保存引用 self.info_labels[field_name] = label diff --git a/widgets/loading_dialog_widget.py b/widgets/loading_dialog_widget.py index eb9b2ce..867416d 100644 --- a/widgets/loading_dialog_widget.py +++ b/widgets/loading_dialog_widget.py @@ -18,12 +18,12 @@ class LoadingDialog(LoadingDialogUI): def __init__(self, parent=None, user_id=None, user_name=None, corp_id=None): super().__init__() self.parent = parent - self.user_id = user_id + self.user_id = user_id, self.user_name = user_name self.corp_id = corp_id # 存储订单数据 - self.order_data = None + # self.order_data = None # 彻底禁用对话框的回车键关闭功能 self.setModal(True) @@ -188,7 +188,7 @@ class LoadingDialog(LoadingDialogUI): inspection_dao = InspectionDAO() order_info = order_response.get("data", {})[0] # 设置轴数 - order_info['user_id'] = self.user_id + order_info['user_id'] = self.user_id[0] order_info['user_name'] = self.user_name order_info['data_corp'] = self.corp_id inspection_dao.save_order_info(order_code, order_info) @@ -289,9 +289,9 @@ class LoadingDialog(LoadingDialogUI): self.order_input.setFocus() return - if not self.order_data: - QMessageBox.warning(self, "提示", "请先查询订单信息") - return + # if not self.order_data: + # QMessageBox.warning(self, "提示", "请先查询订单信息") + # return try: # 保存托盘档案信息 @@ -299,7 +299,7 @@ class LoadingDialog(LoadingDialogUI): success = pallet_manager.save_pallet_archives( pallet_code=pallet_code, tier=int(tier_value), - user_id=self.user_id, + user_id=self.user_id[0], user_name=self.user_name ) diff --git a/widgets/main_window.py b/widgets/main_window.py index c0de696..8bae4f8 100644 --- a/widgets/main_window.py +++ b/widgets/main_window.py @@ -826,7 +826,7 @@ class MainWindow(MainWindowUI): # 'position': config.get('position'), # 'config_id': config.get('id'), # 'value': value, - # 'status': 'pass', # 默认设置为通过状态 + # 'status': 'init', # 设置初始状态 # 'remark': '', # 'tray_id': tray_id # }] @@ -872,7 +872,7 @@ class MainWindow(MainWindowUI): 'position': config.get('position'), 'config_id': config.get('id'), 'value': '', - 'status': '', # 默认设置为通过状态 + 'status': 'init', # 设置初始状态 'remark': '', 'tray_id': tray_id }] @@ -884,13 +884,15 @@ class MainWindow(MainWindowUI): 'position': position, 'config_id': position, 'value': '', - 'status': 'pass', # 默认设置为通过状态 + 'status': 'init', # 设置初始状态 'remark': '', 'tray_id': tray_id }] inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) - logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条") + # 初始化产品状态为init + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'init') + logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条,初始状态为init") except Exception as e: logging.error(f"添加新记录失败: {str(e)}") @@ -980,15 +982,13 @@ class MainWindow(MainWindowUI): # 显示临时状态消息 self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) - # 验证数据有效性 - if self.validate_inspection_value(config, value): - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - status = 'pass' - else: - # 设置单元格颜色为警告 - cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色 - status = 'warning' + # 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) + + # 保持当前状态不变,由状态管理逻辑处理 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) # 保存到数据库 self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) @@ -1022,6 +1022,9 @@ class MainWindow(MainWindowUI): # 记录详细日志 logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}") + + # 检查是否完成检验并更新状态 + self.check_inspection_completed(row) except Exception as e: logging.error(f"处理检验单元格变更失败: {str(e)}") @@ -1032,65 +1035,7 @@ class MainWindow(MainWindowUI): if not self._loading_data_in_progress: QTimer.singleShot(1000, self._safe_load_data) - def validate_inspection_value(self, config, value): - """验证检验值是否有效 - - Args: - config: 检验配置 - value: 检验值 - - Returns: - bool: 是否有效 - """ - try: - # 特殊处理贴标和称重数据 - 这些数据默认都是有效的 - if config.get('position') in [11, 12]: # 11是贴标,12是称重 - return True - - # 检查值是否为空 - if not value and config.get('required', False): - return False - - # 根据数据类型验证 - data_type = config.get('data_type') - - if data_type == 'number': - # 数值类型验证 - try: - # 如果值为空且不是必填,则视为有效 - if not value and not config.get('required', False): - return True - - num_value = float(value) - min_value = config.get('min_value') - max_value = config.get('max_value') - - if min_value is not None and num_value < min_value: - return False - - if max_value is not None and num_value > max_value: - return False - - return True - except ValueError: - return False - - elif data_type == 'enum': - # 枚举类型验证 - enum_values = config.get('enum_values') - if enum_values and isinstance(enum_values, list): - # 如果值为空且不是必填,则视为有效 - if not value and not config.get('required', False): - return True - return value in enum_values - return False - - # 文本类型不做特殊验证 - return True - - except Exception as e: - logging.error(f"验证检验值失败: {str(e)}") - return False + def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status): """保存检验数据到数据库 @@ -1108,6 +1053,14 @@ class MainWindow(MainWindowUI): inspection_dao = InspectionDAO() modbus = ModbusUtils() client = modbus.get_client() + + # 获取当前产品状态,优先使用产品状态管理中的状态 + current_status = inspection_dao.get_product_status(order_id, gc_note, tray_id) + + # 如果当前状态不是初始状态,则使用当前状态而不是传入的status + if current_status not in ['', 'init']: + status = current_status + # 记录保存前的详细日志 logging.info(f"正在保存检验数据: 工程号={gc_note}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}") @@ -1123,6 +1076,7 @@ class MainWindow(MainWindowUI): # 保存到数据库 inspection_dao.save_inspection_data(order_id, gc_note, data) + except Exception as e: logging.error(f"保存检验数据失败: {str(e)}") # 显示错误消息 @@ -1943,21 +1897,41 @@ class MainWindow(MainWindowUI): # 计算净重列索引 - 净重位置在检验列之后的第三列(称重后面) net_weight_col = 2 + len(enabled_configs) + 2 - # 查找第一个没有称重数据的行 + # 基于状态查找行:优先查找状态为inspected的行 data_row = None - for row in range(2, self.process_table.rowCount()): - weight_item = self.process_table.item(row, weight_col) - if not weight_item or not weight_item.text().strip(): - data_row = row - break + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() - # 如果没有找到没有称重数据的行,使用当前选中行或第一个数据行 + # 首先查找状态为inspected的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'inspected': + data_row = row + logging.info(f"找到状态为inspected的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到inspected状态的行,回退到原有逻辑 if data_row is None: - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) - logging.info(f"未找到没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") + # 查找第一个没有称重数据的行 + for row in range(2, self.process_table.rowCount()): + weight_item = self.process_table.item(row, weight_col) + if not weight_item or not weight_item.text().strip(): + data_row = row + break + + # 如果仍然没有找到,使用当前选中行或第一个数据行 + if data_row is None: + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为inspected的行或没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"找到没有称重数据的行: {data_row}") else: - logging.info(f"找到没有称重数据的行: {data_row}") + logging.info(f"将使用状态为inspected的行: {data_row}") # 获取工程号 gc_note = self.process_table.item(data_row, 1) @@ -2003,7 +1977,7 @@ class MainWindow(MainWindowUI): net_weight_item = QTableWidgetItem(str(net_weight_kg)) net_weight_item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(data_row, net_weight_col, net_weight_item) - + axios_num = self.get_axios_num_by_order_id(self._current_order_code) + 1 # 如果开启 api 模式,则调用接口添加到包装记录 if AppMode.is_api(): from dao.inspection_dao import InspectionDAO @@ -2011,7 +1985,6 @@ class MainWindow(MainWindowUI): inspection_dao = InspectionDAO() # 调用接口 gc_api = GcApi() - axios_num = self.get_axios_num_by_order_id(self._current_order_code) + 1 # 获取订单信息和其他信息,两者都已经是字典格式 info = {} order_info = inspection_dao.get_order_info(self._current_order_code) @@ -2058,6 +2031,10 @@ class MainWindow(MainWindowUI): logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}") + # 更新产品状态为weighed + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed') + logging.info(f"工程号 {gc_note} 的称重已完成,状态更新为weighed") + except Exception as e: logging.error(f"处理称重数据时发生错误: {str(e)}") # 确保重新连接信号 @@ -2090,14 +2067,50 @@ class MainWindow(MainWindowUI): # 只有当信号为贴标完成(1)时才进行处理 if signal == 1: try: + modbus = ModbusUtils() + client = None + try: + # 获取Modbus客户端(现在使用连接池,不会每次都创建新连接) + client = modbus.get_client() + if not client: + logging.error("无法获取Modbus客户端连接") + return + # 先将寄存器回复为0,否则复原周期内、会把新来的数据也pass + modbus.write_register_until_success(client, 13, 0) + except Exception as e: + logging.error(f"复原寄存器失败{e}") + finally: + client.close() # 获取数据行数 if self.process_table.rowCount() <= 2: # 没有数据行 logging.warning("没有可用的数据行来写入贴标数据") return - - # 获取当前选中的行或第一个数据行 - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + + # 基于状态查找行:优先查找状态为weighed的行 + data_row = None + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 首先查找状态为weighed的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'weighed': + data_row = row + logging.info(f"找到状态为weighed的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到weighed状态的行,回退到原有逻辑 + if data_row is None: + # 使用当前选中的行或第一个数据行 + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为weighed的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"将使用状态为weighed的行: {data_row}") # 确保行存在 if data_row >= self.process_table.rowCount(): @@ -2144,6 +2157,10 @@ class MainWindow(MainWindowUI): # 在这里添加保存贴标数据到数据库的代码 self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass") logging.info(f"已将贴标数据 {axios_num} 保存到数据库") + + # 更新产品状态为labeled + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'labeled') + logging.info(f"工程号 {gc_note} 的贴标已完成,状态更新为labeled") # 调用加载到包装记录的方法 self.load_finished_record_to_package_record(self._current_order_code,gc_note, tray_id) @@ -2868,7 +2885,7 @@ class MainWindow(MainWindowUI): bccd, tccd = inspection_dao.get_xj_range(self._current_order_code) if bccd is not None and tccd is not None: - if bccd - 0.5 <= final_value <= tccd + 0.5: + if float(bccd) - 0.5 <= final_value <= float(tccd) + 0.5: # 使用set_inspection_value保存数据 self.set_inspection_value('xj', xj_config, final_value) logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") @@ -3727,4 +3744,152 @@ class MainWindow(MainWindowUI): logging.info(f"当前焦点控件: 类型={widget_type}, 名称={widget_name}, 文本={widget_text}") except Exception as e: - logging.error(f"记录焦点控件信息失败: {e}") \ No newline at end of file + logging.error(f"记录焦点控件信息失败: {e}") + + def handle_inspection_cell_changed(self, row, column): + """处理检验表格单元格内容变更事件""" + try: + # 只处理数据行的检验列变更 + if row < 2: # 忽略表头行 + return + + # 忽略首尾两列(序号和工程号) + if column < 2: + return + + # 获取工程号 + order_item = self.process_table.item(row, 1) + if not order_item: + return + + gc_note = order_item.text().strip() + if not gc_note: + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 判断是否是检验列(非包装列) + packaging_start_col = 2 + len(enabled_configs) + + # 获取单元格内容 + cell_item = self.process_table.item(row, column) + if not cell_item: + return + + value = cell_item.text().strip() + + # 默认设置为通过状态 + status = 'pass' + + # 记录当前正在处理的数据类型,用于日志输出 + data_type = "检验" + + if column >= 2 and column < packaging_start_col: + # 是检验列 + config_index = column - 2 + if config_index < len(enabled_configs): + config = enabled_configs[config_index] + data_type = config['display_name'] + + # 显示临时状态消息 + self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) + + # 验证数据有效性 + # 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) + + # 保持当前状态不变,由状态管理逻辑处理 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) + self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) + + # 判断是否是包装列 + elif column == packaging_start_col: + # 贴标列 + data_type = "贴标" + self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存贴标数据,position和config_id都是11 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, value, status) + + elif column == packaging_start_col + 1: + # 毛重列 + data_type = "毛重" + self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存毛重数据,position和config_id都是12 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, value, status) + elif column == packaging_start_col + 2: + # 净重列 + data_type = "净重" + self.statusBar().showMessage(f"正在保存净重数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存净重数据,position和config_id都是13 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, value, status) + + # 记录详细日志 + logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}") + + # 检查是否完成检验并更新状态 + self.check_inspection_completed(row) + + except Exception as e: + logging.error(f"处理检验单元格变更失败: {str(e)}") + self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) + finally: + # 延迟一段时间后再触发查询,避免频繁刷新UI + # 但要避免在加载过程中触发新的加载 + if not self._loading_data_in_progress: + QTimer.singleShot(1000, self._safe_load_data) + + + + def check_inspection_completed(self, row): + """检查行是否有至少一个检验项已完成,如果是则更新状态为inspected + + Args: + row: 行索引 + + Returns: + bool: 是否有至少一个检验项已完成 + """ + try: + # 获取工程号 + gc_note_item = self.process_table.item(row, 1) + if not gc_note_item: + return False + + gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 检查是否有至少一个检验项有值 + has_any_value = False + for i, config in enumerate(enabled_configs): + col_index = 2 + i + item = self.process_table.item(row, col_index) + if item and item.text().strip(): + has_any_value = True + break + + # 如果有至少一个检验项有值,更新状态为inspected + if has_any_value: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'inspected') + logging.info(f"工程号 {gc_note} 的检验已完成,状态更新为inspected") + + return has_any_value + except Exception as e: + logging.error(f"检查检验完成状态失败: {str(e)}") + return False \ No newline at end of file diff --git a/widgets/main_window.py.bak b/widgets/main_window.py.bak new file mode 100644 index 0000000..4000ee8 --- /dev/null +++ b/widgets/main_window.py.bak @@ -0,0 +1,4019 @@ +import os +import sys +import logging +import json +from datetime import datetime +from pathlib import Path +from utils.modbus_utils import ModbusUtils +from utils.modbus_monitor import get_instance as get_modbus_monitor +from utils.app_mode import AppMode +from apis.gc_api import GcApi +from utils.register_handlers import ( + NGHandler, + WeightDataHandler, + LabelSignalHandler, + MachineStatusHandlers, + LoadingFeedbackHandler, + UnloadingFeedbackHandler, + Error1Handler, + Error2Handler, + Error3Handler, + UnloadingLevelHandler, + UnloadingPositionHandler, + EmergencyStopHandler +) +from utils.electricity_monitor import ElectricityHandler +# 导入PySide6 +from PySide6.QtWidgets import ( + QWidget, QMessageBox, QTableWidgetItem, QStackedWidget, QLabel, + QTableWidget, QMenu, QComboBox, QFormLayout, QDialog, QVBoxLayout, + QFrame, QHBoxLayout, QSplitter, QPushButton +) +from PySide6.QtCore import Qt, QTimer, Slot, Signal +from PySide6.QtGui import QBrush, QColor +import time + +# 导入UI +from ui.main_window_ui import MainWindowUI +# 导入相机显示组件 +from widgets.camera_display_widget import CameraDisplayWidget + +# 导入检验配置管理器 +from utils.inspection_config_manager import InspectionConfigManager +# 导入托盘类型管理器 +from utils.pallet_type_manager import PalletTypeManager +# 导入串口管理 +from utils.serial_manager import SerialManager +from widgets.report_dialog import ReportDialog +from widgets.unloading_dialog_widget import UnloadingDialog + +class MainWindow(MainWindowUI): + """主窗口""" + + # 定义信号作为类变量 + loading_feedback_signal = Signal(str, str) # 参数:status_type, desc + unloading_feedback_signal = Signal(str, str) # 参数:status_type, desc + unloading_level_ui_signal = Signal(int) # 用于在主线程中更新下料层数UI + unloading_position_ui_signal = Signal(int) # 用于在主线程中更新下料位置UI + emergency_stop_signal = Signal(int, str) # 用于在主线程中处理急停信号 + + def __init__(self, user_id=None, user_name=None, corp_name=None, corp_id=None): + """初始化主窗口""" + super().__init__(user_id) + + # 初始化用户信息 + self.user_id = user_id + self.user_name = user_name + self.corp_name = corp_name + self.corp_id = corp_id + + # 初始化焦点跟踪器 + from utils.focus_tracker import FocusTracker + self.focus_tracker = FocusTracker.get_instance() + self.focus_tracker.initialize() + + # 连接焦点变化信号 + self.focus_tracker.focus_changed.connect(self._log_focus_widget_info) + + # 初始化系统变量 + self._current_weight = 0.0 # 当前重量 + self._last_weight_time = 0 # 上次称重时间 + self._stability_check_timer = None # 稳定性检查定时器 + self._weight_stable_threshold = 2 # 重量稳定阈值(秒) + self._weight_processed = False # 新增:标记当前重量是否已处理,避免重复处理 + self._last_processed_weight = 0.0 # 新增:记录上次处理的重量 + + # 线径数据处理相关属性 + self._last_diameter_value = 0 # 最后一次有效的线径值 + self._diameter_stable = False # 保留此属性以避免引用错误 + + # 初始化数据加载状态标志 + self._loading_data_in_progress = False # 数据加载状态标志,防止循环调用 + self._current_order_code = None # 存储当前订单号 + self.init_seq = {} # 初始化轴包装的序号 + + # 初始化拆垛和下料相关的属性 + self._current_stow_num = 0 # 当前拆垛层数 + self._current_unload_num = 0 # 当前下料层数 + self._total_unload_num = 0 + self._current_unload_info = None # 存储当前下料信息 + self._loading_info = None # 存储上料对话框的信息 + self._is_loading_active = False # 标识上料任务是否正在进行 + self._current_gc_qd = 0 # 当前工程号的强度数据 + + # 信号的连接在connect_signals方法中统一处理,不在这里连接 + + # 称重相关变量 + self._current_weight = None # 当前称重值(千克) + self._last_weight_time = None # 最后一次称重时间 + self._weight_stable_threshold = 2 # 重量稳定阈值(秒) + self._stability_check_timer = None # 用于检查重量稳定性的定时器 + + # 设置窗口标题 + if user_name and corp_name: + self.setWindowTitle(f"腾智微丝产线包装系统 ({corp_name})") + + # 加载配置文件 + self.config = self.load_config() + + # 初始化检验配置管理器 + self.inspection_manager = InspectionConfigManager.get_instance() + + # 初始化托盘类型管理器 + self.pallet_type_manager = PalletTypeManager.get_instance() + + # 创建表单布局,用于添加托盘类型选择控件 + self.material_form_layout = QFormLayout() + self.material_content_layout.addLayout(self.material_form_layout) + + self.output_form_layout = QFormLayout() + self.output_content_layout.addLayout(self.output_form_layout) + + # 为下料区添加占位标签,确保它保持为空 + self.output_placeholder = QWidget() + self.output_placeholder.setStyleSheet("background-color: #f0f0f0;") + placeholder_layout = QVBoxLayout(self.output_placeholder) + placeholder_layout.setAlignment(Qt.AlignCenter) + + # 添加标题标签 + title_label = QLabel("下料区") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("color: #888888;") + title_label.setFont(self.second_title_font) + placeholder_layout.addWidget(title_label) + + self.output_content_layout.addWidget(self.output_placeholder) + + # 添加下料信息标签 + self.unloading_level_label = QLabel("下料层数:--") + self.unloading_position_label = QLabel("下料位置:--") + placeholder_layout.addWidget(self.unloading_level_label) + placeholder_layout.addWidget(self.unloading_position_label) + self.unloading_level_label.setStyleSheet("color: #888888; font-weight: bold;") + self.unloading_position_label.setStyleSheet("color: #888888; font-weight: bold;") + self.unloading_level_label.setFont(self.normal_font) + self.unloading_position_label.setFont(self.normal_font) + + # 创建堆叠部件 + self.stacked_widget = QStackedWidget() + self.stacked_widget.addWidget(self.central_widget) # 主页面 + + # 设置中央部件为堆叠部件 + self.setCentralWidget(self.stacked_widget) + + # # 添加托盘类型选择下拉框 + # self.add_pallet_type_selectors() + + # 连接信号和槽 + self.connect_signals() + + # 默认显示主页面 + self.stacked_widget.setCurrentIndex(0) + + # 配置检验列 - 使用检验配置管理器获取启用的列数和标题 + self.update_inspection_columns() + + # 设置表格上下文菜单 + self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) + + # 加载未完成的检验数据 + self._safe_load_data() + + # 加载已完成检验数据 + self.show_pack_item() + # 创建状态处理器实例 + self.machine_handlers = MachineStatusHandlers() + + # 添加状态显示到状态栏 + self.modbus_status_label = QLabel("Modbus: 未连接") + self.weight_label = QLabel("重量: --") + self.label_status_label = QLabel("贴标: 无贴标") + self.error_status_label = QLabel("故障: 无") + + # 设置样式 + self.error_status_label.setStyleSheet("color: green; font-weight: bold;") + + # 添加到状态栏 + self.statusBar().addPermanentWidget(self.modbus_status_label) + self.statusBar().addPermanentWidget(self.weight_label) + self.statusBar().addPermanentWidget(self.label_status_label) + self.statusBar().addPermanentWidget(self.error_status_label) + self.statusBar().addPermanentWidget(QLabel(" ")) + logging.info(f"主窗口已创建,用户: {user_name}") + + # 初始化串口管理器 + self.serial_manager = SerialManager() + + # 注册串口数据回调函数 + self.register_serial_callbacks() + + # 加载托盘号列表 + self.load_pallet_codes() + + # 恢复开始按钮原始样式 + self.restore_start_button_style() + + # 恢复上料和下料按钮原始样式 + if hasattr(self, 'restore_input_button_style'): + self.restore_input_button_style() + if hasattr(self, 'restore_output_button_style'): + self.restore_output_button_style() + + # 启动Modbus监控,确保电力消耗数据在应用启动时就能显示 + self.setup_modbus_monitor() + + # 更新订单数量和产量统计数据 + self.update_order_statistics() + + logging.info("主窗口初始化时已启动Modbus监控系统") + + def get_axios_num(self,tray_id): + """获取托盘号对应的轴号""" + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + axios_num = inspection_dao.get_axios_num(tray_id) + return axios_num + def get_axios_num_by_order_id(self, order_id): + """获取订单号对应的轴号""" + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + axios_num = inspection_dao.get_axios_num_by_order_id(order_id) + return axios_num + def load_config(self): + """加载配置文件""" + config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "app_config.json") + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + logging.info(f"已加载配置文件: {config_path}") + return config + except Exception as e: + logging.error(f"加载配置文件失败: {e}") + return {} + + def connect_signals(self): + """连接信号槽""" + # 连接微丝产线表格单元格变更信号槽 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + # 连接菜单动作 + self.main_action.triggered.connect(self.show_main_page) + self.settings_action.triggered.connect(self.show_settings_page) + + # 工程号输入框回车事件 + self.order_edit.returnPressed.connect(self.handle_order_enter) + + # 托盘号输入框回车和切换事件,触发未加载数据查询 + # QComboBox没有returnPressed信号,只有currentTextChanged和activated信号 + self.tray_edit.currentTextChanged.connect(self.handle_tray_changed) + self.tray_edit.activated.connect(self.handle_tray_changed) # 当用户选择一项时触发 + + # 连接按钮事件 + self.input_button.clicked.connect(self.handle_input) + self.output_button.clicked.connect(self.handle_output) + self.start_button.clicked.connect(self.handle_start) + self.stop_button.clicked.connect(self.handle_stop) + self.delete_row_button.clicked.connect(self.handle_delete_row) + + # 连接托盘完成按钮事件 + self.tray_complete_button.clicked.connect(self.handle_tray_complete) + + # 设置表格上下文菜单 + self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) + self.process_table.customContextMenuRequested.connect(self.show_table_context_menu) + + # 不再需要连接相机信号 + + # 连接报表按钮点击事件 + self.report_button.clicked.connect(self.on_report) + + # 连接加载反馈信号 + self.loading_feedback_signal.connect(self._handle_loading_feedback_ui) + + # 连接下料反馈信号 + self.unloading_feedback_signal.connect(self._handle_unloading_feedback_ui) + + # 连接下料层数和位置UI更新信号 + self.unloading_level_ui_signal.connect(self.handle_unloading_level_ui) + self.unloading_position_ui_signal.connect(self.handle_unloading_position_ui) + + # 连接急停信号 + self.emergency_stop_signal.connect(self._handle_emergency_stop_ui) + + def update_inspection_columns(self): + """更新检验列配置 - 使用检验配置管理器获取启用的列数和标题""" + try: + # 获取已启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 获取启用的列数 + column_count = len(enabled_configs) + if column_count == 0: + # 如果没有启用的列,至少显示一列 + column_count = 1 + headers = ["检验项"] + else: + # 如果有启用的列,使用配置的标题 + headers = [config['display_name'] for config in enabled_configs] + + # 设置检验列 + self.set_inspection_columns(column_count, headers) + logging.info(f"已更新检验列配置:{column_count}列, 标题: {headers}") + except Exception as e: + logging.error(f"更新检验列配置失败: {str(e)}") + # 如果更新失败,使用默认配置 + self.set_inspection_columns(1, ["检验项"]) + + def show_main_page(self): + self.stacked_widget.setCurrentWidget(self.central_widget) + + # 更新检验列配置 + self.update_inspection_columns() + + # 加载未完成的检验数据 + self._safe_load_data() + + # 加载托盘号列表 + self.load_pallet_codes() + + logging.info("显示主页面") + + def load_pallet_codes(self): + """从托盘类型管理器加载托盘号并更新到tray_edit""" + try: + # 获取当前文本,以便保留用户选择 + current_text = self.tray_edit.currentText() + + # 清空当前项目 + self.tray_edit.clear() + + # 获取托盘号 + pallet_codes = self.pallet_type_manager.get_pallet_code() + + if pallet_codes and len(pallet_codes) > 0: + # 添加托盘号到下拉框 + self.tray_edit.addItems(pallet_codes) + + # 如果有之前的选择,尝试恢复它 + index = self.tray_edit.findText(current_text) + if index != -1: + self.tray_edit.setCurrentIndex(index) + else: + self.tray_edit.setCurrentIndex(-1) + self.tray_edit.setCurrentText("") + + logging.info(f"已加载托盘号,共 {len(pallet_codes)} 个") + else: + # 如果没有托盘号,则不添加任何项目,保持为空 + logging.warning("未找到托盘号,托盘号列表将为空") + self.tray_edit.setCurrentText("") + + + except Exception as e: + logging.error(f"加载托盘号失败: {str(e)}") + # 如果加载失败,确保下拉框为空 + self.tray_edit.clear() + self.tray_edit.setCurrentText("") + + def show_settings_page(self): + """显示设置页面""" + # 创建设置窗口 + if not hasattr(self, 'settings_window'): + from widgets.settings_window import SettingsWindow + self.settings_window = SettingsWindow(self) + # 连接设置改变信号 + self.settings_window.settings_changed.connect(self.on_settings_changed) + + # 显示设置窗口 + self.settings_window.show() + logging.info("显示设置窗口") + + def on_settings_changed(self): + """设置改变时触发""" + # 重新加载配置 + from utils.config_loader import ConfigLoader + config_loader = ConfigLoader.get_instance() + config_loader.load_config() + self.config = self.load_config() # 重新加载配置到 self.config + + # 更新串口管理器配置 + self.serial_manager.reload_config() + + # 重新打开已配置的串口 + self.serial_manager.auto_open_configured_ports() + + # 重新注册串口回调函数 + self.register_serial_callbacks() + + # 重新加载托盘号 + self.load_pallet_codes() + + logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册米电阻、线径和扫码器回调") + + def handle_input(self): + """处理上料按钮点击事件""" + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 启动监听(不论后续是否确认上料) + # 启动Modbus监控 + if not hasattr(self, 'modbus_monitor') or not self.modbus_monitor.is_running(): + self.setup_modbus_monitor() + logging.info("已在上料操作前启动Modbus监控") + + # 启动串口监听 + self.serial_manager.auto_open_configured_ports() + + # 启动键盘监听器 + self.serial_manager.start_keyboard_listener() + logging.info("已在上料操作前启动键盘监听器") + + # 创建上料对话框 + from widgets.loading_dialog_widget import LoadingDialog + dialog = LoadingDialog(parent=self,user_id=self.user_id,user_name=self.user_name,corp_id=self.corp_id) + + # 如果已有上料信息,作为参考显示在对话框中,但允许用户修改 + if self._loading_info and self._current_stow_num > 0: + dialog.order_input.setText(self._loading_info.get('order_code', '')) + dialog.tray_input.setText(self._loading_info.get('tray_code', '')) + dialog.axis_value.setText(self._loading_info.get('axis_value', '--')) + dialog.quantity_value.setText(self._loading_info.get('quantity_value', '--')) + dialog.weight_value.setText(self._loading_info.get('weight_value', '--')) + dialog.pallet_tier_value.setText(str(self._current_stow_num)) + # 不禁用输入框,允许用户修改 + + # 连接订单号信号 + dialog.order_code_signal.connect(self.handle_order_code_received) + + # 显示对话框 + result = dialog.exec() + + # 如果用户确认,则执行上料操作 + if result == QDialog.Accepted: + # 从对话框中获取订单号和托盘号,并更新到主窗口 + order_code = dialog.order_input.text() + tray_code = dialog.tray_input.text() + self._current_order_code = order_code + self.tray_edit.setCurrentText(tray_code) + + # 获取托盘料值作为拆垛层数 + stow_num = dialog.pallet_tier_value.text() + if stow_num == "--" or not stow_num: + QMessageBox.warning(self, "错误", "未获取到托盘料信息,请重试") + return + + # 始终使用用户最新输入的信息 + self._current_stow_num = int(stow_num) + # 保存上料信息 + self._loading_info = { + 'order_code': dialog.order_input.text(), + 'tray_code': dialog.tray_input.text(), + 'axis_value': dialog.axis_value.text(), + 'quantity_value': dialog.quantity_value.text(), + 'weight_value': dialog.weight_value.text(), + } + + # 执行Modbus操作 + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 上料 D0 给到层数,等待点击开始后,进行上料 + success0 = modbus.write_register_until_success(client, 0, self._current_stow_num) + # 读取D0寄存器值 + current_stow_num = modbus.read_holding_register(client, 0) + logging.info(f"上料初始化成功:层数 {current_stow_num} 已写入寄存器0") + if success0: + # 创建状态标签并显示在右上角 + self.show_operation_status("拆垛层数", "input", str(current_stow_num if current_stow_num else self._current_stow_num)) + else: + QMessageBox.information(self, "操作提示", "上料失败") + except Exception as e: + logging.error(f"上料操作失败: {str(e)}") + QMessageBox.critical(self, "错误", f"上料操作失败: {str(e)}") + finally: + modbus.close_client(client) + + def handle_output(self): + """处理下料按钮点击事件""" + try: + # 启动监听(不论后续是否确认下料) + if not hasattr(self, 'modbus_monitor') or not self.modbus_monitor.is_running(): + self.setup_modbus_monitor() + self.serial_manager.auto_open_configured_ports() + self.serial_manager.start_keyboard_listener() + + dialog = UnloadingDialog(self, self.user_id) + + # 如果有之前的下料信息,作为参考显示在对话框中,但允许用户修改 + if self._current_unload_info: + dialog.set_unloading_info(self._current_unload_info) + logging.info(f"显示之前的下料信息作为参考") + + if dialog.exec_() == QDialog.Accepted: + # 获取用户最新输入的下料信息 + unloading_info = dialog.get_unloading_info() + + # 始终使用用户最新输入的信息 + self._total_unload_num = int(unloading_info.get('tier', '3')) + self._current_unload_num = int(unloading_info.get('tier', '3')) # 直接使用用户输入的层数,而不是从1开始 + self._current_unload_info = unloading_info + logging.info(f"下料任务设置:总层数={self._total_unload_num}, 当前层数={self._current_unload_num}") + + # 将用户输入的层数写入寄存器 + modbus = ModbusUtils() + client = modbus.get_client() + try: + modbus.write_register_until_success(client, 4, self._current_unload_num) + logging.info(f"下料初始化成功:层数 {self._current_unload_num} 已写入寄存器4") + finally: + modbus.close_client(client) + + # 读取D4寄存器值 + current_unload_num = modbus.read_holding_register(client, 4) + logging.info(f"下料初始化成功:层数 {current_unload_num} 已写入寄存器4") + + # 统一更新UI显示 + tray_code = self._current_unload_info.get('tray_code', '') + self.show_operation_status("码垛层数", "output", f"{current_unload_num if current_unload_num else self._current_unload_num}/{current_unload_num if current_unload_num else self._total_unload_num} ") + else: + logging.info("下料对话框已取消") + except Exception as e: + logging.error(f"处理下料操作失败: {str(e)}") + QMessageBox.critical(self, "错误", f"处理下料操作失败: {str(e)}") + + def restore_start_button_style(self): + """恢复开始按钮的原始样式""" + try: + # 使用与main_window_ui.py中初始化时相同的样式,只恢复背景色 + button_style = """ + QPushButton { + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + border: 1px solid #4caf50; + } + QPushButton:hover { + background-color: #d7eeda; + } + """ + self.start_button.setStyleSheet(button_style) + logging.info("已恢复开始按钮原始样式") + except Exception as e: + logging.error(f"恢复开始按钮样式失败: {str(e)}") + + def fill_start_button_style(self): + """填充开始按钮样式 - 绿色背景,白色字体""" + try: + # 使用与main_window_ui.py中初始化时相同的样式,只改变背景色和文字颜色 + button_style = """ + QPushButton { + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + background-color: #4caf50; + color: white; + border: 1px solid #4caf50; + } + QPushButton:hover { + background-color: #45a049; + color: white; + } + """ + self.start_button.setStyleSheet(button_style) + logging.info("已填充开始按钮样式") + except Exception as e: + logging.error(f"填充开始按钮样式失败: {str(e)}") + + def handle_start(self): + """ + 处理开始按钮点击事件 + 根据当前操作类型(上料/下料)写入相应的寄存器 + - 上料: 将当前层数写入D0寄存器,并将D2寄存器设置为1 + - 下料: 确保D3寄存器设置为1,D4寄存器已包含当前下料层数 + """ + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 判断当前操作类型(通过检查当前下料信息是否存在) + if self._current_unload_info and self._current_unload_num > 0: + # 下料模式 - 开始下料操作 + # 确保寄存器3(下料启动)设为1,寄存器4已在handle_output中设置了当前层数 + success2 = modbus.write_register_until_success(client, 2, 1) + success3 = modbus.write_register_until_success(client, 3, 1) + + if success2 and success3: + logging.info(f"开始下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}") + QMessageBox.information(self, "操作提示", f"开始下料操作:当前第{self._current_unload_num}层") + # 填充按钮样式 + self.fill_start_button_style() + else: + QMessageBox.warning(self, "错误", "开始下料操作失败") + else: + # 上料模式 - 默认操作 + # 写入当前层数到D0寄存器 + success0 = modbus.write_register_until_success(client, 0, self._current_stow_num) + success2 = modbus.write_register_until_success(client, 2, 1) + + if success0 and success2: + self._is_loading_active = True # 标记上料任务已开始 + logging.info(f"开始上料操作:当前层数 {self._current_stow_num}") + # 填充按钮样式 + self.fill_start_button_style() + else: + QMessageBox.warning(self, "错误", "开始上料操作失败") + + except Exception as e: + logging.error(f"开始操作失败: {str(e)}") + QMessageBox.critical(self, "错误", f"开始操作失败: {str(e)}") + finally: + modbus.close_client(client) + + def handle_stop(self): + """处理停止按钮点击事件,根据当前操作类型(上料/下料)停止相应的操作并关闭modbus监控""" + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 判断当前操作类型(通过检查当前下料信息是否存在) + if self._current_unload_info and self._current_unload_num > 0: + # 下料模式 - 停止下料操作 + success3 = modbus.write_register_until_success(client, 3, 0) + + if success3: + logging.info(f"停止下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}") + QMessageBox.information(self, "操作提示", "已停止下料操作") + # 恢复开始按钮和下料按钮原始样式 + self.restore_start_button_style() + self.restore_output_button_style() + else: + QMessageBox.warning(self, "错误", "停止下料操作失败") + else: + # 上料模式 - 停止上料操作 + success2 = modbus.write_register_until_success(client, 2, 0) + + if success2: + self._is_loading_active = False # 标记上料任务已停止 + logging.info("停止上料操作") + QMessageBox.information(self, "操作提示", "已停止上料操作") + # 恢复开始按钮和上料按钮原始样式 + self.restore_start_button_style() + self.restore_input_button_style() + else: + QMessageBox.warning(self, "错误", "停止上料操作失败") + + except Exception as e: + logging.error(f"停止操作失败: {str(e)}") + QMessageBox.critical(self, "错误", f"停止操作失败: {str(e)}") + finally: + modbus.close_client(client) + # 停止Modbus监控 + if hasattr(self, 'modbus_monitor'): + logging.info("停止Modbus监控") + self.modbus_monitor.stop() + # 停止串口监听 + self.serial_manager.stop_keyboard_listener() + self.serial_manager.close_all_ports() + + def clear_operation_status(self, operation_type): + """清除右上角的操作状态显示。""" + status_label_name = f"{operation_type}_status_label" + if hasattr(self, status_label_name): + try: + getattr(self, status_label_name).deleteLater() + delattr(self, status_label_name) + logging.info(f"已清除 '{operation_type}' 状态标签。") + except AttributeError: + pass # Failsafe + + def handle_camera_status(self, is_connected, message): + """相机状态处理的空方法,保留是为了兼容性""" + pass + + def handle_camera_connection(self, is_connected, message): + """相机连接处理的空方法,保留是为了兼容性""" + pass + + def handle_camera_params_changed(self, exposure_time, gain, frame_rate): + """相机参数变更处理的空方法,保留是为了兼容性""" + pass + + def handle_camera_error(self, error_msg): + """相机错误处理的空方法,保留是为了兼容性""" + pass + + def closeEvent(self, event): + """窗口关闭事件""" + # 停止Modbus监控 + if hasattr(self, 'modbus_monitor'): + logging.info("停止Modbus监控") + self.modbus_monitor.stop() + + # 停止串口监听 + self.serial_manager.stop_keyboard_listener() + self.serial_manager.close_all_ports() + + # 接受关闭事件 + event.accept() + + + def handle_order_enter(self): + """处理工程号输入框按下回车事件""" + logging.info("工程号输入框按下回车事件") + # 获取当前输入的工程号 + gc_note = self.order_edit.text().strip() + if gc_note: + logging.info(f"输入的工程号: {gc_note}") + #判断是否是接口,如果不是接口直接添加如果是则走接口 + # 如果开启接口模式,则需要调用接口同步到业务库 + if AppMode.is_api(): + from dao.inspection_dao import InspectionDAO + from apis.gc_api import GcApi + inspection_dao = InspectionDAO() + # 调用接口 + gc_api = GcApi() + response = gc_api.get_gc_info(gc_note) + if response.get("status", False): + gc_info = response.get("data", {}) + self._current_gc_qd = gc_info.get("qd",0) + # 先获取当前 info_table 已有的数据 + order_info = {} + for field_name, label in self.info_values.items(): + order_info_key = self.FIELD_MAPPING.get(field_name) + if order_info_key: + order_info[order_info_key] = label.text() + # 更新/补充 qd 字段 + order_info["qd"] = self._current_gc_qd + # 再调用 update_info_table + self.update_info_table(order_info) + self.add_new_inspection_row(gc_note, self._current_order_code) + else: + self.add_new_inspection_row(gc_note, self._current_order_code) + + else: + logging.warning("工程号为空") + QMessageBox.warning(self, "输入提示", "请输入有效的工程号") + + def add_new_inspection_row(self, gc_note, order_code): + """在微丝产线表格中添加一条新记录,添加到表格末尾 + + Args: + gc_note: 工程号 + order_info: 从接口获取的工程号信息 + """ + try: + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 断开单元格变更信号,避免加载过程中触发保存 + try: + self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) + except: + pass + + # 计算新行的行索引(添加到末尾) + data_start_row = self.process_table.rowCount() + + # 在末尾添加新行 + self.process_table.insertRow(data_start_row) + + # 计算新行的序号(最后一个序号+1) + new_seq = 1 # 默认为1 + if data_start_row > 2: # 如果有其他数据行 + prev_seq_item = self.process_table.item(data_start_row - 1, 0) + if prev_seq_item: + try: + prev_seq = int(prev_seq_item.text()) + new_seq = prev_seq + 1 + except ValueError: + new_seq = data_start_row - 1 # 备选方案:使用行索引作为序号 + + # 添加工程号到表格的第二列 + item = QTableWidgetItem(gc_note) + item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(data_start_row, 1, item) + + # 添加序号到表格的第一列 + item = QTableWidgetItem(str(new_seq)) + item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(data_start_row, 0, item) + + # 获取订单信息 + order_info = self.inspection_manager.get_order_info(order_code) + + # 检验列设置为可编辑状态 + for i, config in enumerate(enabled_configs): + col_index = 2 + i # 检验列从第3列开始 + + # 创建单元格 + item = QTableWidgetItem("") + item.setTextAlignment(Qt.AlignCenter) + + # 如果有order_info数据,尝试匹配字段并设置值 + # if order_info: + # config_name = config.get('name') + # # 检查order_info中是否有与config_name匹配的键 + # if config_name in order_info: + # value = str(order_info[config_name]) + # item = QTableWidgetItem(value) + # item.setTextAlignment(Qt.AlignCenter) + # # 设置单元格背景为浅绿色,表示自动填充 + # item.setBackground(QBrush(QColor("#c8e6c9"))) + + # # 保存到数据库 + # from dao.inspection_dao import InspectionDAO + # inspection_dao = InspectionDAO() + # tray_id = self.tray_edit.currentText() + # data = [{ + # 'position': config.get('position'), + # 'config_id': config.get('id'), + # 'value': value, + # 'status': 'pass', # 默认设置为通过状态 + # 'remark': '', + # 'tray_id': tray_id + # }] + # inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) + # logging.info(f"自动填充字段 {config_name} 值为 {value}") + + # 设置单元格属性以标识其关联的检验项 + item.setData(Qt.UserRole, config.get('id')) + self.process_table.setItem(data_start_row, col_index, item) + + # 包装列设置为可编辑状态 + packaging_start_col = 2 + len(enabled_configs) + for i in range(2): # 贴标和称重 + col_index = packaging_start_col + i + item = QTableWidgetItem("") + item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(data_start_row, col_index, item) + + # 设置表格为可编辑状态 + self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) + + # 重新连接单元格内容变更信号 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + # 选中新添加的行 + self.process_table.selectRow(data_start_row) + + # 限制最大行数 + self.limit_table_rows(10) # 最多保留10行数据 + + # 将工程号和托盘号保存到数据库,确保能够正确关联 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + tray_id = self.tray_edit.currentText() + + # 为每个检验位置创建一个空记录,确保工程号在数据库中存在 + # 只为没有自动填充值的配置创建空记录 + for config in enabled_configs: + config_name = config.get('name') + # 如果order_info中没有对应的键,或者order_info为None + if not order_info or config_name not in order_info: + data = [{ + 'position': config.get('position'), + 'config_id': config.get('id'), + 'value': '', + 'status': 'init', # 设置初始状态 + 'remark': '', + 'tray_id': tray_id + }] + inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) + + # 为贴标和称重也创建空记录 + for position in [11, 12, 13]: # 11是贴标,12是毛重,13是净重 + data = [{ + 'position': position, + 'config_id': position, + 'value': '', + 'status': 'init', # 设置初始状态 + 'remark': '', + 'tray_id': tray_id + }] + inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) + + # 初始化产品状态为init + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'init') + logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条,初始状态为init") + + except Exception as e: + logging.error(f"添加新记录失败: {str(e)}") + QMessageBox.warning(self, "添加失败", f"添加新记录失败: {str(e)}") + finally: + # 重新加载数据,确保UI显示正确 + self._safe_load_data() + + def limit_table_rows(self, max_rows): + """限制表格最大行数 + + Args: + max_rows: 最大行数(不包括表头行) + """ + try: + # 计算数据总行数 + data_rows = self.process_table.rowCount() - 2 # 减去表头行 + + # 如果超过最大行数,删除多余的行 + if data_rows > max_rows: + # 要删除的行数 + rows_to_remove = data_rows - max_rows + + # 从最后一行开始删除 + for i in range(rows_to_remove): + self.process_table.removeRow(self.process_table.rowCount() - 1) + + logging.info(f"已限制表格最大行数为 {max_rows} 行数据,删除了 {rows_to_remove} 行") + + except Exception as e: + logging.error(f"限制表格行数失败: {str(e)}") + + def handle_inspection_cell_changed(self, row, column): + """处理微丝包装单元格内容变更 + + Args: + row: 行索引 + column: 列索引 + """ + try: + # 只处理数据行的检验列变更 + if row < 2: # 忽略表头行 + return + + # 忽略首尾两列(序号和工程号) + if column < 2: + return + + # 获取工程号 + order_item = self.process_table.item(row, 1) + if not order_item: + return + + gc_note = order_item.text().strip() + if not gc_note: + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 判断是否是检验列(非包装列) + packaging_start_col = 2 + len(enabled_configs) + + # 获取单元格内容 + cell_item = self.process_table.item(row, column) + if not cell_item: + return + + value = cell_item.text().strip() + + # 默认设置为通过状态 + status = 'pass' + + # 记录当前正在处理的数据类型,用于日志输出 + data_type = "检验" + + if column >= 2 and column < packaging_start_col: + # 是检验列 + config_index = column - 2 + if config_index < len(enabled_configs): + config = enabled_configs[config_index] + data_type = config['display_name'] + + # 显示临时状态消息 + self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) + + # 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) + + # 保持当前状态不变,由状态管理逻辑处理 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) + + # 保存到数据库 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) + + # 判断是否是包装列 + elif column == packaging_start_col: + # 贴标列 + data_type = "贴标" + self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存贴标数据,position和config_id都是11 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, value, status) + + elif column == packaging_start_col + 1: + # 毛重列 + data_type = "毛重" + self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存毛重数据,position和config_id都是12 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, value, status) + elif column == packaging_start_col + 2: + # 净重列 + data_type = "净重" + self.statusBar().showMessage(f"正在保存净重数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存净重数据,position和config_id都是13 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, value, status) + + # 记录详细日志 + logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}") + + # 检查是否完成检验并更新状态 + self.check_inspection_completed(row) + + except Exception as e: + logging.error(f"处理检验单元格变更失败: {str(e)}") + self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) + finally: + # 延迟一段时间后再触发查询,避免频繁刷新UI + # 但要避免在加载过程中触发新的加载 + if not self._loading_data_in_progress: + QTimer.singleShot(1000, self._safe_load_data) + + def validate_inspection_value(self, config, value): + """验证检验值是否有效 + + Args: + config: 检验配置 + value: 检验值 + + Returns: + bool: 是否有效 + """ + try: + # 特殊处理贴标和称重数据 - 这些数据默认都是有效的 + if config.get('position') in [11, 12]: # 11是贴标,12是称重 + return True + + # 检查值是否为空 + if not value and config.get('required', False): + return False + + # 根据数据类型验证 + data_type = config.get('data_type') + + if data_type == 'number': + # 数值类型验证 + try: + # 如果值为空且不是必填,则视为有效 + if not value and not config.get('required', False): + return True + + num_value = float(value) + min_value = config.get('min_value') + max_value = config.get('max_value') + + if min_value is not None and num_value < min_value: + return False + + if max_value is not None and num_value > max_value: + return False + + return True + except ValueError: + return False + + elif data_type == 'enum': + # 枚举类型验证 + enum_values = config.get('enum_values') + if enum_values and isinstance(enum_values, list): + # 如果值为空且不是必填,则视为有效 + if not value and not config.get('required', False): + return True + return value in enum_values + return False + + # 文本类型不做特殊验证 + return True + + except Exception as e: + logging.error(f"验证检验值失败: {str(e)}") + return False + + def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status): + """保存检验数据到数据库 + + Args: + order_id: 订单号 + gc_note: 工程号 + position: 位置序号 + config_id: 配置ID + value: 检验值 + status: 状态 + """ + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + modbus = ModbusUtils() + client = modbus.get_client() + + # 获取当前产品状态,优先使用产品状态管理中的状态 + current_status = inspection_dao.get_product_status(order_id, gc_note, tray_id) + + # 如果当前状态不是初始状态,则使用当前状态而不是传入的status + if current_status not in ['', 'init']: + status = current_status + + # 记录保存前的详细日志 + logging.info(f"正在保存检验数据: 工程号={gc_note}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}") + + # 构建数据 + data = [{ + 'position': position, + 'config_id': config_id, + 'value': value, + 'status': status, + 'remark': '', + 'tray_id': tray_id + }] + + # 保存到数据库 + inspection_dao.save_inspection_data(order_id, gc_note, data) + 'value': value, + 'status': status, + 'remark': '', + 'tray_id': tray_id + }] + + # 保存到数据库 + inspection_dao.save_inspection_data(order_id, gc_note, data) + except Exception as e: + logging.error(f"保存检验数据失败: {str(e)}") + # 显示错误消息 + QMessageBox.warning(self, "保存失败", f"保存检验数据错误: {str(e)[:50]}...") + self.statusBar().showMessage(f"保存检验数据错误: {str(e)[:50]}...", 3000) + + def _safe_load_data(self): + """安全地加载数据,避免循环调用""" + # 获取当前托盘号,用于日志记录 + tray_id = self.tray_edit.currentText() + + if self._loading_data_in_progress: + # 如果已经在加载数据,不要再次触发 + logging.debug(f"已有数据加载正在进行,忽略此次请求 (托盘号: {tray_id})") + return + + try: + self._loading_data_in_progress = True + self.load_finished_inspection_data() + logging.info(f"数据加载完成,托盘号: {tray_id}") + except Exception as e: + logging.error(f"安全加载数据失败: {str(e)}, 托盘号: {tray_id}") + # 即使加载失败,也尝试显示包装记录 + try: + self.show_pack_item() + logging.info(f"加载失败后尝试显示包装记录, 托盘号: {tray_id}") + except Exception as ex: + logging.error(f"加载失败后显示包装记录失败: {str(ex)}, 托盘号: {tray_id}") + finally: + self._loading_data_in_progress = False + + def load_finished_inspection_data(self): + """加载未完成的检验数据并显示在表格中""" + # 注意:此方法通常应通过_safe_load_data调用,以防止循环 + try: + # 使用InspectionDAO获取未完成的检验数据 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 使用get_inspection_data_unfinished获取未完成的数据 + unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id) + + # 断开单元格变更信号,避免加载过程中触发保存 + try: + self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) + except: + pass + + # 清空表格现有数据行,只保留表头 + while self.process_table.rowCount() > 2: + self.process_table.removeRow(2) + + if not unfinished_data: + logging.info(f"托盘号 {tray_id} 没有未完成的检验数据") + # 确保表格完全清空,只保留表头行 + self.process_table.setRowCount(2) # 只保留表头的两行 + + # 重新连接单元格变更信号 + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + except: + pass + + # 加载包装记录 + return + + logging.info(f"已加载未完成的检验数据,共 {len(unfinished_data)} 条记录") + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 按工程号分组 + orders_data = {} + for data in unfinished_data: + gc_note = data['gc_note'] + if gc_note not in orders_data: + orders_data[gc_note] = [] + orders_data[gc_note].append(data) + + # 添加数据到表格 - 从第3行开始添加数据 + row_idx = 2 + + # 使用DAO方法按创建时间排序工程号,确保FIFO顺序(最早创建的在最前面) + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + sorted_gc_notes = inspection_dao.get_orders_by_create_time(list(orders_data.keys())) + + for gc_note in sorted_gc_notes: + items = orders_data[gc_note] + + # 添加新行 + self.process_table.insertRow(row_idx) + + # 添加序号到第一列 + seq_item = QTableWidgetItem(str(row_idx - 1)) + seq_item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(row_idx, 0, seq_item) + + # 添加工程号到第二列 + order_item = QTableWidgetItem(gc_note) + order_item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(row_idx, 1, order_item) + + # 添加检验数据 + for item in items: + position = item['position'] + value = item['value'] if item['value'] else "" + status = item['status'] + config_id = item['config_id'] + + + # 找到对应的列索引 + col_index = None + for i, config in enumerate(enabled_configs): + if config.get('position') == position: + col_index = 2 + i # 检验列从第3列开始 + break + + if col_index is not None: + # 创建单元格并设置值 + cell_item = QTableWidgetItem(str(value)) + cell_item.setTextAlignment(Qt.AlignCenter) + # 存储配置ID,用于保存时确定是哪个检验项 + cell_item.setData(Qt.UserRole, config_id) + # 设置单元格 + self.process_table.setItem(row_idx, col_index, cell_item) + # 添加贴标(11)和称重数据(12) + if position == 11: # 贴标 + # 贴标列索引 = 2(序号和工程号) + 检验列数 + label_col = 2 + len(enabled_configs) + self.process_table.setItem(row_idx, label_col, QTableWidgetItem(str(value))) + elif position == 12: # 称重 + # 称重列索引 = 2(序号和工程号) + 检验列数 + 1(贴标) + weight_col = 2 + len(enabled_configs) + 1 + self.process_table.setItem(row_idx, weight_col, QTableWidgetItem(str(value))) + elif position == 13: # 净重 + # 净重列索引 = 2(序号和工程号) + 检验列数 + 2(贴标和称重) + net_weight_col = 2 + len(enabled_configs) + 2 + self.process_table.setItem(row_idx, net_weight_col, QTableWidgetItem(str(value))) + row_idx += 1 + + # 设置表格为可编辑状态 + self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) + + # 重新连接单元格变更信号 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + + except Exception as e: + logging.error(f"加载未完成的检验数据失败: {str(e)}") + QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}") + + finally: + # 加载包装记录,但要避免循环调用 + # 设置一个标志,防止 show_pack_item 触发更多的数据加载 + # 只有在_safe_load_data调用此方法,且没有明确设置加载状态的情况下才调用 + has_loading_flag = hasattr(self, '_loading_data_in_progress') + is_loading = getattr(self, '_loading_data_in_progress', False) + + # 如果是被_safe_load_data调用(即已经设置了_loading_data_in_progress),则无需额外设置 + if has_loading_flag and is_loading: + # 直接调用show_pack_item,不改变加载状态 + try: + self.show_pack_item() + logging.info("在load_finished_inspection_data中调用show_pack_item") + except Exception as e: + logging.error(f"在load_finished_inspection_data中调用show_pack_item失败: {str(e)}") + # 否则,这是直接调用此方法(非_safe_load_data),需要设置加载状态 + elif not is_loading: + self._loading_data_in_progress = True + try: + self.show_pack_item() + logging.info("在load_finished_inspection_data中直接调用show_pack_item") + finally: + self._loading_data_in_progress = False + + def load_finished_record_to_package_record(self, order_id, gc_note, tray_id): + """加载已完成检验数据到包装记录 + + Args: + order_id: 工程号 + tray_id: 托盘号 + """ + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 获取该工程号的所有检验数据 + inspection_data = inspection_dao.get_inspection_data_by_order(order_id, gc_note, tray_id) + + if not inspection_data: + logging.warning(f"未找到工程号 {gc_note} 托盘号 {tray_id} 的检验数据") + return + + # 获取轴号并保存 + label_value = self.get_axios_num_by_order_id(self._current_order_code) + + # 从检验数据中获取贴标和称重数据 + weight_value = "" + net_weight_value = "" + for item in inspection_data: + if item['position'] == 12: # 称重 + weight_value = item['value'] + elif item['position'] == 13: # 净重 + net_weight_value = item['value'] + + # 只要贴标字段有值,就可以写入包装记录 + if label_value == None or label_value == "": + logging.warning(f"工程号 {order_id} 托盘号 {tray_id} 的贴标字段为空,不添加到包装记录") + return + + + # 获取当前时间作为完成时间 + finish_time = datetime.now() + + # 将数据写入到数据库表 inspection_pack_data + inspection_dao.save_package_record(order_id, tray_id, str(label_value+1), weight_value,net_weight_value, finish_time,gc_note) + + # 回显数据,但避免循环调用 + if not getattr(self, '_loading_data_in_progress'): + self._loading_data_in_progress = True + try: + self.show_pack_item() + finally: + self._loading_data_in_progress = False + + logging.info(f"已将工程号 {order_id} 托盘号 {tray_id} 的检验数据添加到包装记录并回显") + + except Exception as e: + logging.error(f"加载已完成检验数据到包装记录失败: {str(e)}") + QMessageBox.warning(self, "加载失败", f"加载已完成检验数据到包装记录失败: {str(e)}") + def show_pack_item(self): + """显示包装记录""" + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + logging.info(f"显示包装记录,当前托盘号: {tray_id}") + + if not tray_id: + logging.warning("托盘号为空,无法显示包装记录") + # 清空表格 + self.record_table.setRowCount(0) + self.update_package_statistics() + return + + # 读取已包装的记录信息 + package_record = inspection_dao.get_package_record(tray_id) + + # 记录获取的数据情况 + if package_record: + logging.info(f"成功获取包装记录,托盘号={tray_id},记录数量={len(package_record)}") + else: + logging.info(f"包装记录为空,托盘号={tray_id}") + + # 清空表格内容 + self.record_table.setRowCount(0) + + # 断开包装记录表的信号连接(如果有) + try: + self.record_table.blockSignals(True) # 使用blockSignals替代手动断开信号 + except Exception as e: + logging.warning(f"阻止信号失败: {str(e)}") + + # 如果没有包装记录,直接返回 + if not package_record: + logging.info(f"托盘号 {tray_id} 没有包装记录数据") + self.update_package_statistics() + self.record_table.blockSignals(False) # 恢复信号 + return + + logging.info(f"托盘号 {tray_id} 已加载包装记录,共 {len(package_record)} 条记录") + + # 添加所有包装记录到表格 + for index, item in enumerate(package_record): + try: + row_index = self.record_table.rowCount() + self.record_table.insertRow(row_index) + + # 设置单元格数据,使用安全的方式访问数据 + cell_data = [ + str(index + 1), # 序号 + str(item[0]) if len(item) > 0 else "", # 订单 + str(item[1]) if len(item) > 1 else "", # 工程号 + str(item[2]) if len(item) > 2 else "", # 品名 + str(item[3]) if len(item) > 3 else "", # 规格 + str(item[4]) if len(item) > 4 else "", # 托号 + str(item[5]) if len(item) > 5 else "", # 轴包装号 + str(item[6]) if len(item) > 6 else "", # 毛重 + str(item[7]) if len(item) > 7 else "", # 净重 + str(item[8]) if len(item) > 8 else "" # 完成时间 + ] + + # 批量设置单元格 + for col, data in enumerate(cell_data): + cell_item = QTableWidgetItem(data) + cell_item.setTextAlignment(Qt.AlignCenter) + self.record_table.setItem(row_index, col, cell_item) + except Exception as e: + logging.error(f"设置第 {index} 行数据时出错: {str(e)}, 数据: {item}") + continue # 继续处理下一行 + + # 恢复信号 + self.record_table.blockSignals(False) + + # 更新包装记录统计数据 + self.update_package_statistics() + logging.info(f"包装记录显示完成,托盘号={tray_id},总记录数={self.record_table.rowCount()}") + + except Exception as e: + logging.error(f"显示包装记录失败: {str(e)}") + self.record_table.blockSignals(False) # 确保信号被恢复 + QMessageBox.warning(self, "显示失败", f"显示包装记录失败: {str(e)}") + def update_package_statistics(self): + """更新包装记录统计数据""" + try: + # 获取包装记录表的行数 + package_count = self.record_table.rowCount() + + # 更新任务表格中的已完成数量 + completed_item = QTableWidgetItem(str(package_count)) + completed_item.setTextAlignment(Qt.AlignCenter) + self.task_table.setItem(2, 2, completed_item) + + # 计算已完成公斤数(如果称重列有数值) + completed_kg = 0 + for row in range(self.record_table.rowCount()): + weight_item = self.record_table.item(row, 6) # 称重列 + if weight_item and weight_item.text(): + try: + completed_kg += float(weight_item.text()) + except ValueError: + pass + + # 更新任务表格中的已完成公斤 + completed_kg_item = QTableWidgetItem(str(completed_kg)) + completed_kg_item.setTextAlignment(Qt.AlignCenter) + self.task_table.setItem(2, 3, completed_kg_item) + + logging.info(f"已更新包装记录统计数据: 完成数量={package_count}, 完成公斤={completed_kg}") + + except Exception as e: + logging.error(f"更新包装记录统计数据失败: {str(e)}") + + def show_table_context_menu(self, pos): + """显示表格上下文菜单 + + Args: + pos: 鼠标位置 + """ + try: + # 获取当前单元格 + cell_index = self.process_table.indexAt(pos) + if not cell_index.isValid(): + return + + row = cell_index.row() + column = cell_index.column() + + # 只对数据行和检验列显示上下文菜单 + if row < 2: # 忽略表头行 + return + + # 获取工程号 + order_id_item = self.process_table.item(row, 1) + if not order_id_item: + return + + order_id = order_id_item.text().strip() + if not order_id: + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 创建上下文菜单 + menu = QMenu(self) + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 判断是否是检验列(非包装列) + packaging_start_col = 2 + len(enabled_configs) + if column >= 2 and column < packaging_start_col: + # 是检验列 + config_index = column - 2 + if config_index < len(enabled_configs): + config = enabled_configs[config_index] + position = config.get('position') + + # 添加查询数据库菜单项 + check_action = menu.addAction("检查数据库记录") + check_action.triggered.connect(lambda: self.check_database_record(order_id, position, tray_id)) + + # 显示菜单 + menu.exec_(self.process_table.viewport().mapToGlobal(pos)) + + except Exception as e: + logging.error(f"显示表格上下文菜单失败: {str(e)}") + + def check_database_record(self, order_id, position, tray_id): + """检查数据库记录 + + Args: + order_id: 工程号 + position: 位置序号 + tray_id: 托盘号 + """ + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 获取检验数据 + inspection_data = inspection_dao.get_inspection_data_by_order(order_id, tray_id) + + # 查找对应位置的数据 + matching_data = None + for data in inspection_data: + if data.get('position') == position: + matching_data = data + break + + # 显示结果 + if matching_data: + value = matching_data.get('value') + status = matching_data.get('status') + + message = f"数据库记录:\n\n" + message += f"工程号: {order_id}\n" + message += f"位置: {position}\n" + message += f"值: {value}\n" + message += f"状态: {status}\n" + + QMessageBox.information(self, "数据库记录", message) + else: + QMessageBox.warning(self, "数据库记录", f"未找到工程号 {order_id} 位置 {position} 的数据") + + except Exception as e: + logging.error(f"检查数据库记录失败: {str(e)}") + QMessageBox.warning(self, "查询失败", f"检查数据库记录失败: {str(e)}") + + def show_operation_status(self, status, operation_type, pallet_type): + """在右上角显示操作状态 + + Args: + status: 状态文本 + operation_type: 操作类型 (input/output) + pallet_type: 托盘类型 + """ + # 确定要添加标签的容器 + if operation_type == "input": + container = self.material_content + else: + container = self.output_content + + # 如果已存在状态标签,则移除它 + status_label_name = f"{operation_type}_status_label" + if hasattr(self, status_label_name): + old_label = getattr(self, status_label_name) + old_label.deleteLater() + + # 创建新的状态标签 + status_label = QLabel(f"{status}: {pallet_type}", container) + status_label.setFont(self.second_title_font) + status_label.setStyleSheet("color: red; background-color: transparent;") + status_label.setAlignment(Qt.AlignRight | Qt.AlignTop) + + # 使用绝对定位,放置在右上角 + status_label.setGeometry(container.width() - 250, 5, 240, 30) + + # 确保标签始终保持在顶层显示 + status_label.raise_() + status_label.show() + + # 保存标签引用 + setattr(self, status_label_name, status_label) + + # 保存原始的resize事件处理函数 + if not hasattr(container, "_original_resize_event"): + container._original_resize_event = container.resizeEvent + + # 添加窗口大小变化事件处理,确保标签位置随窗口调整 + container.resizeEvent = lambda event: self.adjust_status_label_position(event, container, status_label) + + def adjust_status_label_position(self, event, container, label): + """调整状态标签位置,确保始终在右上角 + + Args: + event: 窗口大小变化事件 + container: 标签所在的容器 + label: 状态标签 + """ + # 更新标签位置,保持在右上角 + label.setGeometry(container.width() - 250, 5, 240, 30) + + # 调用原始的resizeEvent(如果有的话) + original_resize = getattr(container, "_original_resize_event", None) + if original_resize: + original_resize(event) + + # ==================== Modbus监控系统相关方法 ==================== + + def setup_modbus_monitor(self): + """设置Modbus监控系统""" + # 获取Modbus监控器实例 + self.modbus_monitor = get_modbus_monitor() + + # 注册寄存器处理器 + self._register_modbus_handlers() + + # 连接信号槽 + self._connect_modbus_signals() + + # 启动监控 + self.modbus_monitor.start() + + logging.info("Modbus监控系统已设置") + + def _register_modbus_handlers(self): + """注册寄存器处理器""" + # 获取Modbus监控器实例 + monitor = get_modbus_monitor() + + # 注册D6处理器,处理NG信号 + monitor.register_handler(6, NGHandler(self.machine_handlers.handle_ng)) + + # 注册D11处理器,处理称重数据 + monitor.register_handler(11, WeightDataHandler(self.machine_handlers.handle_weight_data)) + + # 注册D13处理器,处理贴标信号 + monitor.register_handler(13, LabelSignalHandler(self.machine_handlers.handle_label_signal)) + + # 注册D20-D24处理器,处理各种状态信息 + monitor.register_handler(20, LoadingFeedbackHandler(self.handle_loading_feedback)) + monitor.register_handler(21, UnloadingFeedbackHandler(self.handle_unloading_feedback)) + monitor.register_handler(22, Error1Handler(self.machine_handlers.handle_error_1)) + monitor.register_handler(23, Error2Handler(self.machine_handlers.handle_error_2)) + monitor.register_handler(24, Error3Handler(self.machine_handlers.handle_error_3)) + + # 注册急停信号处理器 + monitor.register_handler(25, EmergencyStopHandler(self.handle_emergency_stop)) + + # 注册下料层数和位置处理器 + monitor.register_handler(4, UnloadingLevelHandler(self.handle_unloading_level)) + monitor.register_handler(5, UnloadingPositionHandler(self.handle_unloading_position)) + + # 注册电力消耗处理器并保存引用以便连接信号 + self.electricity_handler = ElectricityHandler() + monitor.register_handler(30, self.electricity_handler) + + logging.info("已注册所有Modbus寄存器处理器") + + def _connect_modbus_signals(self): + """连接Modbus信号""" + # 连接Modbus状态变化信号 + self.modbus_monitor.monitor_status_changed.connect(self.handle_modbus_status_change) + + # 连接寄存器变化信号 + self.modbus_monitor.register_changed.connect(self.handle_register_change) + + # 连接寄存器错误信号 + self.modbus_monitor.register_error.connect(self.handle_register_error) + + # 连接电力数据变化信号 + self.electricity_handler.electricity_data_changed.connect(self.update_electricity_statistics) + + # 连接贴标信号 + self.machine_handlers.label_signal_changed.connect(self.handle_label_signal) + + # 连接称重数据变化信号 + self.machine_handlers.weight_changed.connect(self.handle_weight_data) + + # 连接NG信号 + self.machine_handlers.ng_changed.connect(self.handle_ng) + + # 连接故障信号 + self.machine_handlers.error_1_changed.connect(self.handle_error_1) + self.machine_handlers.error_2_changed.connect(self.handle_error_2) + self.machine_handlers.error_3_changed.connect(self.handle_error_3) + + # 连接上下料反馈信号 + self.machine_handlers.loading_feedback_changed.connect(self.handle_loading_feedback) + self.machine_handlers.unloading_feedback_changed.connect(self.handle_unloading_feedback) + + # 连接急停信号 + self.emergency_stop_signal.connect(self._handle_emergency_stop_ui) + + # 立即更新一次用电量数据 + self.update_electricity_statistics() + + # 立即更新一次订单数量和产量统计数据 + self.update_order_statistics() + + logging.info("已连接所有Modbus信号") + + def update_electricity_statistics(self, value=None): + """更新电力消耗统计数据到项目表格 + + Args: + value: 可选的当前电力消耗值,用于信号触发时 + """ + try: + from dao.electricity_dao import ElectricityDAO + electricity_dao = ElectricityDAO() + + # 获取电力消耗统计数据 + statistics = electricity_dao.get_electricity_statistics() + + # 设置表格项(日、月、年、累计用电量) + # 当日用电量 + day_item = QTableWidgetItem(str(round(statistics['day'], 2))) + day_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(0, 0, day_item) + + # 当月用电量 + month_item = QTableWidgetItem(str(round(statistics['month'], 2))) + month_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(1, 0, month_item) + + # 当年用电量 + year_item = QTableWidgetItem(str(round(statistics['year'], 2))) + year_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(2, 0, year_item) + + # 累计用电量 + all_item = QTableWidgetItem(str(round(statistics['all'], 2))) + all_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(3, 0, all_item) + + # 只在调试级别输出详细信息,减少日志量 + if value is not None: + logging.info(f"电力数据变化触发UI更新:当前值={value}") + else: + logging.debug(f"已更新电力消耗统计数据: 日={statistics['day']}, 月={statistics['month']}, 年={statistics['year']}, 累计={statistics['all']}") + + except Exception as e: + logging.error(f"更新电力消耗统计数据失败: {str(e)}") + + def _convert_to_kg(self, weight_in_g): + """ + 将克转换为千克 + Args: + weight_in_g: 重量(克) + Returns: + float: 重量(千克) + """ + return round(weight_in_g / 1000.0, 3) # 保留3位小数 + + @Slot(int) + def handle_weight_data(self, weight_in_g): + """处理称重数据变化""" + try: + current_time = time.time() + + # 转换重量单位并立即更新UI显示 + weight_in_kg = self._convert_to_kg(weight_in_g) + logging.info(f"[显示] 称重数据: {weight_in_kg}kg (原始值: {weight_in_g}g)") + self.weight_label.setText(f"重量: {weight_in_kg}kg") + + # 检测重量从接近0到较大值的变化,判断为新产品 + if self._current_weight is not None and self._current_weight < 0.1 and weight_in_kg > 0.5: + logging.info(f"检测到新产品放上,重量从 {self._current_weight}kg 变为 {weight_in_kg}kg") + self._weight_processed = False # 重置处理标记,允许处理新产品 + + # 更新当前重量和时间 + self._current_weight = weight_in_kg + self._last_weight_time = current_time + + # 取消之前的定时器(如果存在) + if self._stability_check_timer is not None: + self._stability_check_timer.stop() + self._stability_check_timer.deleteLater() + + # 创建新的定时器 + self._stability_check_timer = QTimer() + self._stability_check_timer.setSingleShot(True) # 单次触发 + self._stability_check_timer.timeout.connect(lambda: self._check_weight_stability(weight_in_kg)) + self._stability_check_timer.start(self._weight_stable_threshold * 1000) # 转换为毫秒 + + # 尝试获取表格行数据,用于日志记录 + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + + # 记录表格行状态,仅用于日志记录,不影响后续处理 + if data_row >= self.process_table.rowCount(): + logging.warning(f"选中的行 {data_row} 超出了表格范围") + else: + # 获取工程号,仅用于日志记录 + gc_note_item = self.process_table.item(data_row, 1) + if gc_note_item: + gc_note = gc_note_item.text().strip() + if gc_note: + logging.info(f"当前处理的工程号: {gc_note}, 行: {data_row}") + else: + logging.warning("工程号为空") + else: + logging.warning("无法获取工程号") + except Exception as e: + logging.error(f"处理称重数据时发生错误: {str(e)}") + # 确保重新连接信号 + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + except: + pass + + def _check_weight_stability(self, original_weight_kg): + """ + 检查重量是否稳定 + Args: + original_weight_kg: 开始检查时的重量(千克) + """ + try: + # 如果当前重量与定时器启动时的重量相同,说明这段时间内没有新的重量数据 + if self._current_weight == original_weight_kg and self._current_weight > 0.5: + logging.info(f"重量 {original_weight_kg}kg 在{self._weight_stable_threshold}秒内保持稳定") + + # 如果这个重量与上一次处理的重量接近(±0.1kg),且标记已处理,则跳过 + if self._weight_processed and abs(original_weight_kg - self._last_processed_weight) < 0.1: + logging.info(f"跳过处理:重量 {original_weight_kg}kg 与上次处理的重量 {self._last_processed_weight}kg 接近且已处理") + return + + # 尝试写入D10寄存器并处理稳定重量,带有重试机制 + self._handle_stable_weight_with_retry(original_weight_kg, max_retries=3) + else: + logging.info(f"重量在{self._weight_stable_threshold}秒内发生变化,从 {original_weight_kg}kg 变为 {self._current_weight}kg") + except Exception as e: + logging.error(f"检查重量稳定性时发生错误: {str(e)}") + finally: + # 清理定时器 + if self._stability_check_timer is not None: + self._stability_check_timer.deleteLater() + self._stability_check_timer = None + + def _handle_stable_weight_with_retry(self, weight_kg, max_retries=3): + """ + 使用重试机制处理稳定重量 + Args: + weight_kg: 稳定的重量值(千克) + max_retries: 最大重试次数 + """ + retry_count = 0 + success = False + last_error = None + modbus = ModbusUtils() + client = None + + try: + # 获取Modbus客户端(现在使用连接池,不会每次都创建新连接) + client = modbus.get_client() + if not client: + logging.error("无法获取Modbus客户端连接") + return + + # 重试机制写入寄存器 + while retry_count < max_retries and not success: + try: + # 称重稳定后,给寄存器 D10 为 1 表示已经称重完成 + success = modbus.write_register_until_success(client, 10, 1) + if success: + logging.info(f"成功写入D10寄存器值为1,表示称重完成") + break + except Exception as e: + last_error = e + retry_count += 1 + if retry_count < max_retries: + delay = 0.5 * (2 ** retry_count) # 指数退避 + logging.warning(f"写入D10寄存器失败,尝试第{retry_count}次重试,等待{delay:.1f}秒: {str(e)}") + time.sleep(delay) + + if not success: + logging.error(f"写入D10寄存器失败,已尝试{max_retries}次: {str(last_error)}") + return + + # 处理稳定重量 + self._process_stable_weight(weight_kg) + # 调用打印方法 + self._print_weight_label(weight_kg) + + # 设置已处理标记和上次处理的重量 + self._weight_processed = True + self._last_processed_weight = weight_kg + logging.info(f"已标记重量 {weight_kg}kg 为已处理") + + except Exception as e: + logging.error(f"处理稳定重量时发生错误: {str(e)}") + finally: + # 释放客户端连接回连接池 + if client: + modbus.close_client(client) + + def _process_stable_weight(self, weight_kg): + """ + 处理稳定的称重数据 + Args: + weight_kg: 稳定的重量值(千克) + """ + try: + # 忽略接近0的重量值,这可能表示产品已被移除 + if weight_kg < 0.1: # 小于100g的重量视为无效 + logging.info(f"忽略接近零的重量值: {weight_kg}kg,可能表示产品已被移除") + return + + # 获取数据行数 + if self.process_table.rowCount() <= 2: # 没有数据行 + logging.warning("没有可用的数据行来写入称重数据") + return + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 计算称重列索引 - 称重位置在检验列之后的第二列(贴标后面) + weight_col = 2 + len(enabled_configs) + 1 + # 计算净重列索引 - 净重位置在检验列之后的第三列(称重后面) + net_weight_col = 2 + len(enabled_configs) + 2 + + # 基于状态查找行:优先查找状态为inspected的行 + data_row = None + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 首先查找状态为inspected的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'inspected': + data_row = row + logging.info(f"找到状态为inspected的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到inspected状态的行,回退到原有逻辑 + if data_row is None: + # 查找第一个没有称重数据的行 + for row in range(2, self.process_table.rowCount()): + weight_item = self.process_table.item(row, weight_col) + if not weight_item or not weight_item.text().strip(): + data_row = row + break + + # 如果仍然没有找到,使用当前选中行或第一个数据行 + if data_row is None: + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为inspected的行或没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"找到没有称重数据的行: {data_row}") + else: + logging.info(f"将使用状态为inspected的行: {data_row}") + + # 获取工程号 + gc_note = self.process_table.item(data_row, 1) + if not gc_note: + logging.warning("无法获取工程号") + return + + gc_note = gc_note.text().strip() + if not gc_note: + logging.warning("工程号为空") + return + + # 暂时断开信号连接,避免触发cellChanged信号 + try: + self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) + except: + pass + + # 设置称重值单元格(显示千克) + weight_item = QTableWidgetItem(str(weight_kg)) + weight_item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(data_row, weight_col, weight_item) + + # 保存到数据库(使用千克) + tray_id = self.tray_edit.currentText() + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, str(weight_kg), "pass") + + # 保存净重到数据库(毛重-工字轮重量,单位都是千克) + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + gzl_zl_raw = inspection_dao.get_gzl_zl(self._current_order_code) + gzl_zl = 0.0 + try: + if gzl_zl_raw: + gzl_zl = float(gzl_zl_raw) + except (ValueError, TypeError): + logging.warning(f"无法将工字轮重量 '{gzl_zl_raw}' 转换为浮点数,将使用默认值 0.0") + + net_weight_kg = round(weight_kg - gzl_zl,3) + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, str(net_weight_kg), "pass") + + # 设置净重单元格(显示千克) + net_weight_item = QTableWidgetItem(str(net_weight_kg)) + net_weight_item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(data_row, net_weight_col, net_weight_item) + + # 如果开启 api 模式,则调用接口添加到包装记录 + if AppMode.is_api(): + from dao.inspection_dao import InspectionDAO + from apis.gc_api import GcApi + inspection_dao = InspectionDAO() + # 调用接口 + gc_api = GcApi() + axios_num = self.get_axios_num_by_order_id(self._current_order_code) + 1 + # 获取订单信息和其他信息,两者都已经是字典格式 + info = {} + order_info = inspection_dao.get_order_info(self._current_order_code) + info.update(order_info) + # 获取包装号 + + info['xpack'] = self.tray_edit.currentText() + info['spack'] = self.tray_edit.currentText() + order_others_info = inspection_dao.get_order_others_info(gc_note, self._current_order_code, tray_id) + info.update(order_others_info) + info['data_corp'] = order_info['data_corp'] + info['zh'] = axios_num + info['mzl'] = weight_kg + info['printsl'] = 1 + info['pono'] = self._current_order_code + info["dycz"] = info.get("cz") + info['qd'] = self._current_gc_qd + # 获取本机IP地址 + # import socket + # try: + # # 通过连接外部服务器获取本机IP(不实际建立连接) + # s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # s.connect(("8.8.8.8", 80)) + # local_ip = s.getsockname()[0] + # s.close() + # info['nw_ip'] = local_ip.replace('.', '') + # except Exception as e: + # logging.error(f"获取本机IP失败: {str(e)}") + # # 如果获取失败,使用本地回环地址 + # info['nw_ip'] = '127.0.0.1'.replace('.', '') + info['nw_ip'] = '192.168.1.246' + # 调用接口添加到包装记录 + response = gc_api.add_order_info(info) + if response.get("status",False): + logging.info(f"添加订单信息成功: {response.get('data',{})}") + else: + QMessageBox.warning(self, f"提示", response.get("message",{})) + + # 保存贴标数据到数据库 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass") + + # 重新连接信号 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}") + + # 更新产品状态为weighed + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed') + logging.info(f"工程号 {gc_note} 的称重已完成,状态更新为weighed") + + except Exception as e: + logging.error(f"处理称重数据时发生错误: {str(e)}") + # 确保重新连接信号 + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + except: + pass + + def _print_weight_label(self, weight_kg): + """ + 打印重量标签 注意:目前打印是写入数据库打印,不需要再次调用 + Args: + weight_kg: 稳定的重量值(千克) + """ + try: + logging.info(f"开始打印重量标签,重量:{weight_kg}kg") + # TODO: 实现打印逻辑 + pass + except Exception as e: + logging.error(f"打印重量标签时发生错误: {str(e)}") + + @Slot(int, str) + def handle_label_signal(self, signal, status): + """处理贴标信号""" + logging.info(f"[处理] 贴标信号: {status} (值={signal})") + + # 更新UI显示 + self.label_status_label.setText(f"贴标: {status}") + + # 只有当信号为贴标完成(1)时才进行处理 + if signal == 1: + try: + modbus = ModbusUtils() + client = None + try: + # 获取Modbus客户端(现在使用连接池,不会每次都创建新连接) + client = modbus.get_client() + if not client: + logging.error("无法获取Modbus客户端连接") + return + # 先将寄存器回复为0,否则复原周期内、会把新来的数据也pass + modbus.write_register_until_success(client, 13, 0) + except Exception as e: + logging.error(f"复原寄存器失败{e}") + finally: + client.close() + # 获取数据行数 + if self.process_table.rowCount() <= 2: # 没有数据行 + logging.warning("没有可用的数据行来写入贴标数据") + return + + # 基于状态查找行:优先查找状态为weighed的行 + data_row = None + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 首先查找状态为weighed的行 + for row in range(2, self.process_table.rowCount()): + gc_note_item = self.process_table.item(row, 1) + if gc_note_item: + row_gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) + if status == 'weighed': + data_row = row + logging.info(f"找到状态为weighed的行: {data_row}, 工程号: {row_gc_note}") + break + + # 如果没有找到weighed状态的行,回退到原有逻辑 + if data_row is None: + # 使用当前选中的行或第一个数据行 + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到状态为weighed的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"将使用状态为weighed的行: {data_row}") + + # 确保行存在 + if data_row >= self.process_table.rowCount(): + logging.warning(f"选中的行 {data_row} 超出了表格范围") + return + + # 获取工程号 + order_id_item = self.process_table.item(data_row, 1) + if not order_id_item: + logging.warning("无法获取工程号") + return + + gc_note = order_id_item.text().strip() + if not gc_note: + logging.warning("工程号为空") + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 计算贴标列索引 - 贴标位置在检验列之后的第一列 + label_col = 2 + len(enabled_configs) + + # 生成贴标号(托盘号+轴号) + axios_num = self.get_axios_num(tray_id)+1 + + # 断开单元格变更信号,避免程序自动写入时触发 + try: + self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) + except: + pass + + # 创建并设置贴标单元格 + label_item = QTableWidgetItem(str(axios_num)) + label_item.setTextAlignment(Qt.AlignCenter) + + # 写入单元格 + self.process_table.setItem(data_row, label_col, label_item) + logging.info(f"已将贴标数据 {axios_num} 写入表格单元格 [{data_row}, {label_col}]") + + # 在这里添加保存贴标数据到数据库的代码 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass") + logging.info(f"已将贴标数据 {axios_num} 保存到数据库") + + # 更新产品状态为labeled + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'labeled') + logging.info(f"工程号 {gc_note} 的贴标已完成,状态更新为labeled") + + # 调用加载到包装记录的方法 + self.load_finished_record_to_package_record(self._current_order_code,gc_note, tray_id) + logging.info(f"贴标完成,已将工程号 {gc_note} 的记录加载到包装记录") + + # 删除当前处理的行 + self.process_table.removeRow(data_row) + logging.info(f"已删除处理完成的行 {data_row}") + + # 复原寄存器 12 为 0 + modbus = ModbusUtils() + client = modbus.get_client() + modbus.write_register_until_success(client, 12, 0) + modbus.close_client(client) + + # 更新订单数量和产量统计数据 + self.update_order_statistics() + + # 重新连接单元格变更信号 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + except Exception as e: + logging.error(f"处理贴标完成信号失败: {str(e)}") + # 确保信号重新连接 + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + except: + pass + + @Slot(bool, str) + def handle_modbus_status_change(self, is_connected, message): + """处理Modbus连接状态变化""" + if is_connected: + self.modbus_status_label.setText("Modbus: 已连接") + self.modbus_status_label.setStyleSheet("color: green;") + logging.info(f"Modbus已连接: {message}") + else: + self.modbus_status_label.setText("Modbus: 未连接") + self.modbus_status_label.setToolTip(message) + self.modbus_status_label.setStyleSheet("color: red;") + logging.warning(f"Modbus连接断开: {message}") + + @Slot(int, str) + def handle_register_error(self, address, error_msg): + """处理寄存器读取错误""" + logging.warning(f"[处理] 寄存器D{address}错误: {error_msg}") + # 在这里可以添加错误处理逻辑 + pass + + # 上料按钮样式控制方法 + def restore_input_button_style(self): + """恢复上料按钮的原始样式""" + try: + button_style = """ + QPushButton { + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + border: 1px solid #2196f3; + } + QPushButton:hover { + background-color: #bbdefb; + } + """ + self.input_button.setStyleSheet(button_style) + logging.info("已恢复上料按钮原始样式") + except Exception as e: + logging.error(f"恢复上料按钮样式失败: {str(e)}") + + def fill_input_button_style(self): + """填充上料按钮样式 - 蓝色背景,白色字体""" + try: + button_style = """ + QPushButton { + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + background-color: #2196F3; + color: white; + border: 1px solid #2196F3; + } + QPushButton:hover { + background-color: #1e88e5; + color: white; + } + """ + self.input_button.setStyleSheet(button_style) + logging.info("已填充上料按钮样式") + except Exception as e: + logging.error(f"填充上料按钮样式失败: {str(e)}") + + # 下料按钮样式控制方法 + def restore_output_button_style(self): + """恢复下料按钮的原始样式""" + try: + button_style = """ + QPushButton { + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + border: 1px solid #ffc107; + } + QPushButton:hover { + background-color: #ffecb3; + } + """ + self.output_button.setStyleSheet(button_style) + logging.info("已恢复下料按钮原始样式") + except Exception as e: + logging.error(f"恢复下料按钮样式失败: {str(e)}") + + def fill_output_button_style(self): + """填充下料按钮样式 - 黄色背景,白色字体""" + try: + button_style = """ + QPushButton { + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + background-color: #FFC107; + color: white; + border: 1px solid #FFC107; + } + QPushButton:hover { + background-color: #ffb300; + color: white; + } + """ + self.output_button.setStyleSheet(button_style) + logging.info("已填充下料按钮样式") + except Exception as e: + logging.error(f"填充下料按钮样式失败: {str(e)}") + + @Slot(int, int) + def handle_register_change(self, address, value): + """处理寄存器变化""" + logging.info(f"[处理] 寄存器D{address}变化: {value}") + + # 当D0寄存器有值时,填充上料按钮样式 + if address == 0 and value > 0: + self.fill_input_button_style() + logging.info(f"D0寄存器有值({value}),填充上料按钮样式") + + # 当D4寄存器有值时,填充下料按钮样式 + elif address == 4 and value > 0: + self.fill_output_button_style() + logging.info(f"D4寄存器有值({value}),填充下料按钮样式") + + # 当D0寄存器为 0 时,恢复上料按钮样式 + if address == 0 and value == 0: + self.restore_input_button_style() + logging.info(f"D0寄存器为 0 ,恢复上料按钮样式") + + # 当D4寄存器为 0 时,恢复下料按钮样式 + elif address == 4 and value == 0: + self.restore_output_button_style() + logging.info(f"D4寄存器为 0 ,恢复下料按钮样式") + elif address == 2 and value == 0: + self.restore_start_button_style() + logging.info(f"D2寄存器为 0 ,恢复开始按钮样式") + elif address == 3 and value == 0: + self.restore_start_button_style() + logging.info(f"D3寄存器为 0 ,恢复开始按钮样式") + elif address ==2 and value == 1: + self.fill_start_button_style() + logging.info(f"D2寄存器为 1 ,填充开始按钮样式") + elif address == 3 and value == 1: + self.fill_start_button_style() + logging.info(f"D3寄存器为 1 ,填充开始按钮样式") + # 当D11寄存器变为0时,复位D10寄存器为0 + elif address == 11 and value == 0: + try: + logging.info("检测到D11寄存器变为0,正在复位D10寄存器") + modbus = ModbusUtils() + client = modbus.get_client() + modbus.write_register_until_success(client, 10, 0) + logging.info("成功复位D10寄存器为0") + modbus.close_client(client) + except Exception as e: + logging.error(f"复位D10寄存器失败: {str(e)}") + @Slot(int, str) + def handle_loading_feedback(self, status, desc): + """处理上料信息反馈""" + message = desc + try: + if status == 1: + modbus = ModbusUtils() + client = modbus.get_client() + # 睡 0.5 秒,用于延缓modbus 监听 + time.sleep(0.5) + modbus.write_register_until_success(client, 2, 0) + if self._current_stow_num > 0: + completed_layer_num = self._current_stow_num + self._current_stow_num -= 1 + if self._current_stow_num == 0: + self._is_loading_active = False # 任务完成,标记为非活动 + self._loading_info = None + logging.info("所有层拆垛完成,清空上料信息") + message = f"第 {completed_layer_num} 层(最后一层)拆垛完成!" + # 重置寄存器 0 和 2 为 0 + modbus.write_register_until_success(client, 0, 0) + modbus.write_register_until_success(client, 2, 0) + self.loading_feedback_signal.emit("input", message) + # 恢复开始按钮原始样式 + self.restore_start_button_style() + else: + logging.info(f"当前层拆垛完成,剩余层数: {self._current_stow_num}") + message = f"第 {completed_layer_num} 层拆垛完成。" + self.loading_feedback_signal.emit("input", message) + #通知寄存器,进行第几层拆垛 + modbus.write_register_until_success(client,0 ,self._current_stow_num) + except Exception as e: + logging.error(f"处理上料信息反馈失败: {str(e)}") + # 不在这里显示对话框,而是通过信号传递错误信息 + self.loading_feedback_signal.emit("error", f"处理上料信息反馈失败: {str(e)}") + finally: + modbus.close_client(client) + + def _handle_loading_feedback_ui(self, status_type, desc): + """在主线程中处理上料UI更新""" + try: + # 如果上料任务仍在进行,更新层数显示 + if self._loading_info and self._current_stow_num > 0: + self.show_operation_status("拆垛层数", "input", str(self._current_stow_num)) + else: + # 上料任务完成,清除状态显示 + self.clear_operation_status("input") + # 上料任务完成,恢复上料按钮样式 + self.restore_input_button_style() + logging.info("上料任务完成,恢复上料按钮样式") + + QMessageBox.information(self, "上料操作", desc) + except Exception as e: + logging.error(f"处理上料UI更新失败: {str(e)}") + + @Slot(int, str) + def handle_unloading_feedback(self, status, desc): + """处理下料信息反馈""" + logging.info(f"[处理] 下料信息: {status}, {desc}") + # 如果下料完成(status=1),显示状态信息,处理下料流程 + if status == 1: + modbus = ModbusUtils() + client = modbus.get_client() + + try: + # 睡 0.5 秒,用于延缓modbus 监听 + time.sleep(0.5) + # 临时重置寄存器3(下料启动)为0,等待用户下一次启动 + modbus.write_register_until_success(client, 3, 0) + + # 如果当前下料层数小于总层数,则将层数加1并写入寄存器4 + if self._current_unload_num < self._total_unload_num: + # 保存当前完成的层数用于消息显示 + completed_tier = self._current_unload_num + + # 当前层已完成,层数加1表示开始下一层 + self._current_unload_num += 1 + logging.info(f"当前层{completed_tier}下料完成,更新到下一层:当前={self._current_unload_num}, 总数={self._total_unload_num}") + + # 将新的层数写入寄存器4 + modbus.write_register_until_success(client, 4, self._current_unload_num) + logging.info(f"已将新层数{self._current_unload_num}写入寄存器4") + + # 不直接更新UI,而是通过信号将数据传递给主线程处理 + # 通过信号触发UI更新 - 显示前一层完成的消息 + message = f"第{completed_tier}层下料完成,请启动第{self._current_unload_num}层下料" + self.unloading_feedback_signal.emit("output", message) + + # 恢复开始按钮原始样式 + self.restore_start_button_style() + else: + # 所有层都下料完成,重置寄存器和计数器 + modbus.write_register_until_success(client, 3, 0) # 确保下料启动寄存器为0 + modbus.write_register_until_success(client, 4, 0) # 重置下料层数寄存器为0 + + # 记录完成的信息用于消息显示 + final_tier = self._current_unload_num + total_tier = self._total_unload_num + tray_code = self._current_unload_info.get('tray_code', '') if self._current_unload_info else '' + + # 重置计数器和信息 + self._current_unload_num = 0 + # 不重置总层数,以便可以继续使用相同的总层数 + # self._total_unload_num = 0 + self._current_unload_info = None + + logging.info(f"托盘 {tray_code} 的所有 {total_tier} 层下料完成,重置当前层数") + + # 通过信号触发UI更新,而不是直接操作UI + message = f"托盘 {tray_code} 的所有 {total_tier} 层下料已全部完成" + self.unloading_feedback_signal.emit("output", message) + + # 恢复开始按钮原始样式 + self.restore_start_button_style() + except Exception as e: + logging.error(f"处理下料反馈时发生错误: {str(e)}") + # 不在这里显示对话框,而是通过信号传递错误信息 + self.unloading_feedback_signal.emit("error", f"处理下料反馈失败: {str(e)}") + finally: + modbus.close_client(client) + + def _handle_unloading_feedback_ui(self, status_type, desc): + """在主线程中处理下料完成的事件通知""" + try: + if status_type == "error": + QMessageBox.critical(self, "错误", desc) + return + + # 显示事件消息 + if "全部完成" in desc: + QMessageBox.information(self, "下料完成", desc) + # 任务完成,清除状态显示 + self.clear_operation_status("output") + self.unloading_level_label.setText("下料层数:--") + self.unloading_position_label.setText("下料位置:--") + # 下料任务完成,恢复下料按钮样式 + self.restore_output_button_style() + logging.info("下料任务完成,恢复下料按钮样式") + elif "请启动" in desc: + QMessageBox.information(self, "下料层完成", desc) + + # 如果当前下料信息存在且层数有效,更新UI显示 + if self._current_unload_info and self._current_unload_num > 0: + self.show_operation_status("下料层数", "output", f"{self._current_unload_num}/{self._total_unload_num}") + + except Exception as e: + logging.error(f"处理下料UI更新失败: {str(e)}") + + def _update_error_status(self): + """更新故障状态显示""" + # 收集所有故障信息 + error_codes = [ + getattr(self, 'error_1', 0), + getattr(self, 'error_2', 0), + getattr(self, 'error_3', 0) + ] + + # 检查是否有故障 + has_error = any(code > 0 for code in error_codes) + + if has_error: + # 收集所有错误信息 + errors = [] + error_map = self.machine_handlers.error_map + + if getattr(self, 'error_1', 0) > 0: + errors.append(f"故障1: {error_map.get(self.error_1, '未知')}") + if getattr(self, 'error_2', 0) > 0: + errors.append(f"故障2: {error_map.get(self.error_2, '未知')}") + if getattr(self, 'error_3', 0) > 0: + errors.append(f"故障3: {error_map.get(self.error_3, '未知')}") + + self.error_status_label.setText("故障: 有") + self.error_status_label.setToolTip("\n".join(errors)) + self.error_status_label.setStyleSheet("color: red; font-weight: bold;") + else: + self.error_status_label.setText("故障: 无") + self.error_status_label.setToolTip("") + self.error_status_label.setStyleSheet("color: green; font-weight: bold;") + + @Slot(int, str) + def handle_error_1(self, error_code, error_desc): + """机器人视觉报警""" + logging.info(f"[处理] 机器人视觉报警: {error_desc}") + from utils.register_handlers import Error1Handler + error_handler = Error1Handler() + detailed_desc = error_handler.error_map.get(error_code, f"机器人视觉报警-{error_code}") + + # 保存故障码 + self.error_1 = error_code + self._update_error_status() + + # 只有当错误码为1、2或3时才弹框提示 + if error_code in [1, 2, 3]: + QMessageBox.warning(self, "机器人视觉报警", f"机器人视觉报警: {detailed_desc}") + # 获取Modbus连接 + modbus = ModbusUtils() + client = modbus.get_client() + + # 根据错误码可以添加不同的处理逻辑 + # 这里先简单处理,对所有错误都复位相关寄存器 + modbus.write_register_until_success(client, 2, 0) + modbus.write_register_until_success(client, 0, 0) + modbus.close_client(client) + + @Slot(int, str) + def handle_error_2(self, error_code, error_desc): + """滚筒线报警""" + logging.info(f"[处理] 滚筒线报警: {error_desc}") + from utils.register_handlers import Error2Handler + error_handler = Error2Handler() + detailed_desc = error_handler.error_map.get(error_code, f"滚筒线报警-{error_code}") + # 保存故障码 + self.error_2 = error_code + self._update_error_status() + + # 如果有故障,显示提示(对任何错误码都弹框) + if error_code in [1, 2]: + QMessageBox.warning(self, "滚筒线报警", f"滚筒线报警: {detailed_desc}") + # 获取Modbus连接 + modbus = ModbusUtils() + client = modbus.get_client() + + # 根据错误码可以添加不同的处理逻辑 + # 这里先简单处理,对所有错误都复位相关寄存器 + modbus.write_register_until_success(client, 3, 0) + modbus.write_register_until_success(client, 4, 0) + modbus.close_client(client) + + @Slot(int, str) + def handle_error_3(self, error_code, error_desc): + """拆码垛报警""" + logging.info(f"[处理] 拆码垛报警: {error_desc}") + from utils.register_handlers import Error3Handler + error_handler = Error3Handler() + detailed_desc = error_handler.error_map.get(error_code, f"拆码垛报警-{error_code}") + # 保存故障码 + self.error_3 = error_code + self._update_error_status() + modbus = ModbusUtils() + client = modbus.get_client() + # 如果有故障,显示提示 + if error_code == 1: + QMessageBox.warning(self, "异常", f"异常: {detailed_desc}") + modbus.write_register_until_success(client, 2, 0) + modbus.write_register_until_success(client, 0, 0) + modbus.close_client(client) + # 移除在下料区域显示异常信息的代码 + elif error_code == 2: + QMessageBox.warning(self, "异常", f"异常: {detailed_desc}") + modbus.write_register_until_success(client, 3, 0) + modbus.write_register_until_success(client, 4, 0) + modbus.close_client(client) + + @Slot(int) + def handle_unloading_level(self, level): + """处理下料层数信息(来自Modbus)""" + # 只在层数发生变化时记录日志 + if self._current_unload_num != level: + logging.info(f"下料层数变化:{self._current_unload_num} -> {level}") + + # 更新当前层数 + self._current_unload_num = level + + # 更新保存的下料信息中的当前层数值 + if self._current_unload_info: + self._current_unload_info['tier'] = str(level) + + # 通过信号在主线程中更新UI + self.unloading_level_ui_signal.emit(level) + else: + # 即使层数相同,也更新UI以确保显示正确 + self.unloading_level_ui_signal.emit(level) + + @Slot(int) + def handle_unloading_level_ui(self, level): + """在主线程中更新下料层数UI""" + try: + # 更新显示 + self.unloading_level_label.setText(f"下料层数:{level}") + + # 如果有下料信息且层数大于0,更新右上角显示 + if level > 0 and self._current_unload_info: + # 确保使用固定的总层数 + total_tier = self._total_unload_num + + # 更新右上角显示,使用实际层数值 + self.show_operation_status("下料层数", "output", f"{level}/{total_tier}") + logging.info(f"更新右上角下料层数显示:{level}/{total_tier}") + except Exception as e: + logging.error(f"更新下料层数UI失败: {str(e)}") + + @Slot(int) + def handle_unloading_position(self, position): + """处理下料位置信息""" + # 通过信号在主线程中更新UI + self.unloading_position_ui_signal.emit(position) + + @Slot(int) + def handle_unloading_position_ui(self, position): + """在主线程中更新下料位置UI""" + try: + self.unloading_position_label.setText(f"下料位置:{position}") + except Exception as e: + logging.error(f"更新下料位置UI失败: {str(e)}") + + @Slot(int) + def handle_ng(self, ng): + """处理NG信号, 将当前处理的数据添加到包装记录中,毛重和净重设为0""" + if ng == 1: + try: + # 获取最后一条数据行 + total_rows = self.process_table.rowCount() + if total_rows <= 2: # 只有表头行,没有数据行 + logging.warning("没有可用的数据行来处理NG信号") + return + + # 使用最后一条数据行 + data_row = total_rows - 1 + + # 获取工程号 + order_id_item = self.process_table.item(data_row, 1) + if not order_id_item: + logging.warning("无法获取工程号") + return + + order_id = order_id_item.text().strip() + if not order_id: + logging.warning("工程号为空") + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 计算贴标列索引 + label_col = 2 + len(enabled_configs) + + # 获取贴标值 + label_item = self.process_table.item(data_row, label_col) + label_value = label_item.text() if label_item else "" + + # 如果贴标值为空,生成一个新的贴标值 + if not label_value: + # 初始化托盘号对应的序号 + if tray_id not in self.init_seq: + self.init_seq[tray_id] = 1 + + # 生成贴标号(托盘号+序号) + label_value = f"{self.init_seq[tray_id]}-NG" + self.init_seq[tray_id] += 1 + + # 保存贴标数据到数据库 + self.save_inspection_data(order_id, tray_id, 11, 11, label_value, "pass") + else: + # 如果贴标值已存在但不包含NG标记,添加NG标记 + if "NG" not in label_value: + label_value = f"{label_value}-NG" + # 更新贴标数据 + self.save_inspection_data(order_id, tray_id, 11, 11, label_value, "pass") + + # 设置毛重和净重为0 + self.save_inspection_data(order_id, tray_id, 12, 12, "0", "pass") + self.save_inspection_data(order_id, tray_id, 13, 13, "0", "pass") + + # 获取当前时间作为完成时间 + finish_time = datetime.now() + + # 将数据写入到数据库表 inspection_pack_data + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + inspection_dao.save_package_record(order_id, tray_id, label_value, "0", "0", finish_time) + + # 删除当前处理的行 + self.process_table.removeRow(data_row) + + # 回显数据 + self.show_pack_item() + + # 更新订单数量和产量统计数据 + self.update_order_statistics() + + logging.info(f"NG信号处理完成: 工程号={order_id}, 托盘号={tray_id}, 贴标值={label_value}") + except Exception as e: + logging.error(f"处理NG信号时发生错误: {str(e)}") + finally: + # 复原NG信号 + modbus = ModbusUtils() + client = modbus.get_client() + modbus.write_register_until_success(client, 6, 0) + modbus.close_client(client) + + def register_serial_callbacks(self): + """注册串口数据回调函数""" + try: + # 注册米电阻数据回调 + self.serial_manager.callbacks['mdz_data'] = self.on_mdz_data_received + + # 注册线径数据回调 + self.serial_manager.callbacks['xj_data'] = self.on_diameter_data_received + + # 注册扫码器数据回调 + self.serial_manager.callbacks['scanner_data'] = self.on_scanner_data_received + + # 自动打开已配置的串口 + self.serial_manager.auto_open_configured_ports() + + logging.info("已注册串口数据回调函数") + except Exception as e: + logging.error(f"注册串口数据回调函数失败: {str(e)}") + + def on_mdz_data_received(self, port_name, data): + """米电阻数据接收回调函数 + + Args: + port_name: 串口名称 + data: 接收到的数据 + """ + try: + # 解析数据 + data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) + logging.info(f"收到米电阻数据: {data_str} 来自 {port_name}") + + # 提取米电阻值,格式为"米电阻数据: xxx" + if "米电阻数据:" in data_str: + value_str = data_str.split("米电阻数据:")[1].strip() + try: + # 转换为浮点数 + mdz_value = float(value_str) + + # 查找米电阻对应的检验项配置 + mdz_config = None + enabled_configs = self.inspection_manager.get_enabled_configs() + for config in enabled_configs: + if config.get('name') == 'mdz' or config.get('display_name') == '米电阻': + mdz_config = config + break + + if mdz_config: + # 找到对应的检验项,将数据写入对应的单元格 + self.set_inspection_value('mdz', mdz_config, mdz_value) + else: + logging.warning("未找到米电阻对应的检验项配置") + + except ValueError: + logging.warning(f"米电阻数据格式错误: {value_str}") + else: + logging.warning(f"收到的数据不包含米电阻数据标记: {data_str}") + except Exception as e: + logging.error(f"处理米电阻数据失败: {str(e)}") + + def on_diameter_data_received(self, port_name, data): + """线径数据接收回调函数 - 采用类似称重的逻辑,不使用会话机制""" + try: + data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) + logging.info(f"收到线径数据: {data_str} 来自 {port_name}") + + # 提取线径值 + if "线径数据:" in data_str: + value_str = data_str.split("线径数据:")[1].strip() + try: + xj_value = round(float(value_str)/10000, 3) + self.statusBar().showMessage(f"线径数据: {xj_value:.3f}", 2000) + + # 查找线径对应的检验项配置和列 + xj_config = None + xj_column = None + enabled_configs = self.inspection_manager.get_enabled_configs() + for i, config in enumerate(enabled_configs): + if config.get('name') == 'xj' or config.get('display_name') == '线径': + xj_config = config + xj_column = 2 + i + break + if not xj_config or xj_column is None: + logging.warning("未找到线径对应的检验项配置或列索引") + return + + # 忽略接近0的值或异常值 + if xj_value < 0.001 or xj_value > 10: + logging.info(f"忽略异常线径值: {xj_value}") + return + + # 保存测量值到内部列表用于稳定性检测 + # 使用类属性存储最近的测量值,用于稳定性检测 + if not hasattr(self, '_diameter_measurements'): + self._diameter_measurements = [] + self._diameter_measurements.append(xj_value) + if len(self._diameter_measurements) > 5: + self._diameter_measurements.pop(0) + + # 显示临时值到状态栏 + if len(self._diameter_measurements) < 5: + self.statusBar().showMessage(f"线径数据收集中: {xj_value:.3f} ({len(self._diameter_measurements)}/5)", 2000) + return + + # 检查稳定性 + measurements = self._diameter_measurements[-5:] + min_value = min(measurements) + max_value = max(measurements) + avg_value = sum(measurements) / len(measurements) + error_range = avg_value * 0.04 # 允许4%误差 + + if max_value - min_value <= error_range: + # 数据稳定,可以保存 + final_value = avg_value # 使用平均值作为最终值 + + # 查找第一个没有线径数据的行 + data_row = None + for row in range(2, self.process_table.rowCount()): + cell_item = self.process_table.item(row, xj_column) + if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0': + data_row = row + break + + # 如果没找到空行,使用当前选中行或第一个数据行 + if data_row is None: + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 + logging.info(f"未找到没有线径数据的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"找到没有线径数据的行: {data_row}") + + # 获取工程号 + gc_note_item = self.process_table.item(data_row, 1) + if not gc_note_item: + logging.warning("无法获取工程号") + return + + gc_note = gc_note_item.text().strip() + if not gc_note: + logging.warning("工程号为空") + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 公差校验 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + bccd, tccd = inspection_dao.get_xj_range(self._current_order_code) + + if bccd is not None and tccd is not None: + if float(bccd) - 0.5 <= final_value <= float(tccd) + 0.5: + # 使用set_inspection_value保存数据 + self.set_inspection_value('xj', xj_config, final_value) + logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") + else: + reply = QMessageBox.question( + self, + '确认保存', + f"线径 {final_value:.3f} 不在公差范围内 ({bccd} - {tccd}),\n是否继续保存?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.Yes: + self.set_inspection_value('xj', xj_config, final_value) + logging.info(f"已将超出公差范围的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") + else: + logging.info(f"用户取消保存超出范围的线径值: {final_value:.3f}") + else: + # 无公差范围,直接保存 + self.set_inspection_value('xj', xj_config, final_value) + logging.info(f"已将线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") + + # 重置测量列表,准备下一次测量 + self._diameter_measurements = [] + self.statusBar().showMessage(f"线径数据已保存: {final_value:.3f}", 2000) + else: + # 数据不稳定,继续收集 + self.statusBar().showMessage(f"线径数据不稳定: {min_value:.3f} - {max_value:.3f}, 继续测量", 2000) + logging.warning(f"线径测量数据不稳定,范围: {min_value:.3f} - {max_value:.3f}, 平均值: {avg_value:.3f}") + + except ValueError: + logging.warning(f"线径数据格式错误: {value_str}") + else: + logging.warning(f"收到的数据不包含线径数据标记: {data_str}") + except Exception as e: + logging.error(f"处理线径数据失败: {str(e)}") + + def _save_diameter_to_order(self, order_id, config, value): + """基于工程号保存线径值,确保即使行号变化也能保存到正确的产品""" + try: + # 查找工程号对应的行 + target_row = None + for row in range(2, self.process_table.rowCount()): + order_id_item = self.process_table.item(row, 1) + if order_id_item and order_id_item.text().strip() == order_id: + target_row = row + break + + if target_row is not None: + # 使用set_inspection_value保存数据 + self.set_inspection_value('xj', config, value) + logging.info(f"已将线径值 {value:.3f} 保存到工程号 {order_id} (行 {target_row})") + else: + logging.warning(f"找不到工程号 {order_id} 对应的行,无法保存线径值") + except Exception as e: + logging.error(f"保存线径值到工程号失败: {str(e)}") + + def _reset_diameter_session(self): + """重置线径测量会话状态""" + self._diameter_session_active = False + self._diameter_session_target_row = None + self._diameter_session_order_id = None + self._diameter_session_measurements = [] + self._diameter_session_start_time = 0 + + def on_scanner_data_received(self, port_name, data): + """扫码器数据接收回调函数 + + Args: + port_name: 串口名称 + data: 接收到的数据 + """ + try: + # 解析数据 + data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) + logging.info(f"收到扫码器数据: {data_str} 来自 {port_name}") + + # 提取扫码数据,格式为"扫码数据: xxx" + if "扫码数据:" in data_str: + scan_data = data_str.split("扫码数据:")[1].strip() + logging.info(f"提取到扫码数据: {scan_data}") + + # 使用焦点跟踪器设置文本并触发回车事件 + from utils.focus_tracker import FocusTracker + focus_tracker = FocusTracker.get_instance() + + if focus_tracker.set_text_and_trigger_enter(scan_data): + logging.info(f"已将扫码数据 '{scan_data}' 设置到当前焦点输入框并触发回车事件") + else: + # 如果没有焦点输入框,则默认设置到工程号输入框 + logging.info(f"没有焦点输入框,默认将扫码数据设置到工程号输入框") + + # 临时断开returnPressed信号连接,避免setText触发信号 + try: + self.order_edit.returnPressed.disconnect(self.handle_order_enter) + except Exception: + logging.debug("returnPressed信号未连接或断开失败") + + # 设置工程号到输入框 + self.order_edit.setText(scan_data) + + # 重新连接returnPressed信号 + self.order_edit.returnPressed.connect(self.handle_order_enter) + + # 调用一次handle_order_enter方法 + self.handle_order_enter() + else: + logging.warning(f"收到的数据不包含扫码数据标记: {data_str}") + except Exception as e: + logging.error(f"处理扫码器数据失败: {str(e)}") + # 确保信号重新连接 + try: + self.order_edit.returnPressed.connect(self.handle_order_enter) + except Exception: + pass + + def set_inspection_value(self, data_type, config, value): + """设置检验项目值到表格中 + + Args: + data_type: 数据类型,'mdz'表示米电阻,'xj'表示线径 + config: 检验项配置 + value: 检验值 + """ + try: + # 获取检验项的列索引 + config_id = config.get('id') + config_position = config.get('position') + col_index = None + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 根据检验项配置查找对应的列索引 + for i, cfg in enumerate(enabled_configs): + if cfg.get('id') == config_id: + col_index = 2 + i # 检验列从第3列开始 + break + + if col_index is None: + logging.warning(f"未找到{data_type}对应的列索引") + return + + # 检查表格是否有数据行 + if self.process_table.rowCount() <= 2: # 只有表头行 + order_id = self.order_edit.text().strip() + if order_id: + self.add_new_inspection_row(order_id) + data_row = 2 # 新添加的行 + else: + logging.warning("无法添加新行,订单号为空") + return + + # 对于线径数据,如果有活跃会话,优先使用会话锁定的工程号 + if data_type == 'xj' and hasattr(self, '_diameter_session_active') and self._diameter_session_active and self._diameter_session_order_id: + # 查找会话工程号对应的行 + data_row = None + for row in range(2, self.process_table.rowCount()): + order_id_item = self.process_table.item(row, 1) + if order_id_item and order_id_item.text().strip() == self._diameter_session_order_id: + data_row = row + break + + if data_row is not None: + logging.info(f"使用线径会话锁定的工程号 {self._diameter_session_order_id} 对应的行 {data_row}") + else: + logging.warning(f"找不到线径会话锁定的工程号 {self._diameter_session_order_id} 对应的行,将使用默认行选择逻辑") + # 继续使用默认逻辑 + data_row = None + else: + # 默认行选择逻辑 + data_row = None + + # 如果没有找到特定行,使用默认逻辑查找第一个没有该检测数据的行 + if data_row is None: + for row in range(2, self.process_table.rowCount()): + cell_item = self.process_table.item(row, col_index) + if not cell_item or not cell_item.text().strip() or (data_type == 'xj' and cell_item.text().strip() == '0'): + data_row = row + break + + # 如果没有找到没有该检测数据的行,使用当前选中行或第一个数据行 + if data_row is None: + current_row = self.process_table.currentRow() + data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) + logging.info(f"未找到没有{data_type}数据的行,使用当前选中行或第一个数据行: {data_row}") + else: + logging.info(f"找到没有{data_type}数据的行: {data_row}") + + # 获取工程号 + order_id_item = self.process_table.item(data_row, 1) + if not order_id_item: + logging.warning("无法获取工程号") + return + + order_id = order_id_item.text().strip() + if not order_id: + logging.warning("工程号为空") + return + + # 暂时断开信号连接,避免触发cellChanged信号 + try: + self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) + except: + pass + + # 格式化值并设置单元格 + formatted_value = str(value) + if config.get('data_type') == 'number': + # 格式化数字,线径保留3位小数,其他保留2位小数 + if data_type == 'xj': + formatted_value = f"{value:.3f}" + else: + formatted_value = f"{value:.2f}" + + # 设置单元格值 + item = QTableWidgetItem(formatted_value) + item.setTextAlignment(Qt.AlignCenter) + item.setData(Qt.UserRole, config_id) # 保存配置ID,用于识别检验项 + self.process_table.setItem(data_row, col_index, item) + + # 验证数据是否在有效范围内 + status = "pass" + if config.get('data_type') == 'number': + min_value = config.get('min_value') + max_value = config.get('max_value') + if (min_value is not None and value < min_value) or (max_value is not None and value > max_value): + status = "fail" + item.setBackground(QBrush(QColor("#ffcdd2"))) # 浅红色 + else: + item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + + # 保存到数据库,但只在非加载状态下 + if not self._loading_data_in_progress: + tray_id = self.tray_edit.currentText() + self.save_inspection_data(self._current_order_code, order_id, tray_id, config_position, config_id, formatted_value, status) + # 不需要在这里主动触发数据重新加载,因为handle_inspection_cell_changed会处理 + + # 重新连接信号 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + logging.info(f"已将{data_type}数据 {formatted_value} 写入工程号 {order_id} (行 {data_row}, 列 {col_index})") + + except Exception as e: + logging.error(f"设置检验项值失败: {str(e)}") + # 确保重新连接信号 + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + except: + pass + + def handle_tray_changed(self): + """处理托盘号变更事件,启动监听并加载数据""" + try: + tray_id = self.tray_edit.currentText() + if tray_id: + logging.info(f"托盘号变更为 {tray_id},启动监听") + # 初始化托盘号对应的序号(如果不存在) + if tray_id not in self.init_seq: + self.init_seq[tray_id] = 1 + logging.info(f"初始化托盘号 {tray_id} 的序号为 1") + + # 加载检验数据 + self._safe_load_data() + + # 无论_safe_load_data是否成功,都确保显示包装记录 + # 临时保存当前加载状态 + prev_loading_state = getattr(self, '_loading_data_in_progress', False) + + try: + # 设置加载状态为True,避免无限循环调用 + self._loading_data_in_progress = True + # 强制显示包装记录 + self.show_pack_item() + logging.info(f"托盘号变更:直接调用显示包装记录, 托盘号={tray_id}") + finally: + # 恢复之前的加载状态 + self._loading_data_in_progress = prev_loading_state + + except Exception as e: + logging.error(f"处理托盘号变更失败: {str(e)}") + + def handle_order_code_received(self, order_code): + """处理从加载对话框接收到的订单号""" + logging.info(f"主窗口接收到订单号: {order_code}") + # 存储当前订单号 + self._current_order_code = order_code + # 更新订单号输入框 + self.order_no_input.setText(order_code) + + # 如果是接口模式 + if AppMode.is_api(): + # 获取订单的详细信息 + from apis.gc_api import GcApi + gc_api = GcApi() + # 获取工程号信息 + order_response = gc_api.get_order_info(order_code, self.corp_id) + + if order_response.get("status", False): + # 获取订单的工程号数据 + gc_notes = order_response.get("data", []) + if gc_notes and len(gc_notes) > 0: + # 更新上料区域信息表格 + self.update_info_table(gc_notes[0]) + else: + # 非接口模式,获取本地数据库中的订单信息 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + order_info = inspection_dao.get_order_info(order_code) + + # 更新上料区域信息表格 + self.update_info_table(order_info) + + def on_report(self): + """报表按钮点击处理""" + try: + dialog = ReportDialog(self) + dialog.exec_() + except Exception as e: + logging.error(f"打开报表对话框失败: {str(e)}") + QMessageBox.warning(self, "错误", f"打开报表对话框失败: {str(e)}") + + def init_camera_display(self): + """初始化上料区信息表格,不包含任何相机相关代码""" + # 上料区的信息表格已经在UI类的create_left_panel方法中初始化 + # 这个方法保留是为了兼容性,但不做任何操作 + pass + + def initialize_camera(self): + """初始化相机的空方法,保留是为了兼容性""" + pass + + def _start_camera_display(self): + """相机显示的空方法,保留是为了兼容性""" + pass + + def update_camera_ui(self, is_camera_ready): + """相机UI更新的空方法,保留是为了兼容性""" + pass + + def handle_camera_status(self, is_connected, message): + """相机状态处理的空方法,保留是为了兼容性""" + pass + + @Slot(int, str) + def handle_emergency_stop(self, value, desc): + """处理急停信号""" + logging.info(f"[处理] 急停信号: {desc}") + + # 保存一个急停状态变量 + self.emergency_stop = value + + # 当急停信号为1时,重置D2和D3寄存器 + if value == 1: + try: + modbus = ModbusUtils() + client = modbus.get_client() + + # 重置D2和D3寄存器 + modbus.write_register_until_success(client, 2, 0) + modbus.write_register_until_success(client, 3, 0) + + # 通过信号在主线程中处理UI更新 + self.emergency_stop_signal.emit(value, desc) + + modbus.close_client(client) + except Exception as e: + logging.error(f"处理急停信号失败: {str(e)}") + else: + # 急停信号解除,在主线程中恢复错误状态显示 + self.emergency_stop_signal.emit(value, desc) + + def _handle_emergency_stop_ui(self, value, desc): + """在主线程中处理急停信号UI更新""" + try: + if value == 1: + # 显示警告对话框 + QMessageBox.warning(self, "急停警告", "监听到急停信号") + + # 恢复按钮样式 - 确保在主线程中执行 + logging.info("急停触发:开始恢复按钮样式") + self.restore_start_button_style() + logging.info("急停触发:已恢复开始按钮样式") + + if hasattr(self, 'restore_output_button_style'): + self.restore_output_button_style() + logging.info("急停触发:已恢复下料按钮样式") + + if hasattr(self, 'restore_input_button_style'): + self.restore_input_button_style() + logging.info("急停触发:已恢复上料按钮样式") + + # 更新错误状态标签 + self.error_status_label.setText("故障: 急停") + self.error_status_label.setToolTip("急停按钮被触发") + self.error_status_label.setStyleSheet("color: red; font-weight: bold;") + + logging.info("急停UI已更新:按钮样式已恢复,状态标签已更新") + else: + # 急停信号解除,恢复错误状态显示 + self._update_error_status() + + # 恢复故障状态标签 + self.error_status_label.setText("故障: 正常") + self.error_status_label.setStyleSheet("color: green; font-weight: bold;") + + logging.info("急停解除UI已更新:状态标签已恢复") + except Exception as e: + logging.error(f"处理急停UI更新失败: {str(e)}") + + def create_right_panel(self): + """创建右侧面板""" + # 创建右侧整体框架 + self.right_frame = QFrame() + self.right_frame.setFrameShape(QFrame.NoFrame) # 移除框架边框 + self.right_frame.setLineWidth(0) + self.right_layout.addWidget(self.right_frame) + + # 右侧整体使用垂直布局,不设置边距 + self.right_frame_layout = QVBoxLayout(self.right_frame) + self.right_frame_layout.setContentsMargins(0, 0, 0, 0) + self.right_frame_layout.setSpacing(0) + + # 创建一个垂直分割器,用于控制两个表格的高度比例 + self.right_splitter = QSplitter(Qt.Vertical) + + # 创建微丝产线表格的容器 + self.process_container = QWidget() + self.process_container_layout = QVBoxLayout(self.process_container) + self.process_container_layout.setContentsMargins(0, 0, 0, 0) + self.process_container_layout.setSpacing(0) + + # 创建包装记录表格的容器 + self.record_container = QWidget() + self.record_container_layout = QVBoxLayout(self.record_container) + self.record_container_layout.setContentsMargins(0, 0, 0, 0) + self.record_container_layout.setSpacing(0) + + # 创建微丝产线表格 + self.create_process_table() + + # 创建带删除按钮的标题行 + self.create_process_title_with_button() + + # 将微丝产线表格添加到容器中 + self.process_container_layout.addWidget(self.process_frame) + + # 创建包装记录表格 + self.create_record_table() + self.record_container_layout.addWidget(self.record_frame) + + # 将两个容器添加到分割器中 + self.right_splitter.addWidget(self.process_container) + self.right_splitter.addWidget(self.record_container) + + # 设置初始大小比例:微丝产线占1/3,包装记录占2/3 + self.right_splitter.setSizes([100, 200]) # 比例为1:2 + + # 将分割器添加到右侧布局 + self.right_frame_layout.addWidget(self.right_splitter) + + def create_process_title_with_button(self): + """创建带删除按钮的微丝产线标题行""" + # 获取原来的标题 + original_title = self.process_title.text() + + # 移除原来的标题 + self.process_layout.removeWidget(self.process_title) + self.process_title.deleteLater() + + # 创建标题容器 + title_container = QWidget() + title_layout = QHBoxLayout(title_container) + title_layout.setContentsMargins(5, 0, 5, 0) + + # 创建新的标题标签 + self.process_title = QLabel(original_title) + self.process_title.setFont(self.second_title_font) + self.process_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # 创建删除按钮 + self.delete_row_button = QPushButton("删除选中行") + self.delete_row_button.setFont(self.normal_font) + self.delete_row_button.setStyleSheet(""" + QPushButton { + padding: 5px 10px; + background-color: #ffebee; + border: 1px solid #f44336; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #ffcdd2; + } + """) + self.delete_row_button.clicked.connect(self.handle_delete_row) + + # 打印托盘号按钮 + self.print_tray_button = QPushButton("打印托盘号") + self.print_tray_button.setFont(self.normal_font) + self.print_tray_button.setStyleSheet(""" + QPushButton { + padding: 5px 8px; + background-color: #e3f2fd; + border: 1px solid #2196f3; + } + QPushButton:hover { + background-color: #c8e6c9; + } + """) + self.print_tray_button.clicked.connect(lambda: self.handle_tray_complete(ismt=False)) + + # 打印选中行按钮 + self.print_row_button = QPushButton("打印选中行") + self.print_row_button.setFont(self.normal_font) + self.print_row_button.setStyleSheet(""" + QPushButton { + padding: 5px 8px; + background-color: #e0e0e0; + border: 1px solid #cccccc; + } + QPushButton:hover { + background-color: #f5f5f5; + } + """) + self.print_row_button.clicked.connect(self.handle_print_row) + + # 将标题和按钮添加到布局中 + title_layout.addWidget(self.process_title) + title_layout.addStretch() + title_layout.addWidget(self.print_row_button) + title_layout.addWidget(self.delete_row_button) + title_layout.addWidget(self.print_tray_button) + + # 设置容器样式 + title_container.setFixedHeight(40) + title_container.setStyleSheet("background-color: #f8f8f8; border-bottom: 1px solid #dddddd;") + + # 将标题容器添加到布局中 + self.process_layout.insertWidget(0, title_container) + + def handle_delete_row(self): + """处理删除按钮点击事件,删除选中的微丝产线表格行""" + try: + # 获取当前选中的行 + selected_rows = self.process_table.selectionModel().selectedRows() + if not selected_rows: + # 如果没有选中整行,则获取当前选中的单元格所在行 + current_row = self.process_table.currentRow() + if current_row >= 2: # 确保不是表头行 + selected_rows = [self.process_table.model().index(current_row, 0)] + else: + QMessageBox.warning(self, "提示", "请先选择要删除的数据行") + return + + # 确认删除 + reply = QMessageBox.question( + self, + "确认删除", + "确定要删除选中的数据吗?此操作不可恢复。", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + # 从数据库中删除数据 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 按行号降序排序,以便从后往前删除 + rows_to_delete = sorted([index.row() for index in selected_rows], reverse=True) + + for row in rows_to_delete: + if row < 2: # 跳过表头行 + continue + + # 获取工程号 + gc_note_item = self.process_table.item(row, 1) + if not gc_note_item: + continue + + gc_note = gc_note_item.text().strip() + if not gc_note: + continue + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 从数据库中删除该工程号的检验数据 + inspection_dao.delete_inspection_data(self._current_order_code, gc_note, tray_id) + logging.info(f"已从数据库中删除工程号 {gc_note} 的检验数据") + + # 从表格中删除行 + self.process_table.removeRow(row) + logging.info(f"已从表格中删除第 {row} 行") + + # 重新加载数据 + self._safe_load_data() + + # 显示成功消息 + QMessageBox.information(self, "删除成功", "已成功删除选中的数据") + + except Exception as e: + logging.error(f"删除数据失败: {str(e)}") + QMessageBox.critical(self, "错误", f"删除数据失败: {str(e)}") + + + + def handle_tray_complete(self, ismt=True): + """托盘完成或打印托盘号事件 + Args: + ismt: 是否满托,True为满托,False为未满托 + """ + try: + # 获取托盘号 + tray_id = self.tray_edit.currentText() + if not tray_id: + QMessageBox.warning(self, "提示", "请先选择托盘号") + return + if ismt: + # 确认对话框 + reply = QMessageBox.question( + self, + "确认完成", + f"确认将托盘 {tray_id} 标记为已满托?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + # 调用接口 + from apis.gc_api import GcApi + gc_api = GcApi() + + # 准备参数 + params = { + 'ismt': ismt, + 'corp_id': self.corp_id, + 'tray_id': tray_id, + 'ip': '192.168.1.246' + } + + # 调用接口 + response = gc_api.ismt_option(params) + + # 处理响应 + if response.get('status', False): + if ismt: + QMessageBox.information(self, "成功", "托盘已标记为完成") + logging.info(f"托盘 {tray_id} 已标记为完成") + else: + QMessageBox.information(self, "成功", "托盘号已打印") + logging.info(f"托盘号 {tray_id} 已打印") + else: + error_msg = response.get('message', '未知错误') + QMessageBox.warning(self, "失败", f"标记托盘完成失败: {error_msg}") + logging.error(f"标记托盘 {tray_id} 完成失败: {error_msg}") + + except Exception as e: + logging.error(f"处理托盘完成失败: {str(e)}") + QMessageBox.critical(self, "错误", f"处理托盘完成失败: {str(e)}") + + def update_info_table(self, order_info): + """根据订单信息更新上料区域的信息表格 + + Args: + order_info: 订单信息字典 + """ + try: + if not order_info: + logging.warning("订单信息为空,无法更新上料区域信息表格") + return + + logging.info(f"更新上料区域信息表格: {order_info}") + + # 使用UI类中定义的字段映射关系 + field_mapping = self.FIELD_MAPPING + + # 记录 order_info 中的所有键,用于调试 + logging.debug(f"订单信息键: {list(order_info.keys())}") + + # 更新表格内容 + for field_name, field_key in field_mapping.items(): + if field_name in self.info_values: + value = "" + # 对可能存在的键进行安全检查 + if field_key and field_key in order_info: + value = str(order_info[field_key]) + # 特殊处理线径公差 + if field_name == "线径公差" and "bccd" in order_info and "tccd" in order_info: + bccd = order_info.get("bccd") + tccd = order_info.get("tccd") + if bccd is not None and tccd is not None: + value = f"{bccd} - {tccd}" + if field_name == "强度范围" and "bqd" in order_info and "tqd" in order_info: + bqd = order_info.get("bqd") + tqd = order_info.get("tqd") + if bqd is not None and tqd is not None: + value = f"{bqd} - {tqd}" + self.info_values[field_name].setText(value) + else: + logging.warning(f"字段名 '{field_name}' 在info_values中不存在") + + logging.info("上料区域信息表格更新完成") + + except Exception as e: + logging.error(f"更新上料区域信息表格失败: {str(e)}") + # 记录异常堆栈,便于调试 + import traceback + logging.error(traceback.format_exc()) + + def handle_print_row(self): + """处理打印按钮点击事件,打印选中的微丝产线表格行""" + try: + # 获取当前选中的行 + selected_rows = self.process_table.selectionModel().selectedRows() + if not selected_rows: + # 如果没有选中整行,则获取当前选中的单元格所在行 + current_row = self.process_table.currentRow() + if current_row >= 2: # 确保不是表头行 + selected_rows = [self.process_table.model().index(current_row, 0)] + else: + QMessageBox.warning(self, "提示", "请先选择要打印的数据行") + return + + # 确认打印 + reply = QMessageBox.question( + self, + "确认打印", + "确定要打印选中的数据吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + # 获取选中的行索引(只处理第一个选中的行) + row = selected_rows[0].row() + if row < 2: # 跳过表头行 + return + + # 获取工程号(用于日志记录) + gc_note_item = self.process_table.item(row, 1) + if not gc_note_item: + QMessageBox.warning(self, "提示", "无法获取工程号信息") + return + + gc_note = gc_note_item.text().strip() + if not gc_note: + QMessageBox.warning(self, "提示", "工程号不能为空") + return + + # 向D12寄存器写入1,触发打印 + from utils.modbus_utils import ModbusUtils + modbus = ModbusUtils() + client = modbus.get_client() + + if not client: + QMessageBox.critical(self, "错误", "无法连接到Modbus服务器") + return + + try: + # 向D12寄存器写入1,表示打印请求 + success = modbus.write_register(client, 12, 1) + if success: + logging.info(f"已向D12寄存器写入1,触发打印工程号 {gc_note} 的数据") + else: + QMessageBox.warning(self, "警告", "发送打印请求失败,请检查PLC连接") + finally: + # 释放客户端连接 + modbus.close_client(client) + + except Exception as e: + logging.error(f"打印数据失败: {str(e)}") + QMessageBox.critical(self, "错误", f"打印数据失败: {str(e)}") + + def update_order_statistics(self): + """更新订单数量和产量统计数据到项目表格""" + try: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + + # 获取订单数量和产量统计数据 + statistics = inspection_dao.get_order_statistics() + + # 设置表格项(日、月、年、累计订单数量) + # 当日订单数量 + day_cnt_item = QTableWidgetItem(str(statistics['order_cnt_day'])) + day_cnt_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(0, 1, day_cnt_item) + + # 当月订单数量 + month_cnt_item = QTableWidgetItem(str(statistics['order_cnt_month'])) + month_cnt_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(1, 1, month_cnt_item) + + # 当年订单数量 + year_cnt_item = QTableWidgetItem(str(statistics['order_cnt_year'])) + year_cnt_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(2, 1, year_cnt_item) + + # 累计订单数量 + all_cnt_item = QTableWidgetItem(str(statistics['order_cnt_all'])) + all_cnt_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(3, 1, all_cnt_item) + + # 设置表格项(日、月、年、累计产量) + # 当日产量 + day_num_item = QTableWidgetItem(str(round(statistics['order_num_day'], 2))) + day_num_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(0, 2, day_num_item) + + # 当月产量 + month_num_item = QTableWidgetItem(str(round(statistics['order_num_month'], 2))) + month_num_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(1, 2, month_num_item) + + # 当年产量 + year_num_item = QTableWidgetItem(str(round(statistics['order_num_year'], 2))) + year_num_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(2, 2, year_num_item) + + # 累计产量 + all_num_item = QTableWidgetItem(str(round(statistics['order_num_all'], 2))) + all_num_item.setTextAlignment(Qt.AlignCenter) + self.project_table.setItem(3, 2, all_num_item) + + logging.debug(f"已更新订单数量和产量统计数据: 日订单={statistics['order_cnt_day']}, 月订单={statistics['order_cnt_month']}, 年订单={statistics['order_cnt_year']}, 累计订单={statistics['order_cnt_all']}, 日产量={statistics['order_num_day']}, 月产量={statistics['order_num_month']}, 年产量={statistics['order_num_year']}, 累计产量={statistics['order_num_all']}") + + except Exception as e: + logging.error(f"更新订单数量和产量统计数据失败: {str(e)}") + import traceback + logging.error(traceback.format_exc()) + + def _log_focus_widget_info(self, widget): + """记录当前焦点控件的信息,用于调试""" + try: + from PySide6.QtWidgets import QLineEdit, QComboBox + widget_type = "未知" + if isinstance(widget, QLineEdit): + widget_type = "输入框" + elif isinstance(widget, QComboBox): + widget_type = "下拉框" + + widget_name = widget.objectName() if widget else "无" + widget_text = "" + + if isinstance(widget, QLineEdit): + widget_text = widget.text() + elif isinstance(widget, QComboBox): + widget_text = widget.currentText() + + logging.info(f"当前焦点控件: 类型={widget_type}, 名称={widget_name}, 文本={widget_text}") + except Exception as e: + logging.error(f"记录焦点控件信息失败: {e}") + + def handle_inspection_cell_changed(self, row, column): + """处理检验表格单元格内容变更事件""" + try: + # 只处理数据行的检验列变更 + if row < 2: # 忽略表头行 + return + + # 忽略首尾两列(序号和工程号) + if column < 2: + return + + # 获取工程号 + order_item = self.process_table.item(row, 1) + if not order_item: + return + + gc_note = order_item.text().strip() + if not gc_note: + return + + # 获取托盘号 + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 判断是否是检验列(非包装列) + packaging_start_col = 2 + len(enabled_configs) + + # 获取单元格内容 + cell_item = self.process_table.item(row, column) + if not cell_item: + return + + value = cell_item.text().strip() + + # 默认设置为通过状态 + status = 'pass' + + # 记录当前正在处理的数据类型,用于日志输出 + data_type = "检验" + + if column >= 2 and column < packaging_start_col: + # 是检验列 + config_index = column - 2 + if config_index < len(enabled_configs): + config = enabled_configs[config_index] + data_type = config['display_name'] + + # 显示临时状态消息 + self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) + + # 验证数据有效性 + # 设置单元格颜色为浅绿色,表示已填写 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) + + # 保持当前状态不变,由状态管理逻辑处理 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) + self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) + + # 判断是否是包装列 + elif column == packaging_start_col: + # 贴标列 + data_type = "贴标" + self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存贴标数据,position和config_id都是11 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, value, status) + + elif column == packaging_start_col + 1: + # 毛重列 + data_type = "毛重" + self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存毛重数据,position和config_id都是12 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, value, status) + elif column == packaging_start_col + 2: + # 净重列 + data_type = "净重" + self.statusBar().showMessage(f"正在保存净重数据: {value}", 1000) + # 设置单元格颜色为通过 + cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 + # 保存净重数据,position和config_id都是13 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, value, status) + + # 记录详细日志 + logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}") + + # 检查是否完成检验并更新状态 + self.check_inspection_completed(row) + + except Exception as e: + logging.error(f"处理检验单元格变更失败: {str(e)}") + self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) + finally: + # 延迟一段时间后再触发查询,避免频繁刷新UI + # 但要避免在加载过程中触发新的加载 + if not self._loading_data_in_progress: + QTimer.singleShot(1000, self._safe_load_data) + + def validate_inspection_value(self, config, value): + """验证检验值是否有效 + + Args: + config: 检验配置 + value: 检验值 + + Returns: + bool: 是否有效 + """ + try: + # 特殊处理贴标和称重数据 - 这些数据默认都是有效的 + if config.get('position') in [11, 12]: # 11是贴标,12是称重 + return True + + # 检查值是否为空 + if not value and config.get('required', False): + return False + + # 根据数据类型验证 + data_type = config.get('data_type') + + if data_type == 'number': + # 数值类型验证 + try: + # 如果值为空且不是必填,则视为有效 + if not value and not config.get('required', False): + return True + + num_value = float(value) + min_value = config.get('min_value') + max_value = config.get('max_value') + + if min_value is not None and num_value < min_value: + return False + + if max_value is not None and num_value > max_value: + return False + + return True + except ValueError: + return False + + elif data_type == 'enum': + # 枚举类型验证 + enum_values = config.get('enum_values') + if enum_values and isinstance(enum_values, list): + # 如果值为空且不是必填,则视为有效 + if not value and not config.get('required', False): + return True + return value in enum_values + return False + + # 文本类型不做特殊验证 + return True + + except Exception as e: + logging.error(f"验证检验值失败: {str(e)}") + return False + + def check_inspection_completed(self, row): + """检查行是否有至少一个检验项已完成,如果是则更新状态为inspected + + Args: + row: 行索引 + + Returns: + bool: 是否有至少一个检验项已完成 + """ + try: + # 获取工程号 + gc_note_item = self.process_table.item(row, 1) + if not gc_note_item: + return False + + gc_note = gc_note_item.text().strip() + tray_id = self.tray_edit.currentText() + + # 获取启用的检验配置 + enabled_configs = self.inspection_manager.get_enabled_configs() + + # 检查是否有至少一个检验项有值 + has_any_value = False + for i, config in enumerate(enabled_configs): + col_index = 2 + i + item = self.process_table.item(row, col_index) + if item and item.text().strip(): + has_any_value = True + break + + # 如果有至少一个检验项有值,更新状态为inspected + if has_any_value: + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'inspected') + logging.info(f"工程号 {gc_note} 的检验已完成,状态更新为inspected") + + return has_any_value + except Exception as e: + logging.error(f"检查检验完成状态失败: {str(e)}") + return False \ No newline at end of file diff --git a/widgets/order_query_dialog.py b/widgets/order_query_dialog.py index 7e014a4..9f4c33d 100644 --- a/widgets/order_query_dialog.py +++ b/widgets/order_query_dialog.py @@ -27,7 +27,7 @@ class OrderQueryDialog(OrderQueryDialogUI): if hasattr(parent, 'corp_id'): self.corp_id = parent.corp_id if hasattr(parent, 'user_id'): - self.user_id = parent.user_id + self.user_id = parent.user_id[0] if hasattr(parent, 'user_name'): self.user_name = parent.user_name