feat(gerp): 员工档案字段重建 + 联系方式子表
EmployeeProfile 按新清单重建:删工号,新增姓名/性别/年龄/种族/政治面貌/生日、网络昵称/志愿/志愿工作、从事行业、设备与备注、派发设备与奖罚意愿、期望工作时段、躯体疾病/精神障碍、紧急联系人关系与自定义联系方式;员工名/部门名/职务改为关联只读展示(不再写 DeptName)。新增 EmployeeContact 子表与 0048 迁移;form_service 下发选项模板(性别/年龄/学历/行业/志愿/关系/疾病/设备)与 subform 列,add/modify 同步子表明细,get_table 注入职务与联系方式明细。
This commit is contained in:
1 parent
dc7f133b5f
commit
46be2bd3e2
3 files changed
+448
-15
No files matched your search
@@ -9,21 +9,52 @@ from gvsds.model_base import QModel
|
||||
|
||||
|
||||
class EmployeeProfile(QModel):
|
||||
"""员工档案表——eHRM:员工基本信息与任职档案"""
|
||||
"""员工档案表——eHRM:员工基本信息与任职档案
|
||||
|
||||
人员归属只认「部门员工关联」(UserRoleDepartmentUUID,表单虚拟列):
|
||||
落库解析出 UserUUID / DeptUUID,员工名称与部门名称前端由关联解析展示(不在本表存名字)。
|
||||
PositionName 是**只读联动**展示列(取该员工 UserRoleDepartmentPosition 的真实职务),不由表单填写。
|
||||
"""
|
||||
|
||||
EmployeeProfileUUID = UUIDField(primary_key=True, verbose_name='档案UUID')
|
||||
UserUUID = UUIDField(db_column='UserUUID', db_index=True, verbose_name='员工用户UUID')
|
||||
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', verbose_name='租户UUID')
|
||||
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
DeptUUID = UUIDField(null=True, blank=True, db_column='DeptUUID', verbose_name='部门UUID')
|
||||
# DeptName 保留列(历史数据兼容):新档案不再写入,展示改用 DeptUUID 关联解析
|
||||
DeptName = models.CharField(max_length=64, default='', verbose_name='部门名称')
|
||||
EmployeeNo = models.CharField(max_length=32, default='', verbose_name='工号')
|
||||
HireDate = models.DateField(null=True, blank=True, verbose_name='入职日期')
|
||||
PositionName = models.CharField(max_length=64, default='', verbose_name='职务')
|
||||
# ===== 基本信息 =====
|
||||
EmployeeName = models.CharField(max_length=64, default='', verbose_name='姓名')
|
||||
Gender = models.CharField(max_length=8, default='', verbose_name='性别')
|
||||
AgeRange = models.CharField(max_length=16, default='', verbose_name='年龄')
|
||||
Race = models.CharField(max_length=32, default='', verbose_name='种族')
|
||||
PoliticalStatus = models.CharField(max_length=16, default='', verbose_name='政治面貌')
|
||||
Education = models.CharField(max_length=32, default='', verbose_name='学历')
|
||||
EmergencyContact = models.CharField(max_length=32, default='', verbose_name='紧急联系人')
|
||||
EmergencyPhone = models.CharField(max_length=32, default='', verbose_name='紧急联系电话')
|
||||
Address = models.CharField(max_length=256, default='', verbose_name='联系地址')
|
||||
Birthday = models.DateField(null=True, blank=True, verbose_name='生日')
|
||||
# ===== 网络与志愿 =====
|
||||
Nickname = models.CharField(max_length=64, default='', verbose_name='网络常用昵称')
|
||||
VolunteerWish = models.TextField(default='', verbose_name='志愿')
|
||||
VolunteerWork = models.CharField(max_length=16, default='', verbose_name='志愿工作')
|
||||
VolunteerWorkOther = models.CharField(max_length=64, default='', verbose_name='其它志愿工作')
|
||||
Industry = models.CharField(max_length=32, default='', verbose_name='从事行业')
|
||||
# ===== 工作条件 =====
|
||||
Devices = models.CharField(max_length=256, default='', verbose_name='可参与工作的设备')
|
||||
DeviceRemark = models.CharField(max_length=256, default='', verbose_name='设备备注')
|
||||
AcceptDispatchDevice = models.BooleanField(default=False, verbose_name='是否愿意使用派发设备')
|
||||
AcceptRewardPunish = models.BooleanField(default=False, verbose_name='是否接受奖罚机制')
|
||||
ExpectedWorkHours = models.CharField(max_length=128, default='', verbose_name='期望工作时段')
|
||||
# ===== 健康状况 =====
|
||||
PhysicalIllness = models.CharField(max_length=512, default='', verbose_name='躯体疾病')
|
||||
MentalIllness = models.CharField(max_length=512, default='', verbose_name='精神障碍')
|
||||
# ===== 紧急联系人 =====
|
||||
EmergencyContact = models.CharField(max_length=32, default='', verbose_name='紧急联系人姓名')
|
||||
EmergencyRelation = models.CharField(max_length=16, default='', verbose_name='与本人关系')
|
||||
EmergencyRelationOther = models.CharField(max_length=32, default='', verbose_name='其它关系说明')
|
||||
EmergencyPhone = models.CharField(max_length=32, default='', verbose_name='紧急联系电话')
|
||||
EmergencyContactCustom = models.CharField(max_length=128, default='', verbose_name='自定义联系方式')
|
||||
Remark = models.CharField(max_length=512, default='', verbose_name='备注')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
@@ -35,6 +66,32 @@ class EmployeeProfile(QModel):
|
||||
verbose_name_plural = '员工档案'
|
||||
|
||||
|
||||
class EmployeeContact(QModel):
|
||||
"""员工联系方式子表——eHRM:一条 = 某份档案的一项联系方式
|
||||
|
||||
与 EmployeeProfile 一对多(每次保存档案时整体重写该档案的联系方式明细)。
|
||||
ContactType 取值见 form_service.CONTACT_TYPE_OPTIONS(1=微信 2=QQ 3=邮箱
|
||||
4=企业微信 5=X 6=个人网站 7=手机号)。
|
||||
"""
|
||||
|
||||
EmployeeContactUUID = UUIDField(primary_key=True, verbose_name='联系方式UUID')
|
||||
EmployeeProfileUUID = UUIDField(db_column='EmployeeProfileUUID', db_index=True, verbose_name='档案UUID')
|
||||
UserUUID = UUIDField(null=True, blank=True, db_column='UserUUID', db_index=True, verbose_name='员工用户UUID')
|
||||
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', verbose_name='租户UUID')
|
||||
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
ContactType = models.SmallIntegerField(default=1, verbose_name='联系方式类型')
|
||||
ContactValue = models.CharField(max_length=256, default='', verbose_name='联系方式')
|
||||
ContactSort = models.IntegerField(default=0, verbose_name='排序')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'EmployeeContact'
|
||||
app_label = 'gerp'
|
||||
ordering = ['ContactSort', 'CreateTime']
|
||||
verbose_name = '员工联系方式'
|
||||
verbose_name_plural = '员工联系方式'
|
||||
|
||||
|
||||
class EmployeeAssessment(QModel):
|
||||
"""员工考核表——eHRM:考核期间、评分与等级结论"""
|
||||
|
||||
|
||||
+254
-11
@@ -1,4 +1,5 @@
|
||||
import uuid
|
||||
import json
|
||||
import datetime
|
||||
import re
|
||||
import logging
|
||||
@@ -18,7 +19,7 @@ from gvsds.apps.gerp.models import (
|
||||
App, AppPosition, AppVirtualPosition, AppVirtualBinding,
|
||||
FinanceVoucher, FinanceVoucherEntry, FinanceCashier, FinanceLedger,
|
||||
FinanceReport, FinanceReimburse, FinanceArap, FinanceBudget,
|
||||
EmployeeProfile, EmployeeAssessment, EmployeePerformance,
|
||||
EmployeeProfile, EmployeeContact, EmployeeAssessment, EmployeePerformance,
|
||||
OaLeave, OaTravel, OaMeeting, OaAnnouncement, OaReport,
|
||||
)
|
||||
from gvsds.apps.core.models import User, UserAvatar, User2FA, UserPending2FA
|
||||
@@ -167,7 +168,8 @@ DEPT_EMPLOYEE_PICKER_MULTI = {
|
||||
|
||||
#: eHRM 员工档案从「部门员工关联」解析后要落的列
|
||||
#: (_ResolveDeptEmployee 返回值含 4 个键,各表只取自己表上存在的列,多带键会撞 model(**kwargs))
|
||||
_PROFILE_DEPT_EMPLOYEE_KEYS = ('UserUUID', 'DeptUUID', 'DeptName')
|
||||
#: DeptName 不在此列:部门名称不再写库(展示由 DeptUUID 关联解析),列仅保留给历史数据
|
||||
_PROFILE_DEPT_EMPLOYEE_KEYS = ('UserUUID', 'DeptUUID')
|
||||
|
||||
|
||||
class FormService(FormServiceBase):
|
||||
@@ -224,6 +226,7 @@ class FormService(FormServiceBase):
|
||||
'FinanceArap': FinanceArap,
|
||||
'FinanceBudget': FinanceBudget,
|
||||
'EmployeeProfile': EmployeeProfile,
|
||||
'EmployeeContact': EmployeeContact,
|
||||
'EmployeeAssessment': EmployeeAssessment,
|
||||
'EmployeePerformance': EmployeePerformance,
|
||||
'OaLeave': OaLeave,
|
||||
@@ -527,6 +530,68 @@ class FormService(FormServiceBase):
|
||||
],
|
||||
}
|
||||
|
||||
# ==================== eHRM 员工档案(EmployeeProfile)选项模板 ====================
|
||||
# 「模板」= 系统统一维护的选项常量(后端唯一下发,前端只渲染,禁止就地另写一份)
|
||||
# 名称型选项统一**存中文文本**(列表直接可读,无需再映射);类型型(ContactType)存数字。
|
||||
EMPLOYEE_PROFILE_SELECT_OPTIONS = {
|
||||
'Gender': ['男', '女', '其他'],
|
||||
'AgeRange': ['12~18', '19~25', '26~40', '41~65', '66以上'],
|
||||
'PoliticalStatus': ['群众', '共青团员', '中共党员', '民主党派', '其他'],
|
||||
'Education': ['小学', '初中', '高中', '中专', '大专', '本科', '硕士', '博士', '其他'],
|
||||
'VolunteerWork': ['程序员', '画师', '测试者', '其它'],
|
||||
'Industry': [
|
||||
'IT/通信/电子/互联网',
|
||||
'金融业',
|
||||
'房地产/建筑业',
|
||||
'商业服务',
|
||||
'贸易/批发/零售/租赁业',
|
||||
'文体教育/工艺美术',
|
||||
'生产/加工/制造',
|
||||
'交通/运输/物流/仓储',
|
||||
'服务业',
|
||||
'文化/传媒/娱乐/体育',
|
||||
'能源/矿产/环保',
|
||||
'政府/非盈利机构',
|
||||
'学校',
|
||||
],
|
||||
'EmergencyRelation': ['父子', '母子', '配偶', '其它'],
|
||||
}
|
||||
|
||||
#: 档案里的多选字段(tags 类型:选项固定 + 允许自定义,逗号分隔字符串存储)
|
||||
EMPLOYEE_PROFILE_TAGS_OPTIONS = {
|
||||
'Devices': ['电脑', '手机', '云服务器'],
|
||||
'PhysicalIllness': [
|
||||
'无', '先天发育畸形', '纹身', '内分泌、营养、代谢疾病', '泌尿生殖系统疾病',
|
||||
'心血管疾病', '神经科疾病', '残疾', '消化系统疾病', '其他',
|
||||
],
|
||||
'MentalIllness': [
|
||||
'无', '焦虑症', '抑郁症', '强迫症', '惊恐障碍', '单相情感障碍', '双相情感障碍', '其他',
|
||||
],
|
||||
}
|
||||
|
||||
#: 档案里用多行文本框的字段(长文本)
|
||||
EMPLOYEE_PROFILE_TEXTAREA_FIELDS = ('VolunteerWish', 'Remark')
|
||||
|
||||
#: 档案表单里不需要用户选/填的字段:
|
||||
#: 人员由「部门员工关联」一次选定,员工名/部门名/职务/部门名冗余列一律只读展示
|
||||
EMPLOYEE_PROFILE_FORM_HIDDEN_FIELDS = (
|
||||
'UserUUID', 'DeptUUID', 'DeptName', 'PositionName', 'TenantUUID', 'CompanyUUID',
|
||||
)
|
||||
|
||||
#: 联系方式类型(EmployeeContact.ContactType):子表一行 = 一种渠道
|
||||
CONTACT_TYPE_OPTIONS = [
|
||||
{'value': 1, 'label': '微信'},
|
||||
{'value': 2, 'label': 'QQ'},
|
||||
{'value': 3, 'label': '邮箱'},
|
||||
{'value': 4, 'label': '企业微信'},
|
||||
{'value': 5, 'label': 'X'},
|
||||
{'value': 6, 'label': '个人网站'},
|
||||
{'value': 7, 'label': '手机号'},
|
||||
]
|
||||
|
||||
#: 档案表单里「联系方式」子表的虚拟列名(值 = 明细行数组,随档案一并提交)
|
||||
EMPLOYEE_CONTACT_FIELD = 'Contacts'
|
||||
|
||||
def get_columns(self, table_name):
|
||||
model = self.TABLE_MAP.get(table_name)
|
||||
if not model:
|
||||
@@ -930,10 +995,63 @@ 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
|
||||
# ===== eHRM 员工档案(EmployeeProfile):选项模板 + 只读联动列 =====
|
||||
# 人员/部门由虚拟列「部门员工关联」一次选定;员工名/部门名/职务都是**只读联动**
|
||||
# 展示(员工名与部门名来自关联 label,职务取该员工真实职务),一律不进表单。
|
||||
if table_name == 'EmployeeProfile':
|
||||
if pascal_name in self.EMPLOYEE_PROFILE_SELECT_OPTIONS:
|
||||
col['type'] = 'select'
|
||||
col['options'] = [
|
||||
{'value': _v, 'label': _v}
|
||||
for _v in self.EMPLOYEE_PROFILE_SELECT_OPTIONS[pascal_name]
|
||||
]
|
||||
col['placeholder'] = f"选择{col['title']}"
|
||||
if pascal_name in self.EMPLOYEE_PROFILE_TAGS_OPTIONS:
|
||||
# 多选:选项固定 + 允许自定义新增(满足「其它」),逗号分隔字符串存储
|
||||
col['type'] = 'tags'
|
||||
col['options'] = [
|
||||
{'value': _v, 'label': _v}
|
||||
for _v in self.EMPLOYEE_PROFILE_TAGS_OPTIONS[pascal_name]
|
||||
]
|
||||
col['tagsSeparator'] = ','
|
||||
col['placeholder'] = f"选择{col['title']}(可多选,可自定义)"
|
||||
if pascal_name in self.EMPLOYEE_PROFILE_TEXTAREA_FIELDS:
|
||||
col['type'] = 'textarea'
|
||||
if pascal_name in ('AcceptDispatchDevice', 'AcceptRewardPunish'):
|
||||
col['type'] = 'switch'
|
||||
col['defaultValue'] = False
|
||||
if pascal_name in self.EMPLOYEE_PROFILE_FORM_HIDDEN_FIELDS:
|
||||
col['editable'] = False
|
||||
col['hideInForm'] = True
|
||||
if pascal_name == 'DeptName':
|
||||
# 部门名称不再由档案维护:展示改用 DeptUUID 关联解析(DeptUUID_label)
|
||||
col['visible'] = False
|
||||
if pascal_name == 'UserUUID':
|
||||
col['title'] = '员工名称'
|
||||
col['editable'] = False
|
||||
if pascal_name == 'DeptUUID':
|
||||
col['title'] = '部门名称'
|
||||
col['editable'] = False
|
||||
if pascal_name == 'PositionName':
|
||||
# 职务只读联动:值由 get_table 从该员工 UserRoleDepartmentPosition 取真实职务
|
||||
col['editable'] = False
|
||||
# EmployeeContact(员工联系方式子表):只作为档案表单内嵌子表使用
|
||||
# (列定义经 EmployeeProfile 的虚拟列 subform 下发,前端按此渲染增删行)
|
||||
if table_name == 'EmployeeContact':
|
||||
if pascal_name == 'ContactType':
|
||||
col['title'] = '联系方式类型'
|
||||
col['type'] = 'select'
|
||||
col['options'] = [dict(_o) for _o in self.CONTACT_TYPE_OPTIONS]
|
||||
col['required'] = True
|
||||
col['placeholder'] = '选择类型'
|
||||
elif pascal_name == 'ContactValue':
|
||||
col['title'] = '联系方式'
|
||||
col['required'] = True
|
||||
col['placeholder'] = '如微信号 / 邮箱地址 / 手机号'
|
||||
elif pascal_name == 'ContactSort':
|
||||
col['type'] = 'number'
|
||||
col['visible'] = False
|
||||
col['hideInForm'] = True
|
||||
if table_name == 'EmployeeAssessment' and pascal_name == 'UserUUID':
|
||||
col['title'] = '员工'
|
||||
if table_name == 'EmployeeAssessment' and pascal_name == 'AssessorUUID':
|
||||
@@ -1119,11 +1237,16 @@ class FormService(FormServiceBase):
|
||||
'visible': False, # 仅表单可见(列表已拆成"用户/角色"两列展示)
|
||||
})
|
||||
columns = virtual_cols + columns
|
||||
# EmployeeProfile(员工档案)虚拟列"部门员工关联":直接选一条用户-角色-部门关系
|
||||
# (UserRoleDepartment),选中即同时确定员工(UserUUID)与部门(DeptUUID/DeptName);
|
||||
# 写库由 add_record / modify_record 解析该 UUID,列表仍展示真实列(员工/部门)。
|
||||
# 选择器复用 DEPT_EMPLOYEE_PICKER(与工作组新增成员同一组件:按部门树分组浏览)
|
||||
# EmployeeProfile(员工档案)两张虚拟列:
|
||||
# 1) 「部门员工关联」(置顶):选一条 UserRoleDepartment,选中即确定员工与部门;
|
||||
# 写库由 add_record / modify_record 解析该 UUID,列表仍展示真实列(员工/部门)
|
||||
# 2) 「联系方式」子表单(置于表单末尾):明细行数组随档案一起提交,
|
||||
# 保存后由 _RewriteEmployeeContacts 整体重写 EmployeeContact 子表
|
||||
if table_name == 'EmployeeProfile':
|
||||
_contact_fields = [
|
||||
_c for _c in self.get_columns('EmployeeContact')
|
||||
if _c.get('field') in ('ContactType', 'ContactValue', 'ContactSort')
|
||||
]
|
||||
columns = [{
|
||||
'field': 'UserRoleDepartmentUUID',
|
||||
'title': '部门员工关联',
|
||||
@@ -1137,7 +1260,23 @@ class FormService(FormServiceBase):
|
||||
# 新增缺省校验由后端在 add_record 内做(提示更精确)
|
||||
'required': False,
|
||||
'visible': False, # 仅表单可见:列表已有"员工/部门"真实列
|
||||
}] + columns
|
||||
}] + columns + [{
|
||||
'field': self.EMPLOYEE_CONTACT_FIELD,
|
||||
'title': '联系方式',
|
||||
'titleEn': self.EMPLOYEE_CONTACT_FIELD,
|
||||
'type': 'subform',
|
||||
'subform': {
|
||||
'table': 'EmployeeContact',
|
||||
'title': '联系方式',
|
||||
'addText': '添加联系方式',
|
||||
'fields': _contact_fields,
|
||||
},
|
||||
'editable': True,
|
||||
'sortable': False,
|
||||
'filterable': False,
|
||||
'required': False,
|
||||
'visible': False, # 仅表单可见(列表不展示明细行)
|
||||
}]
|
||||
# 表单字段顺序:「部门员工关联」选择器置顶 —— 新增时先选它,员工/部门/角色随之确定,
|
||||
# 其余字段(职务/类型等)都是派生或可选的
|
||||
if table_name in self.DEPT_EMPLOYEE_FIRST_TABLES:
|
||||
@@ -1555,6 +1694,23 @@ class FormService(FormServiceBase):
|
||||
_prof_dept_names = _StrKeyMap(DepartmentManager.GetDepartmentNameMap(
|
||||
[_urd.DeptUUID for _urd in _prof_pair.values() if _urd.DeptUUID]
|
||||
))
|
||||
# 职务只读联动:各档案对应「部门员工关联」的职务集合(UserRoleDepartmentPosition)
|
||||
_prof_urd_ids = [
|
||||
_pu for _pu in (
|
||||
getattr(_u, 'UserRoleDepartmentUUID', None) for _u in _prof_pair.values()
|
||||
) if _pu
|
||||
]
|
||||
_prof_pos_map = {}
|
||||
if _prof_urd_ids:
|
||||
_prof_pos_rows = PositionManager.GetUrdPositionRowsForUrds(_prof_urd_ids)
|
||||
_prof_pos_names = _StrKeyMap(PositionManager.GetPositionNameMap(
|
||||
{_p.PositionUUID for _p in _prof_pos_rows if _p.PositionUUID}
|
||||
))
|
||||
for _p in _prof_pos_rows:
|
||||
_prod_key = _ProfStr(_p.UserRoleDepartmentUUID)
|
||||
_prof_pos_map.setdefault(_prod_key, []).append(
|
||||
_prof_pos_names.get(_ProfStr(_p.PositionUUID)) or _ProfStr(_p.PositionUUID)
|
||||
)
|
||||
for r in records:
|
||||
_urd = _prof_pair.get((_ProfStr(r.get('UserUUID')), _ProfStr(r.get('DeptUUID'))))
|
||||
if _urd is None:
|
||||
@@ -1565,6 +1721,29 @@ class FormService(FormServiceBase):
|
||||
r['UserRoleDepartmentUUID_label'] = (
|
||||
f'{_dn} · {_un}' if (_dn or _un) else r['UserRoleDepartmentUUID']
|
||||
)
|
||||
_pl = _prof_pos_map.get(_ProfStr(_urd.UserRoleDepartmentUUID), [])
|
||||
r['PositionName'] = '、'.join(_pl)
|
||||
r['PositionName_label'] = r['PositionName']
|
||||
# 联系方式子表明细(子表单回显):一次查出本页所有档案的明细行。
|
||||
# 键统一用 UUIDObj 的字符串形式(模型读出来是 UUIDObj,记录字典里是字符串,
|
||||
# 必须同一形态才能对上,见 _StrKeyMap 的同类说明)
|
||||
_prof_uuids = [r.get('EmployeeProfileUUID') for r in records if r.get('EmployeeProfileUUID')]
|
||||
if _prof_uuids:
|
||||
_contact_map = {}
|
||||
for _ct in EmployeeContact.objects.filter(
|
||||
EmployeeProfileUUID__in=[_to_uuid_bytes(_x) for _x in _prof_uuids]):
|
||||
_contact_map.setdefault(str(_ct.EmployeeProfileUUID), []).append({
|
||||
'ContactType': _ct.ContactType,
|
||||
'ContactValue': _ct.ContactValue,
|
||||
'ContactSort': _ct.ContactSort,
|
||||
})
|
||||
for r in records:
|
||||
_raw_pk = r.get('EmployeeProfileUUID')
|
||||
_prof_key = str(UUIDObj(_to_uuid_bytes(_raw_pk))) if _raw_pk else ''
|
||||
_rows = _contact_map.get(_prof_key, [])
|
||||
r[self.EMPLOYEE_CONTACT_FIELD] = sorted(
|
||||
_rows, key=lambda _x: _x.get('ContactSort') or 0
|
||||
)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.error('[FormService] get_table error: table=%s, error=%s', table_name, e, exc_info=True)
|
||||
@@ -1618,6 +1797,62 @@ class FormService(FormServiceBase):
|
||||
'UserRoleUUID': user_role_bytes,
|
||||
}, None
|
||||
|
||||
def _ParseContactsPayload(self, data):
|
||||
"""解析档案表单提交的「联系方式」子表明细(虚拟列 Contacts)
|
||||
|
||||
一行 = {ContactType, ContactValue};空值行丢弃,行序即排序(ContactSort)。
|
||||
|
||||
Returns:
|
||||
list[dict] | None: 未提交该字段返回 None(调用方据此保持原明细不动)
|
||||
"""
|
||||
raw = data.get(self.EMPLOYEE_CONTACT_FIELD)
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except Exception:
|
||||
return []
|
||||
if not isinstance(raw, (list, tuple)):
|
||||
return []
|
||||
rows = []
|
||||
for _i, _item in enumerate(raw):
|
||||
if not isinstance(_item, dict):
|
||||
continue
|
||||
_value = str(_item.get('ContactValue') or '').strip()
|
||||
if not _value:
|
||||
continue # 空行(用户加了行没填)直接丢弃
|
||||
try:
|
||||
_ctype = int(_item.get('ContactType') or 1)
|
||||
except (TypeError, ValueError):
|
||||
_ctype = 1
|
||||
rows.append({'ContactType': _ctype, 'ContactValue': _value, 'ContactSort': _i})
|
||||
return rows
|
||||
|
||||
def _RewriteEmployeeContacts(self, profile, rows):
|
||||
"""按提交的明细整体重写某份档案的联系方式子表(rows=None 表示未提交,保持不动)"""
|
||||
if rows is None:
|
||||
return
|
||||
profile_uuid = _to_uuid_bytes(getattr(profile, 'EmployeeProfileUUID', None))
|
||||
if not profile_uuid:
|
||||
return
|
||||
try:
|
||||
EmployeeContact.objects.filter(EmployeeProfileUUID=profile_uuid).delete()
|
||||
except Exception:
|
||||
logger.warning('[FormService] 清理员工联系方式失败: profile=%s', profile_uuid, exc_info=True)
|
||||
return
|
||||
for _row in rows:
|
||||
EmployeeContact.objects.create(
|
||||
EmployeeContactUUID=uuid.uuid4().bytes,
|
||||
EmployeeProfileUUID=profile_uuid,
|
||||
UserUUID=_to_uuid_bytes(getattr(profile, 'UserUUID', None)),
|
||||
TenantUUID=_to_uuid_bytes(getattr(profile, 'TenantUUID', None)),
|
||||
CompanyUUID=_to_uuid_bytes(getattr(profile, 'CompanyUUID', None)),
|
||||
ContactType=_row['ContactType'],
|
||||
ContactValue=_row['ContactValue'],
|
||||
ContactSort=_row['ContactSort'],
|
||||
)
|
||||
|
||||
def add_record(self, data):
|
||||
table_name = data.get('Table', '')
|
||||
model = self.TABLE_MAP.get(table_name)
|
||||
@@ -2003,6 +2238,10 @@ class FormService(FormServiceBase):
|
||||
CompanyUUID=getattr(_urd, 'CompanyUUID', None),
|
||||
TenantUUID=getattr(_urd, 'TenantUUID', None),
|
||||
)
|
||||
# EmployeeProfile(员工档案):联系方式子表(虚拟列 Contacts)随档案一并提交,
|
||||
# 主表没有该列 → 档案落库后按提交明细整体重写子表(未提交该字段则保持原明细)
|
||||
if table_name == 'EmployeeProfile':
|
||||
self._RewriteEmployeeContacts(obj, self._ParseContactsPayload(data))
|
||||
# 审计日志:记录新增操作
|
||||
self._record_audit(table_name, 'add', target_obj=obj, new_data=self._get_record_snapshot(obj))
|
||||
# 工单创建:指派了处理人则通知对方(MsgType=2 工单通知)
|
||||
@@ -2251,6 +2490,10 @@ class FormService(FormServiceBase):
|
||||
self._send_workorder_notify(obj, _receiver, _wo_title, _wo_content)
|
||||
if update_fields:
|
||||
self._SaveObject(obj, update_fields)
|
||||
# EmployeeProfile(员工档案):联系方式子表(虚拟列 Contacts)随档案一并提交,
|
||||
# 主表没有该列 → 保存后按提交明细整体重写子表(未提交该字段则保持原明细)
|
||||
if table_name == 'EmployeeProfile':
|
||||
self._RewriteEmployeeContacts(obj, self._ParseContactsPayload(data))
|
||||
# UserRole 表(员工列表)编辑"部门/职务":UserRole 自身无这两个字段(虚拟列),
|
||||
# 提交时同步关系表 UserRoleDepartment(主关联:该 UserRole+公司首条)与职务子表
|
||||
if table_name == 'UserRole' and ('DeptUUID' in data or 'PositionUUID' in data or 'PositionUUID_id' in data):
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""员工档案(EmployeeProfile)字段重建 + 新增联系方式子表(EmployeeContact)
|
||||
|
||||
删除 EmployeeNo(工号);新增姓名/性别/年龄/种族/政治面貌/生日、网络昵称/志愿/志愿工作、
|
||||
从事行业、可参与工作的设备与备注、派发设备与奖罚机制意愿、期望工作时段、躯体疾病/精神障碍、
|
||||
紧急联系人的关系与自定义联系方式;保留入职日期/职务(只读联动)/学历/地址/紧急联系人/备注。
|
||||
新表 EmployeeContact 存档案的联系方式明细(档案 1:N)。
|
||||
|
||||
手写迁移:自动生成会夹带 communitycategory 等既有漂移,并触发交互式重命名询问。
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import gvsds.apps.core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gerp', '0047_app_pending_scope'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name='employeeprofile', name='EmployeeNo'),
|
||||
migrations.AlterField(
|
||||
model_name='employeeprofile', name='EmergencyContact',
|
||||
field=models.CharField(default='', max_length=32, verbose_name='紧急联系人姓名'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='EmployeeName',
|
||||
field=models.CharField(default='', max_length=64, verbose_name='姓名'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='Gender',
|
||||
field=models.CharField(default='', max_length=8, verbose_name='性别'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='AgeRange',
|
||||
field=models.CharField(default='', max_length=16, verbose_name='年龄'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='Race',
|
||||
field=models.CharField(default='', max_length=32, verbose_name='种族'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='PoliticalStatus',
|
||||
field=models.CharField(default='', max_length=16, verbose_name='政治面貌'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='Birthday',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='生日'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='Nickname',
|
||||
field=models.CharField(default='', max_length=64, verbose_name='网络常用昵称'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='VolunteerWish',
|
||||
field=models.TextField(default='', verbose_name='志愿'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='VolunteerWork',
|
||||
field=models.CharField(default='', max_length=16, verbose_name='志愿工作'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='VolunteerWorkOther',
|
||||
field=models.CharField(default='', max_length=64, verbose_name='其它志愿工作'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='Industry',
|
||||
field=models.CharField(default='', max_length=32, verbose_name='从事行业'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='Devices',
|
||||
field=models.CharField(default='', max_length=256, verbose_name='可参与工作的设备'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='DeviceRemark',
|
||||
field=models.CharField(default='', max_length=256, verbose_name='设备备注'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='AcceptDispatchDevice',
|
||||
field=models.BooleanField(default=False, verbose_name='是否愿意使用派发设备'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='AcceptRewardPunish',
|
||||
field=models.BooleanField(default=False, verbose_name='是否接受奖罚机制'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='ExpectedWorkHours',
|
||||
field=models.CharField(default='', max_length=128, verbose_name='期望工作时段'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='PhysicalIllness',
|
||||
field=models.CharField(default='', max_length=512, verbose_name='躯体疾病'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='MentalIllness',
|
||||
field=models.CharField(default='', max_length=512, verbose_name='精神障碍'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='EmergencyRelation',
|
||||
field=models.CharField(default='', max_length=16, verbose_name='与本人关系'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='EmergencyRelationOther',
|
||||
field=models.CharField(default='', max_length=32, verbose_name='其它关系说明'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employeeprofile', name='EmergencyContactCustom',
|
||||
field=models.CharField(default='', max_length=128, verbose_name='自定义联系方式'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmployeeContact',
|
||||
fields=[
|
||||
('EmployeeContactUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='联系方式UUID')),
|
||||
('EmployeeProfileUUID', gvsds.apps.core.models.UUIDField(db_column='EmployeeProfileUUID', db_index=True, verbose_name='档案UUID')),
|
||||
('UserUUID', gvsds.apps.core.models.UUIDField(blank=True, db_column='UserUUID', db_index=True, null=True, verbose_name='员工用户UUID')),
|
||||
('TenantUUID', gvsds.apps.core.models.UUIDField(blank=True, db_column='TenantUUID', null=True, verbose_name='租户UUID')),
|
||||
('CompanyUUID', gvsds.apps.core.models.UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||
('ContactType', models.SmallIntegerField(default=1, verbose_name='联系方式类型')),
|
||||
('ContactValue', models.CharField(default='', max_length=256, verbose_name='联系方式')),
|
||||
('ContactSort', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'EmployeeContact',
|
||||
'ordering': ['ContactSort', 'CreateTime'],
|
||||
'verbose_name': '员工联系方式',
|
||||
'verbose_name_plural': '员工联系方式',
|
||||
},
|
||||
),
|
||||
]
|
||||
Reference in new issue
Block a user