This commit is contained in:
anhduy-tech
2026-02-09 16:56:55 +07:00
parent d6eec950e9
commit c2efa46260
37 changed files with 199 additions and 121 deletions

View File

@@ -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 = False
DEBUG = True
ALLOWED_HOSTS = ['*']
@@ -81,7 +81,7 @@ 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 = 'prod'
MODE = 'dev'
DBHOST = '172.17.0.1' if MODE == 'prod' else '5.223.42.146'
DATABASES = {

View File

@@ -246,9 +246,11 @@ def allocate_payment_to_schedules(product_id):
# --- BƯỚC 4: LÃI DỰ PHÒNG ĐẾN NAY ---
days_from_entry_to_today = max(0, (today - entry_date).days)
print(f" - Lai du phong: {days_from_entry_to_today} , ngay nhap: {entry_date}, ngay hien tai: {today}")
additional_penalty_to_today = Decimal('0')
if amount_remain_after > 0:
additional_penalty_to_today = amount_remain_after * Decimal(days_from_entry_to_today) * DAILY_PENALTY_RATE
print(f"lai du phong la : {additional_penalty_to_today}")
# --- CẬP NHẬT DỮ LIỆU ---
sch.paid_amount = Decimal(str(sch.paid_amount or 0)) + to_principal
@@ -257,8 +259,11 @@ def allocate_payment_to_schedules(product_id):
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
if amount_remain_after > 0:
sch.ovd_days = max(0, (today - sch.to_date).days)
else :
sch.ovd_days = days_for_trace + days_from_entry_to_today
print(f"Lai la : {penalty_to_this_entry + additional_penalty_to_today} = {sch.penalty_amount}")
# Ghi Trace
sch_entry_list = sch.entry or []
sch_entry_list.append({
@@ -586,8 +591,11 @@ def account_multi_entry(request):
@api_view(['POST'])
def delete_entry(request):
"""View function để xóa bút toán (tương thích với urls.py)"""
"""Xóa bút toán - reset sạch entry = [], lưu hết trước, xóa entry sau, đặt lại txn/txndetail, rồi phân bổ lại"""
entry_id = request.data.get('id')
if not entry_id:
return Response({'error': 'Thiếu id bút toán'}, status=400)
try:
with transaction.atomic():
@@ -597,7 +605,7 @@ def delete_entry(request):
return Response({
'error': f'Bút toán với ID {entry_id} không tồn tại'
}, status=404)
entry_info = {
'id': entry.id,
'code': entry.code,
@@ -609,59 +617,70 @@ def delete_entry(request):
'allocation_amount': float(entry.allocation_amount or 0),
'allocation_remain': float(entry.allocation_remain or 0)
}
if entry.type.code != 'CR':
return Response({'error': 'Hiện chỉ hỗ trợ xóa bút toán thu tiền (CR)'}, status=400)
product_id = entry.product_id
if not product_id:
return Response({'error': 'Bút toán không gắn với product nào'}, status=400)
allocation_detail = entry.allocation_detail or []
schedules_reversed = []
total_principal_reversed = Decimal('0')
total_penalty_reversed = Decimal('0')
total_reduction_reversed = Decimal('0')
# =================================================================
# Bước 1: Reset các lịch bị ảnh hưởng theo công thức & lưu
# =================================================================
for allocation in allocation_detail:
schedule_id = allocation.get('schedule_id')
allocated_amount = Decimal(str(allocation.get('amount', 0)))
principal = Decimal(str(allocation.get('principal', 0)))
penalty = Decimal(str(allocation.get('penalty', 0)))
allocation_type = allocation.get('type', 'PAYMENT')
if not schedule_id or allocated_amount <= 0:
continue
try:
schedule = Payment_Schedule.objects.select_for_update().get(id=schedule_id)
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)))
if allocation_type == 'REDUCTION':
schedule.penalty_reduce = (schedule.penalty_reduce or Decimal('0')) - allocated_amount
schedule.penalty_remain = (schedule.penalty_remain or Decimal('0')) + allocated_amount
schedule.remain_amount = (schedule.remain_amount or Decimal('0')) + allocated_amount
total_reduction_reversed += allocated_amount
schedule.save(update_fields=['penalty_reduce', 'penalty_remain', 'remain_amount'])
else:
schedule.paid_amount = (schedule.paid_amount or Decimal('0')) - principal
schedule.penalty_paid = (schedule.penalty_paid or Decimal('0')) - penalty
schedule.amount_remain = (schedule.amount_remain or Decimal('0')) + principal
schedule.penalty_remain = (schedule.penalty_remain or Decimal('0')) + penalty
schedule.remain_amount = (schedule.remain_amount or Decimal('0')) + allocated_amount
total_principal_reversed += principal
total_penalty_reversed += penalty
if schedule.amount_remain > 0 or schedule.penalty_remain > 0:
try:
unpaid_status = Payment_Status.objects.get(id=1)
schedule.status = unpaid_status
except Payment_Status.DoesNotExist:
pass
schedule.save(update_fields=[
'paid_amount', 'penalty_paid', 'amount_remain',
'penalty_remain', 'remain_amount', 'status'
])
# Xóa entry trace của bút toán này
schedule_entries = schedule.entry or []
schedule_entries = [e for e in schedule_entries if e.get('code') != entry.code]
schedule.entry = safe_json_serialize(schedule_entries)
schedule.save(update_fields=['entry'])
# Reset theo công thức
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 = Payment_Status.objects.get(id=1)
schedule.save(update_fields=[
'entry', 'paid_amount', 'amount_remain', 'remain_amount',
'penalty_paid', 'penalty_reduce', 'penalty_remain', 'ovd_days', 'status','penalty_amount'
])
schedules_reversed.append({
'schedule_id': schedule.id,
'schedule_code': schedule.code,
@@ -670,101 +689,146 @@ def delete_entry(request):
'penalty_reversed': float(penalty),
'type': allocation_type
})
except Payment_Schedule.DoesNotExist:
continue
# =================================================================
# Bước 2: Reset allocation của tất cả entry CR của sản phẩm & lưu
# =================================================================
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: Hoàn tác số dư tài khoản (lưu trước khi xóa entry)
# =================================================================
account = Internal_Account.objects.select_for_update().get(id=entry.account_id)
entry_amount = float(entry.amount)
if entry.type.code == 'CR':
account.balance = (account.balance or 0) - entry_amount
else:
account.balance = (account.balance or 0) + entry_amount
account.save(update_fields=['balance'])
# =================================================================
# Bước 4: XÓA ENTRY (sau khi reset và lưu hết)
# =================================================================
entry.delete()
# =================================================================
# Bước 5: ĐẶT LẠI Transaction & Transaction_Detail TRƯỚC KHI PHÂN BỔ LẠI
# =================================================================
txn_detail_updated = False
txn_updated = False
if entry.product:
try:
booked = Product_Booked.objects.filter(product=entry.product).first()
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
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 txn_detail:
if entry.account_id != 5:
fields_to_update = []
if total_principal_reversed > 0:
txn_detail.amount_received = F('amount_received') - total_principal_reversed
txn_detail.amount_remaining = F('amount_remaining') + total_principal_reversed
fields_to_update.extend(['amount_received', 'amount_remaining'])
if total_penalty_reversed > 0 and hasattr(txn_detail, 'penalty_amount'):
txn_detail.penalty_amount = F('penalty_amount') - total_penalty_reversed
fields_to_update.append('penalty_amount')
if fields_to_update:
txn_detail.save(update_fields=fields_to_update)
txn_detail.refresh_from_db()
txn_detail_updated = True
if txn_detail.amount_remaining > 0:
try:
unpaid_status = Transaction_Status.objects.get(id=1)
txn_detail.status = unpaid_status
txn_detail.save(update_fields=['status'])
except:
pass
if entry.account_id != 5:
fields_to_update = []
if total_principal_reversed > 0:
txn.amount_received = F('amount_received') - total_principal_reversed
txn.amount_remain = F('amount_remain') + total_principal_reversed
fields_to_update.extend(['amount_received', 'amount_remain'])
if total_penalty_reversed > 0 and hasattr(txn, 'penalty_amount'):
txn.penalty_amount = F('penalty_amount') - total_penalty_reversed
fields_to_update.append('penalty_amount')
if fields_to_update:
txn.save(update_fields=fields_to_update)
txn.refresh_from_db()
txn_updated = True
if txn.amount_remain > 0:
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
txn_detail.save(update_fields=['amount_received', 'amount_remaining'])
txn_detail.refresh_from_db()
txn_detail_updated = True
if txn_detail.amount_remaining > 0:
try:
unpaid_status = Transaction_Status.objects.get(id=1)
txn.status = unpaid_status
txn.save(update_fields=['status'])
txn_detail.status = unpaid_status
txn_detail.save(update_fields=['status'])
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
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.save(update_fields=['amount_received', 'amount_remain', 'deposit_received', 'deposit_remaining'])
txn.refresh_from_db()
txn_updated = True
if txn.amount_remain > 0:
try:
unpaid_status = Transaction_Status.objects.get(id=1)
txn.status = unpaid_status
txn.save(update_fields=['status'])
except:
pass
except Exception as e:
print(f"Lỗi khi hoàn tác Transaction: {str(e)}")
account = Internal_Account.objects.select_for_update().get(id=entry.account_id)
entry_amount = float(entry.amount)
if entry.type.code == 'CR':
account.balance = (account.balance or 0) - entry_amount
else:
account.balance = (account.balance or 0) + entry_amount
account.save(update_fields=['balance'])
entry.delete()
return Response({
'success': True,
'message': 'Đã xóa bút toán và hoàn tác tất cả phân bổ thành công',
'entry': entry_info,
'reversed': {
'schedules_count': len(schedules_reversed),
'schedules': schedules_reversed,
'total_principal_reversed': float(total_principal_reversed),
'total_penalty_reversed': float(total_penalty_reversed),
'total_reduction_reversed': float(total_reduction_reversed),
'transaction_detail_updated': txn_detail_updated,
'transaction_updated': txn_updated
},
'account_balance_restored': True
})
print(f"Lỗi khi đặt lại Transaction trước phân bổ: {str(e)}")
# =================================================================
# Bước 6: Phân bổ lại toàn bộ sản phẩm (sẽ tự tính lại txn/txndetail đúng)
# =================================================================
def trigger_reallocate():
if product_id:
try:
allocate_payment_to_schedules(product_id)
allocate_penalty_reduction(product_id)
except Exception as exc:
print(f"Lỗi khi re-allocate sau xóa: {exc}")
traceback.print_exc()
transaction.on_commit(trigger_reallocate)
return Response({
'success': True,
'message': 'Đã xóa bút toán, reset sạch entry = [], lưu hết trước, xóa entry sau, đặt lại txn/txndetail trước phân bổ, đang phân bổ lại toàn bộ...',
'entry': entry_info,
'reversed': {
'schedules_count': len(schedules_reversed),
'schedules': schedules_reversed,
'transaction_detail_updated': txn_detail_updated,
'transaction_updated': txn_updated
},
'account_balance_restored': True
})
except Exception as e:
import traceback
print(traceback.format_exc())

