changes
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
284
app/payment.py
284
app/payment.py
@@ -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())
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user