changes
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -54,5 +54,6 @@ urlpatterns = [
|
|||||||
re_path('model-fields/(?P<name>.+)/', importdata.model_fields),
|
re_path('model-fields/(?P<name>.+)/', importdata.model_fields),
|
||||||
re_path('read-excel/', importdata.read_excel),
|
re_path('read-excel/', importdata.read_excel),
|
||||||
re_path('find-key/$', importdata.find_key),
|
re_path('find-key/$', importdata.find_key),
|
||||||
re_path('email-preview/$', views.preview_email_template)
|
re_path('email-preview/$', views.preview_email_template),
|
||||||
|
re_path('delete-entry/$', payment.delete_entry)
|
||||||
]
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2026-02-02 01:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('app', '0373_remove_transaction_ovd_days_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='payment_schedule',
|
||||||
|
name='link',
|
||||||
|
field=models.CharField(max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='payment_schedule',
|
||||||
|
name='ref_code',
|
||||||
|
field=models.CharField(max_length=30, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1694,6 +1694,8 @@ class Payment_Schedule(AutoCodeModel):
|
|||||||
code_prefix = "SH"
|
code_prefix = "SH"
|
||||||
code_padding = 5
|
code_padding = 5
|
||||||
code = models.CharField(max_length=30, null=True, unique=True)
|
code = models.CharField(max_length=30, null=True, unique=True)
|
||||||
|
link = models.CharField(max_length=100, null=True)
|
||||||
|
ref_code = models.CharField(max_length=30, null=True)
|
||||||
from_date = models.DateField(null=False)
|
from_date = models.DateField(null=False)
|
||||||
to_date = models.DateField(null=False)
|
to_date = models.DateField(null=False)
|
||||||
amount = models.DecimalField(max_digits=35, decimal_places=2)
|
amount = models.DecimalField(max_digits=35, decimal_places=2)
|
||||||
|
|||||||
625
app/payment.py
625
app/payment.py
@@ -2,10 +2,55 @@ from app.models import *
|
|||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
import threading
|
import threading
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================================================================
|
||||||
|
# HELPER FUNCTIONS
|
||||||
|
# ==========================================================================================
|
||||||
|
def safe_json_serialize(obj):
|
||||||
|
"""Serialize an toàn cho JSONField"""
|
||||||
|
if isinstance(obj, (datetime, date)):
|
||||||
|
return obj.isoformat()
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {k: safe_json_serialize(v) for k, v in obj.items()}
|
||||||
|
if isinstance(obj, (list, tuple)):
|
||||||
|
return [safe_json_serialize(item) for item in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_payment_date(schedule):
|
||||||
|
"""Lấy ngày nộp gần nhất từ entry (type='PAYMENT')"""
|
||||||
|
if not schedule.entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entry_data = schedule.entry
|
||||||
|
if isinstance(entry_data, str):
|
||||||
|
try:
|
||||||
|
entry_data = json.loads(entry_data)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(entry_data, list):
|
||||||
|
return None
|
||||||
|
|
||||||
|
payment_dates = []
|
||||||
|
for item in entry_data:
|
||||||
|
if item.get('type') == 'PAYMENT' and item.get('date'):
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(item['date'], "%Y-%m-%d").date()
|
||||||
|
payment_dates.append(dt)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if payment_dates:
|
||||||
|
return max(payment_dates)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def getcode(code, Model):
|
def getcode(code, Model):
|
||||||
@@ -18,6 +63,7 @@ def getcode(code, Model):
|
|||||||
text = '0' * (6 - length)
|
text = '0' * (6 - length)
|
||||||
return f"{code}{text}{val}"
|
return f"{code}{text}{val}"
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def account_entry_api(code, amount, content, type, category, userid, ref=None, product=None, customer=None, date=None):
|
def account_entry_api(code, amount, content, type, category, userid, ref=None, product=None, customer=None, date=None):
|
||||||
try:
|
try:
|
||||||
@@ -43,7 +89,6 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p
|
|||||||
account.refresh_from_db()
|
account.refresh_from_db()
|
||||||
new_balance = account.balance
|
new_balance = account.balance
|
||||||
|
|
||||||
# Tất cả entry CR đều có allocation_remain ban đầu = amount
|
|
||||||
entry = Internal_Entry.objects.create(
|
entry = Internal_Entry.objects.create(
|
||||||
category=entry_category,
|
category=entry_category,
|
||||||
content=content,
|
content=content,
|
||||||
@@ -79,93 +124,173 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}, None
|
return {'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}, None
|
||||||
|
|
||||||
# ==========================================================================================
|
|
||||||
# HÀM LẤY RULE TỪ BIZ_SETTING (detail là string)
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def get_allocation_rule():
|
def get_allocation_rule():
|
||||||
try:
|
return 'principal-fee'
|
||||||
rule_setting = Biz_Setting.objects.get(code='rule')
|
|
||||||
rule_value = (rule_setting.detail or 'principal-fee').strip()
|
|
||||||
|
|
||||||
if rule_value.lower() in ['fee-principal', 'phạt trước', 'phat truoc']:
|
|
||||||
return 'fee-principal'
|
|
||||||
else:
|
|
||||||
return 'principal-fee'
|
|
||||||
|
|
||||||
except Biz_Setting.DoesNotExist:
|
|
||||||
return 'principal-fee'
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
# HÀM PHÂN BỔ THEO PRODUCT_ID - QUÉT LẠI TOÀN BỘ ENTRY CŨ CÓ TIỀN THỪA
|
# LOGIC TÍNH LÃI ĐÚNG - CÁCH ĐƠN GIẢN NHẤT
|
||||||
|
# ==========================================================================================
|
||||||
|
|
||||||
|
def recalculate_penalty_amount(schedule, up_to_date=None):
|
||||||
|
"""
|
||||||
|
TÍNH LẠI TOÀN BỘ LÃI PHẠT từ đầu đến ngày chỉ định
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
1. Quét tất cả các PAYMENT entry để xây dựng timeline
|
||||||
|
2. Tính lãi cho từng khoảng thời gian với gốc tương ứng
|
||||||
|
3. Trả về tổng lãi (bao gồm cả lãi đã trả + lãi còn lại)
|
||||||
|
|
||||||
|
Đây là cách DUY NHẤT để tránh cộng dồn sai!
|
||||||
|
"""
|
||||||
|
if up_to_date is None:
|
||||||
|
up_to_date = datetime.now().date()
|
||||||
|
elif isinstance(up_to_date, str):
|
||||||
|
up_to_date = datetime.strptime(up_to_date, "%Y-%m-%d").date()
|
||||||
|
|
||||||
|
to_date = schedule.to_date
|
||||||
|
if isinstance(to_date, datetime):
|
||||||
|
to_date = to_date.date()
|
||||||
|
|
||||||
|
# Nếu chưa quá hạn
|
||||||
|
if up_to_date <= to_date:
|
||||||
|
return Decimal('0')
|
||||||
|
|
||||||
|
# Xây dựng timeline: [(date, gốc còn lại sau ngày đó)]
|
||||||
|
timeline = []
|
||||||
|
|
||||||
|
# Lấy tất cả PAYMENT entries, sắp xếp theo thời gian
|
||||||
|
entry_data = schedule.entry or []
|
||||||
|
if isinstance(entry_data, str):
|
||||||
|
try:
|
||||||
|
entry_data = json.loads(entry_data)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
entry_data = []
|
||||||
|
|
||||||
|
payments = [e for e in entry_data if e.get('type') == 'PAYMENT']
|
||||||
|
payments.sort(key=lambda x: x.get('date', ''))
|
||||||
|
|
||||||
|
# Điểm bắt đầu: to_date với gốc ban đầu
|
||||||
|
original_amount = Decimal(str(schedule.amount or 0))
|
||||||
|
current_principal = original_amount
|
||||||
|
|
||||||
|
timeline.append((to_date, current_principal))
|
||||||
|
|
||||||
|
# Thêm các điểm thanh toán
|
||||||
|
for payment in payments:
|
||||||
|
payment_date = datetime.strptime(payment['date'], "%Y-%m-%d").date()
|
||||||
|
principal_paid = Decimal(str(payment.get('principal', 0)))
|
||||||
|
current_principal -= principal_paid
|
||||||
|
if current_principal < 0:
|
||||||
|
current_principal = Decimal('0')
|
||||||
|
timeline.append((payment_date, current_principal))
|
||||||
|
|
||||||
|
# Tính lãi cho từng khoảng
|
||||||
|
total_penalty = Decimal('0')
|
||||||
|
|
||||||
|
for i in range(len(timeline)):
|
||||||
|
start_date, principal = timeline[i]
|
||||||
|
|
||||||
|
# Xác định ngày kết thúc của khoảng này
|
||||||
|
if i < len(timeline) - 1:
|
||||||
|
end_date = timeline[i + 1][0]
|
||||||
|
else:
|
||||||
|
end_date = up_to_date
|
||||||
|
|
||||||
|
# Tính số ngày và lãi
|
||||||
|
days = (end_date - start_date).days
|
||||||
|
if days > 0 and principal > 0:
|
||||||
|
penalty = principal * Decimal('0.0005') * Decimal(days)
|
||||||
|
penalty = penalty.quantize(Decimal('0.01'))
|
||||||
|
total_penalty += penalty
|
||||||
|
|
||||||
|
return total_penalty
|
||||||
|
|
||||||
|
|
||||||
|
def update_penalty_after_allocation(schedule):
|
||||||
|
"""
|
||||||
|
Cập nhật lãi phạt SAU KHI đã phân bổ xong
|
||||||
|
Gọi hàm này SAU KHI đã cập nhật paid_amount, amount_remain
|
||||||
|
"""
|
||||||
|
today = datetime.now().date()
|
||||||
|
|
||||||
|
# Tính lại TOÀN BỘ lãi từ đầu đến giờ
|
||||||
|
total_penalty = recalculate_penalty_amount(schedule, up_to_date=today)
|
||||||
|
|
||||||
|
# Cập nhật
|
||||||
|
schedule.penalty_amount = total_penalty
|
||||||
|
penalty_paid = Decimal(str(schedule.penalty_paid or 0))
|
||||||
|
schedule.penalty_remain = total_penalty - penalty_paid
|
||||||
|
schedule.remain_amount = Decimal(str(schedule.amount_remain or 0)) + schedule.penalty_remain
|
||||||
|
schedule.batch_date = today
|
||||||
|
|
||||||
|
# Lưu trace
|
||||||
|
entry_list = schedule.entry or []
|
||||||
|
if isinstance(entry_list, str):
|
||||||
|
try:
|
||||||
|
entry_list = json.loads(entry_list)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
entry_list = []
|
||||||
|
|
||||||
|
# Xóa trace ongoing cũ
|
||||||
|
entry_list = [e for e in entry_list if e.get('type') != 'PENALTY_RECALC']
|
||||||
|
|
||||||
|
entry_list.append({
|
||||||
|
"type": "PENALTY_RECALC",
|
||||||
|
"date": today.isoformat(),
|
||||||
|
"penalty_total": float(total_penalty),
|
||||||
|
"penalty_paid": float(penalty_paid),
|
||||||
|
"penalty_remain": float(schedule.penalty_remain),
|
||||||
|
"note": f"Tính lại tổng lãi đến {today}: {total_penalty:,.0f}"
|
||||||
|
})
|
||||||
|
schedule.entry = safe_json_serialize(entry_list)
|
||||||
|
|
||||||
|
schedule.save(update_fields=[
|
||||||
|
'penalty_amount', 'penalty_remain', 'remain_amount',
|
||||||
|
'batch_date', 'entry'
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def allocate_payment_to_schedules(product_id):
|
def allocate_payment_to_schedules(product_id):
|
||||||
"""
|
|
||||||
Phân bổ thanh toán cho một sản phẩm cụ thể.
|
|
||||||
Quét tất cả entry CR có allocation_remain > 0 của product này,
|
|
||||||
phân bổ tiếp vào các lịch chưa thanh toán (status=1).
|
|
||||||
"""
|
|
||||||
if not product_id:
|
if not product_id:
|
||||||
return {"status": "no_product", "message": "Không có product_id"}
|
return {"status": "no_product", "message": "Không có product_id"}
|
||||||
|
|
||||||
allocation_rule = get_allocation_rule()
|
|
||||||
updated_schedules = []
|
updated_schedules = []
|
||||||
updated_entries = []
|
updated_entries = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Lấy status "đã thanh toán" một lần
|
paid_payment_status = Payment_Status.objects.filter(id=2).first()
|
||||||
paid_payment_status = None
|
paid_txn_status = Transaction_Status.objects.filter(id=2).first()
|
||||||
paid_txn_status = None
|
|
||||||
try:
|
|
||||||
paid_payment_status = Payment_Status.objects.get(id=2)
|
|
||||||
except Payment_Status.DoesNotExist:
|
|
||||||
errors.append("Không tìm thấy Payment_Status id=2 (đã thanh toán)")
|
|
||||||
|
|
||||||
try:
|
|
||||||
paid_txn_status = Transaction_Status.objects.get(id=2)
|
|
||||||
except Transaction_Status.DoesNotExist:
|
|
||||||
errors.append("Không tìm thấy Transaction_Status id=2 (đã thanh toán)")
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
# Lấy product
|
|
||||||
product = Product.objects.get(id=product_id)
|
product = Product.objects.get(id=product_id)
|
||||||
|
|
||||||
# Lấy transaction của product
|
|
||||||
booked = Product_Booked.objects.filter(product=product).first()
|
booked = Product_Booked.objects.filter(product=product).first()
|
||||||
if not booked or not booked.transaction:
|
if not booked or not booked.transaction:
|
||||||
errors.append(f"Product {product_id}: Không tìm thấy Transaction")
|
errors.append(f"Product {product_id}: Không tìm thấy Transaction")
|
||||||
return {
|
return {"status": "error", "errors": errors}
|
||||||
"status": "error",
|
|
||||||
"errors": errors
|
|
||||||
}
|
|
||||||
|
|
||||||
txn = booked.transaction
|
txn = booked.transaction
|
||||||
|
|
||||||
# Lấy transaction detail
|
|
||||||
txn_detail = None
|
txn_detail = None
|
||||||
try:
|
try:
|
||||||
current = Transaction_Current.objects.get(transaction=txn)
|
current = Transaction_Current.objects.get(transaction=txn)
|
||||||
txn_detail = current.detail
|
txn_detail = current.detail
|
||||||
except (Transaction_Current.DoesNotExist, AttributeError):
|
except (Transaction_Current.DoesNotExist, AttributeError):
|
||||||
txn_detail = Transaction_Detail.objects.filter(
|
txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by('-create_time').first()
|
||||||
transaction=txn
|
|
||||||
).order_by('-create_time').first()
|
|
||||||
|
|
||||||
if not txn_detail:
|
if not txn_detail:
|
||||||
errors.append(f"Product {product_id}: Không tìm thấy Transaction_Detail")
|
errors.append(f"Product {product_id}: Không tìm thấy Transaction_Detail")
|
||||||
return {
|
return {"status": "error", "errors": errors}
|
||||||
"status": "error",
|
|
||||||
"errors": errors
|
|
||||||
}
|
|
||||||
|
|
||||||
# QUÉT TẤT CẢ ENTRY CR CÓ TIỀN THỪA (allocation_remain > 0) - KHÔNG PHẢI TÀI KHOẢN MIỄN LÃI
|
|
||||||
entries_with_remain = Internal_Entry.objects.select_for_update().filter(
|
entries_with_remain = Internal_Entry.objects.select_for_update().filter(
|
||||||
product=product,
|
product=product,
|
||||||
type__code='CR',
|
type__code='CR',
|
||||||
allocation_remain__gt=0
|
allocation_remain__gt=0
|
||||||
).exclude(
|
).exclude(account__id=5).order_by('date', 'create_time')
|
||||||
account__id=5 # Loại trừ tài khoản miễn lãi
|
|
||||||
).order_by('date', 'create_time')
|
|
||||||
|
|
||||||
if not entries_with_remain.exists():
|
if not entries_with_remain.exists():
|
||||||
return {
|
return {
|
||||||
@@ -176,7 +301,6 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Lấy các lịch chưa thanh toán (status=1)
|
|
||||||
schedules = Payment_Schedule.objects.select_for_update().filter(
|
schedules = Payment_Schedule.objects.select_for_update().filter(
|
||||||
txn_detail=txn_detail,
|
txn_detail=txn_detail,
|
||||||
status__id=1
|
status__id=1
|
||||||
@@ -191,14 +315,14 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# TỔNG TIỀN PHÂN BỔ THÀNH CÔNG (PRINCIPAL + PENALTY)
|
|
||||||
total_principal_allocated = Decimal('0')
|
total_principal_allocated = Decimal('0')
|
||||||
total_penalty_allocated = Decimal('0')
|
total_penalty_allocated = Decimal('0')
|
||||||
|
|
||||||
# PHÂN BỔ TỪNG ENTRY
|
|
||||||
for entry in entries_with_remain:
|
for entry in entries_with_remain:
|
||||||
remaining = Decimal(str(entry.allocation_remain))
|
entry_date = entry.date
|
||||||
|
entry_date_str = entry_date if isinstance(entry_date, str) else entry_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
remaining = Decimal(str(entry.allocation_remain))
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -206,50 +330,32 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
entry_principal_allocated = Decimal('0')
|
entry_principal_allocated = Decimal('0')
|
||||||
entry_penalty_allocated = Decimal('0')
|
entry_penalty_allocated = Decimal('0')
|
||||||
|
|
||||||
# Phân bổ vào các lịch
|
|
||||||
for sch in schedules:
|
for sch in schedules:
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# RULE: principal-fee (gốc trước, lãi sau)
|
||||||
penalty_remain = Decimal(str(sch.penalty_remain or 0))
|
penalty_remain = Decimal(str(sch.penalty_remain or 0))
|
||||||
amount_remain = Decimal(str(sch.amount_remain or 0))
|
amount_remain = Decimal(str(sch.amount_remain or 0))
|
||||||
paid_amount = Decimal(str(sch.paid_amount or 0))
|
paid_amount = Decimal(str(sch.paid_amount or 0))
|
||||||
penalty_paid = Decimal(str(sch.penalty_paid or 0))
|
penalty_paid = Decimal(str(sch.penalty_paid or 0))
|
||||||
remain_amount = Decimal(str(sch.remain_amount or 0))
|
|
||||||
|
|
||||||
to_penalty = Decimal('0')
|
# Trả gốc trước
|
||||||
to_principal = Decimal('0')
|
to_principal = min(remaining, amount_remain)
|
||||||
|
remaining -= to_principal
|
||||||
|
paid_amount += to_principal
|
||||||
|
amount_remain -= to_principal
|
||||||
|
|
||||||
# Áp dụng quy tắc phân bổ
|
# Trả lãi sau (nếu còn tiền)
|
||||||
if allocation_rule == 'fee-principal':
|
to_penalty = min(remaining, penalty_remain)
|
||||||
# Phạt trước
|
remaining -= to_penalty
|
||||||
to_penalty = min(remaining, penalty_remain)
|
penalty_paid += to_penalty
|
||||||
remaining -= to_penalty
|
penalty_remain -= to_penalty
|
||||||
penalty_paid += to_penalty
|
|
||||||
penalty_remain -= to_penalty
|
|
||||||
|
|
||||||
to_principal = min(remaining, amount_remain)
|
|
||||||
remaining -= to_principal
|
|
||||||
paid_amount += to_principal
|
|
||||||
amount_remain -= to_principal
|
|
||||||
else:
|
|
||||||
# Gốc trước
|
|
||||||
to_principal = min(remaining, amount_remain)
|
|
||||||
remaining -= to_principal
|
|
||||||
paid_amount += to_principal
|
|
||||||
amount_remain -= to_principal
|
|
||||||
|
|
||||||
to_penalty = min(remaining, penalty_remain)
|
|
||||||
remaining -= to_penalty
|
|
||||||
penalty_paid += to_penalty
|
|
||||||
penalty_remain -= to_penalty
|
|
||||||
|
|
||||||
allocated_here = to_penalty + to_principal
|
|
||||||
|
|
||||||
|
allocated_here = to_principal + to_penalty
|
||||||
if allocated_here <= 0:
|
if allocated_here <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Cập nhật entry tracking
|
|
||||||
entry_principal_allocated += to_principal
|
entry_principal_allocated += to_principal
|
||||||
entry_penalty_allocated += to_penalty
|
entry_penalty_allocated += to_penalty
|
||||||
|
|
||||||
@@ -258,41 +364,41 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
sch.penalty_paid = penalty_paid
|
sch.penalty_paid = penalty_paid
|
||||||
sch.amount_remain = amount_remain
|
sch.amount_remain = amount_remain
|
||||||
sch.penalty_remain = penalty_remain
|
sch.penalty_remain = penalty_remain
|
||||||
sch.remain_amount = max(Decimal('0'), remain_amount - allocated_here)
|
sch.remain_amount = amount_remain + penalty_remain
|
||||||
|
|
||||||
# Lưu trace vào schedule
|
if amount_remain <= 0:
|
||||||
schedule_entry_list = sch.entry or []
|
sch.batch_date = datetime.strptime(entry_date_str, "%Y-%m-%d").date()
|
||||||
|
|
||||||
date_value = entry.date
|
# Lưu trace PAYMENT
|
||||||
if hasattr(date_value, 'isoformat'):
|
sch_entry_list = sch.entry or []
|
||||||
date_value = date_value.isoformat()
|
sch_entry_list.append({
|
||||||
else:
|
"type": "PAYMENT",
|
||||||
date_value = str(date_value)
|
|
||||||
|
|
||||||
schedule_entry_list.append({
|
|
||||||
"code": entry.code,
|
"code": entry.code,
|
||||||
|
"date": entry_date_str,
|
||||||
"amount": float(allocated_here),
|
"amount": float(allocated_here),
|
||||||
"date": date_value,
|
|
||||||
"type": "CR",
|
|
||||||
"principal": float(to_principal),
|
"principal": float(to_principal),
|
||||||
"penalty": float(to_penalty),
|
"penalty": float(to_penalty),
|
||||||
"rule": allocation_rule
|
"rule": "principal-fee"
|
||||||
})
|
})
|
||||||
sch.entry = schedule_entry_list
|
sch.entry = safe_json_serialize(sch_entry_list)
|
||||||
|
|
||||||
# Kiểm tra xem lịch đã thanh toán đủ chưa
|
|
||||||
if sch.amount_remain <= 0 and sch.penalty_remain <= 0 and paid_payment_status:
|
|
||||||
sch.status = paid_payment_status
|
|
||||||
|
|
||||||
|
# Lưu schedule (chưa tính lại lãi)
|
||||||
sch.save(update_fields=[
|
sch.save(update_fields=[
|
||||||
'paid_amount', 'penalty_paid', 'amount_remain',
|
'paid_amount', 'penalty_paid', 'amount_remain',
|
||||||
'penalty_remain', 'remain_amount', 'entry', 'status'
|
'penalty_remain', 'remain_amount', 'entry', 'batch_date'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# ===== KEY: TÍNH LẠI LÃI SAU KHI PHÂN BỔ =====
|
||||||
|
update_penalty_after_allocation(sch)
|
||||||
|
|
||||||
|
# Cập nhật lại status nếu đã trả hết
|
||||||
|
if sch.amount_remain <= 0 and sch.penalty_remain <= 0 and paid_payment_status:
|
||||||
|
sch.status = paid_payment_status
|
||||||
|
sch.save(update_fields=['status'])
|
||||||
|
|
||||||
if sch.id not in updated_schedules:
|
if sch.id not in updated_schedules:
|
||||||
updated_schedules.append(sch.id)
|
updated_schedules.append(sch.id)
|
||||||
|
|
||||||
# Lưu chi tiết phân bổ vào entry
|
|
||||||
entry_allocation_detail.append({
|
entry_allocation_detail.append({
|
||||||
"schedule_id": sch.id,
|
"schedule_id": sch.id,
|
||||||
"schedule_code": sch.code,
|
"schedule_code": sch.code,
|
||||||
@@ -302,65 +408,48 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
})
|
})
|
||||||
|
|
||||||
# Cập nhật entry allocation info
|
total_allocated = entry_principal_allocated + entry_penalty_allocated
|
||||||
total_allocated_for_entry = entry_principal_allocated + entry_penalty_allocated
|
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + total_allocated
|
||||||
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + total_allocated_for_entry
|
entry.allocation_remain = remaining
|
||||||
entry.allocation_remain = remaining # Số tiền còn thừa
|
|
||||||
entry.allocation_detail = entry_allocation_detail
|
entry.allocation_detail = entry_allocation_detail
|
||||||
|
|
||||||
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
||||||
|
|
||||||
if entry.id not in updated_entries:
|
if entry.id not in updated_entries:
|
||||||
updated_entries.append(entry.id)
|
updated_entries.append(entry.id)
|
||||||
|
|
||||||
# Cộng vào tổng
|
|
||||||
total_principal_allocated += entry_principal_allocated
|
total_principal_allocated += entry_principal_allocated
|
||||||
total_penalty_allocated += entry_penalty_allocated
|
total_penalty_allocated += entry_penalty_allocated
|
||||||
|
|
||||||
# Cập nhật Transaction_Detail - TÁCH RIÊNG PRINCIPAL VÀ PENALTY
|
# Cập nhật Transaction & Transaction_Detail
|
||||||
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
||||||
# Cập nhật amount_received (chỉ cộng principal)
|
|
||||||
txn_detail.amount_received = F('amount_received') + total_principal_allocated
|
txn_detail.amount_received = F('amount_received') + total_principal_allocated
|
||||||
txn_detail.amount_remaining = F('amount_remaining') - total_principal_allocated
|
txn_detail.amount_remaining = F('amount_remaining') - total_principal_allocated
|
||||||
|
|
||||||
# Cập nhật penalty_amount
|
|
||||||
if hasattr(txn_detail, 'penalty_amount'):
|
if hasattr(txn_detail, 'penalty_amount'):
|
||||||
txn_detail.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
txn_detail.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
||||||
txn_detail.save(update_fields=['amount_received', 'amount_remaining', 'penalty_amount'])
|
txn_detail.save(update_fields=['amount_received', 'amount_remaining', 'penalty_amount'])
|
||||||
else:
|
else:
|
||||||
txn_detail.save(update_fields=['amount_received', 'amount_remaining'])
|
txn_detail.save(update_fields=['amount_received', 'amount_remaining'])
|
||||||
|
|
||||||
txn_detail.refresh_from_db()
|
txn_detail.refresh_from_db()
|
||||||
|
|
||||||
# Kiểm tra và cập nhật status nếu đã thanh toán đủ
|
|
||||||
if txn_detail.amount_remaining <= 0 and paid_txn_status:
|
if txn_detail.amount_remaining <= 0 and paid_txn_status:
|
||||||
txn_detail.status = paid_txn_status
|
txn_detail.status = paid_txn_status
|
||||||
txn_detail.save(update_fields=['status'])
|
txn_detail.save(update_fields=['status'])
|
||||||
|
|
||||||
# Cập nhật Transaction - TÁCH RIÊNG PRINCIPAL VÀ PENALTY
|
|
||||||
if total_principal_allocated > 0 or total_penalty_allocated > 0:
|
|
||||||
# Cập nhật amount_received (chỉ cộng principal)
|
|
||||||
txn.amount_received = F('amount_received') + total_principal_allocated
|
txn.amount_received = F('amount_received') + total_principal_allocated
|
||||||
txn.amount_remain = F('amount_remain') - total_principal_allocated
|
txn.amount_remain = F('amount_remain') - total_principal_allocated
|
||||||
|
|
||||||
# Cập nhật penalty_amount
|
|
||||||
if hasattr(txn, 'penalty_amount'):
|
if hasattr(txn, 'penalty_amount'):
|
||||||
txn.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
txn.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
||||||
txn.save(update_fields=['amount_received', 'amount_remain', 'penalty_amount'])
|
txn.save(update_fields=['amount_received', 'amount_remain', 'penalty_amount'])
|
||||||
else:
|
else:
|
||||||
txn.save(update_fields=['amount_received', 'amount_remain'])
|
txn.save(update_fields=['amount_received', 'amount_remain'])
|
||||||
|
|
||||||
txn.refresh_from_db()
|
txn.refresh_from_db()
|
||||||
|
|
||||||
# Kiểm tra và cập nhật status nếu đã thanh toán đủ
|
|
||||||
if txn.amount_remain <= 0 and paid_txn_status:
|
if txn.amount_remain <= 0 and paid_txn_status:
|
||||||
txn.status = paid_txn_status
|
txn.status = paid_txn_status
|
||||||
txn.save(update_fields=['status'])
|
txn.save(update_fields=['status'])
|
||||||
|
|
||||||
except Product.DoesNotExist:
|
|
||||||
errors.append(f"Product {product_id}: Không tồn tại")
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"Product {product_id}: Lỗi phân bổ - {str(exc)}")
|
errors.append(str(exc))
|
||||||
import traceback
|
import traceback
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
@@ -369,13 +458,12 @@ def allocate_payment_to_schedules(product_id):
|
|||||||
"updated_schedules": updated_schedules,
|
"updated_schedules": updated_schedules,
|
||||||
"updated_entries": updated_entries,
|
"updated_entries": updated_entries,
|
||||||
"errors": errors,
|
"errors": errors,
|
||||||
"rule_used": allocation_rule,
|
"rule_used": "principal-fee",
|
||||||
"total_principal_allocated": float(total_principal_allocated),
|
"total_principal_allocated": float(total_principal_allocated),
|
||||||
"total_penalty_allocated": float(total_penalty_allocated)
|
"total_penalty_allocated": float(total_penalty_allocated)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================================================
|
|
||||||
# HÀM MIỄN LÃI - XỬ LÝ ENTRY TỪ TÀI KHOẢN MIỄN LÃI (ID=5)
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def allocate_penalty_reduction(product_id):
|
def allocate_penalty_reduction(product_id):
|
||||||
"""
|
"""
|
||||||
@@ -391,7 +479,6 @@ def allocate_penalty_reduction(product_id):
|
|||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
# Lấy product
|
|
||||||
product = Product.objects.get(id=product_id)
|
product = Product.objects.get(id=product_id)
|
||||||
|
|
||||||
booked = Product_Booked.objects.filter(product=product).first()
|
booked = Product_Booked.objects.filter(product=product).first()
|
||||||
@@ -420,7 +507,6 @@ def allocate_penalty_reduction(product_id):
|
|||||||
"errors": errors
|
"errors": errors
|
||||||
}
|
}
|
||||||
|
|
||||||
# Lấy các entry CR từ tài khoản miễn lãi (id=5) có tiền thừa
|
|
||||||
reduction_entries = Internal_Entry.objects.select_for_update().filter(
|
reduction_entries = Internal_Entry.objects.select_for_update().filter(
|
||||||
product=product,
|
product=product,
|
||||||
type__code='CR',
|
type__code='CR',
|
||||||
@@ -437,7 +523,6 @@ def allocate_penalty_reduction(product_id):
|
|||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Lấy các lịch chưa thanh toán (status=1)
|
|
||||||
schedules = Payment_Schedule.objects.select_for_update().filter(
|
schedules = Payment_Schedule.objects.select_for_update().filter(
|
||||||
txn_detail=txn_detail,
|
txn_detail=txn_detail,
|
||||||
status=1
|
status=1
|
||||||
@@ -452,7 +537,6 @@ def allocate_penalty_reduction(product_id):
|
|||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Xử lý từng entry miễn lãi
|
|
||||||
for entry in reduction_entries:
|
for entry in reduction_entries:
|
||||||
remaining_reduce = Decimal(str(entry.allocation_remain))
|
remaining_reduce = Decimal(str(entry.allocation_remain))
|
||||||
|
|
||||||
@@ -470,7 +554,6 @@ def allocate_penalty_reduction(product_id):
|
|||||||
current_penalty_reduce = Decimal(str(schedule.penalty_reduce or 0))
|
current_penalty_reduce = Decimal(str(schedule.penalty_reduce or 0))
|
||||||
current_remain_amount = Decimal(str(schedule.remain_amount or 0))
|
current_remain_amount = Decimal(str(schedule.remain_amount or 0))
|
||||||
|
|
||||||
# Chỉ miễn tối đa bằng số phạt còn lại
|
|
||||||
to_reduce = min(remaining_reduce, current_penalty_remain)
|
to_reduce = min(remaining_reduce, current_penalty_remain)
|
||||||
|
|
||||||
if to_reduce <= 0:
|
if to_reduce <= 0:
|
||||||
@@ -479,34 +562,19 @@ def allocate_penalty_reduction(product_id):
|
|||||||
remaining_reduce -= to_reduce
|
remaining_reduce -= to_reduce
|
||||||
entry_reduction_allocated += to_reduce
|
entry_reduction_allocated += to_reduce
|
||||||
|
|
||||||
# Cập nhật các trường
|
|
||||||
schedule.penalty_reduce = current_penalty_reduce + to_reduce
|
schedule.penalty_reduce = current_penalty_reduce + to_reduce
|
||||||
schedule.penalty_remain = current_penalty_remain - to_reduce
|
schedule.penalty_remain = current_penalty_remain - to_reduce
|
||||||
|
schedule.remain_amount = current_remain_amount - to_reduce
|
||||||
|
|
||||||
# GIẢM TỔNG CÒN LẠI (remain_amount)
|
sch_entry_list = schedule.entry or []
|
||||||
schedule.remain_amount = max(Decimal('0'), current_remain_amount - to_reduce)
|
sch_entry_list.append({
|
||||||
|
|
||||||
# KHÔNG ĐỘNG ĐẾN amount_remain (nợ gốc còn lại)
|
|
||||||
|
|
||||||
# Ghi trace bút toán miễn lãi vào schedule
|
|
||||||
schedule_entry_list = schedule.entry or []
|
|
||||||
|
|
||||||
date_value = entry.date
|
|
||||||
if hasattr(date_value, 'isoformat'):
|
|
||||||
date_value = date_value.isoformat()
|
|
||||||
else:
|
|
||||||
date_value = str(date_value)
|
|
||||||
|
|
||||||
schedule_entry_list.append({
|
|
||||||
"code": entry.code,
|
|
||||||
"amount": float(to_reduce),
|
|
||||||
"date": date_value,
|
|
||||||
"type": "REDUCTION",
|
"type": "REDUCTION",
|
||||||
"note": "Miễn lãi phạt quá hạn"
|
"code": entry.code,
|
||||||
|
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||||
|
"amount": float(to_reduce)
|
||||||
})
|
})
|
||||||
schedule.entry = schedule_entry_list
|
schedule.entry = safe_json_serialize(sch_entry_list)
|
||||||
|
|
||||||
# Lưu lại schedule
|
|
||||||
schedule.save(update_fields=[
|
schedule.save(update_fields=[
|
||||||
'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry'
|
'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry'
|
||||||
])
|
])
|
||||||
@@ -514,7 +582,6 @@ def allocate_penalty_reduction(product_id):
|
|||||||
if schedule.id not in updated_schedules:
|
if schedule.id not in updated_schedules:
|
||||||
updated_schedules.append(schedule.id)
|
updated_schedules.append(schedule.id)
|
||||||
|
|
||||||
# Lưu chi tiết vào entry
|
|
||||||
entry_allocation_detail.append({
|
entry_allocation_detail.append({
|
||||||
"schedule_id": schedule.id,
|
"schedule_id": schedule.id,
|
||||||
"schedule_code": schedule.code,
|
"schedule_code": schedule.code,
|
||||||
@@ -523,20 +590,16 @@ def allocate_penalty_reduction(product_id):
|
|||||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
})
|
})
|
||||||
|
|
||||||
# Cập nhật entry allocation info
|
|
||||||
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + entry_reduction_allocated
|
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + entry_reduction_allocated
|
||||||
entry.allocation_remain = remaining_reduce
|
entry.allocation_remain = remaining_reduce
|
||||||
entry.allocation_detail = entry_allocation_detail
|
entry.allocation_detail = entry_allocation_detail
|
||||||
|
|
||||||
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
||||||
|
|
||||||
if entry.id not in updated_entries:
|
if entry.id not in updated_entries:
|
||||||
updated_entries.append(entry.id)
|
updated_entries.append(entry.id)
|
||||||
|
|
||||||
except Product.DoesNotExist:
|
|
||||||
errors.append(f"Product {product_id}: Không tồn tại")
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"Product {product_id}: Lỗi miễn lãi - {str(exc)}")
|
errors.append(str(exc))
|
||||||
import traceback
|
import traceback
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
@@ -544,25 +607,17 @@ def allocate_penalty_reduction(product_id):
|
|||||||
"status": "success" if not errors else "partial_failure",
|
"status": "success" if not errors else "partial_failure",
|
||||||
"updated_schedules": updated_schedules,
|
"updated_schedules": updated_schedules,
|
||||||
"updated_entries": updated_entries,
|
"updated_entries": updated_entries,
|
||||||
"errors": errors,
|
"errors": errors
|
||||||
"message": f"Đã miễn lãi cho {len(updated_schedules)} lịch thanh toán"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================================================
|
|
||||||
# BACKGROUND FUNCTION - NHẬN PRODUCT_ID
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
def background_allocate(product_id):
|
def background_allocate(product_id):
|
||||||
"""
|
"""Background task để chạy allocation sau khi tạo entry"""
|
||||||
Chạy phân bổ ngầm cho một product_id cụ thể.
|
|
||||||
Quét tất cả entry cũ + mới có tiền thừa để phân bổ.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation started for product_id={product_id}")
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation started for product_id={product_id}")
|
||||||
|
|
||||||
# Phân bổ thanh toán thông thường
|
|
||||||
normal_result = allocate_payment_to_schedules(product_id)
|
normal_result = allocate_payment_to_schedules(product_id)
|
||||||
|
|
||||||
# Phân bổ miễn lãi
|
|
||||||
reduction_result = allocate_penalty_reduction(product_id)
|
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(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation completed for product_id={product_id}:")
|
||||||
@@ -574,12 +629,13 @@ def background_allocate(product_id):
|
|||||||
import traceback
|
import traceback
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
# API TẠO MỘT BÚT TOÁN
|
# API VIEWS
|
||||||
# ==========================================================================================
|
# ==========================================================================================
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def account_entry(request):
|
def account_entry(request):
|
||||||
print(request.data.get('date'))
|
"""View function để tạo bút toán (được gọi từ urls.py)"""
|
||||||
ref = request.data.get('ref')
|
ref = request.data.get('ref')
|
||||||
|
|
||||||
response_data, created_entry = account_entry_api(
|
response_data, created_entry = account_entry_api(
|
||||||
@@ -598,37 +654,28 @@ def account_entry(request):
|
|||||||
if 'error' in response_data:
|
if 'error' in response_data:
|
||||||
return Response(response_data, status=400)
|
return Response(response_data, status=400)
|
||||||
|
|
||||||
# Lưu product_id để chạy sau khi response
|
product_id = created_entry.product_id if created_entry else None
|
||||||
product_id_to_allocate = created_entry.product_id if created_entry else None
|
|
||||||
|
|
||||||
# Tạo response trước
|
|
||||||
response = Response({
|
response = Response({
|
||||||
**response_data,
|
**response_data,
|
||||||
"message": "Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm..."
|
"message": "Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm..."
|
||||||
})
|
})
|
||||||
|
|
||||||
# Chạy background allocation SAU KHI transaction đã commit
|
if product_id:
|
||||||
if product_id_to_allocate:
|
|
||||||
def run_allocation():
|
def run_allocation():
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(target=background_allocate, args=(product_id,), daemon=True)
|
||||||
target=background_allocate,
|
|
||||||
args=(product_id_to_allocate,),
|
|
||||||
daemon=True
|
|
||||||
)
|
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
transaction.on_commit(run_allocation)
|
transaction.on_commit(run_allocation)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# ==========================================================================================
|
|
||||||
# API TẠO NHIỀU BÚT TOÁN
|
|
||||||
# ==========================================================================================
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def account_multi_entry(request):
|
def account_multi_entry(request):
|
||||||
|
"""Tạo nhiều bút toán cùng lúc"""
|
||||||
try:
|
try:
|
||||||
result = []
|
result = []
|
||||||
product_ids = set() # Thu thập các product_id cần phân bổ
|
product_ids = set()
|
||||||
data_list = request.data.get('data', [])
|
data_list = request.data.get('data', [])
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -651,13 +698,11 @@ def account_multi_entry(request):
|
|||||||
if created_entry and created_entry.product_id:
|
if created_entry and created_entry.product_id:
|
||||||
product_ids.add(created_entry.product_id)
|
product_ids.add(created_entry.product_id)
|
||||||
|
|
||||||
# Tạo response
|
|
||||||
response = Response({
|
response = Response({
|
||||||
"entries": result,
|
"entries": result,
|
||||||
"message": f"Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm cho {len(product_ids)} sản phẩm..."
|
"message": f"Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầm cho {len(product_ids)} sản phẩm..."
|
||||||
})
|
})
|
||||||
|
|
||||||
# Chạy background allocation SAU KHI transaction đã commit
|
|
||||||
if product_ids:
|
if product_ids:
|
||||||
def run_allocations():
|
def run_allocations():
|
||||||
for product_id in product_ids:
|
for product_id in product_ids:
|
||||||
@@ -675,3 +720,195 @@ def account_multi_entry(request):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print({'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"})
|
print({'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"})
|
||||||
return Response({'error': str(e)}, status=400)
|
return Response({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['POST'])
|
||||||
|
def delete_entry(request):
|
||||||
|
"""View function để xóa bút toán (tương thích với urls.py)"""
|
||||||
|
entry_id = request.data.get('id')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
entry = Internal_Entry.objects.select_for_update().get(id=entry_id)
|
||||||
|
except Internal_Entry.DoesNotExist:
|
||||||
|
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,
|
||||||
|
'amount': float(entry.amount),
|
||||||
|
'type': entry.type.code,
|
||||||
|
'account_code': entry.account.code,
|
||||||
|
'account_id': entry.account_id,
|
||||||
|
'product_id': entry.product_id if entry.product else None,
|
||||||
|
'allocation_amount': float(entry.allocation_amount or 0),
|
||||||
|
'allocation_remain': float(entry.allocation_remain or 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
allocation_detail = entry.allocation_detail or []
|
||||||
|
schedules_reversed = []
|
||||||
|
total_principal_reversed = Decimal('0')
|
||||||
|
total_penalty_reversed = Decimal('0')
|
||||||
|
total_reduction_reversed = Decimal('0')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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'])
|
||||||
|
|
||||||
|
# Tính lại lãi sau khi hoàn tác
|
||||||
|
update_penalty_after_allocation(schedule)
|
||||||
|
|
||||||
|
schedules_reversed.append({
|
||||||
|
'schedule_id': schedule.id,
|
||||||
|
'schedule_code': schedule.code,
|
||||||
|
'amount_reversed': float(allocated_amount),
|
||||||
|
'principal_reversed': float(principal),
|
||||||
|
'penalty_reversed': float(penalty),
|
||||||
|
'type': allocation_type
|
||||||
|
})
|
||||||
|
|
||||||
|
except Payment_Schedule.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return Response({
|
||||||
|
'error': f'Đã xảy ra lỗi khi xóa bút toán: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
@@ -156,7 +156,7 @@ def start():
|
|||||||
Khởi động APScheduler và thêm tác vụ quét job.
|
Khởi động APScheduler và thêm tác vụ quét job.
|
||||||
"""
|
"""
|
||||||
scheduler = BackgroundScheduler(timezone='Asia/Ho_Chi_Minh')
|
scheduler = BackgroundScheduler(timezone='Asia/Ho_Chi_Minh')
|
||||||
# Chạy tác vụ quét job mỗi 60 giây
|
# Chạy tác vụ quét job mỗi 5 giây
|
||||||
scheduler.add_job(scan_and_run_due_jobs, 'interval', seconds=60, id='scan_due_jobs_job', replace_existing=True)
|
scheduler.add_job(scan_and_run_due_jobs, 'interval', seconds=5, id='scan_due_jobs_job', replace_existing=True)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
#logger.info("APScheduler started... Jobs will be scanned every 60 seconds.")
|
#logger.info("APScheduler started... Jobs will be scanned every 5 seconds.")
|
||||||
|
|||||||
@@ -288,3 +288,88 @@ def lookup_data_action(params, context):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [LOOKUP] EXCEPTION: {str(e)}")
|
print(f" [LOOKUP] EXCEPTION: {str(e)}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Quét và phân bổ toàn bộ bút toán còn phần dư
|
||||||
|
# ============================
|
||||||
|
@register_action("ALLOCATE_ALL_PENDING", schema={})
|
||||||
|
def allocate_all_pending_action(params, context):
|
||||||
|
"""
|
||||||
|
Quét toàn bộ Internal_Entry có allocation_remain > 0 (type CR),
|
||||||
|
group by product_id, gọi phân bổ cho từng product cho đến khi hết.
|
||||||
|
"""
|
||||||
|
from app.payment import allocate_payment_to_schedules, allocate_penalty_reduction
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
Internal_Entry = apps.get_model("app", "Internal_Entry")
|
||||||
|
Payment_Schedule = apps.get_model("app", "Payment_Schedule")
|
||||||
|
Product_Booked = apps.get_model("app", "Product_Booked")
|
||||||
|
Transaction_Current = apps.get_model("app", "Transaction_Current")
|
||||||
|
Transaction_Detail = apps.get_model("app", "Transaction_Detail")
|
||||||
|
|
||||||
|
# ---------- Lấy toàn bộ product_id còn entry chưa phân bổ hết ----------
|
||||||
|
product_ids = list(
|
||||||
|
Internal_Entry.objects.filter(
|
||||||
|
type__code="CR",
|
||||||
|
allocation_remain__gt=0,
|
||||||
|
product__isnull=False
|
||||||
|
)
|
||||||
|
.values_list("product_id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" [ALLOCATE_ALL] Tìm được {len(product_ids)} product có entry còn phần dư")
|
||||||
|
|
||||||
|
if not product_ids:
|
||||||
|
return {"total_products": 0, "results": []}
|
||||||
|
|
||||||
|
# ---------- DEBUG: dump trạng thái trước khi phân bổ ----------
|
||||||
|
for pid in product_ids:
|
||||||
|
print(f"\n [DEBUG] ===== Product {pid} — trạng thái TRƯỚC phân bổ =====")
|
||||||
|
|
||||||
|
# Entries
|
||||||
|
entries = Internal_Entry.objects.filter(
|
||||||
|
product_id=pid, type__code="CR", allocation_remain__gt=0
|
||||||
|
).order_by("date", "create_time")
|
||||||
|
for e in entries:
|
||||||
|
print(f" Entry id={e.id} | account_id={e.account_id} | amount={e.amount} | allocation_remain={e.allocation_remain} | date={e.date}")
|
||||||
|
|
||||||
|
# Lấy txn_detail của product
|
||||||
|
booked = Product_Booked.objects.filter(product_id=pid).first()
|
||||||
|
if not booked or not booked.transaction:
|
||||||
|
print(f" !! Không có Product_Booked / Transaction")
|
||||||
|
continue
|
||||||
|
|
||||||
|
txn = booked.transaction
|
||||||
|
txn_detail = None
|
||||||
|
try:
|
||||||
|
current = Transaction_Current.objects.get(transaction=txn)
|
||||||
|
txn_detail = current.detail
|
||||||
|
except Exception:
|
||||||
|
txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by("-create_time").first()
|
||||||
|
|
||||||
|
if not txn_detail:
|
||||||
|
print(f" !! Không có Transaction_Detail")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Schedules
|
||||||
|
all_schedules = Payment_Schedule.objects.filter(txn_detail=txn_detail).order_by("cycle", "from_date")
|
||||||
|
unpaid = all_schedules.filter(status__id=1)
|
||||||
|
print(f" Tổng schedule: {all_schedules.count()} | Chưa thanh toán (status=1): {unpaid.count()}")
|
||||||
|
for s in all_schedules:
|
||||||
|
print(f" Schedule id={s.id} | cycle={s.cycle} | status_id={s.status_id} | amount_remain={s.amount_remain} | penalty_remain={s.penalty_remain} | remain_amount={s.remain_amount}")
|
||||||
|
|
||||||
|
# ---------- Chạy phân bổ ----------
|
||||||
|
results = []
|
||||||
|
for product_id in product_ids:
|
||||||
|
try:
|
||||||
|
normal = allocate_payment_to_schedules(product_id)
|
||||||
|
reduction = allocate_penalty_reduction(product_id)
|
||||||
|
results.append({"product_id": product_id, "normal": normal, "reduction": reduction})
|
||||||
|
print(f" [ALLOCATE_ALL] Product {product_id}: OK — normal={normal}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [ALLOCATE_ALL] Product {product_id}: ERROR - {str(e)}")
|
||||||
|
results.append({"product_id": product_id, "error": str(e)})
|
||||||
|
|
||||||
|
return {"total_products": len(product_ids), "results": results}
|
||||||
Reference in New Issue
Block a user