View File

@@ -35,7 +35,6 @@ def scan_and_run_due_jobs():
if uninitialized_jobs.exists():
#logger.info(f"Found {uninitialized_jobs.count()} uninitialized jobs. Calculating next run time...")
# Lấy timezone của dự án
tz = pytz.timezone(settings.TIME_ZONE)
for job in uninitialized_jobs:
@@ -69,12 +68,19 @@ def scan_and_run_due_jobs():
# BƯỚC 2: Quét và chạy các job đến hạn như bình thường
#logger.info("Scanning for due batch jobs...")
# Lấy các job cần chạy (có next_run_at không null và đã đến hạn)
due_jobs = Batch_Job.objects.filter(is_active=True, next_run_at__lte=now)
active_jobs = Batch_Job.objects.filter(is_active=True)
# Tách riêng job chạy mỗi phút (* * * * *)
every_minute_jobs = active_jobs.filter(cron_schedule="* * * * *")
# Các job bình thường (không phải * * * * *), kiểm tra next_run_at
normal_due_jobs = active_jobs.filter(next_run_at__lte=now).exclude(cron_schedule="* * * * *")
# Gộp hai QuerySet bằng union (hoặc |)
due_jobs = every_minute_jobs | normal_due_jobs
if not due_jobs.exists():
#logger.info("-> No due jobs found at this time.")
logger.info("-> No due jobs found at this time.")
return
#logger.info(f"-> Found {due_jobs.count()} due jobs to run.")
@@ -159,4 +165,3 @@ def start():
# Chạy tác vụ quét job mỗi 5 giây
scheduler.add_job(scan_and_run_due_jobs, 'interval', seconds=5, id='scan_due_jobs_job', replace_existing=True)
scheduler.start()
#logger.info("APScheduler started... Jobs will be scanned every 5 seconds.")

View File

@@ -58,9 +58,18 @@ def generic_post_save_handler(sender, instance, created, **kwargs):
"""
def send_update_after_commit():
change_type = "created" if created else "updated"
# Re-fetch the instance to ensure we have the committed data
refreshed_instance = sender.objects.get(pk=instance.pk)
send_model_update(refreshed_instance, change_type)
try:
# Re-fetch the instance to ensure we have the committed data
refreshed_instance = sender.objects.get(pk=instance.pk)
send_model_update(refreshed_instance, change_type)
except sender.DoesNotExist:
# Object đã bị xóa (ví dụ: delete_entry vừa xóa Internal_Entry)
# Bỏ qua việc gửi update, hoặc gửi thông báo "deleted" nếu cần
print(f"Object {sender.__name__} {instance.pk} đã bị xóa, bỏ qua gửi update.")
# Optional: vẫn gửi "deleted" để frontend biết object không còn
send_model_update(instance, "deleted")
except Exception as exc:
print(f"Lỗi trong send_update_after_commit: {exc}")
transaction.on_commit(send_update_after_commit)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.