From 63e5d71ca2534dc82d9f9eaa2b375d0f9f72fc7f Mon Sep 17 00:00:00 2001 From: anhduy-tech Date: Tue, 10 Feb 2026 10:14:23 +0700 Subject: [PATCH] changes --- api/__pycache__/settings.cpython-313.pyc | Bin 3451 -> 3451 bytes api/settings.py | 6 +- app/__pycache__/models.cpython-313.pyc | Bin 140720 -> 141383 bytes app/__pycache__/payment.cpython-313.pyc | Bin 37162 -> 45698 bytes .../0375_alter_internal_entry_ref.py | 18 + ...6_remove_payment_schedule_link_and_more.py | 36 ++ app/models.py | 17 +- app/payment.py | 324 ++++++++++++++---- 8 files changed, 326 insertions(+), 75 deletions(-) create mode 100644 app/migrations/0375_alter_internal_entry_ref.py create mode 100644 app/migrations/0376_remove_payment_schedule_link_and_more.py diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index a3bf485e221d31bd5c422242b4fa2aeaf2497c3a..bfe18a0074d1b9ea96013a8e38646b6097d5098f 100644 GIT binary patch delta 32 mcmew@^;?SjGcPX}0}wo2*tL=SHyfL&k)ENY@n#pgJ{tg`#R$;= diff --git a/api/settings.py b/api/settings.py index 3be27995..32b12399 100644 --- a/api/settings.py +++ b/api/settings.py @@ -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' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False ALLOWED_HOSTS = ['*'] @@ -81,8 +81,8 @@ ASGI_APPLICATION = 'api.asgi.application' # https://docs.djangoproject.com/en/4.1/ref/settings/#databases #prod:5.223.52.193 dev:5.223.42.146 -MODE = 'dev' -DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.42.146' +MODE = 'prod' +DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.52.193' DATABASES = { 'default': { diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc index 002b9b24886fc73587d5431ea8c8f6ed8b279850..7f8d4936efc7f20741ed9ceeda54e1bc7c9c9f67 100644 GIT binary patch delta 4336 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 delta 3898 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 ZRV~)QShQdG^kpCZyV?()G40hE{1+&AdW--7 diff --git a/app/__pycache__/payment.cpython-313.pyc b/app/__pycache__/payment.cpython-313.pyc index 242703f77cf25ab8db1e7d75da09ce764d11eab9..294e8a229b5507c1ca1ebd4fd18232d6fd17e20c 100644 GIT binary patch delta 11456 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 qpSGVO<$pLRg delta 4914 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 0: sch.ovd_days = max(0, (today - sch.to_date).days) else : - sch.ovd_days = days_for_trace + days_from_entry_to_today + sch.ovd_days = days_for_trace print(f"Lai la : {penalty_to_this_entry + additional_penalty_to_today} = {sch.penalty_amount}") # Ghi Trace sch_entry_list = sch.entry or [] @@ -336,6 +336,7 @@ def allocate_penalty_reduction(product_id): """ Xử lý miễn giảm tiền phạt cho một sản phẩm cụ thể. Quét các entry CR từ tài khoản miễn lãi (id=5) có allocation_remain > 0. + Sau khi miễn giảm, kiểm tra và đóng lịch/giao dịch nếu đủ điều kiện. """ if not product_id: return {"status": "no_product", "message": "Không có product_id"} @@ -351,10 +352,7 @@ def allocate_penalty_reduction(product_id): booked = Product_Booked.objects.filter(product=product).first() if not booked or not booked.transaction: errors.append(f"Product {product_id}: Không tìm thấy Transaction") - return { - "status": "error", - "errors": errors - } + return {"status": "error", "errors": errors} txn = booked.transaction @@ -369,10 +367,7 @@ def allocate_penalty_reduction(product_id): if not txn_detail: errors.append(f"Product {product_id}: Không tìm thấy Transaction_Detail") - return { - "status": "error", - "errors": errors - } + return {"status": "error", "errors": errors} reduction_entries = Internal_Entry.objects.select_for_update().filter( product=product, @@ -385,53 +380,44 @@ def allocate_penalty_reduction(product_id): return { "status": "success", "message": "Không có entry miễn lãi cần xử lý", - "updated_schedules": [], - "updated_entries": [], - "errors": [] + "updated_schedules": [], "updated_entries": [], "errors": [] } schedules = Payment_Schedule.objects.select_for_update().filter( txn_detail=txn_detail, - status=1 + status__id=1 # Chỉ xử lý các lịch chưa thanh toán ).order_by('cycle', 'from_date') if not schedules.exists(): return { "status": "success", "message": "Không có lịch thanh toán cần miễn lãi", - "updated_schedules": [], - "updated_entries": [], - "errors": [] + "updated_schedules": [], "updated_entries": [], "errors": [] } for entry in reduction_entries: remaining_reduce = Decimal(str(entry.allocation_remain)) - - if remaining_reduce <= 0: - continue + if remaining_reduce <= 0: continue entry_allocation_detail = entry.allocation_detail or [] entry_reduction_allocated = Decimal('0') for schedule in schedules: - if remaining_reduce <= 0: - break + if remaining_reduce <= 0: break + # Chỉ miễn giảm cho các lịch còn nợ phạt current_penalty_remain = Decimal(str(schedule.penalty_remain or 0)) - current_penalty_reduce = Decimal(str(schedule.penalty_reduce or 0)) - current_remain_amount = Decimal(str(schedule.remain_amount or 0)) + if current_penalty_remain <= 0: continue to_reduce = min(remaining_reduce, current_penalty_remain) - - if to_reduce <= 0: - continue + if to_reduce <= 0: continue remaining_reduce -= to_reduce entry_reduction_allocated += to_reduce - schedule.penalty_reduce = current_penalty_reduce + to_reduce - schedule.penalty_remain = current_penalty_remain - to_reduce - schedule.remain_amount = current_remain_amount - to_reduce + schedule.penalty_reduce = (schedule.penalty_reduce or Decimal('0')) + to_reduce + schedule.penalty_remain -= to_reduce + schedule.remain_amount -= to_reduce sch_entry_list = schedule.entry or [] sch_entry_list.append({ @@ -442,18 +428,23 @@ def allocate_penalty_reduction(product_id): }) schedule.entry = safe_json_serialize(sch_entry_list) - schedule.save(update_fields=[ - 'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry' - ]) + schedule.save(update_fields=['penalty_reduce', 'penalty_remain', 'remain_amount', 'entry']) + + # KIỂM TRA ĐỂ ĐÓNG LỊCH + if schedule.remain_amount <= 0 and schedule.amount_remain <= 0: + try: + paid_status = Payment_Status.objects.get(id=2) + schedule.status = paid_status + schedule.save(update_fields=['status']) + except Payment_Status.DoesNotExist: + errors.append("Không tìm thấy Payment_Status id=2") if schedule.id not in updated_schedules: updated_schedules.append(schedule.id) entry_allocation_detail.append({ - "schedule_id": schedule.id, - "schedule_code": schedule.code, - "amount": float(to_reduce), - "type": "REDUCTION", + "schedule_id": schedule.id, "schedule_code": schedule.code, + "amount": float(to_reduce), "type": "REDUCTION", "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) @@ -465,6 +456,23 @@ def allocate_penalty_reduction(product_id): if entry.id not in updated_entries: updated_entries.append(entry.id) + # KIỂM TRA ĐỂ ĐÓNG TOÀN BỘ GIAO DỊCH + try: + txn_detail.refresh_from_db() + 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: + paid_txn_status = Transaction_Status.objects.get(id=2) + txn_detail.status = paid_txn_status + txn.status = paid_txn_status + txn_detail.save(update_fields=['status']) + txn.save(update_fields=['status']) + print(f"Transaction for product {product_id} closed after penalty reduction.") + except Transaction_Status.DoesNotExist: + errors.append("Không tìm thấy Transaction_Status id=2") + except Exception as e: + errors.append(f"Lỗi khi đóng transaction sau miễn giảm: {str(e)}") + except Exception as exc: errors.append(str(exc)) import traceback @@ -479,17 +487,180 @@ def allocate_penalty_reduction(product_id): # ========================================================================================== +def reset_product_state_before_allocation(product_id): + """ + Reset toàn bộ trạng thái công nợ của sản phẩm về ban đầu TRƯỚC KHI chạy phân bổ. + Sử dụng logic tính toán giống delete_entry nhưng không xóa entry và không hoàn tác tài khoản. + """ + with transaction.atomic(): + try: + product = Product.objects.get(id=product_id) + booked = Product_Booked.objects.filter(product=product).first() + if not (booked and booked.transaction): + print(f"Reset: Không tìm thấy transaction cho product {product_id}") + return + + txn = booked.transaction + + try: + current = Transaction_Current.objects.get(transaction=txn) + txn_detail = current.detail + except Transaction_Current.DoesNotExist: + txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by('-create_time').first() + + if not txn_detail: + print(f"Reset: Không tìm thấy txn_detail cho product {product_id}") + return + + # ================================================================= + # Bước 1: Reset các lịch thanh toán theo công thức (GIỐNG delete_entry) + # ================================================================= + all_schedules = Payment_Schedule.objects.select_for_update().filter(txn_detail=txn_detail) + unpaid_status = Payment_Status.objects.get(id=1) + + for schedule in all_schedules: + current_amount_remain = Decimal(str(schedule.amount_remain or 0)) + + # Tính tổng principal đã phân bổ vào đúng lịch này từ tất cả entry CR + principal_allocated_to_schedule = Decimal('0') + for e in Internal_Entry.objects.filter(product_id=product_id, type__code='CR'): + for alloc in (e.allocation_detail or []): + if alloc.get('schedule_id') == schedule.id: + principal_allocated_to_schedule += Decimal(str(alloc.get('principal', 0))) + + # Reset theo công thức - GIỐNG delete_entry + schedule.entry = [] + schedule.amount_remain = current_amount_remain + principal_allocated_to_schedule + schedule.remain_amount = schedule.amount_remain + schedule.paid_amount = schedule.amount - schedule.amount_remain + schedule.penalty_paid = Decimal('0') + schedule.penalty_reduce = Decimal('0') + schedule.penalty_amount = Decimal('0') + schedule.penalty_remain = Decimal('0') + schedule.ovd_days = 0 + schedule.status = unpaid_status + + schedule.save(update_fields=[ + 'entry', 'paid_amount', 'amount_remain', 'remain_amount', + 'penalty_paid', 'penalty_reduce', 'penalty_remain', 'ovd_days', 'status', 'penalty_amount' + ]) + + # ================================================================= + # Bước 2: Reset allocation của tất cả entry CR của sản phẩm (GIỐNG delete_entry) + # ================================================================= + all_cr_entries = Internal_Entry.objects.filter( + product_id=product_id, + type__code='CR' + ) + for e in all_cr_entries: + e.allocation_detail = [] + e.allocation_amount = Decimal('0') + e.allocation_remain = Decimal(str(e.amount)) + e.save(update_fields=[ + 'allocation_detail', 'allocation_amount', 'allocation_remain' + ]) + + # ================================================================= + # Bước 3: Đặt lại Transaction_Detail từ các lịch và LƯU TRƯỚC (GIỐNG delete_entry) + # ================================================================= + if txn_detail: + # Lấy tổng paid của các lịch THUỘC detail này + schedules_of_this_detail = Payment_Schedule.objects.filter(txn_detail=txn_detail) + detail_total_paid = Decimal('0') + + for sch in schedules_of_this_detail: + paid = Decimal(str(sch.paid_amount or 0)) + detail_total_paid += paid + + # Cập nhật Transaction_Detail + txn_detail.amount_received = detail_total_paid + txn_detail.amount_remaining = Decimal(str(txn_detail.amount or 0)) - detail_total_paid + + # ===== LƯU NGAY TRANSACTION_DETAIL ===== + txn_detail.save(update_fields=['amount_received', 'amount_remaining']) + txn_detail.refresh_from_db() + + # Đặt lại status về unpaid nếu còn nợ + if txn_detail.amount_remaining > 0: + try: + unpaid_txn_status = Transaction_Status.objects.get(id=1) + txn_detail.status = unpaid_txn_status + txn_detail.save(update_fields=['status']) + except: + pass + + # ================================================================= + # Bước 4: SAU KHI LƯU TXN_DETAIL, Query lại TẤT CẢ DETAIL và tính Transaction (GIỐNG delete_entry) + # ================================================================= + # Query lại TẤT CẢ detail sau khi đã lưu + all_details = Transaction_Detail.objects.filter(transaction=txn) + + txn_total_received = Decimal('0') + txn_total_deposit_received = Decimal('0') + + for detail in all_details: + # Refresh để lấy giá trị mới nhất từ DB + detail.refresh_from_db() + txn_total_received += Decimal(str(detail.amount_received or 0)) + + # Tính deposit từ các lịch deposit thuộc detail này + deposit_schedules = Payment_Schedule.objects.filter( + txn_detail=detail, + type_id=1 + ) + for dep_sch in deposit_schedules: + txn_total_deposit_received += Decimal(str(dep_sch.paid_amount or 0)) + + # Cập nhật Transaction - SỬA: amount -> sale_price + txn.amount_received = txn_total_received + txn.amount_remain = Decimal(str(txn.sale_price or 0)) - txn_total_received # ← SỬA ĐÂY + + if hasattr(txn, 'deposit_received'): + txn.deposit_received = txn_total_deposit_received + if hasattr(txn, 'deposit_remaining') and hasattr(txn, 'deposit_amount'): + txn.deposit_remaining = Decimal(str(txn.deposit_amount or 0)) - txn_total_deposit_received + + # ===== LƯU TRANSACTION ===== + txn.save(update_fields=['amount_received', 'amount_remain', 'deposit_received', 'deposit_remaining']) + txn.refresh_from_db() + + # Đặt lại status về unpaid nếu còn nợ + if txn.amount_remain > 0: + try: + unpaid_txn_status = Transaction_Status.objects.get(id=1) + txn.status = unpaid_txn_status + txn.save(update_fields=['status']) + except: + pass + + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Đã reset thành công trạng thái cho product_id={product_id}") + + except Exception as e: + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Lỗi khi reset product state for product_id={product_id}: {str(e)}") + import traceback + print(traceback.format_exc()) + + def background_allocate(product_id): - """Background task để chạy allocation sau khi tạo entry""" + """ + Background task để chạy allocation. + Luồng xử lý: Reset toàn bộ -> Phân bổ tiền trả nợ -> Phân bổ miễn giảm. + """ try: print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation started for product_id={product_id}") + # 1. Reset toàn bộ trạng thái công nợ của sản phẩm về gốc + reset_product_state_before_allocation(product_id) + + # 2. Chạy phân bổ thanh toán (tiền khách trả) normal_result = allocate_payment_to_schedules(product_id) + + # 3. Chạy phân bổ miễn giảm (tiền công ty giảm cho khách) reduction_result = allocate_penalty_reduction(product_id) print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation completed for product_id={product_id}:") - print("Normal:", normal_result) - print("Reduction:", reduction_result) + print("Normal allocation result:", normal_result) + print("Penalty reduction result:", reduction_result) except Exception as e: print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation error for product_id={product_id}: {str(e)}") @@ -736,27 +907,7 @@ def delete_entry(request): if booked and booked.transaction: txn = booked.transaction - # Lấy tất cả lịch liên quan - all_schedules = Payment_Schedule.objects.filter( - txn_detail__transaction=txn - ) - - # Tính tổng paid_amount từ các lịch (trước khi phân bổ lại) - total_paid_all = Decimal('0') - total_remain_all = Decimal('0') - total_deposit_paid = Decimal('0') - - for sch in all_schedules: - paid = Decimal(str(sch.paid_amount or 0)) - remain = Decimal(str(sch.amount_remain or 0)) - - total_paid_all += paid - total_remain_all += remain - - if sch.type_id == 1: # type=1 là lịch đặt cọc - total_deposit_paid += paid - - # Tính lại Transaction_Detail + # Tính lại Transaction_Detail hiện tại TRƯỚC try: current = Transaction_Current.objects.get(transaction=txn) txn_detail = current.detail @@ -764,10 +915,23 @@ def delete_entry(request): txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by('-create_time').first() if txn_detail: - # amount_received = amount_remaining hiện tại + tổng paid_amount từ lịch - txn_detail.amount_received = Decimal(str(txn_detail.amount_remaining or 0)) + total_paid_all - # amount_remaining = amount - amount_received vừa tính - txn_detail.amount_remaining = Decimal(str(txn_detail.amount or 0)) - txn_detail.amount_received + # Lấy tổng paid của các lịch THUỘC detail này + schedules_of_this_detail = Payment_Schedule.objects.filter(txn_detail=txn_detail) + detail_total_paid = Decimal('0') + detail_deposit_paid = Decimal('0') + + for sch in schedules_of_this_detail: + paid = Decimal(str(sch.paid_amount or 0)) + detail_total_paid += paid + + if sch.type_id == 1: # deposit + detail_deposit_paid += paid + + # Cập nhật Transaction_Detail + txn_detail.amount_received = detail_total_paid + txn_detail.amount_remaining = Decimal(str(txn_detail.amount or 0)) - detail_total_paid + + # ===== LƯU NGAY ===== txn_detail.save(update_fields=['amount_received', 'amount_remaining']) txn_detail.refresh_from_db() txn_detail_updated = True @@ -780,12 +944,34 @@ def delete_entry(request): except: pass - # Tính lại Transaction - đặt lại các trường về trạng thái ban đầu - txn.amount_received = Decimal('0') - txn.amount_remain = Decimal(str(txn.amount or 0)) # amount gốc hợp đồng + # ====== SAU KHI LƯU HẾT TXN_DETAIL, MỚI TÍNH TRANSACTION ====== + # Query lại TẤT CẢ detail sau khi đã lưu + all_details = Transaction_Detail.objects.filter(transaction=txn) + + txn_total_received = Decimal('0') + txn_total_deposit_received = Decimal('0') + + for detail in all_details: + # Lấy giá trị mới nhất từ DB (đã refresh) + detail.refresh_from_db() + txn_total_received += Decimal(str(detail.amount_received or 0)) + + # Tính deposit từ các lịch deposit thuộc detail này + deposit_schedules = Payment_Schedule.objects.filter( + txn_detail=detail, + type_id=1 + ) + for dep_sch in deposit_schedules: + txn_total_deposit_received += Decimal(str(dep_sch.paid_amount or 0)) + + # Cập nhật Transaction + txn.amount_received = txn_total_received + txn.amount_remain = Decimal(str(txn.sale_price or 0)) - txn_total_received + if hasattr(txn, 'deposit_received'): - txn.deposit_received = Decimal('0') - txn.deposit_remaining = txn.deposit_amount if hasattr(txn, 'deposit_amount') else Decimal('0') + txn.deposit_received = txn_total_deposit_received + if hasattr(txn, 'deposit_remaining') and hasattr(txn, 'deposit_amount'): + txn.deposit_remaining = Decimal(str(txn.deposit_amount or 0)) - txn_total_deposit_received txn.save(update_fields=['amount_received', 'amount_remain', 'deposit_received', 'deposit_remaining']) txn.refresh_from_db()