From 2fb44a2a9b72f0111e68f08990281260b927640c Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 9 Feb 2026 13:17:25 +0800 Subject: [PATCH] feat: add material type management dialog with checkbox filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MaterialTypeManagementDialog for managing MaterialsTypeToBeDeleted table - Implement checkbox-based filtering by manager name with dynamic manager list - Support CRUD operations: create, read, update, delete with local caching - Add "类型管理" button in material validation tab to open the dialog - Include select all/deselect all functionality for manager filters - Track pending changes and show unsaved changes warning Co-Authored-By: Claude Sonnet 4.5 --- gui/material_type_management_dialog.py | 704 +++++++++++++++++++++++++ gui/material_validation_tab.py | 13 + 2 files changed, 717 insertions(+) create mode 100644 gui/material_type_management_dialog.py diff --git a/gui/material_type_management_dialog.py b/gui/material_type_management_dialog.py new file mode 100644 index 0000000..3b17d83 --- /dev/null +++ b/gui/material_type_management_dialog.py @@ -0,0 +1,704 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +物料类型管理对话框 + +提供对 MaterialsTypeToBeDeleted 表的管理功能: +- 查看所有记录 +- 按负责人筛选 +- 新增/删除/编辑记录 +- 保存更改到数据库 +""" + +import tkinter as tk +from tkinter import ttk, messagebox, simpledialog +from typing import List, Dict, Any, Optional, Tuple + + +class MaterialTypeManagementDialog(tk.Toplevel): + """物料类型管理对话框 + + 管理 MaterialsTypeToBeDeleted 表的 CRUD 操作。 + """ + + def __init__(self, parent, title: str = "类型管理"): + """初始化对话框 + + Args: + parent: 父窗口 + title: 窗口标题 + """ + super().__init__(parent) + self.title(title) + self.geometry("900x600") + + # 使窗口模态 + self.transient(parent) + self.grab_set() + + # 数据缓存 + self.original_data: List[Dict[str, Any]] = [] # 原始数据(从数据库加载) + self.current_data: List[Dict[str, Any]] = [] # 当前显示的数据(可能已筛选) + self.pending_changes: Dict[str, List[Dict[str, Any]]] = { + 'insert': [], # 待插入的记录 + 'delete': [], # 待删除的记录 + 'update': [] # 待更新的记录 (old, new) + } + + # 筛选相关 + self.managers: List[str] = [] # 所有的负责人列表 + self.manager_checkboxes: Dict[str, tk.BooleanVar] = {} # 负责人 -> Checkbox变量 + self.select_all_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框变量 + + # 创建界面 + self._create_widgets() + + # 加载数据 + self._load_data() + + # 等待窗口关闭 + self.wait_window() + + def _create_widgets(self): + """创建界面组件""" + # 主容器 + main_container = ttk.Frame(self, padding=10) + main_container.pack(fill=tk.BOTH, expand=True) + + # 顶部:筛选区域 + filter_frame = ttk.LabelFrame(main_container, text="筛选(按负责人)", padding=10) + filter_frame.pack(fill=tk.X, pady=(0, 10)) + + # 创建复选框容器(使用Canvas实现滚动) + canvas_container = ttk.Frame(filter_frame) + canvas_container.pack(fill=tk.BOTH, expand=True) + + # Canvas和滚动条 + self.filter_canvas = tk.Canvas(canvas_container, height=80) + scrollbar = ttk.Scrollbar(canvas_container, orient="vertical", command=self.filter_canvas.yview) + self.filter_frame = ttk.Frame(self.filter_canvas) + + self.filter_frame.bind( + "", + lambda e: self.filter_canvas.configure(scrollregion=self.filter_canvas.bbox("all")) + ) + + self.filter_canvas.create_window((0, 0), window=self.filter_frame, anchor="nw") + self.filter_canvas.configure(yscrollcommand=scrollbar.set) + + # 布局 + self.filter_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 鼠标滚轮支持 + def _on_mousewheel(event): + self.filter_canvas.yview_scroll(int(-1*(event.delta/120)), "units") + self.filter_canvas.bind("", _on_mousewheel) # Windows + + # 复选框将在加载数据后动态创建 + self._create_filter_checkboxes() + + # 快捷按钮 + button_frame = ttk.Frame(filter_frame) + button_frame.pack(fill=tk.X, pady=(5, 0)) + + ttk.Button( + button_frame, + text="全选", + command=self._select_all_managers + ).pack(side=tk.LEFT, padx=5) + + ttk.Button( + button_frame, + text="取消全选", + command=self._deselect_all_managers + ).pack(side=tk.LEFT, padx=5) + + # 中部:数据表格 + table_frame = ttk.LabelFrame(main_container, text="数据列表", padding=10) + table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + # 创建 Treeview + columns = ("material_name", "manager_name") + self.tree = ttk.Treeview( + table_frame, + columns=columns, + show="headings", + selectmode="extended" + ) + + # 设置列标题和宽度 + self.tree.heading("material_name", text="物料名称") + self.tree.heading("manager_name", text="负责人") + + self.tree.column("material_name", width=400) + self.tree.column("manager_name", width=200) + + # 添加滚动条 + scrollbar_y = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview) + scrollbar_x = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.tree.xview) + + self.tree.configure( + yscrollcommand=scrollbar_y.set, + xscrollcommand=scrollbar_x.set + ) + + # 布局 + self.tree.grid(row=0, column=0, sticky="nsew") + scrollbar_y.grid(row=0, column=1, sticky="ns") + scrollbar_x.grid(row=1, column=0, sticky="ew") + + table_frame.rowconfigure(0, weight=1) + table_frame.columnconfigure(0, weight=1) + + # 绑定双击事件用于编辑 + self.tree.bind("", self._on_item_double_click) + + # 底部:操作按钮 + button_frame = ttk.Frame(main_container) + button_frame.pack(fill=tk.X) + + ttk.Button( + button_frame, + text="新增", + command=self._add_record + ).pack(side=tk.LEFT, padx=5) + + ttk.Button( + button_frame, + text="删除", + command=self._delete_records + ).pack(side=tk.LEFT, padx=5) + + ttk.Button( + button_frame, + text="编辑", + command=self._edit_selected_record + ).pack(side=tk.LEFT, padx=5) + + ttk.Button( + button_frame, + text="刷新", + command=self._load_data + ).pack(side=tk.LEFT, padx=5) + + # 待保存提示 + self.status_label = ttk.Label(button_frame, text="") + self.status_label.pack(side=tk.LEFT, padx=20) + + # 右侧按钮 + right_frame = ttk.Frame(button_frame) + right_frame.pack(side=tk.RIGHT) + + ttk.Button( + right_frame, + text="保存", + command=self._save_changes + ).pack(side=tk.LEFT, padx=5) + + ttk.Button( + right_frame, + text="关闭", + command=self._close_dialog + ).pack(side=tk.LEFT, padx=5) + + def _create_filter_checkboxes(self): + """创建筛选复选框(在数据加载后调用)""" + # 清空现有复选框 + for widget in self.filter_frame.winfo_children(): + widget.destroy() + self.manager_checkboxes.clear() + + # 获取所有负责人并排序 + from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO + dao = MaterialsTypeToBeDeletedDAO() + self.managers = dao.get_managers() + + # 如果没有数据,稍后会在 _load_data 中重新创建 + if not self.managers: + ttk.Label(self.filter_frame, text="正在加载数据...").pack(anchor="w") + return + + # 计算列数(每列最多显示4个负责人) + items_per_column = 6 + num_columns = max(1, (len(self.managers) + items_per_column - 1) // items_per_column) + + # 创建全选复选框 + select_all_cb = ttk.Checkbutton( + self.filter_frame, + text="全选", + variable=self.select_all_var, + command=self._on_select_all_toggle + ) + select_all_cb.grid(row=0, column=0, sticky="w", padx=5, pady=2) + + # 为每个负责人创建复选框 + for idx, manager in enumerate(self.managers): + var = tk.BooleanVar(value=True) + self.manager_checkboxes[manager] = var + + row = (idx // items_per_column) + 1 + col = idx % items_per_column + + cb = ttk.Checkbutton( + self.filter_frame, + text=manager, + variable=var, + command=self._on_manager_checkbox_change + ) + cb.grid(row=row, column=col, sticky="w", padx=5, pady=2) + + def _on_select_all_toggle(self): + """全选复选框状态改变""" + select_all = self.select_all_var.get() + for var in self.manager_checkboxes.values(): + var.set(select_all) + self._apply_filter() + + def _on_manager_checkbox_change(self): + """单个负责人复选框状态改变""" + # 检查是否所有负责人都被选中 + all_selected = all(var.get() for var in self.manager_checkboxes.values()) + self.select_all_var.set(all_selected) + self._apply_filter() + + def _select_all_managers(self): + """全选所有负责人""" + self.select_all_var.set(True) + for var in self.manager_checkboxes.values(): + var.set(True) + self._apply_filter() + + def _deselect_all_managers(self): + """取消全选""" + self.select_all_var.set(False) + for var in self.manager_checkboxes.values(): + var.set(False) + self._apply_filter() + + def _get_selected_managers(self) -> List[str]: + """获取选中的负责人列表""" + return [ + manager for manager, var in self.manager_checkboxes.items() + if var.get() + ] + + def _load_data(self): + """从数据库加载数据""" + try: + from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO + + dao = MaterialsTypeToBeDeletedDAO() + self.original_data = dao.get_all_materials() + self.current_data = list(self.original_data) + + # 清空待保存的更改 + self.pending_changes = {'insert': [], 'delete': [], 'update': []} + + # 重新创建筛选复选框(负责人列表可能变化) + self._create_filter_checkboxes() + + # 刷新显示 + self._apply_filter() # 使用 _apply_filter 而不是 _refresh_tree + self._update_status() + + except Exception as e: + messagebox.showerror("错误", f"加载数据失败:{str(e)}") + + def _refresh_tree(self): + """刷新表格显示""" + # 清空表格 + for item in self.tree.get_children(): + self.tree.delete(item) + + # 填充数据 + for record in self.current_data: + self.tree.insert( + "", + tk.END, + values=( + record.get('MaterialName', ''), + record.get('ManagerName', '') + ) + ) + + def _apply_filter(self): + """应用筛选条件(基于选中的负责人复选框)""" + selected_managers = self._get_selected_managers() + + if not selected_managers: + # 没有选中任何负责人,显示空表格 + self.current_data = [] + else: + # 获取当前有效数据(原始数据减去已删除的,加上新插入的) + deleted_set = { + (r['MaterialName'], r['ManagerName']) + for r in self.pending_changes['delete'] + } + inserted = [ + r for r in self.pending_changes['insert'] + if (r['MaterialName'], r['ManagerName']) not in deleted_set + ] + + base_data = [ + r for r in self.original_data + if (r['MaterialName'], r['ManagerName']) not in deleted_set + ] + inserted + + # 按选中的负责人筛选 + self.current_data = [ + r for r in base_data + if r.get('ManagerName') in selected_managers + ] + + self._refresh_tree() + + def _add_record(self): + """添加新记录""" + # 弹出输入对话框 + dialog = tk.Toplevel(self) + dialog.title("新增记录") + dialog.geometry("400x200") + dialog.transient(self) + dialog.grab_set() + + # 物料名称 + ttk.Label(dialog, text="物料名称:").pack(pady=5) + material_entry = ttk.Entry(dialog, width=40) + material_entry.pack(pady=5) + + # 负责人 + ttk.Label(dialog, text="负责人:").pack(pady=5) + manager_entry = ttk.Entry(dialog, width=40) + manager_entry.pack(pady=5) + + # 按钮框架 + button_frame = ttk.Frame(dialog) + button_frame.pack(pady=20) + + result = {'material': '', 'manager': '', 'confirmed': False} + + def on_confirm(): + material = material_entry.get().strip() + manager = manager_entry.get().strip() + + if not material: + messagebox.showwarning("警告", "请输入物料名称") + return + if not manager: + messagebox.showwarning("警告", "请输入负责人") + return + + result['material'] = material + result['manager'] = manager + result['confirmed'] = True + dialog.destroy() + + def on_cancel(): + dialog.destroy() + + ttk.Button(button_frame, text="确定", command=on_confirm).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="取消", command=on_cancel).pack(side=tk.LEFT, padx=5) + + # 设置焦点到第一个输入框 + material_entry.focus_set() + + # 等待对话框关闭 + dialog.wait_window() + + if result['confirmed']: + # 检查是否已存在 + for record in self.current_data: + if (record['MaterialName'] == result['material'] and + record['ManagerName'] == result['manager']): + messagebox.showinfo("提示", "该记录已存在") + return + + # 添加到待插入列表 + new_record = { + 'MaterialName': result['material'], + 'ManagerName': result['manager'] + } + self.pending_changes['insert'].append(new_record) + + # 立即显示在表格中 + self.current_data.append(new_record) + self._refresh_tree() + self._update_status() + + def _delete_records(self): + """删除选中的记录""" + selection = self.tree.selection() + if not selection: + messagebox.showwarning("警告", "请先选择要删除的记录") + return + + if not messagebox.askyesno("确认", f"确定要删除选中的 {len(selection)} 条记录吗?"): + return + + indices_to_delete = [] + for item in selection: + values = self.tree.item(item, 'values') + material_name = values[0] + manager_name = values[1] + + # 查找在 current_data 中的索引 + for idx, record in enumerate(self.current_data): + if (record['MaterialName'] == material_name and + record['ManagerName'] == manager_name): + indices_to_delete.append(idx) + + # 检查是否是新插入的记录 + is_new_insert = any( + r['MaterialName'] == material_name and r['ManagerName'] == manager_name + for r in self.pending_changes['insert'] + ) + + if is_new_insert: + # 从待插入列表中移除 + self.pending_changes['insert'] = [ + r for r in self.pending_changes['insert'] + if not (r['MaterialName'] == material_name and + r['ManagerName'] == manager_name) + ] + else: + # 添加到待删除列表 + self.pending_changes['delete'].append({ + 'MaterialName': material_name, + 'ManagerName': manager_name + }) + break + + # 从 current_data 中删除(倒序删除,避免索引问题) + for idx in sorted(indices_to_delete, reverse=True): + del self.current_data[idx] + + self._refresh_tree() + self._update_status() + + def _edit_selected_record(self): + """编辑选中的记录""" + selection = self.tree.selection() + if not selection: + messagebox.showwarning("警告", "请先选择要编辑的记录") + return + + if len(selection) > 1: + messagebox.showwarning("警告", "请只选择一条记录进行编辑") + return + + item = selection[0] + values = self.tree.item(item, 'values') + old_material = values[0] + old_manager = values[1] + + # 弹出编辑对话框 + dialog = tk.Toplevel(self) + dialog.title("编辑记录") + dialog.geometry("400x200") + dialog.transient(self) + dialog.grab_set() + + # 物料名称 + ttk.Label(dialog, text="物料名称:").pack(pady=5) + material_entry = ttk.Entry(dialog, width=40) + material_entry.insert(0, old_material) + material_entry.pack(pady=5) + + # 负责人 + ttk.Label(dialog, text="负责人:").pack(pady=5) + manager_entry = ttk.Entry(dialog, width=40) + manager_entry.insert(0, old_manager) + manager_entry.pack(pady=5) + + # 按钮框架 + button_frame = ttk.Frame(dialog) + button_frame.pack(pady=20) + + result = {'material': old_material, 'manager': old_manager, 'confirmed': False} + + def on_confirm(): + material = material_entry.get().strip() + manager = manager_entry.get().strip() + + if not material: + messagebox.showwarning("警告", "物料名称不能为空") + return + if not manager: + messagebox.showwarning("警告", "负责人不能为空") + return + + result['material'] = material + result['manager'] = manager + result['confirmed'] = True + dialog.destroy() + + def on_cancel(): + dialog.destroy() + + ttk.Button(button_frame, text="确定", command=on_confirm).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="取消", command=on_cancel).pack(side=tk.LEFT, padx=5) + + # 等待对话框关闭 + dialog.wait_window() + + if result['confirmed']: + new_material = result['material'] + new_manager = result['manager'] + + # 如果值没有变化,不处理 + if new_material == old_material and new_manager == old_manager: + return + + # 更新 current_data + for record in self.current_data: + if (record['MaterialName'] == old_material and + record['ManagerName'] == old_manager): + record['MaterialName'] = new_material + record['ManagerName'] = new_manager + break + + # 添加到待更新列表 + self.pending_changes['update'].append({ + 'old': {'MaterialName': old_material, 'ManagerName': old_manager}, + 'new': {'MaterialName': new_material, 'ManagerName': new_manager} + }) + + self._refresh_tree() + self._update_status() + + def _on_item_double_click(self, event): + """处理双击事件,编辑记录""" + self._edit_selected_record() + + def _save_changes(self): + """保存更改到数据库""" + total_changes = ( + len(self.pending_changes['insert']) + + len(self.pending_changes['delete']) + + len(self.pending_changes['update']) + ) + + if total_changes == 0: + messagebox.showinfo("提示", "没有需要保存的更改") + return + + # 确认保存 + msg_parts = [] + if self.pending_changes['insert']: + msg_parts.append(f"新增 {len(self.pending_changes['insert'])} 条") + if self.pending_changes['delete']: + msg_parts.append(f"删除 {len(self.pending_changes['delete'])} 条") + if self.pending_changes['update']: + msg_parts.append(f"更新 {len(self.pending_changes['update'])} 条") + + if not messagebox.askyesno( + "确认保存", + "确定要将以下更改保存到数据库吗?\n\n" + "\n".join(msg_parts) + ): + return + + try: + from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO + + dao = MaterialsTypeToBeDeletedDAO() + + stats = { + 'insert_success': 0, + 'insert_failed': 0, + 'delete_success': 0, + 'delete_failed': 0, + 'update_success': 0, + 'update_failed': 0 + } + + # 执行插入 + for record in self.pending_changes['insert']: + if dao.insert_material(record['MaterialName'], record['ManagerName']): + stats['insert_success'] += 1 + else: + stats['insert_failed'] += 1 + + # 执行删除 + for record in self.pending_changes['delete']: + if dao.delete_material(record['MaterialName'], record['ManagerName']): + stats['delete_success'] += 1 + else: + stats['delete_failed'] += 1 + + # 执行更新 + for update in self.pending_changes['update']: + old = update['old'] + new = update['new'] + # 先删除旧记录,再插入新记录 + if dao.delete_material(old['MaterialName'], old['ManagerName']): + if dao.insert_material(new['MaterialName'], new['ManagerName']): + stats['update_success'] += 1 + else: + # 删除成功但插入失败,尝试回滚 + dao.insert_material(old['MaterialName'], old['ManagerName']) + stats['update_failed'] += 1 + else: + stats['update_failed'] += 1 + + # 显示结果 + result_parts = [] + if stats['insert_success'] > 0: + result_parts.append(f"新增成功:{stats['insert_success']} 条") + if stats['insert_failed'] > 0: + result_parts.append(f"新增失败:{stats['insert_failed']} 条") + if stats['delete_success'] > 0: + result_parts.append(f"删除成功:{stats['delete_success']} 条") + if stats['delete_failed'] > 0: + result_parts.append(f"删除失败:{stats['delete_failed']} 条") + if stats['update_success'] > 0: + result_parts.append(f"更新成功:{stats['update_success']} 条") + if stats['update_failed'] > 0: + result_parts.append(f"更新失败:{stats['update_failed']} 条") + + result_msg = "保存完成!\n\n" + "\n".join(result_parts) + + if (stats['insert_failed'] + stats['delete_failed'] + stats['update_failed']) > 0: + messagebox.showwarning("完成(部分失败)", result_msg) + else: + messagebox.showinfo("成功", result_msg) + + # 重新加载数据 + self._load_data() + + except Exception as e: + messagebox.showerror("错误", f"保存失败:{str(e)}") + + def _update_status(self): + """更新状态标签""" + total_changes = ( + len(self.pending_changes['insert']) + + len(self.pending_changes['delete']) + + len(self.pending_changes['update']) + ) + + if total_changes > 0: + self.status_label.config( + text=f"有 {total_changes} 项待保存的更改", + foreground="red" + ) + else: + self.status_label.config(text="") + + def _close_dialog(self): + """关闭对话框""" + # 检查是否有未保存的更改 + total_changes = ( + len(self.pending_changes['insert']) + + len(self.pending_changes['delete']) + + len(self.pending_changes['update']) + ) + + if total_changes > 0: + if not messagebox.askyesno( + "警告", + f"有 {total_changes} 项未保存的更改,确定要关闭吗?" + ): + return + + self.destroy() diff --git a/gui/material_validation_tab.py b/gui/material_validation_tab.py index ec1473e..25884b5 100644 --- a/gui/material_validation_tab.py +++ b/gui/material_validation_tab.py @@ -18,6 +18,7 @@ from io import StringIO from contextlib import redirect_stdout from gui.widgets import FileSelector, LogText from gui.config_manager import ConfigManager +from gui.material_type_management_dialog import MaterialTypeManagementDialog import pandas as pd @@ -287,6 +288,14 @@ class MaterialValidationTab(ttk.Frame): ) self.export_button.pack(side=tk.LEFT, padx=5) + # 类型管理按钮 + self.type_management_button = ttk.Button( + button_frame, + text="类型管理", + command=self.open_type_management + ) + self.type_management_button.pack(side=tk.LEFT, padx=5) + def _create_result_table(self, parent): """创建结果表格""" # 新列定义:选择、材料名称、材料代码、规格、型号、负责人 @@ -802,3 +811,7 @@ class MaterialValidationTab(ttk.Frame): except Exception as e: messagebox.showerror("错误", f"导出失败:{str(e)}") self.log_text.error(f"导出失败:{str(e)}") + + def open_type_management(self): + """打开类型管理窗口""" + dialog = MaterialTypeManagementDialog(self, title="物料类型管理")