changes
This commit is contained in:
Binary file not shown.
@@ -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.
363
app/payment.py
363
app/payment.py
@@ -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:
|
def safe_json_serialize(obj):
|
||||||
1. Quét tất cả các PAYMENT entry để xây dựng timeline
|
"""Serialize an toàn cho JSONField"""
|
||||||
2. Tính lãi cho từng khoảng thời gian với gốc tương ứng
|
if isinstance(obj, (datetime, date)):
|
||||||
3. Trả về tổng lãi (bao gồm cả lãi đã trả + lãi còn lại)
|
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
|
||||||
|
|
||||||
Đâ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):
|
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))
|
|
||||||
paid_amount = Decimal(str(sch.paid_amount or 0))
|
|
||||||
penalty_paid = Decimal(str(sch.penalty_paid or 0))
|
|
||||||
|
|
||||||
# Trả gốc trước
|
# --- BƯỚC 1: LẤY LÃI TÍCH LŨY TỪ TRACE ---
|
||||||
to_principal = min(remaining, amount_remain)
|
last_entry_date = None
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- BƯỚC 2: TÍNH LÃI PHÁT SINH MỚI ---
|
||||||
|
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'])
|
|
||||||
|
|
||||||
if sch.id not in updated_schedules:
|
sch.save()
|
||||||
updated_schedules.append(sch.id)
|
if sch.id not in updated_schedules: 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,
|
||||||
|
|||||||
BIN
static/contract/PXLTTDSH_703_1770264134.docx
Normal file
BIN
static/contract/PXLTTDSH_703_1770264134.docx
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_703_1770264134.pdf
Normal file
BIN
static/contract/PXLTTDSH_703_1770264134.pdf
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_703_1770264219.docx
Normal file
BIN
static/contract/PXLTTDSH_703_1770264219.docx
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_703_1770264219.pdf
Normal file
BIN
static/contract/PXLTTDSH_703_1770264219.pdf
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_704_1770264500.docx
Normal file
BIN
static/contract/PXLTTDSH_704_1770264500.docx
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_704_1770264500.pdf
Normal file
BIN
static/contract/PXLTTDSH_704_1770264500.pdf
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_704_1770264627.docx
Normal file
BIN
static/contract/PXLTTDSH_704_1770264627.docx
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_704_1770264627.pdf
Normal file
BIN
static/contract/PXLTTDSH_704_1770264627.pdf
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_708_1770272884.docx
Normal file
BIN
static/contract/PXLTTDSH_708_1770272884.docx
Normal file
Binary file not shown.
BIN
static/contract/PXLTTDSH_708_1770272884.pdf
Normal file
BIN
static/contract/PXLTTDSH_708_1770272884.pdf
Normal file
Binary file not shown.
BIN
static/contract/TTTHNVDSH_703_1770264222.docx
Normal file
BIN
static/contract/TTTHNVDSH_703_1770264222.docx
Normal file
Binary file not shown.
BIN
static/contract/TTTHNVDSH_703_1770264222.pdf
Normal file
BIN
static/contract/TTTHNVDSH_703_1770264222.pdf
Normal file
Binary file not shown.
BIN
static/contract/TTTHNVDSH_704_1770264631.docx
Normal file
BIN
static/contract/TTTHNVDSH_704_1770264631.docx
Normal file
Binary file not shown.
BIN
static/contract/TTTHNVDSH_704_1770264631.pdf
Normal file
BIN
static/contract/TTTHNVDSH_704_1770264631.pdf
Normal file
Binary file not shown.
BIN
static/files/20260205025933-entry.xlsx
Normal file
BIN
static/files/20260205025933-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205030543-entry.xlsx
Normal file
BIN
static/files/20260205030543-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205031633-entry.xlsx
Normal file
BIN
static/files/20260205031633-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205032018-entry.xlsx
Normal file
BIN
static/files/20260205032018-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205032528-entry.xlsx
Normal file
BIN
static/files/20260205032528-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205034954-entry.xlsx
Normal file
BIN
static/files/20260205034954-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205044325-entry.xlsx
Normal file
BIN
static/files/20260205044325-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205045500-entry.xlsx
Normal file
BIN
static/files/20260205045500-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205053000-entry.xlsx
Normal file
BIN
static/files/20260205053000-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205053214-entry.xlsx
Normal file
BIN
static/files/20260205053214-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205053411-entry.xlsx
Normal file
BIN
static/files/20260205053411-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205054105-entry.xlsx
Normal file
BIN
static/files/20260205054105-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205054615-entry.xlsx
Normal file
BIN
static/files/20260205054615-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260205055513-entry.xlsx
Normal file
BIN
static/files/20260205055513-entry.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user