From e23db9dc6c6de6b0525f8df646bcce1a240b3c4e Mon Sep 17 00:00:00 2001 From: zty Date: Fri, 31 Jul 2026 03:47:29 -0400 Subject: [PATCH] =?UTF-8?q?feat(hrm):=20=E5=91=98=E5=B7=A5Excel=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=94=AF=E6=8C=81=E5=85=A8=E9=87=8F=E8=A6=86=E7=9B=96?= =?UTF-8?q?(=E7=A9=BA=E5=8D=95=E5=85=83=E6=A0=BC=E6=B8=85=E7=A9=BA,?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E7=A9=BA=E5=AD=97=E6=AE=B5=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=8E=9F=E5=80=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- apps/hrm/views.py | 58 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/apps/hrm/views.py b/apps/hrm/views.py index 9b9cc201..b0558c44 100755 --- a/apps/hrm/views.py +++ b/apps/hrm/views.py @@ -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):