diff --git a/gui/material_validation_tab.py b/gui/material_validation_tab.py index 9210a96..ec1473e 100644 --- a/gui/material_validation_tab.py +++ b/gui/material_validation_tab.py @@ -29,16 +29,18 @@ class CheckboxTreeview(ttk.Treeview): - ☑ 选中 """ - def __init__(self, parent, **kwargs): + def __init__(self, parent, on_checkbox_change=None, **kwargs): """初始化 CheckboxTreeview Args: parent: 父容器 + on_checkbox_change: checkbox 状态改变时的回调函数 **kwargs: 传递给 Treeview 的参数 """ super().__init__(parent, **kwargs) self.checkboxes = {} # item_id -> bool self.checkbox_column = "选择" + self.on_checkbox_change = on_checkbox_change # checkbox 状态改变回调 # 绑定点击事件 self.bind("", self._on_click) @@ -56,7 +58,13 @@ class CheckboxTreeview(ttk.Treeview): if column == "#1" and item: # 切换 checkbox 状态 current_state = self.checkboxes.get(item, False) - self.set_checked(item, not current_state) + new_state = not current_state + self.set_checked(item, new_state) + + # 通知父组件 checkbox 状态已改变 + if self.on_checkbox_change: + self.on_checkbox_change(item, new_state) + return "break" # 阻止默认行为 def set_checked(self, item, checked: bool): @@ -283,7 +291,13 @@ class MaterialValidationTab(ttk.Frame): """创建结果表格""" # 新列定义:选择、材料名称、材料代码、规格、型号、负责人 columns = ("选择", "材料名称", "材料代码", "规格", "型号", "负责人") - self.tree = CheckboxTreeview(parent, columns=columns, show="headings", height=10) + self.tree = CheckboxTreeview( + parent, + columns=columns, + show="headings", + height=10, + on_checkbox_change=self._sync_checkbox_by_material_code + ) # 设置列标题和宽度 self.tree.heading("选择", text="选择") @@ -369,6 +383,38 @@ class MaterialValidationTab(ttk.Frame): self.log_text.info(f"已更新负责人: {current_value} -> {new_value}") + def _sync_checkbox_by_material_code(self, changed_item: str, new_state: bool): + """同步相同材料代码的所有记录的选择状态 + + 当用户点击某个记录的 checkbox 时,自动将所有具有相同材料代码的记录 + 的 checkbox 状态同步更新。 + + Args: + changed_item: 被点击的 item ID + new_state: 新的 checkbox 状态 + """ + # 获取被点击行的材料代码 + values = self.tree.item(changed_item, "values") + if not values or len(values) <= 2: + return + material_code = values[2] # 材料代码在第3列(索引2) + + # 同步所有具有相同材料代码的记录 + synced_count = 0 + for item in self.tree.get_children(): + item_values = self.tree.item(item, "values") + if item_values and len(item_values) > 2: + if item_values[2] == material_code and item != changed_item: + # 只更新状态不同的行,避免重复更新 + current_state = self.tree.checkboxes.get(item, False) + if current_state != new_state: + self.tree.set_checked(item, new_state) + synced_count += 1 + + if synced_count > 0: + action = "选中" if new_state else "取消选中" + self.log_text.info(f"已同步 {synced_count} 条相同材料代码的记录{action}") + def _select_all(self, check: bool = True): """全选或取消全选 @@ -558,87 +604,136 @@ class MaterialValidationTab(ttk.Frame): self._update_log(f"加载结果失败:{str(e)}", "ERROR") def confirm_deletion(self): - """确认删除主流程""" - # 获取选中的 item - checked_items = self.tree.get_checked_items() + """确认删除主流程 - if not checked_items: - messagebox.showwarning("警告", "请先选择要删除的记录") + 处理所有行: + - 已勾选的行:写入/更新到数据库(负责人不能为空) + - 未勾选的行:从数据库中删除 + """ + if not self.tree.get_children(): + messagebox.showwarning("警告", "没有可处理的数据") return - # 收集数据 - materials = [] - missing_manager = [] + # 收集所有数据 + to_upsert = [] # 需要写入/更新的记录 + to_delete = [] # 需要删除的记录 + missing_manager = [] # 缺少负责人的记录 - for item in checked_items: + for item in self.tree.get_children(): values = self.tree.item(item, "values") + is_checked = self.tree.checkboxes.get(item, False) material_code = values[2] if len(values) > 2 else "" manager_name = values[5] if len(values) > 5 else "" + # 跳过材料代码为空的行 if not material_code or not material_code.strip(): continue - if not manager_name or not manager_name.strip(): - missing_manager.append(material_code) - else: - materials.append({ - "material_code": material_code.strip(), - "manager_name": manager_name.strip() - }) + material_code = material_code.strip() - # 验证负责人 + if is_checked: + # 已勾选:需要写入/更新 + if not manager_name or not manager_name.strip(): + missing_manager.append(material_code) + else: + to_upsert.append({ + "material_code": material_code, + "manager_name": manager_name.strip() + }) + else: + # 未勾选:需要删除 + to_delete.append(material_code) + + # 验证:已勾选的记录必须有负责人 if missing_manager: - msg = f"以下选中的记录缺少负责人信息,无法删除:\n\n" + \ - "\n".join(missing_manager[:10]) # 最多显示10个 + msg = f"以下已勾选的记录缺少负责人信息,无法保存:\n\n" + \ + "\n".join(missing_manager[:10]) if len(missing_manager) > 10: msg += f"\n... 共 {len(missing_manager)} 条" messagebox.showwarning("警告", msg) return - if not materials: - messagebox.showwarning("警告", "没有有效的记录可删除") + # 构建确认消息 + confirm_parts = [] + if to_upsert: + confirm_parts.append(f"写入/更新 {len(to_upsert)} 条记录") + if to_delete: + confirm_parts.append(f"删除 {len(to_delete)} 条记录") + + if not to_upsert and not to_delete: + messagebox.showinfo("提示", "没有需要处理的记录") return - # 显示确认对话框 - confirm_msg = f"确认要将 {len(materials)} 条记录写入删除表吗?\n\n" - confirm_msg += "这些记录将被标记为待删除状态。" - - if not messagebox.askyesno("确认删除", confirm_msg): + confirm_msg = "确认以下操作吗?\n\n" + "\n".join(confirm_parts) + if not messagebox.askyesno("确认操作", confirm_msg): return - # 执行删除操作(后台线程) - self._update_log(f"开始处理 {len(materials)} 条记录...", "INFO") + # 在后台线程中执行 + self._update_log(f"开始处理:写入/更新 {len(to_upsert)} 条,删除 {len(to_delete)} 条...", "INFO") deletion_thread = threading.Thread( - target=self._execute_deletion_in_background, - args=(materials,), + target=self._execute_sync_in_background, + args=(to_upsert, to_delete), daemon=True, ) deletion_thread.start() - def _execute_deletion_in_background(self, materials: list): - """后台线程执行删除操作""" + def _execute_sync_in_background(self, to_upsert: list, to_delete: list): + """后台线程执行同步操作 + + Args: + to_upsert: 需要写入/更新的记录列表 + to_delete: 需要删除的材料代码列表 + """ try: from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO dao = MaterialsToBeDeletedDAO() - stats = dao.upsert_batch(materials) - self.after(0, lambda: self._deletion_complete(stats)) + # 执行写入/更新操作 + upsert_stats = {'total': 0, 'success': 0, 'failed': 0} + if to_upsert: + upsert_stats = dao.upsert_batch(to_upsert) + + # 执行删除操作 + delete_count = 0 + if to_delete: + delete_count = dao.delete_by_material_codes(to_delete) + + # 组合统计信息 + combined_stats = { + 'upsert_success': upsert_stats['success'], + 'upsert_failed': upsert_stats['failed'], + 'deleted': delete_count + } + + self.after(0, lambda: self._sync_complete(combined_stats)) except Exception as e: - self.after(0, lambda: self._update_log(f"删除操作失败:{str(e)}", "ERROR")) + self.after(0, lambda: self._update_log(f"同步操作失败:{str(e)}", "ERROR")) - def _deletion_complete(self, stats: dict): - """删除完成回调,更新 UI""" - total = stats.get('total', 0) - success = stats.get('success', 0) - failed = stats.get('failed', 0) + def _sync_complete(self, stats: dict): + """同步完成回调,更新 UI - msg = f"删除操作完成!\n\n总计:{total} 条\n成功:{success} 条\n失败:{failed} 条" + Args: + stats: 包含 upsert_success, upsert_failed, deleted 的字典 + """ + upsert_success = stats.get('upsert_success', 0) + upsert_failed = stats.get('upsert_failed', 0) + deleted = stats.get('deleted', 0) - if failed > 0: - messagebox.showwarning("完成", msg) + msg_parts = [] + if upsert_success > 0: + msg_parts.append(f"写入/更新成功:{upsert_success} 条") + if upsert_failed > 0: + msg_parts.append(f"写入/更新失败:{upsert_failed} 条") + if deleted > 0: + msg_parts.append(f"删除成功:{deleted} 条") + + msg = "同步操作完成!\n\n" + "\n".join(msg_parts) + + if upsert_failed > 0: + messagebox.showwarning("完成(部分失败)", msg) self._update_log(msg, "WARNING") else: messagebox.showinfo("成功", msg)