changes
This commit is contained in:
Binary file not shown.
@@ -82,7 +82,7 @@ ASGI_APPLICATION = 'api.asgi.application'
|
|||||||
#prod:5.223.52.193 dev:5.223.42.146
|
#prod:5.223.52.193 dev:5.223.42.146
|
||||||
|
|
||||||
MODE = 'prod'
|
MODE = 'prod'
|
||||||
DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.52.193'
|
DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.42.146'
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
19
app/migrations/0377_alter_invoice_ref_code.py
Normal file
19
app/migrations/0377_alter_invoice_ref_code.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2026-02-11 03:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('app', '0376_remove_payment_schedule_link_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='ref_code',
|
||||||
|
field=models.CharField(default=10, max_length=30),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1722,7 +1722,7 @@ class Payment_Schedule(AutoCodeModel):
|
|||||||
|
|
||||||
class Invoice(models.Model):
|
class Invoice(models.Model):
|
||||||
link = models.CharField(max_length=100, null=True)
|
link = models.CharField(max_length=100, null=True)
|
||||||
ref_code = models.CharField(max_length=30, null=True)
|
ref_code = models.CharField(max_length=30, null=False)
|
||||||
amount = models.DecimalField(max_digits=35, decimal_places=2)
|
amount = models.DecimalField(max_digits=35, decimal_places=2)
|
||||||
payment = models.ForeignKey(Payment_Schedule, null=False, related_name='invoice', on_delete=models.PROTECT)
|
payment = models.ForeignKey(Payment_Schedule, null=False, related_name='invoice', on_delete=models.PROTECT)
|
||||||
note = models.CharField(max_length=100, null=True)
|
note = models.CharField(max_length=100, null=True)
|
||||||
|
|||||||
@@ -122,8 +122,6 @@ def get_allocation_rule():
|
|||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
|
|
||||||
DAILY_PENALTY_RATE = Decimal('0.0005') # 0.05% mỗi ngày
|
|
||||||
|
|
||||||
def safe_json_serialize(obj):
|
def safe_json_serialize(obj):
|
||||||
"""Serialize an toàn cho JSONField"""
|
"""Serialize an toàn cho JSONField"""
|
||||||
if isinstance(obj, (datetime, date)):
|
if isinstance(obj, (datetime, date)):
|
||||||
@@ -220,6 +218,47 @@ def reset_cr_entries_allocation(product_id, exclude_entry_id=None):
|
|||||||
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
|
def close_paid_schedules(txn):
|
||||||
|
"""
|
||||||
|
Quét toàn bộ Payment_Schedule thuộc txn_detail và đóng (status=2) những lịch
|
||||||
|
đã được thanh toán đầy đủ.
|
||||||
|
|
||||||
|
Điều kiện đóng một lịch:
|
||||||
|
- status_id == 1 (đang mở/chưa đóng)
|
||||||
|
- amount_remain <= 0 (đã trả hết gốc)
|
||||||
|
- penalty_remain <= 0 (đã trả hết phạt)
|
||||||
|
|
||||||
|
Hàm không raise exception — lỗi từng lịch được bỏ qua và in ra log,
|
||||||
|
các lịch còn lại vẫn được xử lý bình thường.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: danh sách id các lịch vừa được đóng.
|
||||||
|
"""
|
||||||
|
all_txn_details = Transaction_Detail.objects.filter(transaction=txn)
|
||||||
|
closed_ids = []
|
||||||
|
try:
|
||||||
|
paid_status = Payment_Status.objects.get(id=2)
|
||||||
|
except Payment_Status.DoesNotExist:
|
||||||
|
print("[close_paid_schedules] Không tìm thấy Payment_Status id=2, bỏ qua.")
|
||||||
|
return closed_ids
|
||||||
|
|
||||||
|
for txn_detail in all_txn_details:
|
||||||
|
schedules = Payment_Schedule.objects.filter(
|
||||||
|
txn_detail=txn_detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
for sch in schedules:
|
||||||
|
try:
|
||||||
|
|
||||||
|
if sch.remain_amount <= 0:
|
||||||
|
sch.status = paid_status
|
||||||
|
sch.save(update_fields=['status'])
|
||||||
|
closed_ids.append(sch.id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[close_paid_schedules] Lỗi khi đóng schedule {sch.id}: {e}")
|
||||||
|
print(f"[close_paid_schedules] Đã đóng {len(closed_ids)} lịch")
|
||||||
|
return closed_ids
|
||||||
|
|
||||||
def recalc_txn_from_schedules(txn, all_txn_details, paid_txn_status):
|
def recalc_txn_from_schedules(txn, all_txn_details, paid_txn_status):
|
||||||
"""
|
"""
|
||||||
Tính lại Transaction và tất cả Transaction_Detail từ trạng thái hiện tại của các lịch.
|
Tính lại Transaction và tất cả Transaction_Detail từ trạng thái hiện tại của các lịch.
|
||||||
@@ -280,6 +319,31 @@ def recalc_txn_from_schedules(txn, all_txn_details, paid_txn_status):
|
|||||||
txn.save(update_fields=['amount_received', 'amount_remain'])
|
txn.save(update_fields=['amount_received', 'amount_remain'])
|
||||||
txn.refresh_from_db()
|
txn.refresh_from_db()
|
||||||
|
|
||||||
|
# ==========================================================================================
|
||||||
|
|
||||||
|
# Mapping phase_id → DAILY_PENALTY_RATE
|
||||||
|
# Phase 1, 2, 4: 0.03%/ngày | Các phase khác: 0.05%/ngày
|
||||||
|
PENALTY_RATE_LOW_PHASES = [1, 2, 4]
|
||||||
|
PENALTY_RATE_LOW = Decimal('0.0003') # 0.03%
|
||||||
|
PENALTY_RATE_HIGH = Decimal('0.0005') # 0.05%
|
||||||
|
|
||||||
|
def get_penalty_rate_for_schedule(schedule):
|
||||||
|
"""
|
||||||
|
Trả về DAILY_PENALTY_RATE tương ứng với phase của Transaction_Detail gắn với lịch.
|
||||||
|
- Phase 1, 2, 4 → 0.03%/ngày (0.0003)
|
||||||
|
- Các phase khác → 0.05%/ngày (0.0005)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
phase_id = schedule.txn_detail.phase_id
|
||||||
|
print(f"phase_id: {phase_id}")
|
||||||
|
if phase_id in PENALTY_RATE_LOW_PHASES:
|
||||||
|
return PENALTY_RATE_LOW
|
||||||
|
return PENALTY_RATE_HIGH
|
||||||
|
except Exception:
|
||||||
|
# Nếu không lấy được phase thì dùng mức cao (an toàn hơn)
|
||||||
|
return PENALTY_RATE_HIGH
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def allocate_payment_to_schedules(product_id):
|
def allocate_payment_to_schedules(product_id):
|
||||||
if not product_id:
|
if not product_id:
|
||||||
@@ -289,7 +353,6 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
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()
|
today = datetime.now().date()
|
||||||
DAILY_PENALTY_RATE = Decimal('0.0005')
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
@@ -338,6 +401,10 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# --- XÁC ĐỊNH LÃI PHẠT THEO PHASE ---
|
||||||
|
DAILY_PENALTY_RATE = get_penalty_rate_for_schedule(sch)
|
||||||
|
print(f"DAILY_PENALTY_RATE: {DAILY_PENALTY_RATE}")
|
||||||
|
|
||||||
current_amount_remain = Decimal(str(sch.amount_remain or 0))
|
current_amount_remain = Decimal(str(sch.amount_remain or 0))
|
||||||
|
|
||||||
# --- BƯỚC 1: LẤY LÃI TÍCH LŨY TỪ TRACE ---
|
# --- BƯỚC 1: LẤY LÃI TÍCH LŨY TỪ TRACE ---
|
||||||
@@ -426,13 +493,10 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
"penalty_to_this_entry": float(penalty_to_this_entry),
|
"penalty_to_this_entry": float(penalty_to_this_entry),
|
||||||
"amount_remain_before": float(current_amount_remain),
|
"amount_remain_before": float(current_amount_remain),
|
||||||
"amount_remain_after_allocation": float(amount_remain_after),
|
"amount_remain_after_allocation": float(amount_remain_after),
|
||||||
|
"DAILY_PENALTY_RATE": float(DAILY_PENALTY_RATE)
|
||||||
})
|
})
|
||||||
sch.entry = sch_entry_list
|
sch.entry = sch_entry_list
|
||||||
|
|
||||||
# Đóng lịch: chỉ khi status hiện tại là 1
|
|
||||||
if sch.status_id == 1 and sch.amount_remain <= 0 and sch.penalty_remain <= 0:
|
|
||||||
sch.status = paid_payment_status
|
|
||||||
|
|
||||||
sch.save()
|
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)
|
||||||
@@ -458,6 +522,7 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
|
|
||||||
# Cập nhật Transaction và Transaction_Detail
|
# Cập nhật Transaction và Transaction_Detail
|
||||||
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
||||||
|
close_paid_schedules(txn)
|
||||||
recalc_txn_from_schedules(txn, all_txn_details, paid_txn_status)
|
recalc_txn_from_schedules(txn, all_txn_details, paid_txn_status)
|
||||||
|
|
||||||
return {"status": "success", "updated_schedules": updated_schedules}
|
return {"status": "success", "updated_schedules": updated_schedules}
|
||||||
|
|||||||
BIN
static/contract/Thông báo đến hạn utopia.docx
Normal file
BIN
static/contract/Thông báo đến hạn utopia.docx
Normal file
Binary file not shown.
BIN
static/files/20260211085916-entry.xlsx
Normal file
BIN
static/files/20260211085916-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260211090329-entry.xlsx
Normal file
BIN
static/files/20260211090329-entry.xlsx
Normal file
Binary file not shown.
BIN
static/files/20260211090624-entry.xlsx
Normal file
BIN
static/files/20260211090624-entry.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user