diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index 641c13af..7877055e 100644 Binary files a/api/__pycache__/settings.cpython-313.pyc and b/api/__pycache__/settings.cpython-313.pyc differ diff --git a/api/settings.py b/api/settings.py index 32b12399..050da398 100644 --- a/api/settings.py +++ b/api/settings.py @@ -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': { diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc index f97bf538..9c5665e2 100644 Binary files a/app/__pycache__/models.cpython-313.pyc and b/app/__pycache__/models.cpython-313.pyc differ diff --git a/app/__pycache__/payment.cpython-313.pyc b/app/__pycache__/payment.cpython-313.pyc index 07b13bbf..5dd40d6e 100644 Binary files a/app/__pycache__/payment.cpython-313.pyc and b/app/__pycache__/payment.cpython-313.pyc differ diff --git a/app/migrations/0377_alter_invoice_ref_code.py b/app/migrations/0377_alter_invoice_ref_code.py new file mode 100644 index 00000000..1e08b993 --- /dev/null +++ b/app/migrations/0377_alter_invoice_ref_code.py @@ -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, + ), + ] diff --git a/app/models.py b/app/models.py index ff3732e7..9618af1d 100644 --- a/app/models.py +++ b/app/models.py @@ -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) diff --git a/app/payment.py b/app/payment.py index ad1d6c18..70b9fc69 100644 --- a/app/payment.py +++ b/app/payment.py @@ -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} diff --git a/static/contract/Thông báo đến hạn utopia.docx b/static/contract/Thông báo đến hạn utopia.docx new file mode 100644 index 00000000..966435be Binary files /dev/null and b/static/contract/Thông báo đến hạn utopia.docx differ diff --git a/static/files/20260211085916-entry.xlsx b/static/files/20260211085916-entry.xlsx new file mode 100644 index 00000000..e37d7290 Binary files /dev/null and b/static/files/20260211085916-entry.xlsx differ diff --git a/static/files/20260211090329-entry.xlsx b/static/files/20260211090329-entry.xlsx new file mode 100644 index 00000000..e37d7290 Binary files /dev/null and b/static/files/20260211090329-entry.xlsx differ diff --git a/static/files/20260211090624-entry.xlsx b/static/files/20260211090624-entry.xlsx new file mode 100644 index 00000000..e37d7290 Binary files /dev/null and b/static/files/20260211090624-entry.xlsx differ