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
|
||||
|
||||
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 = {
|
||||
'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):
|
||||
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)
|
||||
payment = models.ForeignKey(Payment_Schedule, null=False, related_name='invoice', on_delete=models.PROTECT)
|
||||
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):
|
||||
"""Serialize an toàn cho JSONField"""
|
||||
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):
|
||||
"""
|
||||
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.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):
|
||||
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_txn_status = Transaction_Status.objects.filter(id=2).first()
|
||||
today = datetime.now().date()
|
||||
DAILY_PENALTY_RATE = Decimal('0.0005')
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
@@ -338,6 +401,10 @@ def allocate_payment_to_schedules(product_id):
|
||||
if remaining <= 0:
|
||||
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))
|
||||
|
||||
# --- 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),
|
||||
"amount_remain_before": float(current_amount_remain),
|
||||
"amount_remain_after_allocation": float(amount_remain_after),
|
||||
"DAILY_PENALTY_RATE": float(DAILY_PENALTY_RATE)
|
||||
})
|
||||
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()
|
||||
if sch.id not in updated_schedules:
|
||||
updated_schedules.append(sch.id)
|
||||
@@ -458,6 +522,7 @@ def allocate_payment_to_schedules(product_id):
|
||||
|
||||
# Cập nhật Transaction và Transaction_Detail
|
||||
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)
|
||||
|
||||
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