feat(hrm): 员工Excel导入支持全量覆盖(空单元格清空,不可空字段保留原值)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zty 2026-07-31 03:47:29 -04:00
parent cf2800daa0
commit e23db9dc6c
1 changed files with 44 additions and 14 deletions

View File

@ -390,7 +390,24 @@ class EmployeeViewSet(CustomModelViewSet):
# 获取模型字段
model_fields = {f.name: f for f in Employee._meta.fields}
from django.db.models import CharField, TextField
_ABSENT = object() # 区分“列没出现”与“列为空”
_KEEP = object() # 该字段不可清空时,保留原值
def empty_value(field_name):
"""全量覆盖时,空单元格应写入的“清空值”;不可清空的字段返回 _KEEP。"""
if field_name == 'belong_dept_id':
return None # 部门 FK 可空
mf = model_fields.get(field_name)
if mf is None:
return _KEEP
if getattr(mf, 'null', False):
return None
if isinstance(mf, (CharField, TextField)) and getattr(mf, 'blank', False):
return ''
return _KEEP # 非空且不可为空(如布尔/性别/类别)-> 保留原值
for row_num, row in enumerate(rows[1:], start=2):
try:
data = {}
@ -419,12 +436,15 @@ class EmployeeViewSet(CustomModelViewSet):
data['type'] = TYPE_MAPPING[excel_type]
else:
raise ParseError(f'{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}')
# 处理部门外键:填了就校验是否存在并赋值;为空时不动(新增场景的必填在下方创建处校验)
dept_name = data.pop('belong_dept', None)
if dept_name:
if dept_name not in dept_map:
raise ParseError(f'{row_num}行,部门"{dept_name}"不存在')
data['belong_dept_id'] = dept_map[dept_name]
# 处理部门外键:填了校验并赋值;列存在但为空 -> 清空(新增场景的必填在创建处校验)
dept_raw = data.pop('belong_dept', _ABSENT)
if dept_raw is not _ABSENT:
if dept_raw:
if dept_raw not in dept_map:
raise ParseError(f'{row_num}行,部门"{dept_raw}"不存在')
data['belong_dept_id'] = dept_map[dept_raw]
else:
data['belong_dept_id'] = None
# 数据验证
if data.get('phone'):
@ -454,11 +474,14 @@ class EmployeeViewSet(CustomModelViewSet):
(e for e in name_matches if e.id_number == id_number), None)
if existing:
# 用 Excel 中填写了值的列覆盖数据库已有数据;空单元格保持原值不变
# 全量覆盖:以本次 Excel 为准。填了值的列覆盖原值;
# 空单元格清空原值(不可为空的字段如布尔/性别/类别则保留原值)。
updated_fields = []
for field_name, value in data.items():
if value in [None, '']:
continue
value = empty_value(field_name)
if value is _KEEP:
continue
if getattr(existing, field_name, None) != value:
setattr(existing, field_name, value)
updated_fields.append(field_name)
@ -470,9 +493,11 @@ class EmployeeViewSet(CustomModelViewSet):
created = False
else:
# 新增人员时所属部门必填
if 'belong_dept_id' not in data:
if not data.get('belong_dept_id'):
raise ParseError(f'{row_num}行,新增人员时所属部门不能为空')
Employee.objects.create(**data)
# 新增时丢弃空值,交给模型默认值/允许为空
create_data = {k: v for k, v in data.items() if v is not None}
Employee.objects.create(**create_data)
created = True
except Exception as e:
raise
@ -501,18 +526,23 @@ def convert_field_value(model_field, value, row_num):
# ===== 日期字段 =====
if isinstance(model_field, CharField):
sval = str(value)
if isinstance(value, str):
if 'e' in value.lower():
try:
return str(int(float(value)))
sval = str(int(float(value)))
except ValueError:
pass
if '.' in value:
try:
return str(int(float(value)))
sval = str(int(float(value)))
except ValueError:
pass
return str(value)
# 超长字段给出明确提示,避免数据库报 value too long
if model_field.max_length and len(sval) > model_field.max_length:
raise ParseError(
f'{row_num}行,字段 {model_field.verbose_name} 长度超过 {model_field.max_length} 个字符(当前 {len(sval)}')
return sval
if isinstance(model_field, DateField):