This commit is contained in:
anhduy-tech
2026-03-19 11:57:52 +07:00
parent 7fc8138f70
commit f76cd880e1
26 changed files with 1248 additions and 2719 deletions

View File

@@ -1041,277 +1041,6 @@ def set_token_expiry(request):
return Response(status = status.HTTP_200_OK)
#=============================================================================
class ExcelImportAPIView(APIView):
parser_classes = (MultiPartParser, FormParser)
def post(self, request, format=None):
excel_file = request.FILES.get('file')
if not excel_file:
return Response({'error': 'No Excel file provided (key "file" not found)'}, status=status.HTTP_400_BAD_REQUEST)
config_str = request.data.get('config')
if not config_str:
return Response({'error': 'No configuration provided (key "config" not found)'}, status=status.HTTP_400_BAD_REQUEST)
try:
config = json.loads(config_str)
except json.JSONDecodeError:
return Response({'error': 'Invalid JSON configuration'}, status=status.HTTP_400_BAD_REQUEST)
model_name = config.get('model_name')
mappings = config.get('mappings', [])
import_mode = config.get('import_mode', 'insert_only')
header_row_excel = config.get('header_row_index', 1)
header_index = max(0, header_row_excel - 1)
# LẤY VÀ PHÂN TÍCH TRƯỜNG UNIQUE KEY
unique_fields_config = config.get('unique_fields', 'code')
if isinstance(unique_fields_config, str):
UNIQUE_KEY_FIELDS = [unique_fields_config]
elif isinstance(unique_fields_config, list):
UNIQUE_KEY_FIELDS = unique_fields_config
else:
return Response({'error': 'Invalid format for unique_fields. Must be a string or a list of strings.'}, status=status.HTTP_400_BAD_REQUEST)
if not model_name or not mappings:
return Response({'error': 'model_name or mappings missing in configuration'}, status=status.HTTP_400_BAD_REQUEST)
try:
TargetModel = apps.get_model('app', model_name)
except LookupError:
return Response({'error': f'Model "{model_name}" not found in app'}, status=status.HTTP_400_BAD_REQUEST)
related_models_cache = {}
for mapping in mappings:
if 'foreign_key' in mapping:
fk_config = mapping['foreign_key']
related_model_name = fk_config.get('model_name')
if related_model_name:
try:
related_models_cache[related_model_name] = apps.get_model('app', related_model_name)
except LookupError:
return Response({'error': f"Related model '{related_model_name}' not found for mapping '{mapping.get('excel_column')}'"}, status=status.HTTP_400_BAD_REQUEST)
try:
file_stream = io.BytesIO(excel_file.read())
if excel_file.name.lower().endswith(('.xlsx', '.xls')):
df = pd.read_excel(file_stream, header=header_index)
else:
df = pd.read_csv(file_stream, header=header_index)
except Exception as e:
return Response({'error': f'Error reading file: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
cleaned_columns = []
for col in df.columns:
col_str = str(col).strip()
col_str = col_str.replace('\n', ' ').strip()
col_str = re.sub(r'\s*\([^)]*\)', '', col_str).strip()
col_str = ' '.join(col_str.split())
cleaned_columns.append(col_str)
df.columns = cleaned_columns
df.reset_index(drop=True, inplace=True)
# Caching Foreign Key objects
related_obj_cache = {}
for related_name, RelatedModel in related_models_cache.items():
lookup_field = next((m['foreign_key']['lookup_field'] for m in mappings if 'foreign_key' in m and m['foreign_key']['model_name'] == related_name), None)
if lookup_field:
try:
related_obj_cache[related_name] = {
str(getattr(obj, lookup_field)).strip().lower(): obj
for obj in RelatedModel.objects.all()
}
if 'pk' not in related_obj_cache[related_name]:
related_obj_cache[related_name].update({
str(obj.pk): obj for obj in RelatedModel.objects.all()
})
except Exception as e:
return Response({'error': f"Error caching related model {related_name}: {e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
objects_to_create = []
errors = []
for index, row in df.iterrows():
instance_data = {}
row_errors = []
is_valid_for_db = True
for mapping in mappings:
excel_column = mapping.get('excel_column')
model_field = mapping.get('model_field')
default_value = mapping.get('default_value')
excel_value = None
is_static_default = False
# 1. XÁC ĐỊNH NGUỒN GIÁ TRỊ (STATIC DEFAULT HOẶC EXCEL)
if not excel_column and default_value is not None:
# Trường hợp 1: Không có cột Excel, luôn dùng giá trị mặc định tĩnh
excel_value = default_value
is_static_default = True
elif excel_column and excel_column in row:
# Trường hợp 2: Có cột Excel
excel_value = row[excel_column]
is_static_default = False
# === BỔ SUNG: KIỂM TRA VÀ SỬ DỤNG default_value NẾU CELL RỖNG ===
# Nếu giá trị từ Excel rỗng VÀ có default_value được cung cấp trong mapping
if (pd.isna(excel_value) or (isinstance(excel_value, str) and str(excel_value).strip() == '')) and default_value is not None:
excel_value = default_value
is_static_default = True # Coi như giá trị tĩnh để bypass Section 2 (kiểm tra NULL)
# === KẾT THÚC BỔ SUNG ===
elif excel_column and excel_column not in row:
row_errors.append(f"Excel column '{excel_column}' not found (Header index: {header_row_excel})")
is_valid_for_db = False
continue
elif excel_column is None and default_value is None:
continue
else:
row_errors.append(f"Invalid mapping entry: {mapping} - requires excel_column or default_value")
is_valid_for_db = False
continue
# 2. XỬ LÝ NULL/EMPTY VALUES (Chỉ khi giá trị đến từ Excel và KHÔNG phải giá trị tĩnh)
if not is_static_default and (pd.isna(excel_value) or (isinstance(excel_value, str) and str(excel_value).strip() == '')):
try:
field_obj = TargetModel._meta.get_field(model_field)
except FieldDoesNotExist:
row_errors.append(f"Model field '{model_field}' not found in model '{model_name}'")
is_valid_for_db = False
continue
# Trường cho phép NULL
if field_obj.null:
instance_data[model_field] = None
continue
# Trường có Default Value (từ Model)
elif field_obj.default is not models_fields.NOT_PROVIDED:
instance_data[model_field] = field_obj.default
continue
# Trường KHÔNG cho phép NULL (Non-nullable field)
else:
# === START: LOGIC BỔ SUNG CHO allow_empty_excel_non_nullable ===
allow_empty_non_nullable = mapping.get('allow_empty_excel_non_nullable', False)
# Chỉ áp dụng bypass nếu là CharField/TextField (có thể lưu "" để thỏa mãn NOT NULL)
if allow_empty_non_nullable and isinstance(field_obj, (CharField, TextField)):
instance_data[model_field] = ""
continue # Chấp nhận chuỗi rỗng và đi tiếp
# Nếu không được phép bypass HOẶC không phải CharField/TextField
row_errors.append(f"Non-nullable field '{model_field}' has empty value in row {index + 1}")
is_valid_for_db = False
instance_data[model_field] = "" if isinstance(field_obj, (CharField, TextField)) else None
continue
# === END: LOGIC BỔ SUNG CHO allow_empty_excel_non_nullable ===
# 3. XỬ LÝ FOREIGN KEY
if 'foreign_key' in mapping:
fk_config = mapping['foreign_key']
related_model_name = fk_config.get('model_name')
key_to_lookup = str(excel_value).strip().lower()
RelatedModelCache = related_obj_cache.get(related_model_name, {})
related_obj = RelatedModelCache.get(key_to_lookup)
# Logic dự phòng để tìm theo ID nếu là giá trị tĩnh và là số
if not related_obj and is_static_default and str(excel_value).isdigit():
related_obj = RelatedModelCache.get(str(excel_value))
if related_obj:
instance_data[model_field] = related_obj
else:
# Kiểm tra lại trường hợp giá trị lookup là rỗng/0 khi model field cho phép NULL
if (pd.isna(excel_value) or str(excel_value).strip() == '' or str(excel_value).strip() == '0') and TargetModel._meta.get_field(model_field).null:
instance_data[model_field] = None
continue
# Báo lỗi và không hợp lệ nếu không tìm thấy object
row_errors.append(f"Related object for '{model_field}' with value '{excel_value}' not found in model '{related_model_name}' (row {index + 1})")
if not TargetModel._meta.get_field(model_field).null:
is_valid_for_db = False
instance_data[model_field] = None
continue
else:
instance_data[model_field] = excel_value
if row_errors:
errors.append({'row': index + 1, 'messages': row_errors})
if is_valid_for_db:
try:
objects_to_create.append(TargetModel(**instance_data))
except Exception as e:
errors.append({'row': index + 1, 'messages': [f"Critical error creating model instance: {str(e)}"]})
successful_row_count = len(objects_to_create)
try:
with transaction.atomic():
# === LOGIC XỬ LÝ CÁC CHẾ ĐỘ NHẬP DỮ LIỆU ===
if import_mode == 'overwrite':
TargetModel.objects.all().delete()
TargetModel.objects.bulk_create(objects_to_create)
message = f'{successful_row_count} records imported successfully after full **overwrite**.'
elif import_mode == 'upsert':
for field in UNIQUE_KEY_FIELDS:
try:
TargetModel._meta.get_field(field)
except FieldDoesNotExist:
return Response({'error': f"Unique field '{field}' not found in model '{model_name}'. Cannot perform upsert."}, status=status.HTTP_400_BAD_REQUEST)
existing_objects_query = TargetModel.objects.only('pk', *UNIQUE_KEY_FIELDS)
existing_map = {}
for obj in existing_objects_query:
key_tuple = tuple(getattr(obj, field) for field in UNIQUE_KEY_FIELDS)
existing_map[key_tuple] = obj
to_update = []
to_insert = []
for new_instance in objects_to_create:
try:
lookup_key = tuple(getattr(new_instance, field) for field in UNIQUE_KEY_FIELDS)
except AttributeError:
continue
if lookup_key in existing_map:
new_instance.pk = existing_map[lookup_key].pk
to_update.append(new_instance)
else:
to_insert.append(new_instance)
update_fields = [
m['model_field']
for m in mappings
if m['model_field'] not in ['pk'] and m['model_field'] not in UNIQUE_KEY_FIELDS
]
TargetModel.objects.bulk_update(to_update, update_fields)
TargetModel.objects.bulk_create(to_insert)
message = f'{len(to_insert)} records inserted, {len(to_update)} records updated successfully (Upsert mode).'
elif import_mode == 'insert_only':
TargetModel.objects.bulk_create(objects_to_create)
message = f'{successful_row_count} records imported successfully (Insert Only mode).'
else:
return Response({'error': f"Invalid import_mode specified: {import_mode}"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': f'Database error during bulk operation (Rollback occurred): {str(e)}', 'rows_attempted': successful_row_count}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if errors:
return Response({'status': 'partial_success', 'message': f'{message} Invalid rows were skipped.', 'errors': errors}, status=status.HTTP_207_MULTI_STATUS)
return Response({'status': 'success', 'message': message}, status=status.HTTP_201_CREATED)
#=============================================================================
executor = ThreadPoolExecutor(max_workers=10)
def background_generate(doc_code, context_pks, output_filename, uid):
@@ -1545,29 +1274,6 @@ class EmailTemplatePreview:
@api_view(['POST'])
def preview_email_template(request):
"""
API để preview email template - trả về nội dung đã thay thế placeholders
POST /api/email/preview/
Body: {
"template_id": 1,
"context_pks": {
"contract_id": 456,
"customer_id": 789
}
}
Response: {
"subject": "Thông báo hợp đồng #HD-001",
"content": "<div>Nội dung y nguyên đã thay thế...</div>",
"recipient_email": "customer@example.com",
"replacements": {
"[contract.code]": "HD-001",
"[customer.name]": "Nguyễn Văn A",
...
}
}
"""
try:
# Validate input
template_id = request.data.get('template_id')