changes
This commit is contained in:
Binary file not shown.
+2
-2
@@ -21,7 +21,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||||||
SECRET_KEY = 'django-insecure-_u202k$8qq2p*cr_eo(7k!0ngr5^n)27@85+5oy8&41(u6&j54'
|
SECRET_KEY = 'django-insecure-_u202k$8qq2p*cr_eo(7k!0ngr5^n)27@85+5oy8&41(u6&j54'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = False
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ ASGI_APPLICATION = 'api.asgi.application'
|
|||||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||||
#prod:5.223.52.193 dev:5.223.42.146
|
#prod:5.223.52.193 dev:5.223.42.146
|
||||||
|
|
||||||
MODE = 'dev'
|
MODE = 'prod'
|
||||||
DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.42.146'
|
DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.42.146'
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
|
|||||||
Binary file not shown.
+115
-256
@@ -12,16 +12,6 @@ import json
|
|||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
# HELPER FUNCTIONS
|
# 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):
|
def get_latest_payment_date(schedule):
|
||||||
"""Lấy ngày nộp gần nhất từ entry (type='PAYMENT')"""
|
"""Lấy ngày nộp gần nhất từ entry (type='PAYMENT')"""
|
||||||
@@ -130,162 +120,42 @@ def get_allocation_rule():
|
|||||||
return 'principal-fee'
|
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):
|
DAILY_PENALTY_RATE = Decimal('0.0005') # 0.05% mỗi ngày
|
||||||
"""
|
|
||||||
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 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 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):
|
def allocate_payment_to_schedules(product_id):
|
||||||
if not product_id:
|
if not product_id:
|
||||||
return {"status": "no_product", "message": "Không có product_id"}
|
return {"status": "no_product", "message": "Không có product_id"}
|
||||||
|
|
||||||
updated_schedules = []
|
updated_schedules = []
|
||||||
updated_entries = []
|
updated_entries = []
|
||||||
errors = []
|
|
||||||
|
|
||||||
paid_payment_status = Payment_Status.objects.filter(id=2).first()
|
paid_payment_status = Payment_Status.objects.filter(id=2).first()
|
||||||
paid_txn_status = Transaction_Status.objects.filter(id=2).first()
|
paid_txn_status = Transaction_Status.objects.filter(id=2).first()
|
||||||
|
today = datetime.now().date()
|
||||||
|
DAILY_PENALTY_RATE = Decimal('0.0005') # Giả định 0.05%
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(id=product_id)
|
product = Product.objects.get(id=product_id)
|
||||||
booked = Product_Booked.objects.filter(product=product).first()
|
booked = Product_Booked.objects.filter(product=product).first()
|
||||||
if not booked or not booked.transaction:
|
if not booked or not booked.transaction:
|
||||||
errors.append(f"Product {product_id}: Không tìm thấy Transaction")
|
return {"status": "error", "errors": ["Không tìm thấy Transaction"]}
|
||||||
return {"status": "error", "errors": errors}
|
|
||||||
|
|
||||||
txn = booked.transaction
|
txn = booked.transaction
|
||||||
|
txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by('-create_time').first()
|
||||||
|
|
||||||
txn_detail = None
|
# Lấy các bút toán CR còn dư tiền (Sắp xếp chính xác để trừ theo thứ tự thời gian)
|
||||||
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(
|
entries_with_remain = Internal_Entry.objects.select_for_update().filter(
|
||||||
product=product,
|
product=product,
|
||||||
type__code='CR',
|
type__code='CR',
|
||||||
@@ -293,177 +163,169 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
).exclude(account__id=5).order_by('date', 'create_time')
|
).exclude(account__id=5).order_by('date', 'create_time')
|
||||||
|
|
||||||
if not entries_with_remain.exists():
|
if not entries_with_remain.exists():
|
||||||
return {
|
return {"status": "success", "message": "Không có tiền để phân bổ"}
|
||||||
"status": "success",
|
|
||||||
"message": "Không có entry nào cần phân bổ",
|
|
||||||
"updated_schedules": [],
|
|
||||||
"updated_entries": [],
|
|
||||||
"errors": []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# Lấy lịch nợ (chỉ lấy status=1)
|
||||||
schedules = Payment_Schedule.objects.select_for_update().filter(
|
schedules = Payment_Schedule.objects.select_for_update().filter(
|
||||||
txn_detail=txn_detail,
|
txn_detail=txn_detail,
|
||||||
status__id=1
|
status__id=1
|
||||||
).order_by('cycle', 'from_date')
|
).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_principal_allocated = Decimal('0')
|
||||||
total_penalty_allocated = Decimal('0')
|
total_penalty_allocated = Decimal('0')
|
||||||
|
|
||||||
for entry in entries_with_remain:
|
for entry in entries_with_remain:
|
||||||
entry_date = entry.date
|
entry_date = entry.date
|
||||||
entry_date_str = entry_date if isinstance(entry_date, str) else entry_date.strftime("%Y-%m-%d")
|
if isinstance(entry_date, str):
|
||||||
|
entry_date = datetime.strptime(entry_date, "%Y-%m-%d").date()
|
||||||
|
|
||||||
remaining = Decimal(str(entry.allocation_remain))
|
remaining = Decimal(str(entry.allocation_remain))
|
||||||
if remaining <= 0:
|
if remaining <= 0: continue
|
||||||
continue
|
|
||||||
|
|
||||||
entry_allocation_detail = entry.allocation_detail or []
|
entry_allocation_detail = entry.allocation_detail or []
|
||||||
entry_principal_allocated = Decimal('0')
|
entry_principal_allocated = Decimal('0')
|
||||||
entry_penalty_allocated = Decimal('0')
|
entry_penalty_allocated = Decimal('0')
|
||||||
|
|
||||||
for sch in schedules:
|
for sch in schedules:
|
||||||
if remaining <= 0:
|
if remaining <= 0: break
|
||||||
break
|
|
||||||
|
|
||||||
# RULE: principal-fee (gốc trước, lãi sau)
|
current_amount_remain = Decimal(str(sch.amount_remain or 0))
|
||||||
penalty_remain = Decimal(str(sch.penalty_remain or 0))
|
|
||||||
amount_remain = Decimal(str(sch.amount_remain or 0))
|
# --- BƯỚC 1: LẤY LÃI TÍCH LŨY TỪ TRACE ---
|
||||||
paid_amount = Decimal(str(sch.paid_amount or 0))
|
last_entry_date = None
|
||||||
penalty_paid = Decimal(str(sch.penalty_paid or 0))
|
accumulated_penalty_to_last = Decimal('0')
|
||||||
|
|
||||||
|
if sch.entry:
|
||||||
|
for e in sch.entry:
|
||||||
|
if e.get('type') == 'PAYMENT':
|
||||||
|
e_date = datetime.strptime(e['date'], "%Y-%m-%d").date()
|
||||||
|
# Dùng <= để lấy được cả bút toán cùng ngày nộp trước đó
|
||||||
|
if e_date <= entry_date:
|
||||||
|
if e.get('penalty_to_this_entry') is not None:
|
||||||
|
# Luôn cập nhật để lấy con số lũy kế MỚI NHẤT
|
||||||
|
accumulated_penalty_to_last = Decimal(str(e['penalty_to_this_entry']))
|
||||||
|
|
||||||
|
if not last_entry_date or e_date > last_entry_date:
|
||||||
|
last_entry_date = e_date
|
||||||
|
|
||||||
# Trả gốc trước
|
# --- BƯỚC 2: TÍNH LÃI PHÁT SINH MỚI ---
|
||||||
to_principal = min(remaining, amount_remain)
|
penalty_added_to_entry = Decimal('0')
|
||||||
|
days_for_trace = 0
|
||||||
|
if current_amount_remain > 0:
|
||||||
|
if last_entry_date:
|
||||||
|
days_between = max(0, (entry_date - last_entry_date).days)
|
||||||
|
penalty_added_to_entry = current_amount_remain * Decimal(days_between) * DAILY_PENALTY_RATE
|
||||||
|
days_for_trace = days_between
|
||||||
|
else:
|
||||||
|
days_overdue_to_entry = max(0, (entry_date - sch.to_date).days)
|
||||||
|
penalty_added_to_entry = current_amount_remain * Decimal(days_overdue_to_entry) * DAILY_PENALTY_RATE
|
||||||
|
days_for_trace = days_overdue_to_entry
|
||||||
|
|
||||||
|
# Tổng nợ lãi tại thời điểm này
|
||||||
|
penalty_to_this_entry = accumulated_penalty_to_last + penalty_added_to_entry
|
||||||
|
|
||||||
|
# QUAN TRỌNG: Nợ lãi thực tế cần trả ngay bây giờ
|
||||||
|
penalty_to_pay_now = max(Decimal('0'), penalty_to_this_entry - Decimal(str(sch.penalty_paid or 0)))
|
||||||
|
|
||||||
|
# --- BƯỚC 3: PHÂN BỔ TIỀN ---
|
||||||
|
to_principal = min(remaining, current_amount_remain)
|
||||||
remaining -= to_principal
|
remaining -= to_principal
|
||||||
paid_amount += to_principal
|
amount_remain_after = current_amount_remain - to_principal
|
||||||
amount_remain -= to_principal
|
|
||||||
|
|
||||||
# Trả lãi sau (nếu còn tiền)
|
to_penalty = min(remaining, penalty_to_pay_now)
|
||||||
to_penalty = min(remaining, penalty_remain)
|
|
||||||
remaining -= to_penalty
|
remaining -= to_penalty
|
||||||
penalty_paid += to_penalty
|
|
||||||
penalty_remain -= to_penalty
|
|
||||||
|
|
||||||
allocated_here = to_principal + to_penalty
|
allocated_here = to_principal + to_penalty
|
||||||
|
|
||||||
|
# Nếu vẫn không có gì để phân bổ cho kỳ này, bỏ qua sang kỳ sau
|
||||||
if allocated_here <= 0:
|
if allocated_here <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entry_principal_allocated += to_principal
|
entry_principal_allocated += to_principal
|
||||||
entry_penalty_allocated += to_penalty
|
entry_penalty_allocated += to_penalty
|
||||||
|
|
||||||
# Cập nhật schedule
|
# --- BƯỚC 4: LÃI DỰ PHÒNG ĐẾN NAY ---
|
||||||
sch.paid_amount = paid_amount
|
days_from_entry_to_today = max(0, (today - entry_date).days)
|
||||||
sch.penalty_paid = penalty_paid
|
additional_penalty_to_today = Decimal('0')
|
||||||
sch.amount_remain = amount_remain
|
if amount_remain_after > 0:
|
||||||
sch.penalty_remain = penalty_remain
|
additional_penalty_to_today = amount_remain_after * Decimal(days_from_entry_to_today) * DAILY_PENALTY_RATE
|
||||||
sch.remain_amount = amount_remain + penalty_remain
|
|
||||||
|
|
||||||
if amount_remain <= 0:
|
# --- CẬP NHẬT DỮ LIỆU ---
|
||||||
sch.batch_date = datetime.strptime(entry_date_str, "%Y-%m-%d").date()
|
sch.paid_amount = Decimal(str(sch.paid_amount or 0)) + to_principal
|
||||||
|
sch.penalty_paid = Decimal(str(sch.penalty_paid or 0)) + to_penalty
|
||||||
|
sch.amount_remain = amount_remain_after
|
||||||
|
sch.penalty_amount = penalty_to_this_entry + additional_penalty_to_today
|
||||||
|
sch.penalty_remain = max(Decimal('0'), sch.penalty_amount - sch.penalty_paid)
|
||||||
|
sch.remain_amount = sch.amount_remain + sch.penalty_remain
|
||||||
|
sch.ovd_days = days_for_trace + days_from_entry_to_today
|
||||||
|
|
||||||
# Lưu trace PAYMENT
|
# Ghi Trace
|
||||||
sch_entry_list = sch.entry or []
|
sch_entry_list = sch.entry or []
|
||||||
sch_entry_list.append({
|
sch_entry_list.append({
|
||||||
"type": "PAYMENT",
|
"type": "PAYMENT",
|
||||||
"code": entry.code,
|
"code": entry.code,
|
||||||
"date": entry_date_str,
|
"date": entry_date.strftime("%Y-%m-%d"),
|
||||||
"amount": float(allocated_here),
|
"amount": float(allocated_here),
|
||||||
"principal": float(to_principal),
|
"principal": float(to_principal),
|
||||||
"penalty": float(to_penalty),
|
"penalty": float(to_penalty),
|
||||||
"rule": "principal-fee"
|
"penalty_added_to_entry": float(penalty_added_to_entry),
|
||||||
|
"penalty_to_this_entry": float(penalty_to_this_entry),
|
||||||
|
"amount_remain_after_allocation": float(amount_remain_after),
|
||||||
})
|
})
|
||||||
sch.entry = safe_json_serialize(sch_entry_list)
|
sch.entry = sch_entry_list # Lưu lại list
|
||||||
|
|
||||||
# Lưu schedule (chưa tính lại lãi)
|
if sch.amount_remain <= 0 and sch.penalty_remain <= 0:
|
||||||
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.status = paid_payment_status
|
||||||
sch.save(update_fields=['status'])
|
|
||||||
|
sch.save()
|
||||||
if sch.id not in updated_schedules:
|
if sch.id not in updated_schedules: updated_schedules.append(sch.id)
|
||||||
updated_schedules.append(sch.id)
|
|
||||||
|
|
||||||
entry_allocation_detail.append({
|
entry_allocation_detail.append({
|
||||||
"schedule_id": sch.id,
|
"schedule_id": sch.id, "amount": float(allocated_here),
|
||||||
"schedule_code": sch.code,
|
"principal": float(to_principal), "penalty": float(to_penalty)
|
||||||
"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
|
# Cập nhật Entry nguồn
|
||||||
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + total_allocated
|
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + (entry_principal_allocated + entry_penalty_allocated)
|
||||||
entry.allocation_remain = remaining
|
entry.allocation_remain = remaining
|
||||||
entry.allocation_detail = entry_allocation_detail
|
entry.allocation_detail = entry_allocation_detail
|
||||||
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
entry.save()
|
||||||
|
|
||||||
if entry.id not in updated_entries:
|
|
||||||
updated_entries.append(entry.id)
|
|
||||||
|
|
||||||
total_principal_allocated += entry_principal_allocated
|
total_principal_allocated += entry_principal_allocated
|
||||||
total_penalty_allocated += entry_penalty_allocated
|
total_penalty_allocated += entry_penalty_allocated
|
||||||
|
|
||||||
# Cập nhật Transaction & Transaction_Detail
|
## Cập nhật Transaction (Giữ nguyên gốc)
|
||||||
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
||||||
|
# Cập nhật tiền gốc đã nhận
|
||||||
txn_detail.amount_received = F('amount_received') + total_principal_allocated
|
txn_detail.amount_received = F('amount_received') + total_principal_allocated
|
||||||
txn_detail.amount_remaining = F('amount_remaining') - total_principal_allocated
|
txn_detail.amount_remaining = F('amount_remaining') - total_principal_allocated
|
||||||
if hasattr(txn_detail, 'penalty_amount'):
|
txn_detail.save()
|
||||||
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_received = F('amount_received') + total_principal_allocated
|
||||||
txn.amount_remain = F('amount_remain') - total_principal_allocated
|
txn.amount_remain = F('amount_remain') - total_principal_allocated
|
||||||
if hasattr(txn, 'penalty_amount'):
|
txn.save()
|
||||||
txn.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
|
||||||
txn.save(update_fields=['amount_received', 'amount_remain', 'penalty_amount'])
|
# QUAN TRỌNG: Kiểm tra để đóng Status
|
||||||
else:
|
txn_detail.refresh_from_db()
|
||||||
txn.save(update_fields=['amount_received', 'amount_remain'])
|
|
||||||
txn.refresh_from_db()
|
txn.refresh_from_db()
|
||||||
|
|
||||||
if txn.amount_remain <= 0 and paid_txn_status:
|
# Nếu gốc đã hết (amount_remaining <= 0)
|
||||||
txn.status = paid_txn_status
|
# VÀ không còn kỳ lịch thanh toán nào chưa hoàn thành (đã sạch nợ lãi)
|
||||||
txn.save(update_fields=['status'])
|
has_pending_sch = Payment_Schedule.objects.filter(txn_detail=txn_detail, status__id=1).exists()
|
||||||
|
|
||||||
|
if txn_detail.amount_remaining <= 0 and not has_pending_sch:
|
||||||
|
if paid_txn_status:
|
||||||
|
txn_detail.status = paid_txn_status
|
||||||
|
txn_detail.save()
|
||||||
|
txn.status = paid_txn_status
|
||||||
|
txn.save()
|
||||||
|
|
||||||
|
return {"status": "success", "updated_schedules": updated_schedules}
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(str(exc))
|
|
||||||
import traceback
|
import traceback
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
return {"status": "error", "errors": [str(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):
|
def allocate_penalty_reduction(product_id):
|
||||||
"""
|
"""
|
||||||
@@ -800,9 +662,6 @@ def delete_entry(request):
|
|||||||
schedule.entry = safe_json_serialize(schedule_entries)
|
schedule.entry = safe_json_serialize(schedule_entries)
|
||||||
schedule.save(update_fields=['entry'])
|
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({
|
schedules_reversed.append({
|
||||||
'schedule_id': schedule.id,
|
'schedule_id': schedule.id,
|
||||||
'schedule_code': schedule.code,
|
'schedule_code': schedule.code,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user