diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index ca5a46d3..369ef950 100644 Binary files a/api/__pycache__/settings.cpython-313.pyc and b/api/__pycache__/settings.cpython-313.pyc differ diff --git a/api/__pycache__/urls.cpython-313.pyc b/api/__pycache__/urls.cpython-313.pyc index d46f4783..07f6f1f5 100644 Binary files a/api/__pycache__/urls.cpython-313.pyc and b/api/__pycache__/urls.cpython-313.pyc differ diff --git a/api/urls.py b/api/urls.py index a27d1693..7f505280 100644 --- a/api/urls.py +++ b/api/urls.py @@ -53,5 +53,6 @@ urlpatterns = [ re_path('generate-document/$',views.generate_document), re_path('model-fields/(?P.+)/', importdata.model_fields), re_path('read-excel/', importdata.read_excel), - re_path('find-key/$', importdata.find_key) + re_path('find-key/$', importdata.find_key), + re_path('email-preview/$', views.preview_email_template) ] \ No newline at end of file diff --git a/app/__pycache__/cleardata.cpython-313.pyc b/app/__pycache__/cleardata.cpython-313.pyc index 10b9a35c..2adf5a09 100644 Binary files a/app/__pycache__/cleardata.cpython-313.pyc and b/app/__pycache__/cleardata.cpython-313.pyc differ diff --git a/app/__pycache__/payment.cpython-313.pyc b/app/__pycache__/payment.cpython-313.pyc index e3cf370c..e9e6ac4d 100644 Binary files a/app/__pycache__/payment.cpython-313.pyc and b/app/__pycache__/payment.cpython-313.pyc differ diff --git a/app/__pycache__/views.cpython-313.pyc b/app/__pycache__/views.cpython-313.pyc index 047e7394..bce3d40e 100644 Binary files a/app/__pycache__/views.cpython-313.pyc and b/app/__pycache__/views.cpython-313.pyc differ diff --git a/app/payment.py b/app/payment.py index a8728d6c..a5c26a50 100644 --- a/app/payment.py +++ b/app/payment.py @@ -3,24 +3,22 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from django.db import transaction from datetime import datetime +from decimal import Decimal +from django.db.models import F +import threading - -decimal = 0 -#========================================================================================== +# ========================================================================================== def getcode(code, Model): - try: - obj = Model.objects.latest('id') - val = 1 if obj == None else obj.id + 1 - except Exception as e: - val = 1 - length = len(str(val)) - text = '' - for i in range(0, 6-length): - text += '0' - return '{}{}{}'.format(code, text, val) + try: + obj = Model.objects.latest('id') + val = 1 if obj is None else obj.id + 1 + except Exception: + val = 1 + length = len(str(val)) + text = '0' * (6 - length) + return f"{code}{text}{val}" - -#========================================================================================== +# ========================================================================================== def account_entry_api(code, amount, content, type, category, userid, ref=None, product=None, customer=None, date=None): try: user = User.objects.get(id=userid) @@ -34,10 +32,9 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p start_balance = account.balance or 0 if entry_type.code == 'DR' and start_balance < amount: - return {'error': 'Số dư không đủ để thực hiện giao dịch.'} + return {'error': 'Số dư không đủ để thực hiện giao dịch.'}, None if entry_type.code == 'CR': - # account.balance = F('balance') + amount account.balance += amount else: account.balance -= amount @@ -58,72 +55,413 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p account=account, date=system_date, ref=ref, - product=None if product==None else Product.objects.get(id=product), - customer=None if customer==None else Customer.objects.get(id=customer) + product=None if product is None else Product.objects.get(id=product), + customer=None if customer is None else Customer.objects.get(id=customer) ) text = 'id,account__currency__code,ref,balance_before,balance_after,code,account,account__code,account__branch__name,account__type__name,date,amount,content,inputer,inputer__fullname,approver,approver__fullname,create_time,update_time,type,type__code,type__name' fields = text.split(',') - return Internal_Entry.objects.filter(id=entry.id).values(*fields).first() + response_data = Internal_Entry.objects.filter(id=entry.id).values(*fields).first() + return response_data, entry except User.DoesNotExist: - return {'error': f"Người dùng với ID {userid} không tồn tại."} + return {'error': f"Người dùng với ID {userid} không tồn tại."}, None except Internal_Account.DoesNotExist: - return {'error': f"Tài khoản nội bộ với mã '{code}' không tồn tại."} + return {'error': f"Tài khoản nội bộ với mã '{code}' không tồn tại."}, None except Entry_Type.DoesNotExist: - return {'error': f"Loại bút toán với mã '{type}' không tồn tại."} + return {'error': f"Loại bút toán với mã '{type}' không tồn tại."}, None except Entry_Category.DoesNotExist: - return {'error': f"Danh mục bút toán với ID '{category}' không tồn tại."} + return {'error': f"Danh mục bút toán với ID '{category}' không tồn tại."}, None except Exception as e: - return {'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"} + return {'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}, None -#========================================================================================== +# ========================================================================================== +# HÀM LẤY RULE TỪ BIZ_SETTING (detail là string) +# ========================================================================================== +def get_allocation_rule(): + try: + rule_setting = Biz_Setting.objects.get(code='rule') + rule_value = (rule_setting.detail or 'principal-fee').strip() + + if rule_value.lower() in ['fee-principal', 'phạt trước', 'phat truoc']: + return 'fee-principal' + else: + return 'principal-fee' + + except Biz_Setting.DoesNotExist: + return 'principal-fee' + +# ========================================================================================== +# HÀM PHÂN BỔ THÔNG THƯỜNG +# ========================================================================================== +def allocate_payment_to_schedules(entries): + if not entries: + return {"status": "no_entries", "message": "Không có bút toán"} + + allocation_rule = get_allocation_rule() + updated_schedules = [] + errors = [] + + # Lấy status "đã thanh toán" một lần + paid_payment_status = None + paid_txn_status = None + try: + paid_payment_status = Payment_Status.objects.get(id=2) + except Payment_Status.DoesNotExist: + errors.append("Không tìm thấy Payment_Status id=2 (đã thanh toán)") + + try: + paid_txn_status = Transaction_Status.objects.get(id=2) + except Transaction_Status.DoesNotExist: + errors.append("Không tìm thấy Transaction_Status id=2 (đã thanh toán)") + + for entry in entries: + if entry.type.code != 'CR' or entry.account.id == 5: + continue + + amount = Decimal(str(entry.amount)) + if amount <= 0: + continue + + with transaction.atomic(): + try: + booked = Product_Booked.objects.filter(product=entry.product).first() + if not booked or not booked.transaction: + errors.append(f"Entry {entry.code}: Không tìm thấy Transaction") + continue + + txn = booked.transaction + + txn_detail = None + try: + current = Transaction_Current.objects.get(transaction=txn) + txn_detail = current.detail + except (Transaction_Current.DoesNotExist, AttributeError): + txn_detail = Transaction_Detail.objects.filter( + transaction=txn + ).order_by('-create_time').first() + + if not txn_detail: + errors.append(f"Entry {entry.code}: Không tìm thấy Transaction_Detail") + continue + + schedules = Payment_Schedule.objects.filter( + txn_detail=txn_detail, + status__id=1 # giả sử status=1 là chưa thanh toán + ).order_by('cycle', 'from_date') + + if not schedules.exists(): + continue + + remaining = amount + total_allocated = Decimal('0') + + for sch in schedules: + if remaining <= 0: + break + + penalty_remain = Decimal(str(sch.penalty_remain or 0)) + amount_remain = Decimal(str(sch.amount_remain or 0)) + paid_amount = Decimal(str(sch.paid_amount or 0)) + penalty_paid = Decimal(str(sch.penalty_paid or 0)) + remain_amount = Decimal(str(sch.remain_amount or 0)) + + to_penalty = Decimal('0') + to_principal = Decimal('0') + + if allocation_rule == 'fee-principal': + to_penalty = min(remaining, penalty_remain) + remaining -= to_penalty + penalty_paid += to_penalty + penalty_remain -= to_penalty + to_principal = min(remaining, amount_remain) + remaining -= to_principal + paid_amount += to_principal + amount_remain -= to_principal + else: + to_principal = min(remaining, amount_remain) + remaining -= to_principal + paid_amount += to_principal + amount_remain -= to_principal + to_penalty = min(remaining, penalty_remain) + remaining -= to_penalty + penalty_paid += to_penalty + penalty_remain -= to_penalty + + allocated_here = to_penalty + to_principal + total_allocated += allocated_here + + sch.paid_amount = paid_amount + sch.penalty_paid = penalty_paid + sch.amount_remain = amount_remain + sch.penalty_remain = penalty_remain + sch.remain_amount = max(Decimal('0'), remain_amount - allocated_here) + + entry_list = sch.entry or [] + + date_value = entry.date + if hasattr(date_value, 'isoformat'): + date_value = date_value.isoformat() + else: + date_value = str(date_value) + + entry_list.append({ + "code": entry.code, + "amount": float(allocated_here), + "date": date_value, + "type": "CR", + "principal": float(to_principal), + "penalty": float(to_penalty), + "rule": allocation_rule + }) + sch.entry = entry_list + + if sch.amount_remain <= 0 and sch.penalty_remain <= 0 and paid_payment_status: + sch.status = paid_payment_status + + sch.save(update_fields=[ + 'paid_amount', 'penalty_paid', 'amount_remain', + 'penalty_remain', 'remain_amount', 'entry', 'status' + ]) + + updated_schedules.append(sch.id) + + # Cập nhật Transaction_Detail + txn_detail.amount_received = F('amount_received') + total_allocated + txn_detail.amount_remaining = F('amount_remaining') - total_allocated + txn_detail.save() + txn_detail.refresh_from_db() + + if txn_detail.amount_remaining <= 0 and paid_txn_status: + txn_detail.status = paid_txn_status + txn_detail.save(update_fields=['status']) + + # Cập nhật Transaction + txn.amount_received = F('amount_received') + total_allocated + txn.amount_remain = F('amount_remain') - total_allocated + txn.save() + txn.refresh_from_db() + + if txn.amount_remain <= 0 and paid_txn_status: + txn.status = paid_txn_status + txn.save(update_fields=['status']) + + except Exception as exc: + errors.append(f"Entry {entry.code}: Lỗi phân bổ - {str(exc)}") + + return { + "status": "success" if not errors else "partial_failure", + "updated_schedules": updated_schedules, + "errors": errors, + "rule_used": allocation_rule + } + +# ========================================================================================== +# HÀM MIỄN LÃI +# ========================================================================================== +def allocate_penalty_reduction(entries): + if not entries: + return {"status": "no_entries", "message": "Không có bút toán"} + + updated_schedules = [] + errors = [] + + for entry in entries: + if entry.type.code != 'CR' or entry.account.id != 5: + continue # Chỉ xử lý bút toán từ tài khoản miễn lãi + + amount = Decimal(str(entry.amount)) + if amount <= 0: + continue + + with transaction.atomic(): + try: + booked = Product_Booked.objects.filter(product=entry.product).first() + if not booked or not booked.transaction: + errors.append(f"Entry {entry.code}: Không tìm thấy Transaction") + continue + + txn = booked.transaction + + txn_detail = None + try: + current = Transaction_Current.objects.get(transaction=txn) + txn_detail = current.detail + except (Transaction_Current.DoesNotExist, AttributeError): + txn_detail = Transaction_Detail.objects.filter( + transaction=txn + ).order_by('-create_time').first() + + if not txn_detail: + errors.append(f"Entry {entry.code}: Không tìm thấy Transaction_Detail") + continue + + # Lấy các lịch chưa thanh toán (status=1) + schedules = Payment_Schedule.objects.filter( + txn_detail=txn_detail, + status=1 + ).order_by('cycle', 'from_date') + + if not schedules.exists(): + continue + + remaining_reduce = amount # số tiền miễn lãi còn lại để phân bổ + + for schedule in schedules: + if remaining_reduce <= 0: + break + + current_penalty_remain = Decimal(str(schedule.penalty_remain or 0)) + current_penalty_reduce = Decimal(str(schedule.penalty_reduce or 0)) + current_remain_amount = Decimal(str(schedule.remain_amount or 0)) + + # Chỉ miễn tối đa bằng số phạt còn lại + to_reduce = min(remaining_reduce, current_penalty_remain) + remaining_reduce -= to_reduce + + # Cập nhật các trường + schedule.penalty_reduce = current_penalty_reduce + to_reduce + schedule.penalty_remain = current_penalty_remain - to_reduce + + # GIẢM TỔNG CÒN LẠI (remain_amount) + schedule.remain_amount = max(Decimal('0'), current_remain_amount - to_reduce) + + # KHÔNG ĐỘNG ĐẾN amount_remain (nợ gốc còn lại) + + # Ghi trace bút toán miễn lãi + entry_list = schedule.entry or [] + + date_value = entry.date + if hasattr(date_value, 'isoformat'): + date_value = date_value.isoformat() + else: + date_value = str(date_value) + + entry_list.append({ + "code": entry.code, + "amount": float(to_reduce), + "date": date_value, + "type": "REDUCTION", + "note": "Miễn lãi phạt quá hạn" + }) + schedule.entry = entry_list + + # Lưu lại + schedule.save(update_fields=[ + 'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry' + ]) + + updated_schedules.append(schedule.id) + + except Exception as exc: + errors.append(f"Entry {entry.code}: Lỗi miễn lãi - {str(exc)}") + + return { + "status": "success" if not errors else "partial_failure", + "updated_schedules": updated_schedules, + "errors": errors, + "message": f"Đã miễn lãi cho {len(updated_schedules)} lịch thanh toán (chỉ giảm phạt + tổng còn lại)" + } + +# ========================================================================================== +# BACKGROUND FUNCTION +# ========================================================================================== +def background_allocate(entries_created): + try: + # Debug type date + for e in entries_created: + print(f"Debug entry {e.code}: date type = {type(e.date)}, value = {e.date}") + + normal_result = allocate_payment_to_schedules(entries_created) + reduction_result = allocate_penalty_reduction(entries_created) + + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation completed:") + print("Normal:", normal_result) + print("Reduction:", reduction_result) + + except Exception as e: + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation error: {str(e)}") + +# ========================================================================================== +# API TẠO MỘT BÚT TOÁN +# ========================================================================================== @api_view(['POST']) def account_entry(request): - print(request.data.get('date')) - ref = request.data.get('ref') - data = account_entry_api( - code=request.data['code'], - amount=request.data['amount'], - content=request.data['content'], - type=request.data['type'], - category=request.data['category'], - userid=request.data['user'], - ref=ref, - product=request.data['product'], - customer=request.data['customer'], - date=request.data.get('date') - ) - - if 'error' in data: - return Response(data, status=400) - - return Response(data) + print(request.data.get('date')) + ref = request.data.get('ref') + response_data, created_entry = account_entry_api( + code=request.data['code'], + amount=request.data['amount'], + content=request.data['content'], + type=request.data['type'], + category=request.data['category'], + userid=request.data['user'], + ref=ref, + product=request.data['product'], + customer=request.data['customer'], + date=request.data.get('date') + ) -#========================================================================================== + if 'error' in response_data: + return Response(response_data, status=400) + + if created_entry: + thread = threading.Thread( + target=background_allocate, + args=([created_entry],), + daemon=True + ) + thread.start() + + return Response({ + **response_data, + "message": "Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm..." + }) + +# ========================================================================================== +# API TẠO NHIỀU BÚT TOÁN +# ========================================================================================== @api_view(['POST']) def account_multi_entry(request): try: result = [] - data = request.data.get('data') - for obj in data: - row = account_entry_api( - code=obj['Tài khoản'], - amount=obj['amount'], - content=obj['content'], - type='CR', - category=obj['category'], - userid=request.data.get('user'), - ref=obj['ref'], - product=obj['product'], - customer=obj['customer'], - date=obj['date'] - ) - result.append(row) + entries_created = [] + data_list = request.data.get('data', []) + + with transaction.atomic(): + for obj in data_list: + response_data, created_entry = account_entry_api( + code=obj['Tài khoản'], + amount=obj['amount'], + content=obj['content'], + type='CR', + category=obj['category'], + userid=request.data.get('user'), + ref=obj['ref'], + product=obj['product'], + customer=obj['customer'], + date=obj['date'] + ) + + result.append(response_data) + + if created_entry: + entries_created.append(created_entry) + + if entries_created: + thread = threading.Thread( + target=background_allocate, + args=(entries_created,), + daemon=True + ) + thread.start() + + return Response({ + "entries": result, + "message": "Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm..." + }) - return Response(result) - except Exception as e: print({'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}) - return Response(data, status=400) \ No newline at end of file + return Response({'error': str(e)}, status=400) \ No newline at end of file diff --git a/app/views.py b/app/views.py index 0b3ed68a..b74f7325 100644 --- a/app/views.py +++ b/app/views.py @@ -27,6 +27,8 @@ from concurrent.futures import ThreadPoolExecutor import json import io import pandas as pd +import numpy as np +from num2words import num2words from django.db import transaction from django.apps import apps from django.core.exceptions import FieldDoesNotExist @@ -1347,4 +1349,266 @@ def generate_document(request): time.sleep(5) count += 1 - return Response({"error": "Timeout chờ generate."}, status=status.HTTP_408_REQUEST_TIMEOUT) \ No newline at end of file + return Response({"error": "Timeout chờ generate."}, status=status.HTTP_408_REQUEST_TIMEOUT) + +#============================================================================= +# EMAIL TEMPLATE PREVIEW +#============================================================================= +class EmailTemplatePreview: + """ + Class để preview nội dung email template với dữ liệu đã được map + Không gửi email, chỉ trả về nội dung đã render + """ + + def __init__(self, template, context_pks: dict): + self.template = template + self.context_pks = context_pks + self.config = self.template.content + self.data_context = {} + self.replacements = {} + + def _get_model(self, model_string): + """Lấy model class từ string 'app.Model'""" + app_label, model_name = model_string.split(".") + return apps.get_model(app_label, model_name) + + def _get_value_from_object(self, obj, field_path): + """Lấy giá trị từ object theo field path (hỗ trợ nested: 'user.profile.name')""" + if obj is None: + return None + value = obj + for part in field_path.replace("__", ".").split("."): + if value is None: + return None + value = getattr(value, part, None) + return value + + def _resolve_lookup_value(self, lookup_from): + """Resolve giá trị lookup từ context_pks hoặc data_context""" + if lookup_from in self.context_pks: + return self.context_pks[lookup_from] + + try: + alias, field_path = lookup_from.split(".", 1) + if alias not in self.data_context: + raise ValueError(f"Alias '{alias}' not found in data context.") + + source_object = self.data_context.get(alias) + return self._get_value_from_object(source_object, field_path) + except ValueError: + raise ValueError(f"Could not resolve '{lookup_from}'.") + + def fetch_data(self): + """Fetch data từ database theo mappings config""" + mappings = self.config.get("mappings", []) + if not isinstance(mappings, list): + raise TypeError("Email template 'mappings' must be a list.") + + # Process trigger object first + trigger_model_mapping = next((m for m in mappings if m.get("is_trigger_object", False)), None) + if trigger_model_mapping: + model_cls = self._get_model(trigger_model_mapping["model"]) + lookup_field = trigger_model_mapping["lookup_field"] + lookup_value = self._resolve_lookup_value(trigger_model_mapping["lookup_value_from"]) + alias = trigger_model_mapping["alias"] + if lookup_value is not None: + self.data_context[alias] = model_cls.objects.filter(**{lookup_field: lookup_value}).first() + else: + self.data_context[alias] = None + + # Process other mappings + for mapping in mappings: + if mapping.get("is_trigger_object", False): + continue + + model_cls = self._get_model(mapping["model"]) + lookup_field = mapping["lookup_field"] + lookup_value = self._resolve_lookup_value(mapping["lookup_value_from"]) + alias = mapping["alias"] + + if lookup_value is None: + self.data_context[alias] = None if mapping.get("type") == "object" else [] + continue + + queryset = model_cls.objects.filter(**{lookup_field: lookup_value}) + + if mapping.get("type") == "object": + self.data_context[alias] = queryset.first() + elif mapping.get("type") == "list": + self.data_context[alias] = list(queryset) + + def _format_value(self, value, format_config): + """Format value theo config (currency, date, number_to_words, conditional)""" + if value is None: + return "" + + format_type = format_config.get("type") + if not format_type: + return str(value) + + try: + if format_type == "currency": + return "{:,}".format(np.int64(value)).replace(",", ".") + if format_type == "date": + date_format = format_config.get("format", "dd/mm/YYYY").replace("dd", "%d").replace("mm", "%m").replace("YYYY", "%Y") + return value.strftime(date_format) + if format_type == "number_to_words": + return num2words(value, lang=format_config.get("lang", "vi")) + if format_type == "conditional": + return format_config["true_value"] if value else format_config["false_value"] + except Exception as e: + print(f"Error formatting value '{value}' with config '{format_config}': {e}") + return "" + + return str(value) + + def prepare_replacements(self): + """Chuẩn bị dict replacements cho placeholders""" + # Add date placeholders + today = datetime.now() + self.replacements['[day]'] = str(today.day) + self.replacements['[month]'] = str(today.month) + self.replacements['[year]'] = str(today.year) + self.replacements['[date]'] = today.strftime("%d/%m/%Y") + + # Process field mappings + mappings = self.config.get("mappings", []) + for mapping in mappings: + alias = mapping["alias"] + data = self.data_context.get(alias) + fields = mapping.get("fields", {}) + + if mapping.get("type") == "object": + if data is None: + for placeholder in fields: + self.replacements[placeholder] = "" + continue + + for placeholder, config in fields.items(): + if isinstance(config, dict): + value = self._get_value_from_object(data, config["source"]) + self.replacements[placeholder] = self._format_value(value, config.get("format", {})) + else: + value = self._get_value_from_object(data, config) + self.replacements[placeholder] = str(value) if value is not None else "" + + def get_preview(self): + """ + Main method để lấy preview email content - trả về nội dung y nguyên đã thay thế placeholders + + Returns: + dict: { + 'subject': subject đã thay thế placeholders, + 'content': body content đã thay thế placeholders (giữ nguyên format, căn lề), + 'recipient_email': email người nhận, + 'replacements': dict của tất cả replacements được áp dụng + } + """ + try: + print(f"Generating preview for template: {self.template.name}") + + # Fetch data and prepare replacements + self.fetch_data() + self.prepare_replacements() + + # Get templates from config + subject_template = self.config.get("subject", "") + body_template = self.config.get("content", "") + recipient_placeholder = self.config.get("recipient_placeholder", "[customer.email]") + + # Apply replacements - giữ nguyên format, căn lề của template gốc + final_subject = subject_template + final_content = body_template + + for key, value in self.replacements.items(): + final_subject = final_subject.replace(key, str(value)) + final_content = final_content.replace(key, str(value)) + + recipient_email = self.replacements.get(recipient_placeholder, "") + + result = { + 'subject': final_subject, + 'content': final_content, # Nội dung y nguyên đã thay thế + 'recipient_email': recipient_email, + 'replacements': self.replacements.copy() + } + + print(f"Preview generated successfully for '{self.template.name}'") + return result + + except Exception as e: + print(f"Error generating preview for template '{self.template.name}': {e}") + import traceback + traceback.print_exc() + return None + + +@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": "
Nội dung y nguyên đã thay thế...
", + "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') + if not template_id: + return Response( + {'error': 'template_id is required'}, + status=status.HTTP_400_BAD_REQUEST + ) + + context_pks = request.data.get('context_pks', {}) + if not isinstance(context_pks, dict): + return Response( + {'error': 'context_pks must be a dictionary'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Get template + try: + template = Email_Template.objects.get(pk=template_id) + except Email_Template.DoesNotExist: + return Response( + {'error': f'Email_Template with id={template_id} does not exist'}, + status=status.HTTP_404_NOT_FOUND + ) + + # Generate preview + previewer = EmailTemplatePreview(template, context_pks) + preview = previewer.get_preview() + + if preview: + return Response(preview, status=status.HTTP_200_OK) + else: + return Response( + {'error': 'Failed to generate preview'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + except Exception as e: + import traceback + traceback.print_exc() + return Response( + {'error': f'Unexpected error: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file