feat(gerp): 员工档案直接绑定「部门-员工」关系
档案表单原来要分别选「员工」(UserSearch) 与「部门」(DepartmentSearch),易出现员工与部门不匹配;现改为一个「部门-员工」选择框,直接绑定一条 UserRoleDepartment 关系,选中即同时确定员工与部门,表结构不变(仍写 UserUUID/DeptUUID/DeptName)。 新增 GET /API/FormAPI/DeptEmployeeSearch:数据源为 UserRoleDepartment,返回 UserRoleDepartmentUUID/DeptUUID/DeptName/UserUUID/UserName 与标签「部门 · 员工」;keyword 同时匹配用户名与部门名;显式 CompanyUUID 优先,否则用当前角色公司,无公司上下文返回空。EmployeeProfile 注入虚拟列 UserRoleDepartmentUUID(仅表单可见),表单内隐藏 UserUUID/DeptUUID/DeptName(列表仍展示);get_table 按 (UserUUID, DeptUUID) 反查关系回填 UUID 与标签保证编辑回显;add_record/modify_record 解析该 UUID 写三列(新增未选时提示「请选择部门-员工」,跨公司关系拒绝)。 顺带修复:/Modify/ 与 /ModifyItem/ 补错误字典透出(原来错误被当成功返回);add_record/modify_record 返回主键由 bytes repr 归一化为标准 UUID 字符串。
This commit is contained in:
1 parent
c5dd5ed43c
commit
6da812bb20
4 files changed
+285
-3
No files matched your search
@@ -888,6 +888,10 @@ class FormService(FormServiceBase):
|
||||
col['editable'] = False
|
||||
col['hideInForm'] = True
|
||||
col['visible'] = False
|
||||
# EmployeeProfile 直接绑「部门-员工」关系:员工/部门/部门名由虚拟列
|
||||
# UserRoleDepartmentUUID 一次选定,故三者在表单中隐藏(列表仍展示)
|
||||
if table_name == 'EmployeeProfile' and pascal_name in ('UserUUID', 'DeptUUID', 'DeptName'):
|
||||
col['hideInForm'] = True
|
||||
if table_name == 'EmployeeAssessment' and pascal_name == 'UserUUID':
|
||||
col['title'] = '员工'
|
||||
if table_name == 'EmployeeAssessment' and pascal_name == 'AssessorUUID':
|
||||
@@ -1054,6 +1058,29 @@ class FormService(FormServiceBase):
|
||||
'multiValue': True, # 前端多选:值/回显为逗号分隔的多个职务
|
||||
})
|
||||
columns = virtual_cols + columns
|
||||
# EmployeeProfile(员工档案)虚拟列"部门-员工":直接选一条用户-角色-部门关系
|
||||
# (UserRoleDepartment),选中即同时确定员工(UserUUID)与部门(DeptUUID/DeptName);
|
||||
# 写库由 add_record / modify_record 解析该 UUID,列表仍展示真实列(员工/部门)
|
||||
if table_name == 'EmployeeProfile':
|
||||
columns = [{
|
||||
'field': 'UserRoleDepartmentUUID',
|
||||
'title': '部门-员工',
|
||||
'titleEn': 'UserRoleDepartmentUUID',
|
||||
'type': 'entityPicker',
|
||||
'entityPicker': {
|
||||
'api': '/API/FormAPI/DeptEmployeeSearch',
|
||||
'labelField': 'DeptEmployeeLabel',
|
||||
'valueField': 'UserRoleDepartmentUUID',
|
||||
'placeholder': '搜索部门-员工(部门 · 姓名)',
|
||||
},
|
||||
'editable': True,
|
||||
'sortable': False,
|
||||
'filterable': False,
|
||||
# 不设 required:编辑时历史档案若查不到对应关系(关系已被删)不应被前端拦住,
|
||||
# 新增缺省校验由后端在 add_record 内做(提示更精确)
|
||||
'required': False,
|
||||
'visible': False, # 仅表单可见:列表已有"员工/部门"真实列
|
||||
}] + columns
|
||||
return columns
|
||||
except Exception:
|
||||
return []
|
||||
@@ -1376,6 +1403,44 @@ class FormService(FormServiceBase):
|
||||
_pl = _urdp_map.get(_udk, [])
|
||||
r['PositionUUID'] = ','.join(_pl)
|
||||
r['PositionUUID_label'] = '、'.join(_pos_name_map.get(x, x) for x in _pl)
|
||||
# EmployeeProfile(员工档案)"部门-员工"虚拟列回显:按 (UserUUID, DeptUUID) 反查
|
||||
# UserRoleDepartment 关系,注入 UserRoleDepartmentUUID 与展示标签,供编辑回显
|
||||
if table_name == 'EmployeeProfile':
|
||||
from gvsdsdk.model_utils import UUIDObj as _UO3
|
||||
|
||||
def _ProfStr(v):
|
||||
return str(_UO3(v)) if v else ''
|
||||
|
||||
_prof_ur_ids = []
|
||||
for _u in {r.get('UserUUID') for r in records if r.get('UserUUID')}:
|
||||
try:
|
||||
_prof_ur_ids.extend(RoleManager.ListUserRoleUUIDs(_u))
|
||||
except Exception:
|
||||
continue
|
||||
if _prof_ur_ids:
|
||||
_prof_ur_user = {
|
||||
_ProfStr(_ur.UserRoleUUID): _ProfStr(_ur.UserUUID)
|
||||
for _ur in RoleManager.GetUserRolesByUUIDs(_prof_ur_ids)
|
||||
}
|
||||
_prof_pair = {}
|
||||
for _urd in RoleManager.GetUserRoleDepartmentsForUserRoles(_prof_ur_ids):
|
||||
_prof_pair.setdefault(
|
||||
(_prof_ur_user.get(_ProfStr(_urd.UserRoleUUID), ''), _ProfStr(_urd.DeptUUID)),
|
||||
_urd,
|
||||
)
|
||||
_prof_dept_names = _StrKeyMap(DepartmentManager.GetDepartmentNameMap(
|
||||
[_urd.DeptUUID for _urd in _prof_pair.values() if _urd.DeptUUID]
|
||||
))
|
||||
for r in records:
|
||||
_urd = _prof_pair.get((_ProfStr(r.get('UserUUID')), _ProfStr(r.get('DeptUUID'))))
|
||||
if _urd is None:
|
||||
continue
|
||||
r['UserRoleDepartmentUUID'] = _ProfStr(_urd.UserRoleDepartmentUUID)
|
||||
_dn = _prof_dept_names.get(_ProfStr(_urd.DeptUUID), '')
|
||||
_un = r.get('UserUUID_label') or ''
|
||||
r['UserRoleDepartmentUUID_label'] = (
|
||||
f'{_dn} · {_un}' if (_dn or _un) else r['UserRoleDepartmentUUID']
|
||||
)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.error('[FormService] get_table error: table=%s, error=%s', table_name, e, exc_info=True)
|
||||
@@ -1383,6 +1448,42 @@ class FormService(FormServiceBase):
|
||||
records = []
|
||||
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
|
||||
|
||||
def _ResolveDeptEmployee(self, data):
|
||||
"""解析「部门-员工」选择器提交的 UserRoleDepartmentUUID(员工档案直接绑关系)
|
||||
|
||||
档案表不存"关系 UUID",只存员工(UserUUID)与部门(DeptUUID/DeptName);
|
||||
前端选择器提交的是 UserRoleDepartment 主键,此处解析成上述三列的值。
|
||||
|
||||
Returns:
|
||||
(updates: dict | None, error: str | None)
|
||||
updates = {'UserUUID': bytes, 'DeptUUID': bytes, 'DeptName': str};
|
||||
未提交该字段时返回 (None, None),调用方保持原有赋值行为。
|
||||
"""
|
||||
raw = data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id')
|
||||
if not raw:
|
||||
return None, None
|
||||
rows = RoleManager.GetUserRoleDepartmentsByUUIDs([raw])
|
||||
if not rows:
|
||||
return None, '所选「部门-员工」关系不存在'
|
||||
urd = rows[0]
|
||||
urd_company = _to_uuid_bytes(getattr(urd, 'CompanyUUID', None))
|
||||
if self.current_company_uuid and urd_company and urd_company != self.current_company_uuid:
|
||||
return None, '所选「部门-员工」关系不属于当前公司'
|
||||
rels = RoleManager.GetUserRolesByUUIDs([getattr(urd, 'UserRoleUUID', None)])
|
||||
if not rels:
|
||||
return None, '所选「部门-员工」关系的用户-角色关联不存在'
|
||||
user_bytes = _to_uuid_bytes(getattr(rels[0], 'UserUUID', None))
|
||||
if not user_bytes:
|
||||
return None, '所选「部门-员工」关系缺少员工信息'
|
||||
dept_bytes = _to_uuid_bytes(getattr(urd, 'DeptUUID', None))
|
||||
dept_name = ''
|
||||
if dept_bytes:
|
||||
try:
|
||||
dept_name = DepartmentManager.GetDepartmentNameMap([dept_bytes]).get(dept_bytes, '')
|
||||
except Exception:
|
||||
dept_name = ''
|
||||
return {'UserUUID': user_bytes, 'DeptUUID': dept_bytes, 'DeptName': dept_name}, None
|
||||
|
||||
def add_record(self, data):
|
||||
table_name = data.get('Table', '')
|
||||
model = self.TABLE_MAP.get(table_name)
|
||||
@@ -1607,6 +1708,17 @@ class FormService(FormServiceBase):
|
||||
elif isinstance(_raw_pos, (list, tuple)):
|
||||
_raw_pos = list(_raw_pos)
|
||||
_urd_positions = list(_raw_pos or [])
|
||||
# EmployeeProfile(员工档案)直接绑「部门-员工」关系:解析选择器提交的
|
||||
# UserRoleDepartmentUUID → UserUUID/DeptUUID/DeptName(表结构不变)
|
||||
if table_name == 'EmployeeProfile':
|
||||
_de_updates, _de_err = self._ResolveDeptEmployee(data)
|
||||
if _de_err:
|
||||
return {'error': _de_err, 'field': 'UserRoleDepartmentUUID'}
|
||||
if _de_updates:
|
||||
create_data.update(_de_updates)
|
||||
elif not create_data.get('UserUUID'):
|
||||
return {'error': '请选择「部门-员工」(员工档案须绑定一条部门-员工关系)',
|
||||
'field': 'UserRoleDepartmentUUID'}
|
||||
# CompanyInfo 唯一性:同一租户仅一条企业信息(epcreate 是"完善本企业"向导,
|
||||
# 防重复提交产生多条;修改走租户详情编辑)
|
||||
if table_name == 'CompanyInfo' and self.current_tenant_uuid:
|
||||
@@ -1743,7 +1855,14 @@ class FormService(FormServiceBase):
|
||||
f'新工单指派:{obj.Title}',
|
||||
f'您有一张新工单「{obj.Title}」待处理,请及时跟进。',
|
||||
)
|
||||
return {'PK': str(obj.pk) if obj.pk else None}
|
||||
# 自定义 UUIDField 主键是 BINARY(16) bytes(或 UUIDObj),str() 会得到 "b'\\x..'"
|
||||
# 这种非法串,统一归一化为标准 UUID 字符串供前端直接使用
|
||||
if not obj.pk:
|
||||
return {'PK': None}
|
||||
try:
|
||||
return {'PK': UUIDObj(obj.pk).str}
|
||||
except Exception:
|
||||
return {'PK': str(obj.pk)}
|
||||
|
||||
def _current_user_uuid_bytes(self):
|
||||
"""当前操作者 UserUUID 的 BINARY(16) bytes(无法解析返回 None)"""
|
||||
@@ -1838,6 +1957,14 @@ class FormService(FormServiceBase):
|
||||
# 工单状态流转:保存修改前的 Status,用于流转判定(时间戳 + 通知)
|
||||
_wo_old_status = getattr(obj, 'Status', None) if table_name == 'WorkOrder' else None
|
||||
pascal_map = self._BuildFieldMap(model)
|
||||
# EmployeeProfile(员工档案)直接绑「部门-员工」关系:解析选择器提交的
|
||||
# UserRoleDepartmentUUID → UserUUID/DeptUUID/DeptName(未提交/解析不出则保持原值)
|
||||
if table_name == 'EmployeeProfile':
|
||||
_de_updates, _de_err = self._ResolveDeptEmployee(data)
|
||||
if _de_err:
|
||||
return {'error': _de_err, 'field': 'UserRoleDepartmentUUID'}
|
||||
if _de_updates:
|
||||
data = {**data, **_de_updates}
|
||||
# UserRoleDepartment / UserRoleWorkgroup 职务多值化(身兼数职):PositionUUID 不在主表
|
||||
# 字段(部门 0034 删列;工作组 0043 删列,职务复用公司员工关系子表),
|
||||
# 编辑时若提交了该字段则整体重写子表(未提交保持不动)
|
||||
@@ -1999,7 +2126,14 @@ class FormService(FormServiceBase):
|
||||
if table_name in self.AUDIT_TABLES:
|
||||
new_snapshot = self._get_record_snapshot(obj)
|
||||
self._record_audit(table_name, 'modify', target_obj=obj, old_data=old_snapshot, new_data=new_snapshot)
|
||||
return {'PK': str(obj.pk) if obj.pk else None}
|
||||
# 自定义 UUIDField 主键是 BINARY(16) bytes(或 UUIDObj),str() 会得到 "b'\\x..'"
|
||||
# 这种非法串,统一归一化为标准 UUID 字符串供前端直接使用
|
||||
if not obj.pk:
|
||||
return {'PK': None}
|
||||
try:
|
||||
return {'PK': UUIDObj(obj.pk).str}
|
||||
except Exception:
|
||||
return {'PK': str(obj.pk)}
|
||||
|
||||
def modify_item(self, data):
|
||||
return self.modify_record(data)
|
||||
|
||||
@@ -82,7 +82,13 @@ from .asset import (
|
||||
)
|
||||
from .message import MessageCompatView
|
||||
from .menu import MenuListCompatView, MenuAdminListView, MenuSaveView, MenuDeleteView
|
||||
from .form_api import FormAPICompatView, FormAPIView, CheckAdminView, WorkOrderStatsView
|
||||
from .form_api import (
|
||||
FormAPICompatView,
|
||||
FormAPIView,
|
||||
CheckAdminView,
|
||||
WorkOrderStatsView,
|
||||
DeptEmployeeSearchView,
|
||||
)
|
||||
from .doc import DocView, SerieView
|
||||
from .position import (
|
||||
PositionListView,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import uuid as _uuid
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
from . import ELTAuthenticatedAPIView
|
||||
@@ -262,6 +263,8 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
result = service.modify_item(data)
|
||||
if result is None:
|
||||
return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404)
|
||||
if isinstance(result, dict) and 'error' in result:
|
||||
return Response({'code': 1, 'msg': result['error'], 'field': result.get('field', '')}, status=400)
|
||||
return Response({'code': 0, 'msg': '修改成功', 'data': result})
|
||||
elif '/Modify/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'modify', request)
|
||||
@@ -274,6 +277,8 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
result = service.modify_record(data)
|
||||
if result is None:
|
||||
return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404)
|
||||
if isinstance(result, dict) and 'error' in result:
|
||||
return Response({'code': 1, 'msg': result['error'], 'field': result.get('field', '')}, status=400)
|
||||
return Response({'code': 0, 'msg': '修改成功', 'data': result})
|
||||
elif '/Delete/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'delete', request)
|
||||
@@ -510,3 +515,139 @@ class WorkOrderStatsView(ELTAuthenticatedAPIView):
|
||||
'categoryOptions': FormService.WORKORDER_CATEGORY_OPTIONS['Category'],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _ToUuidBytes(value):
|
||||
"""UUID 值归一化为 16 字节(无效/空返回 None)"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return UUIDObj(value).bytes
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _ToUuidStr(value):
|
||||
"""UUID 值归一化为标准字符串(无效/空返回空串)"""
|
||||
if not value:
|
||||
return ''
|
||||
try:
|
||||
return UUIDObj(value).str
|
||||
except Exception:
|
||||
return str(value)
|
||||
|
||||
|
||||
class DeptEmployeeSearchView(ELTAuthenticatedAPIView):
|
||||
"""「部门-员工」关系搜索——"部门-员工"选择框的数据源
|
||||
|
||||
GET /API/FormAPI/DeptEmployeeSearch?keyword=&CompanyUUID=&DeptUUID=&offset=
|
||||
|
||||
设计要点:
|
||||
- 数据源是 UserRoleDepartment(部门-员工关系),而非"用户表 + 部门表"两张独立表:
|
||||
选择器里一项 = 一条部门-员工关系,选中即可同时确定员工与部门(及部门名)。
|
||||
- 返回字段:UserRoleDepartmentUUID / DeptUUID / DeptName / UserUUID / UserName /
|
||||
DeptEmployeeLabel("部门名 · 用户名",选择器展示文本)
|
||||
- 范围:显式传 CompanyUUID 时强制该公司(超管也不豁免);否则取当前角色公司;
|
||||
无公司上下文返回空列表(不退化成全表)
|
||||
- keyword 同时匹配用户名与部门名,任一命中即保留(并集)
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
from gvsdsdk.auth.managers import AuthManager, DepartmentManager, RoleManager
|
||||
|
||||
keyword = (request.query_params.get('keyword') or '').strip()
|
||||
company_str = (request.query_params.get('CompanyUUID') or '').strip()
|
||||
if company_str:
|
||||
company_bytes = _ToUuidBytes(company_str)
|
||||
else:
|
||||
# 未显式指定公司:用当前角色公司(与表单服务同一来源)
|
||||
_, company_bytes, _ = _get_current_context(request)
|
||||
if not company_bytes:
|
||||
return Response({'code': 0, 'data': {'list': [], 'has_more': False}})
|
||||
|
||||
dept_param = (request.query_params.get('DeptUUID') or '').strip()
|
||||
qs = RoleManager.QueryUserRoleDepartmentsByCompany(
|
||||
company_bytes, [dept_param] if dept_param else None,
|
||||
)
|
||||
|
||||
if keyword:
|
||||
from django.db.models import Q
|
||||
# 部门名命中:收窄部门
|
||||
dept_bytes = [
|
||||
b for b in (
|
||||
_ToUuidBytes(d.DeptUUID)
|
||||
for d in DepartmentManager.QueryByCompany(company_bytes).filter(
|
||||
DeptName__icontains=keyword
|
||||
)
|
||||
) if b
|
||||
]
|
||||
# 用户名命中:收窄到这些用户在本公司的"用户-角色对"
|
||||
user_uuids, _role_uuids = RoleManager.MatchUserRoleIds(keyword)
|
||||
ur_bytes = []
|
||||
if user_uuids:
|
||||
ur_bytes = [
|
||||
b for b in (
|
||||
_ToUuidBytes(x)
|
||||
for x in RoleManager.QueryUserRolesByCompany(company_bytes)
|
||||
.filter(UserUUID__in=list(user_uuids))
|
||||
.values_list('UserRoleUUID', flat=True)
|
||||
) if b
|
||||
]
|
||||
if dept_bytes and ur_bytes:
|
||||
qs = qs.filter(Q(DeptUUID__in=dept_bytes) | Q(UserRoleUUID__in=ur_bytes))
|
||||
elif dept_bytes:
|
||||
qs = qs.filter(DeptUUID__in=dept_bytes)
|
||||
elif ur_bytes:
|
||||
qs = qs.filter(UserRoleUUID__in=ur_bytes)
|
||||
else:
|
||||
qs = qs.none()
|
||||
|
||||
offset = int(request.query_params.get('offset') or 0)
|
||||
try:
|
||||
total = qs.count()
|
||||
except Exception:
|
||||
total = 0
|
||||
rows = list(qs[offset:offset + 20])
|
||||
|
||||
# 批量解析:UserRoleUUID → UserUUID(UserRoleDepartment 只存"用户-角色对")
|
||||
ur_objs = RoleManager.GetUserRolesByUUIDs([r.UserRoleUUID for r in rows if r.UserRoleUUID])
|
||||
ur_user_bytes = {} # UserRoleUUID(bytes) → UserUUID(bytes)
|
||||
for ur in ur_objs:
|
||||
ur_key = _ToUuidBytes(ur.UserRoleUUID)
|
||||
user_key = _ToUuidBytes(ur.UserUUID)
|
||||
if ur_key and user_key:
|
||||
ur_user_bytes[ur_key] = user_key
|
||||
user_name_map = {}
|
||||
if ur_user_bytes:
|
||||
try:
|
||||
user_name_map = AuthManager.GetUserNames(list(set(ur_user_bytes.values())))
|
||||
except Exception:
|
||||
user_name_map = {}
|
||||
dept_name_map = {}
|
||||
dept_keys = [_ToUuidBytes(r.DeptUUID) for r in rows if r.DeptUUID]
|
||||
if dept_keys:
|
||||
try:
|
||||
dept_name_map = DepartmentManager.GetDepartmentNameMap(dept_keys)
|
||||
except Exception:
|
||||
dept_name_map = {}
|
||||
|
||||
data = []
|
||||
for r in rows:
|
||||
urd_str = _ToUuidStr(r.UserRoleDepartmentUUID)
|
||||
dept_str = _ToUuidStr(r.DeptUUID)
|
||||
user_bytes = ur_user_bytes.get(_ToUuidBytes(r.UserRoleUUID))
|
||||
user_str = _ToUuidStr(user_bytes)
|
||||
user_name = user_name_map.get(user_bytes, '') if user_bytes else ''
|
||||
dept_name = dept_name_map.get(_ToUuidBytes(r.DeptUUID), '')
|
||||
label = f'{dept_name} · {user_name}' if (dept_name or user_name) else urd_str
|
||||
data.append({
|
||||
'UserRoleDepartmentUUID': urd_str,
|
||||
'DeptUUID': dept_str,
|
||||
'DeptName': dept_name,
|
||||
'UserUUID': user_str,
|
||||
'UserName': user_name,
|
||||
'DeptEmployeeLabel': label,
|
||||
})
|
||||
return Response({'code': 0, 'data': {'list': data, 'has_more': total > offset + 20}})
|
||||
@@ -136,6 +136,7 @@ urlpatterns = [
|
||||
path('API/FormAPI/TenantSearch', gerp_views.TenantViewSet.as_view({'get': 'search'}), name='form_tenant_search'),
|
||||
path('API/FormAPI/UserSearch', gerp_views.SiteUserManagementView.as_view(), name='form_user_search'),
|
||||
path('API/FormAPI/DepartmentSearch', gerp_views.DepartmentViewSet.as_view({'get': 'search'}), name='form_department_search'),
|
||||
path('API/FormAPI/DeptEmployeeSearch', gerp_views.DeptEmployeeSearchView.as_view(), name='form_dept_employee_search'),
|
||||
path('API/FormAPI/PermissionSearch', gerp_views.PermissionViewSet.as_view({'get': 'search'}), name='form_permission_search'),
|
||||
path('API/FormAPI/PositionTemplateSearch', gerp_views.PositionTemplateViewSet.as_view({'get': 'search'}), name='form_position_template_search'),
|
||||
path('API/FormAPI/PositionSearch', gerp_views.PositionViewSet.as_view({'get': 'search'}), name='form_position_search'),
|
||||
|
||||
Reference in new issue
Block a user