from app.models import * from rest_framework.decorators import api_view from rest_framework.response import Response from django.db import transaction from datetime import datetime, date from decimal import Decimal from django.db.models import F import threading import json # ========================================================================================== # HELPER FUNCTIONS # ========================================================================================== def safe_json_serialize(obj): """Serialize an toàn cho JSONField""" if isinstance(obj, (datetime, date)): return obj.isoformat() if isinstance(obj, dict): return {k: safe_json_serialize(v) for k, v in obj.items()} if isinstance(obj, (list, tuple)): return [safe_json_serialize(item) for item in obj] return obj def get_latest_payment_date(schedule): """Lấy ngày nộp gần nhất từ entry (type='PAYMENT')""" if not schedule.entry: return None entry_data = schedule.entry if isinstance(entry_data, str): try: entry_data = json.loads(entry_data) except json.JSONDecodeError: return None if not isinstance(entry_data, list): return None payment_dates = [] for item in entry_data: if item.get('type') == 'PAYMENT' and item.get('date'): try: dt = datetime.strptime(item['date'], "%Y-%m-%d").date() payment_dates.append(dt) except: pass if payment_dates: return max(payment_dates) return None # ========================================================================================== def getcode(code, Model): 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) entry_type = Entry_Type.objects.get(code=type) entry_category = Entry_Category.objects.get(id=category) system_date = date if date else datetime.now().strftime("%Y-%m-%d") amount = float(amount) with transaction.atomic(): account = Internal_Account.objects.select_for_update().get(code=code) 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.'}, None if entry_type.code == 'CR': account.balance += amount else: account.balance -= amount account.save() account.refresh_from_db() new_balance = account.balance entry = Internal_Entry.objects.create( category=entry_category, content=content, amount=amount, inputer=user, approver=user, type=entry_type, balance_before=start_balance, balance_after=new_balance, account=account, date=system_date, ref=ref, product=None if product is None else Product.objects.get(id=product), customer=None if customer is None else Customer.objects.get(id=customer), allocation_amount=Decimal('0'), allocation_remain=Decimal(str(amount)) if entry_type.code == 'CR' else Decimal('0'), allocation_detail=[] ) 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,allocation_amount,allocation_remain' fields = text.split(',') 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."}, 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."}, None except Entry_Type.DoesNotExist: 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."}, None except Exception as e: return {'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}, None # ========================================================================================== def get_allocation_rule(): return 'principal-fee' # ========================================================================================== # LOGIC TÍNH LÃI ĐÚNG - CÁCH ĐƠN GIẢN NHẤT # ========================================================================================== def recalculate_penalty_amount(schedule, up_to_date=None): """ TÍNH LẠI TOÀN BỘ LÃI PHẠT từ đầu đến ngày chỉ định Logic: 1. Quét tất cả các PAYMENT entry để xây dựng timeline 2. Tính lãi cho từng khoảng thời gian với gốc tương ứng 3. Trả về tổng lãi (bao gồm cả lãi đã trả + lãi còn lại) Đây là cách DUY NHẤT để tránh cộng dồn sai! """ if up_to_date is None: up_to_date = datetime.now().date() elif isinstance(up_to_date, str): up_to_date = datetime.strptime(up_to_date, "%Y-%m-%d").date() to_date = schedule.to_date if isinstance(to_date, datetime): to_date = to_date.date() # Nếu chưa quá hạn if up_to_date <= to_date: return Decimal('0') # Xây dựng timeline: [(date, gốc còn lại sau ngày đó)] timeline = [] # Lấy tất cả PAYMENT entries, sắp xếp theo thời gian entry_data = schedule.entry or [] if isinstance(entry_data, str): try: entry_data = json.loads(entry_data) except json.JSONDecodeError: entry_data = [] payments = [e for e in entry_data if e.get('type') == 'PAYMENT'] payments.sort(key=lambda x: x.get('date', '')) # Điểm bắt đầu: to_date với gốc ban đầu original_amount = Decimal(str(schedule.amount or 0)) current_principal = original_amount timeline.append((to_date, current_principal)) # Thêm các điểm thanh toán for payment in payments: payment_date = datetime.strptime(payment['date'], "%Y-%m-%d").date() principal_paid = Decimal(str(payment.get('principal', 0))) current_principal -= principal_paid if current_principal < 0: current_principal = Decimal('0') timeline.append((payment_date, current_principal)) # Tính lãi cho từng khoảng total_penalty = Decimal('0') for i in range(len(timeline)): start_date, principal = timeline[i] # Xác định ngày kết thúc của khoảng này if i < len(timeline) - 1: end_date = timeline[i + 1][0] else: end_date = up_to_date # Tính số ngày và lãi days = (end_date - start_date).days if days > 0 and principal > 0: penalty = principal * Decimal('0.0005') * Decimal(days) penalty = penalty.quantize(Decimal('0.01')) total_penalty += penalty return total_penalty def update_penalty_after_allocation(schedule): """ Cập nhật lãi phạt SAU KHI đã phân bổ xong Gọi hàm này SAU KHI đã cập nhật paid_amount, amount_remain """ today = datetime.now().date() # Tính lại TOÀN BỘ lãi từ đầu đến giờ total_penalty = recalculate_penalty_amount(schedule, up_to_date=today) # Cập nhật schedule.penalty_amount = total_penalty penalty_paid = Decimal(str(schedule.penalty_paid or 0)) schedule.penalty_remain = total_penalty - penalty_paid schedule.remain_amount = Decimal(str(schedule.amount_remain or 0)) + schedule.penalty_remain schedule.batch_date = today # Lưu trace entry_list = schedule.entry or [] if isinstance(entry_list, str): try: entry_list = json.loads(entry_list) except json.JSONDecodeError: entry_list = [] # Xóa trace ongoing cũ entry_list = [e for e in entry_list if e.get('type') != 'PENALTY_RECALC'] entry_list.append({ "type": "PENALTY_RECALC", "date": today.isoformat(), "penalty_total": float(total_penalty), "penalty_paid": float(penalty_paid), "penalty_remain": float(schedule.penalty_remain), "note": f"Tính lại tổng lãi đến {today}: {total_penalty:,.0f}" }) schedule.entry = safe_json_serialize(entry_list) schedule.save(update_fields=[ 'penalty_amount', 'penalty_remain', 'remain_amount', 'batch_date', 'entry' ]) # ========================================================================================== def allocate_payment_to_schedules(product_id): if not product_id: return {"status": "no_product", "message": "Không có product_id"} updated_schedules = [] updated_entries = [] errors = [] paid_payment_status = Payment_Status.objects.filter(id=2).first() paid_txn_status = Transaction_Status.objects.filter(id=2).first() with transaction.atomic(): try: product = Product.objects.get(id=product_id) booked = Product_Booked.objects.filter(product=product).first() if not booked or not booked.transaction: errors.append(f"Product {product_id}: Không tìm thấy Transaction") return {"status": "error", "errors": errors} 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"Product {product_id}: Không tìm thấy Transaction_Detail") return {"status": "error", "errors": errors} entries_with_remain = Internal_Entry.objects.select_for_update().filter( product=product, type__code='CR', allocation_remain__gt=0 ).exclude(account__id=5).order_by('date', 'create_time') if not entries_with_remain.exists(): return { "status": "success", "message": "Không có entry nào cần phân bổ", "updated_schedules": [], "updated_entries": [], "errors": [] } schedules = Payment_Schedule.objects.select_for_update().filter( txn_detail=txn_detail, status__id=1 ).order_by('cycle', 'from_date') if not schedules.exists(): return { "status": "success", "message": "Không có lịch thanh toán cần phân bổ", "updated_schedules": [], "updated_entries": [], "errors": [] } total_principal_allocated = Decimal('0') total_penalty_allocated = Decimal('0') for entry in entries_with_remain: entry_date = entry.date entry_date_str = entry_date if isinstance(entry_date, str) else entry_date.strftime("%Y-%m-%d") remaining = Decimal(str(entry.allocation_remain)) if remaining <= 0: continue entry_allocation_detail = entry.allocation_detail or [] entry_principal_allocated = Decimal('0') entry_penalty_allocated = Decimal('0') for sch in schedules: if remaining <= 0: break # RULE: principal-fee (gốc trước, lãi sau) 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)) # Trả gốc trước to_principal = min(remaining, amount_remain) remaining -= to_principal paid_amount += to_principal amount_remain -= to_principal # Trả lãi sau (nếu còn tiền) to_penalty = min(remaining, penalty_remain) remaining -= to_penalty penalty_paid += to_penalty penalty_remain -= to_penalty allocated_here = to_principal + to_penalty if allocated_here <= 0: continue entry_principal_allocated += to_principal entry_penalty_allocated += to_penalty # Cập nhật schedule sch.paid_amount = paid_amount sch.penalty_paid = penalty_paid sch.amount_remain = amount_remain sch.penalty_remain = penalty_remain sch.remain_amount = amount_remain + penalty_remain if amount_remain <= 0: sch.batch_date = datetime.strptime(entry_date_str, "%Y-%m-%d").date() # Lưu trace PAYMENT sch_entry_list = sch.entry or [] sch_entry_list.append({ "type": "PAYMENT", "code": entry.code, "date": entry_date_str, "amount": float(allocated_here), "principal": float(to_principal), "penalty": float(to_penalty), "rule": "principal-fee" }) sch.entry = safe_json_serialize(sch_entry_list) # Lưu schedule (chưa tính lại lãi) sch.save(update_fields=[ 'paid_amount', 'penalty_paid', 'amount_remain', 'penalty_remain', 'remain_amount', 'entry', 'batch_date' ]) # ===== KEY: TÍNH LẠI LÃI SAU KHI PHÂN BỔ ===== update_penalty_after_allocation(sch) # Cập nhật lại status nếu đã trả hết if sch.amount_remain <= 0 and sch.penalty_remain <= 0 and paid_payment_status: sch.status = paid_payment_status sch.save(update_fields=['status']) if sch.id not in updated_schedules: updated_schedules.append(sch.id) entry_allocation_detail.append({ "schedule_id": sch.id, "schedule_code": sch.code, "amount": float(allocated_here), "principal": float(to_principal), "penalty": float(to_penalty), "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) total_allocated = entry_principal_allocated + entry_penalty_allocated entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + total_allocated entry.allocation_remain = remaining entry.allocation_detail = entry_allocation_detail entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail']) if entry.id not in updated_entries: updated_entries.append(entry.id) total_principal_allocated += entry_principal_allocated total_penalty_allocated += entry_penalty_allocated # Cập nhật Transaction & Transaction_Detail if total_principal_allocated > 0 or total_penalty_allocated > 0: txn_detail.amount_received = F('amount_received') + total_principal_allocated txn_detail.amount_remaining = F('amount_remaining') - total_principal_allocated if hasattr(txn_detail, 'penalty_amount'): txn_detail.penalty_amount = F('penalty_amount') + total_penalty_allocated txn_detail.save(update_fields=['amount_received', 'amount_remaining', 'penalty_amount']) else: txn_detail.save(update_fields=['amount_received', 'amount_remaining']) 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']) txn.amount_received = F('amount_received') + total_principal_allocated txn.amount_remain = F('amount_remain') - total_principal_allocated if hasattr(txn, 'penalty_amount'): txn.penalty_amount = F('penalty_amount') + total_penalty_allocated txn.save(update_fields=['amount_received', 'amount_remain', 'penalty_amount']) else: txn.save(update_fields=['amount_received', 'amount_remain']) 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(str(exc)) import traceback print(traceback.format_exc()) return { "status": "success" if not errors else "partial_failure", "updated_schedules": updated_schedules, "updated_entries": updated_entries, "errors": errors, "rule_used": "principal-fee", "total_principal_allocated": float(total_principal_allocated), "total_penalty_allocated": float(total_penalty_allocated) } # ========================================================================================== def allocate_penalty_reduction(product_id): """ Xử lý miễn giảm tiền phạt cho một sản phẩm cụ thể. Quét các entry CR từ tài khoản miễn lãi (id=5) có allocation_remain > 0. """ if not product_id: return {"status": "no_product", "message": "Không có product_id"} updated_schedules = [] updated_entries = [] errors = [] with transaction.atomic(): try: product = Product.objects.get(id=product_id) booked = Product_Booked.objects.filter(product=product).first() if not booked or not booked.transaction: errors.append(f"Product {product_id}: Không tìm thấy Transaction") return { "status": "error", "errors": errors } 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"Product {product_id}: Không tìm thấy Transaction_Detail") return { "status": "error", "errors": errors } reduction_entries = Internal_Entry.objects.select_for_update().filter( product=product, type__code='CR', account__id=5, allocation_remain__gt=0 ).order_by('date', 'create_time') if not reduction_entries.exists(): return { "status": "success", "message": "Không có entry miễn lãi cần xử lý", "updated_schedules": [], "updated_entries": [], "errors": [] } schedules = Payment_Schedule.objects.select_for_update().filter( txn_detail=txn_detail, status=1 ).order_by('cycle', 'from_date') if not schedules.exists(): return { "status": "success", "message": "Không có lịch thanh toán cần miễn lãi", "updated_schedules": [], "updated_entries": [], "errors": [] } for entry in reduction_entries: remaining_reduce = Decimal(str(entry.allocation_remain)) if remaining_reduce <= 0: continue entry_allocation_detail = entry.allocation_detail or [] entry_reduction_allocated = Decimal('0') 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)) to_reduce = min(remaining_reduce, current_penalty_remain) if to_reduce <= 0: continue remaining_reduce -= to_reduce entry_reduction_allocated += to_reduce schedule.penalty_reduce = current_penalty_reduce + to_reduce schedule.penalty_remain = current_penalty_remain - to_reduce schedule.remain_amount = current_remain_amount - to_reduce sch_entry_list = schedule.entry or [] sch_entry_list.append({ "type": "REDUCTION", "code": entry.code, "date": datetime.now().strftime("%Y-%m-%d"), "amount": float(to_reduce) }) schedule.entry = safe_json_serialize(sch_entry_list) schedule.save(update_fields=[ 'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry' ]) if schedule.id not in updated_schedules: updated_schedules.append(schedule.id) entry_allocation_detail.append({ "schedule_id": schedule.id, "schedule_code": schedule.code, "amount": float(to_reduce), "type": "REDUCTION", "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + entry_reduction_allocated entry.allocation_remain = remaining_reduce entry.allocation_detail = entry_allocation_detail entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail']) if entry.id not in updated_entries: updated_entries.append(entry.id) except Exception as exc: errors.append(str(exc)) import traceback print(traceback.format_exc()) return { "status": "success" if not errors else "partial_failure", "updated_schedules": updated_schedules, "updated_entries": updated_entries, "errors": errors } # ========================================================================================== def background_allocate(product_id): """Background task để chạy allocation sau khi tạo entry""" try: print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation started for product_id={product_id}") normal_result = allocate_payment_to_schedules(product_id) reduction_result = allocate_penalty_reduction(product_id) print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation completed for product_id={product_id}:") 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 for product_id={product_id}: {str(e)}") import traceback print(traceback.format_exc()) # ========================================================================================== # API VIEWS # ========================================================================================== @api_view(['POST']) def account_entry(request): """View function để tạo bút toán (được gọi từ urls.py)""" 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.get('product'), customer=request.data.get('customer'), date=request.data.get('date') ) if 'error' in response_data: return Response(response_data, status=400) product_id = created_entry.product_id if created_entry else None response = 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..." }) if product_id: def run_allocation(): thread = threading.Thread(target=background_allocate, args=(product_id,), daemon=True) thread.start() transaction.on_commit(run_allocation) return response @api_view(['POST']) def account_multi_entry(request): """Tạo nhiều bút toán cùng lúc""" try: result = [] product_ids = set() 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.get('ref'), product=obj.get('product'), customer=obj.get('customer'), date=obj.get('date') ) result.append(response_data) if created_entry and created_entry.product_id: product_ids.add(created_entry.product_id) response = Response({ "entries": result, "message": f"Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm cho {len(product_ids)} sản phẩm..." }) if product_ids: def run_allocations(): for product_id in product_ids: thread = threading.Thread( target=background_allocate, args=(product_id,), daemon=True ) thread.start() transaction.on_commit(run_allocations) return response except Exception as e: print({'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}) return Response({'error': str(e)}, status=400) @api_view(['POST']) def delete_entry(request): """View function để xóa bút toán (tương thích với urls.py)""" entry_id = request.data.get('id') try: with transaction.atomic(): try: entry = Internal_Entry.objects.select_for_update().get(id=entry_id) except Internal_Entry.DoesNotExist: return Response({ 'error': f'Bút toán với ID {entry_id} không tồn tại' }, status=404) entry_info = { 'id': entry.id, 'code': entry.code, 'amount': float(entry.amount), 'type': entry.type.code, 'account_code': entry.account.code, 'account_id': entry.account_id, 'product_id': entry.product_id if entry.product else None, 'allocation_amount': float(entry.allocation_amount or 0), 'allocation_remain': float(entry.allocation_remain or 0) } allocation_detail = entry.allocation_detail or [] schedules_reversed = [] total_principal_reversed = Decimal('0') total_penalty_reversed = Decimal('0') total_reduction_reversed = Decimal('0') for allocation in allocation_detail: schedule_id = allocation.get('schedule_id') allocated_amount = Decimal(str(allocation.get('amount', 0))) principal = Decimal(str(allocation.get('principal', 0))) penalty = Decimal(str(allocation.get('penalty', 0))) allocation_type = allocation.get('type', 'PAYMENT') if not schedule_id or allocated_amount <= 0: continue try: schedule = Payment_Schedule.objects.select_for_update().get(id=schedule_id) if allocation_type == 'REDUCTION': schedule.penalty_reduce = (schedule.penalty_reduce or Decimal('0')) - allocated_amount schedule.penalty_remain = (schedule.penalty_remain or Decimal('0')) + allocated_amount schedule.remain_amount = (schedule.remain_amount or Decimal('0')) + allocated_amount total_reduction_reversed += allocated_amount schedule.save(update_fields=['penalty_reduce', 'penalty_remain', 'remain_amount']) else: schedule.paid_amount = (schedule.paid_amount or Decimal('0')) - principal schedule.penalty_paid = (schedule.penalty_paid or Decimal('0')) - penalty schedule.amount_remain = (schedule.amount_remain or Decimal('0')) + principal schedule.penalty_remain = (schedule.penalty_remain or Decimal('0')) + penalty schedule.remain_amount = (schedule.remain_amount or Decimal('0')) + allocated_amount total_principal_reversed += principal total_penalty_reversed += penalty if schedule.amount_remain > 0 or schedule.penalty_remain > 0: try: unpaid_status = Payment_Status.objects.get(id=1) schedule.status = unpaid_status except Payment_Status.DoesNotExist: pass schedule.save(update_fields=[ 'paid_amount', 'penalty_paid', 'amount_remain', 'penalty_remain', 'remain_amount', 'status' ]) # Xóa entry trace của bút toán này schedule_entries = schedule.entry or [] schedule_entries = [e for e in schedule_entries if e.get('code') != entry.code] schedule.entry = safe_json_serialize(schedule_entries) schedule.save(update_fields=['entry']) # Tính lại lãi sau khi hoàn tác update_penalty_after_allocation(schedule) schedules_reversed.append({ 'schedule_id': schedule.id, 'schedule_code': schedule.code, 'amount_reversed': float(allocated_amount), 'principal_reversed': float(principal), 'penalty_reversed': float(penalty), 'type': allocation_type }) except Payment_Schedule.DoesNotExist: continue txn_detail_updated = False txn_updated = False if entry.product: try: booked = Product_Booked.objects.filter(product=entry.product).first() if booked and booked.transaction: txn = booked.transaction try: current = Transaction_Current.objects.get(transaction=txn) txn_detail = current.detail except Transaction_Current.DoesNotExist: txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by('-create_time').first() if txn_detail: if entry.account_id != 5: fields_to_update = [] if total_principal_reversed > 0: txn_detail.amount_received = F('amount_received') - total_principal_reversed txn_detail.amount_remaining = F('amount_remaining') + total_principal_reversed fields_to_update.extend(['amount_received', 'amount_remaining']) if total_penalty_reversed > 0 and hasattr(txn_detail, 'penalty_amount'): txn_detail.penalty_amount = F('penalty_amount') - total_penalty_reversed fields_to_update.append('penalty_amount') if fields_to_update: txn_detail.save(update_fields=fields_to_update) txn_detail.refresh_from_db() txn_detail_updated = True if txn_detail.amount_remaining > 0: try: unpaid_status = Transaction_Status.objects.get(id=1) txn_detail.status = unpaid_status txn_detail.save(update_fields=['status']) except: pass if entry.account_id != 5: fields_to_update = [] if total_principal_reversed > 0: txn.amount_received = F('amount_received') - total_principal_reversed txn.amount_remain = F('amount_remain') + total_principal_reversed fields_to_update.extend(['amount_received', 'amount_remain']) if total_penalty_reversed > 0 and hasattr(txn, 'penalty_amount'): txn.penalty_amount = F('penalty_amount') - total_penalty_reversed fields_to_update.append('penalty_amount') if fields_to_update: txn.save(update_fields=fields_to_update) txn.refresh_from_db() txn_updated = True if txn.amount_remain > 0: try: unpaid_status = Transaction_Status.objects.get(id=1) txn.status = unpaid_status txn.save(update_fields=['status']) except: pass except Exception as e: print(f"Lỗi khi hoàn tác Transaction: {str(e)}") account = Internal_Account.objects.select_for_update().get(id=entry.account_id) entry_amount = float(entry.amount) if entry.type.code == 'CR': account.balance = (account.balance or 0) - entry_amount else: account.balance = (account.balance or 0) + entry_amount account.save(update_fields=['balance']) entry.delete() return Response({ 'success': True, 'message': 'Đã xóa bút toán và hoàn tác tất cả phân bổ thành công', 'entry': entry_info, 'reversed': { 'schedules_count': len(schedules_reversed), 'schedules': schedules_reversed, 'total_principal_reversed': float(total_principal_reversed), 'total_penalty_reversed': float(total_penalty_reversed), 'total_reduction_reversed': float(total_reduction_reversed), 'transaction_detail_updated': txn_detail_updated, 'transaction_updated': txn_updated }, 'account_balance_restored': True }) except Exception as e: import traceback print(traceback.format_exc()) return Response({ 'error': f'Đã xảy ra lỗi khi xóa bút toán: {str(e)}' }, status=500)