changes
This commit is contained in:
294
app/views.py
294
app/views.py
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user