Files
api/app/payment.py
anhduy-tech 5b360753d8 changes
2026-02-04 21:01:54 +07:00

914 lines
38 KiB
Python

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)