feat(hrm): 员工Excel导入支持全量覆盖(空单元格清空,不可空字段保留原值)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cf2800daa0
commit
e23db9dc6c
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue