diff --git a/docs/entity_picker_standard.md b/docs/entity_picker_standard.md index 88ec9431..b8d3db8c 100644 --- a/docs/entity_picker_standard.md +++ b/docs/entity_picker_standard.md @@ -109,16 +109,24 @@ | 常量 | valueField | 附加行为 | 使用点 | |------|-----------|---------|--------| -| `DEPT_TREE_GROUP` | —(公共 treeGroup) | 部门树分组 + 未分配部门兜底 | 下列两处 + `UserRoleDepartment.UserRoleUUID` | -| `DEPT_EMPLOYEE_PICKER` | `UserRoleDepartmentUUID` | 单选 | `EmployeeProfile.UserRoleDepartmentUUID`(eHRM 员工档案) | +| `DEPT_TREE_GROUP` | —(公共 treeGroup) | 部门树分组 + 未分配部门兜底 | 被下面两个常量引用(勿单独就地使用) | +| `DEPT_EMPLOYEE_PICKER` | `UserRoleDepartmentUUID` | 单选 | `EmployeeProfile.UserRoleDepartmentUUID`(eHRM 员工档案)、`UserRoleDepartment.UserRoleDepartmentUUID`(部门新增成员,虚拟列) | | `DEPT_EMPLOYEE_PICKER_MULTI` | `UserRoleDepartmentUUID` | 多选暂存 + 成员/负责人类型 | `UserRoleWorkgroup.UserRoleDepartmentUUID`(工作组新增成员,虚拟列) | -**两个使用点取值一致**(都是「部门员工关联」= 部门 + 该用户在本公司的用户-角色对), -区别只在是否多选暂存;落库时由 `FormService._ResolveDeptEmployee()` **统一解析**(唯一实现): +**三个使用点取值一致**(都是「部门员工关联」= 部门 + 该用户在本公司的用户-角色对), +区别只在单/多选;落库时由 `FormService._ResolveDeptEmployee()` **统一解析**(唯一实现): - `EmployeeProfile`:取 `UserUUID` / `DeptUUID` / `DeptName` 三列落库(常量 `_PROFILE_DEPT_EMPLOYEE_KEYS` 声明) - `UserRoleWorkgroup`:取 `UserRoleUUID` 落库到关系列(关系表只存用户-角色对,**部门仅用于选择/分组展示**); 表单里该关系列 `hideInForm`,由虚拟列 `UserRoleDepartmentUUID` 作为唯一人员选择入口 +- `UserRoleDepartment`:取 `UserRoleUUID` 落库;`DeptUUID` **表单上下文优先**(部门详情页 = 当前部门), + 无上下文时取选择器解析出的部门(员工详情页新增"部门职务"即此场景); + 员工详情页上下文里 `UserUUID` 由 query 注入,选择器选到别的员工会被后端拒绝 + (`所选「部门员工关联」不属于当前员工`)。两张关联表的 `UserUUID`/`RoleUUID` 列一律 + `editable=False`(纯列表展示),人员输入只有选择器一个入口 + +> 已知取舍:选择器数据源是 `UserRoleDepartment`,因此**尚未挂靠任何部门的"用户-角色对" +> 选不到**。公司员工列表(`UserRole`)新增/编辑时若填了部门会同步建主关联,正常路径不受影响。 **唯一性约束**:员工档案一人一份 —— 新增与改绑均由 `form_service.add_record/modify_record` 拦截 (前端在已有档案时隐藏新增入口)。 diff --git a/gvsds/apps/gerp/form_service.py b/gvsds/apps/gerp/form_service.py index f07ac3d6..cd2da8c5 100644 --- a/gvsds/apps/gerp/form_service.py +++ b/gvsds/apps/gerp/form_service.py @@ -124,7 +124,8 @@ def create_tenant_admin_role(tenant): # (落库时再解析出 UserUUID / DeptUUID / DeptName,一人一份档案) # 2) UserRoleWorkgroup.UserRoleDepartmentUUID(工作组新增成员):多选暂存批量新增, # 落库时解析出 UserRoleUUID(关系表只存"用户-角色对",部门仅用于选择/分组展示) -# 3) UserRoleDepartment.UserRoleUUID(部门挂靠,表单隐藏):仅复用 DEPT_TREE_GROUP +# 3) UserRoleDepartment.UserRoleDepartmentUUID(部门新增成员):单选, +# 落库时解析出 UserRoleUUID(部门取表单上下文注入的 DeptUUID,选择器的部门只用于分组) #: 「按部门树分组」浏览面板的公共 treeGroup 配置(EntityPicker): #: 下拉里先展示部门树、点部门看该部门成员,未挂部门的归入 ungroupedTitle @@ -704,14 +705,16 @@ class FormService(FormServiceBase): if table_name == 'UserRole' and pascal_name == 'CompanyUUID': col['visible'] = False # UserRoleDepartment 关联表冗余字段:仅用于隔离过滤与上下文注入。 - # DeptUUID 列表展示部门(回显部门名),表单可编辑(无 query 注入时(如员工详情页 - # 新增成员)用户自选部门;详情页场景由 query 注入自动从表单排除);TenantUUID/CompanyUUID - # 始终内部注入,表单隐藏 + # DeptUUID 列表展示部门(回显部门名),表单隐藏 —— 人员输入统一走 + # "部门员工关联"选择器:部门详情页由 query 注入当前部门,员工详情页则取 + # 选择器解析出的部门(add_record 里 DeptUUID 表单上下文优先); + # TenantUUID/CompanyUUID 始终内部注入,表单隐藏 if table_name == 'UserRoleDepartment' and pascal_name == 'DeptUUID': col['title'] = '部门' col['type'] = 'entityPicker' col['entityPicker'] = {**self.ENTITY_PICKER_MAP['Department'], 'placeholder': '选择部门'} - col['editable'] = True + col['editable'] = False + col['hideInForm'] = True col['visible'] = True if table_name == 'UserRoleDepartment' and pascal_name in ('TenantUUID', 'CompanyUUID'): col['editable'] = False @@ -728,29 +731,14 @@ class FormService(FormServiceBase): col['type'] = 'entityPicker' col['entityPicker'] = {**self.ENTITY_PICKER_MAP['PositionCategory'], 'placeholder': '选择绑定的通用部门'} col['editable'] = True - # UserRoleDepartment 的 UserRoleUUID:仅列表展示"角色名(用户名)", - # 表单隐藏——改用下方两级级联"用户→角色"选择 - # (UserRoleWorkgroup 的 UserRoleUUID 同为其关系列,表单改由虚拟列 - # UserRoleDepartmentUUID 的"部门员工关联"选择器承担,见该表的虚拟列配置) - if table_name == 'UserRoleDepartment' and pascal_name == 'UserRoleUUID': - col['type'] = 'entityPicker' - # 复用公共部门树分组配置(取值仍是 UserRoleUUID,数据源为 UserRoleSearch) - col['entityPicker'] = { - 'api': '/API/FormAPI/UserRoleSearch', - 'labelField': 'UserName', - 'secondaryField': 'RoleName', - 'valueField': 'UserRoleUUID', - 'placeholder': '选择部门员工关联(按部门分组展示)', - 'treeGroup': dict(DEPT_TREE_GROUP), - } - col['editable'] = True - col['hideInForm'] = True - col['entityPicker'] = {**col['entityPicker'], 'dependCompany': True} - # UserRoleDepartment 列表可读性:UserRoleUUID 关联列列表隐藏(表单仍隐藏), - # 用户/角色由下方虚拟列(UserUUID/RoleUUID)分开展示(值经 get_table 反查 UserRole 注入); + # UserRoleDepartment 列表可读性:UserRoleUUID 是"用户-角色对"外键列,列表与表单 + # 都隐藏 —— 用户/角色由下方虚拟列(UserUUID/RoleUUID)分开展示(值经 get_table + # 反查 UserRole 注入),人员输入由虚拟列 UserRoleDepartmentUUID 的"部门员工关联" + # 选择器承担(原先就地写的 UserRoleSearch 选择器已删除,避免同一语义两套配置); # PositionUUID 列标题改"职务"(值为职务名,由 _EnrichEntityLabels 回显) if table_name == 'UserRoleDepartment' and pascal_name == 'UserRoleUUID': col['visible'] = False + col['hideInForm'] = True if table_name == 'UserRoleDepartment' and pascal_name == 'PositionUUID': col['title'] = '职务' # UserRoleWorkgroup 列表可读性:与 UserRoleDepartment 对齐—— @@ -1042,13 +1030,12 @@ class FormService(FormServiceBase): columns = columns[:_idx + 1] + [_dept_col, _pos_col] + columns[_idx + 1:] else: columns = columns + [_dept_col, _pos_col] - # 虚拟列"用户/角色":UserRoleDepartment 承担新增表单两级级联(用户选好后, - # 角色只显示该用户已绑定且属于目标公司的角色,RoleSearch 级联过滤;提交时后端 - # 从 (用户, 角色) 解析出 UserRole 关联写入关联表,不重复新增 UserRole); - # UserRoleWorkgroup 列表同样拆分展示用户/角色(值经 get_table 反查 UserRole 注入), - # 其新增表单用 UserRoleDepartmentUUID"部门员工关联"选择器(见下方虚拟列) + # 虚拟列"用户/角色":两张关联表都要把"用户-角色对"拆成两列展示 + # (值经 get_table 反查 UserRole 注入,关联表本身只存 UserRoleUUID)。 + # 这两列只用于**列表展示**(editable=False → 任何表单都不出现), + # 人员输入统一走 UserRoleDepartmentUUID"部门员工关联"选择器(见下方虚拟列): + # 部门新增成员与工作组新增成员同口径 if table_name in ('UserRoleDepartment', 'UserRoleWorkgroup'): - is_ur_dept = table_name == 'UserRoleDepartment' virtual_cols = [ { 'field': 'UserUUID', @@ -1056,12 +1043,11 @@ class FormService(FormServiceBase): 'titleEn': 'UserUUID', 'type': 'entityPicker', 'entityPicker': {**self.ENTITY_PICKER_MAP['User'], 'placeholder': '用户'}, - 'editable': is_ur_dept, + 'editable': False, 'sortable': False, 'filterable': False, - 'required': is_ur_dept, + 'required': False, 'visible': True, # 列表展示"用户"(值经 get_table 反查注入) - **({'hideInEditForm': True} if is_ur_dept else {}), # 级联列仅新增表单显示 }, { 'field': 'RoleUUID', @@ -1074,12 +1060,11 @@ class FormService(FormServiceBase): 'dependCompany': True, 'placeholder': '该用户绑定的角色', }, - 'editable': is_ur_dept, + 'editable': False, 'sortable': False, 'filterable': False, - 'required': is_ur_dept, + 'required': False, 'visible': True, # 列表展示"角色"(值经 get_table 反查注入) - **({'hideInEditForm': True} if is_ur_dept else {}), # 级联列仅新增表单显示 }, { # 角色覆盖名(UserRole.RoleName):与"角色"分列展示,仅列表可见 @@ -1111,23 +1096,28 @@ class FormService(FormServiceBase): 'visible': True, 'multiValue': True, # 前端多选:值/回显为逗号分隔的多个职务 }) - # UserRoleWorkgroup 的"部门员工关联"虚拟列(表单里唯一的人员选择入口): - # 选一条"部门 + 用户-角色对"的关联,落库时后端解析出 UserRoleUUID - # (关系表只存用户-角色对;与 eHRM 员工档案共用 DEPT_EMPLOYEE_PICKER* 单一来源) - if table_name == 'UserRoleWorkgroup': - virtual_cols.append({ - 'field': 'UserRoleDepartmentUUID', - 'title': '部门员工关联', - 'titleEn': 'UserRoleDepartmentUUID', - 'type': 'entityPicker', - 'entityPicker': dict(DEPT_EMPLOYEE_PICKER_MULTI), - 'editable': True, - 'sortable': False, - 'filterable': False, - # 不设 required:新增缺省校验由后端 add_record 内做(提示更精确) - 'required': False, - 'visible': False, # 仅表单可见(列表已拆成"用户/角色"两列展示) - }) + # "部门员工关联"虚拟列(表单里唯一的人员选择入口):选一条"部门 + 用户-角色对" + # 的关联,落库时后端解析出 UserRoleUUID(关系表只存用户-角色对;部门仅用于分组 + # 展示,或由表单上下文注入的 DeptUUID 决定)。与 eHRM 员工档案共用 + # DEPT_EMPLOYEE_PICKER* 单一来源,两张关联表只在"是否多选暂存"上不同: + # UserRoleWorkgroup —— 多选暂存,一次批量加入工作组 + # UserRoleDepartment —— 单选,一次新增一名部门成员 + virtual_cols.append({ + 'field': 'UserRoleDepartmentUUID', + 'title': '部门员工关联', + 'titleEn': 'UserRoleDepartmentUUID', + 'type': 'entityPicker', + 'entityPicker': dict( + DEPT_EMPLOYEE_PICKER_MULTI if table_name == 'UserRoleWorkgroup' + else DEPT_EMPLOYEE_PICKER + ), + 'editable': True, + 'sortable': False, + 'filterable': False, + # 不设 required:新增缺省校验由后端 add_record 内做(提示更精确) + 'required': False, + 'visible': False, # 仅表单可见(列表已拆成"用户/角色"两列展示) + }) columns = virtual_cols + columns # EmployeeProfile(员工档案)虚拟列"部门员工关联":直接选一条用户-角色-部门关系 # (UserRoleDepartment),选中即同时确定员工(UserUUID)与部门(DeptUUID/DeptName); @@ -1348,6 +1338,19 @@ class FormService(FormServiceBase): r['UserRoleUUID_label'] = urw_info[_uid]['RoleName'] r['UserRoleUUID_UserName'] = urw_info[_uid]['UserName'] r['RoleNameCover'] = urw_info[_uid]['RoleNameCover'] + # UserRoleDepartment 编辑表单"部门员工关联"选择器回显:本行的 + # UserRoleDepartmentUUID 就是取值本身,标签与选择器选项格式一致 + # (DeptEmployeeSearch 的 DeptEmployeeLabel = "部门名 · 用户名") + if table_name == 'UserRoleDepartment': + for r in records: + _urd_key = r.get('UserRoleDepartmentUUID') + if not _urd_key: + continue + _dn = r.get('DeptUUID_label') or '' + _un = r.get('UserUUID_label') or '' + _rd_label = f'{_dn} · {_un}' if (_dn and _un) else (_dn or _un) + if _rd_label: + r['UserRoleDepartmentUUID_label'] = _rd_label # UserRoleDepartment 职务多值聚合(身兼数职):PositionUUID 从子表取, # 列表值/回显为逗号分隔(前端 multiValue 拆多标签展示) if table_name == 'UserRoleDepartment': @@ -1795,20 +1798,27 @@ class FormService(FormServiceBase): create_data['RoleName'] = None # UserRoleWorkgroup / UserRoleDepartment 规则拦截:新增"用户-角色对 → 工作组/部门"关系时, # 支持三种提交方式: - # 1) 直接提交 UserRoleUUID(旧路径,兼容) - # 2) 前端"用户→角色"两级级联:提交 UserUUID + RoleUUID,后端解析出 UserRole 关联 - # 3) 前端"部门员工关联"选择器:提交 UserRoleDepartmentUUID,后端解析出 UserRole 关联 - # (工作组新增成员;部门仅用于选择,不落库) + # 1) 前端"部门员工关联"选择器:提交 UserRoleDepartmentUUID,后端解析出 UserRole 关联 + # (两张表的表单唯一入口:工作组多选批量新增、部门单选新增成员; + # UserRoleDepartment 的部门优先取表单上下文注入的 DeptUUID,否则取选择器解析结果) + # 2) 前端"用户→角色"两级级联:提交 UserUUID + RoleUUID,后端解析出 UserRole 关联(兼容旧调用) + # 3) 直接提交 UserRoleUUID(旧路径,兼容) # 校验 UserRole 关联存在,且避免同一用户-角色对重复挂同一工作组/部门 if table_name in ('UserRoleWorkgroup', 'UserRoleDepartment'): - if (table_name == 'UserRoleWorkgroup' - and (data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id'))): + if data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id'): # 选择器提交的"部门员工关联"优先(其值就是本次要绑定的人员) _urw_de, _urw_de_err = self._ResolveDeptEmployee(data) if _urw_de_err: return {'error': _urw_de_err, 'field': 'UserRoleDepartmentUUID'} - if _urw_de and _urw_de.get('UserRoleUUID'): - create_data['UserRoleUUID'] = _urw_de['UserRoleUUID'] + _ctx_user = _to_uuid_bytes(data.get('UserUUID')) + if (table_name == 'UserRoleDepartment' and _ctx_user + and _urw_de.get('UserUUID') and _urw_de['UserUUID'] != _ctx_user): + # 员工详情页新增"部门职务":员工由上下文固定,选择器不得选到别人 + return {'error': '所选「部门员工关联」不属于当前员工', 'field': 'UserRoleDepartmentUUID'} + create_data['UserRoleUUID'] = _urw_de['UserRoleUUID'] + # 部门:表单上下文注入的 DeptUUID 优先(部门详情页新增成员),否则用选择器解析出的部门 + if table_name == 'UserRoleDepartment' and not create_data.get('DeptUUID'): + create_data['DeptUUID'] = _urw_de['DeptUUID'] urw_ur = create_data.get('UserRoleUUID') target_label = '工作组' if table_name == 'UserRoleWorkgroup' else '部门' urw_target = (create_data.get('WorkgroupUUID') or create_data.get('WorkgroupUUID_id') @@ -1819,10 +1829,8 @@ class FormService(FormServiceBase): if urw_ur is None: # 走"用户→角色"级联:从 (用户, 角色) 解析 UserRole 关联(同公司约束) if not urw_user or not urw_role: - if table_name == 'UserRoleWorkgroup': - return {'error': '请选择「部门员工关联」(或直接指定用户与角色)', - 'field': 'UserRoleDepartmentUUID'} - return {'error': f'必须指定用户、角色和{target_label}', 'field': 'UserRoleUUID'} + return {'error': f'请选择「部门员工关联」({target_label}成员须绑定一条部门员工关联)', + 'field': 'UserRoleDepartmentUUID'} try: ur_obj = RoleManager.GetUserRoleRelation(urw_user, urw_role, urw_company) except Exception: @@ -2115,15 +2123,19 @@ class FormService(FormServiceBase): # 工单状态流转:保存修改前的 Status,用于流转判定(时间戳 + 通知) _wo_old_status = getattr(obj, 'Status', None) if table_name == 'WorkOrder' else None pascal_map = self._BuildFieldMap(model) - # UserRoleWorkgroup:表单用「部门员工关联」(UserRoleDepartmentUUID)选择器, - # 编辑时改绑人员 → 解析出 UserRoleUUID 覆盖提交值(关系表只存用户-角色对,部门不落库) - if table_name == 'UserRoleWorkgroup' and ( + # UserRoleWorkgroup / UserRoleDepartment:表单用「部门员工关联」(UserRoleDepartmentUUID) + # 选择器,编辑时改绑人员 → 解析出 UserRoleUUID 覆盖提交值(关系表只存用户-角色对, + # 部门不落库;UserRoleDepartment 的部门保持原值/表单上下文值,不随改绑人员漂移) + if table_name in ('UserRoleWorkgroup', 'UserRoleDepartment') and ( data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id')): _urw_de, _urw_de_err = self._ResolveDeptEmployee(data) if _urw_de_err: return {'error': _urw_de_err, 'field': 'UserRoleDepartmentUUID'} if _urw_de and _urw_de.get('UserRoleUUID'): data = {**data, 'UserRoleUUID': _urw_de['UserRoleUUID']} + if table_name == 'UserRoleDepartment' and not ( + data.get('DeptUUID') or data.get('DeptUUID_id')): + data = {**data, 'DeptUUID': _urw_de['DeptUUID']} # EmployeeProfile(员工档案)直接绑「部门-员工」关系:解析选择器提交的 # UserRoleDepartmentUUID → UserUUID/DeptUUID/DeptName(未提交/解析不出则保持原值) if table_name == 'EmployeeProfile':