changes
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
22
app/migrations/0373_remove_transaction_ovd_days_and_more.py
Normal file
22
app/migrations/0373_remove_transaction_ovd_days_and_more.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.7 on 2026-01-30 08:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0372_payment_schedule_amount_remain_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='transaction',
|
||||
name='ovd_days',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='internal_entry',
|
||||
name='allocation_remain',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=35, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1408,7 +1408,6 @@ class Transaction(AutoCodeModel):
|
||||
deposit_remaining = models.DecimalField(max_digits=35, decimal_places=2, null=True)
|
||||
amount_received = models.DecimalField(max_digits=35, decimal_places=2, null=True)
|
||||
amount_remain = models.DecimalField(max_digits=35, decimal_places=2, null=True)
|
||||
ovd_days = models.IntegerField(null=True)
|
||||
penalty_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2)
|
||||
early_discount_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True)
|
||||
payment_plan = models.JSONField(null=True)
|
||||
@@ -1672,6 +1671,7 @@ class Internal_Entry(AutoCodeModel):
|
||||
customer = models.ForeignKey(Customer, null=True, related_name='entrycus', on_delete=models.PROTECT)
|
||||
product = models.ForeignKey(Product, null=True, related_name='+', on_delete=models.PROTECT)
|
||||
allocation_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2)
|
||||
allocation_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2)
|
||||
allocation_detail = models.JSONField(null=True)
|
||||
create_time = models.DateTimeField(null=True, auto_now_add=True)
|
||||
update_time = models.DateTimeField(null=True, auto_now=True)
|
||||
|
||||
492
app/payment.py
492
app/payment.py
@@ -43,6 +43,7 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p
|
||||
account.refresh_from_db()
|
||||
new_balance = account.balance
|
||||
|
||||
# Tất cả entry CR đều có allocation_remain ban đầu = amount
|
||||
entry = Internal_Entry.objects.create(
|
||||
category=entry_category,
|
||||
content=content,
|
||||
@@ -56,10 +57,13 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p
|
||||
date=system_date,
|
||||
ref=ref,
|
||||
product=None if product is None else Product.objects.get(id=product),
|
||||
customer=None if customer is None else Customer.objects.get(id=customer)
|
||||
customer=None if customer is None else Customer.objects.get(id=customer),
|
||||
allocation_amount=Decimal('0'),
|
||||
allocation_remain=Decimal(str(amount)) if entry_type.code == 'CR' else Decimal('0'),
|
||||
allocation_detail=[]
|
||||
)
|
||||
|
||||
text = 'id,account__currency__code,ref,balance_before,balance_after,code,account,account__code,account__branch__name,account__type__name,date,amount,content,inputer,inputer__fullname,approver,approver__fullname,create_time,update_time,type,type__code,type__name'
|
||||
text = 'id,account__currency__code,ref,balance_before,balance_after,code,account,account__code,account__branch__name,account__type__name,date,amount,content,inputer,inputer__fullname,approver,approver__fullname,create_time,update_time,type,type__code,type__name,allocation_amount,allocation_remain'
|
||||
fields = text.split(',')
|
||||
response_data = Internal_Entry.objects.filter(id=entry.id).values(*fields).first()
|
||||
return response_data, entry
|
||||
@@ -92,14 +96,20 @@ def get_allocation_rule():
|
||||
return 'principal-fee'
|
||||
|
||||
# ==========================================================================================
|
||||
# HÀM PHÂN BỔ THÔNG THƯỜNG
|
||||
# HÀM PHÂN BỔ THEO PRODUCT_ID - QUÉT LẠI TOÀN BỘ ENTRY CŨ CÓ TIỀN THỪA
|
||||
# ==========================================================================================
|
||||
def allocate_payment_to_schedules(entries):
|
||||
if not entries:
|
||||
return {"status": "no_entries", "message": "Không có bút toán"}
|
||||
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:
|
||||
return {"status": "no_product", "message": "Không có product_id"}
|
||||
|
||||
allocation_rule = get_allocation_rule()
|
||||
updated_schedules = []
|
||||
updated_entries = []
|
||||
errors = []
|
||||
|
||||
# Lấy status "đã thanh toán" một lần
|
||||
@@ -115,47 +125,88 @@ def allocate_payment_to_schedules(entries):
|
||||
except Transaction_Status.DoesNotExist:
|
||||
errors.append("Không tìm thấy Transaction_Status id=2 (đã thanh toán)")
|
||||
|
||||
for entry in entries:
|
||||
if entry.type.code != 'CR' or entry.account.id == 5:
|
||||
continue
|
||||
with transaction.atomic():
|
||||
try:
|
||||
# Lấy product
|
||||
product = Product.objects.get(id=product_id)
|
||||
|
||||
# Lấy transaction của product
|
||||
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
|
||||
}
|
||||
|
||||
amount = Decimal(str(entry.amount))
|
||||
if amount <= 0:
|
||||
continue
|
||||
txn = booked.transaction
|
||||
|
||||
with transaction.atomic():
|
||||
# Lấy transaction detail
|
||||
txn_detail = None
|
||||
try:
|
||||
booked = Product_Booked.objects.filter(product=entry.product).first()
|
||||
if not booked or not booked.transaction:
|
||||
errors.append(f"Entry {entry.code}: Không tìm thấy Transaction")
|
||||
current = Transaction_Current.objects.get(transaction=txn)
|
||||
txn_detail = current.detail
|
||||
except (Transaction_Current.DoesNotExist, AttributeError):
|
||||
txn_detail = Transaction_Detail.objects.filter(
|
||||
transaction=txn
|
||||
).order_by('-create_time').first()
|
||||
|
||||
if not txn_detail:
|
||||
errors.append(f"Product {product_id}: Không tìm thấy Transaction_Detail")
|
||||
return {
|
||||
"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(
|
||||
product=product,
|
||||
type__code='CR',
|
||||
allocation_remain__gt=0
|
||||
).exclude(
|
||||
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():
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Không có entry nào cần phân bổ",
|
||||
"updated_schedules": [],
|
||||
"updated_entries": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# Lấy các lịch chưa thanh toán (status=1)
|
||||
schedules = Payment_Schedule.objects.select_for_update().filter(
|
||||
txn_detail=txn_detail,
|
||||
status__id=1
|
||||
).order_by('cycle', 'from_date')
|
||||
|
||||
if not schedules.exists():
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Không có lịch thanh toán cần phân bổ",
|
||||
"updated_schedules": [],
|
||||
"updated_entries": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# TỔNG TIỀN PHÂN BỔ THÀNH CÔNG (PRINCIPAL + PENALTY)
|
||||
total_principal_allocated = Decimal('0')
|
||||
total_penalty_allocated = Decimal('0')
|
||||
|
||||
# PHÂN BỔ TỪNG ENTRY
|
||||
for entry in entries_with_remain:
|
||||
remaining = Decimal(str(entry.allocation_remain))
|
||||
|
||||
if remaining <= 0:
|
||||
continue
|
||||
|
||||
txn = booked.transaction
|
||||
|
||||
txn_detail = None
|
||||
try:
|
||||
current = Transaction_Current.objects.get(transaction=txn)
|
||||
txn_detail = current.detail
|
||||
except (Transaction_Current.DoesNotExist, AttributeError):
|
||||
txn_detail = Transaction_Detail.objects.filter(
|
||||
transaction=txn
|
||||
).order_by('-create_time').first()
|
||||
|
||||
if not txn_detail:
|
||||
errors.append(f"Entry {entry.code}: Không tìm thấy Transaction_Detail")
|
||||
continue
|
||||
|
||||
schedules = Payment_Schedule.objects.filter(
|
||||
txn_detail=txn_detail,
|
||||
status__id=1 # giả sử status=1 là chưa thanh toán
|
||||
).order_by('cycle', 'from_date')
|
||||
|
||||
if not schedules.exists():
|
||||
continue
|
||||
|
||||
remaining = amount
|
||||
total_allocated = Decimal('0')
|
||||
entry_allocation_detail = entry.allocation_detail or []
|
||||
entry_principal_allocated = Decimal('0')
|
||||
entry_penalty_allocated = Decimal('0')
|
||||
|
||||
# Phân bổ vào các lịch
|
||||
for sch in schedules:
|
||||
if remaining <= 0:
|
||||
break
|
||||
@@ -169,35 +220,48 @@ def allocate_payment_to_schedules(entries):
|
||||
to_penalty = Decimal('0')
|
||||
to_principal = Decimal('0')
|
||||
|
||||
# Áp dụng quy tắc phân bổ
|
||||
if allocation_rule == 'fee-principal':
|
||||
# Phạt trước
|
||||
to_penalty = min(remaining, penalty_remain)
|
||||
remaining -= 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
|
||||
total_allocated += allocated_here
|
||||
|
||||
if allocated_here <= 0:
|
||||
continue
|
||||
|
||||
# Cập nhật entry tracking
|
||||
entry_principal_allocated += to_principal
|
||||
entry_penalty_allocated += to_penalty
|
||||
|
||||
# Cập nhật schedule
|
||||
sch.paid_amount = paid_amount
|
||||
sch.penalty_paid = penalty_paid
|
||||
sch.amount_remain = amount_remain
|
||||
sch.penalty_remain = penalty_remain
|
||||
sch.remain_amount = max(Decimal('0'), remain_amount - allocated_here)
|
||||
|
||||
entry_list = sch.entry or []
|
||||
# Lưu trace vào schedule
|
||||
schedule_entry_list = sch.entry or []
|
||||
|
||||
date_value = entry.date
|
||||
if hasattr(date_value, 'isoformat'):
|
||||
@@ -205,7 +269,7 @@ def allocate_payment_to_schedules(entries):
|
||||
else:
|
||||
date_value = str(date_value)
|
||||
|
||||
entry_list.append({
|
||||
schedule_entry_list.append({
|
||||
"code": entry.code,
|
||||
"amount": float(allocated_here),
|
||||
"date": date_value,
|
||||
@@ -214,8 +278,9 @@ def allocate_payment_to_schedules(entries):
|
||||
"penalty": float(to_penalty),
|
||||
"rule": allocation_rule
|
||||
})
|
||||
sch.entry = entry_list
|
||||
sch.entry = schedule_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
|
||||
|
||||
@@ -224,88 +289,178 @@ def allocate_payment_to_schedules(entries):
|
||||
'penalty_remain', 'remain_amount', 'entry', 'status'
|
||||
])
|
||||
|
||||
updated_schedules.append(sch.id)
|
||||
if sch.id not in updated_schedules:
|
||||
updated_schedules.append(sch.id)
|
||||
|
||||
# Cập nhật Transaction_Detail
|
||||
txn_detail.amount_received = F('amount_received') + total_allocated
|
||||
txn_detail.amount_remaining = F('amount_remaining') - total_allocated
|
||||
txn_detail.save()
|
||||
# Lưu chi tiết phân bổ vào entry
|
||||
entry_allocation_detail.append({
|
||||
"schedule_id": sch.id,
|
||||
"schedule_code": sch.code,
|
||||
"amount": float(allocated_here),
|
||||
"principal": float(to_principal),
|
||||
"penalty": float(to_penalty),
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
|
||||
# Cập nhật entry allocation info
|
||||
total_allocated_for_entry = entry_principal_allocated + entry_penalty_allocated
|
||||
entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + total_allocated_for_entry
|
||||
entry.allocation_remain = remaining # Số tiền còn thừa
|
||||
entry.allocation_detail = entry_allocation_detail
|
||||
|
||||
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
||||
|
||||
if entry.id not in updated_entries:
|
||||
updated_entries.append(entry.id)
|
||||
|
||||
# Cộng vào tổng
|
||||
total_principal_allocated += entry_principal_allocated
|
||||
total_penalty_allocated += entry_penalty_allocated
|
||||
|
||||
# Cập nhật Transaction_Detail - 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_detail.amount_received = F('amount_received') + 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'):
|
||||
txn_detail.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
||||
txn_detail.save(update_fields=['amount_received', 'amount_remaining', 'penalty_amount'])
|
||||
else:
|
||||
txn_detail.save(update_fields=['amount_received', 'amount_remaining'])
|
||||
|
||||
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:
|
||||
txn_detail.status = paid_txn_status
|
||||
txn_detail.save(update_fields=['status'])
|
||||
|
||||
# Cập nhật Transaction
|
||||
txn.amount_received = F('amount_received') + total_allocated
|
||||
txn.amount_remain = F('amount_remain') - total_allocated
|
||||
txn.save()
|
||||
# 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_remain = F('amount_remain') - total_principal_allocated
|
||||
|
||||
# Cập nhật penalty_amount
|
||||
if hasattr(txn, 'penalty_amount'):
|
||||
txn.penalty_amount = F('penalty_amount') + total_penalty_allocated
|
||||
txn.save(update_fields=['amount_received', 'amount_remain', 'penalty_amount'])
|
||||
else:
|
||||
txn.save(update_fields=['amount_received', 'amount_remain'])
|
||||
|
||||
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:
|
||||
txn.status = paid_txn_status
|
||||
txn.save(update_fields=['status'])
|
||||
|
||||
except Exception as exc:
|
||||
errors.append(f"Entry {entry.code}: Lỗi phân bổ - {str(exc)}")
|
||||
except Product.DoesNotExist:
|
||||
errors.append(f"Product {product_id}: Không tồn tại")
|
||||
except Exception as exc:
|
||||
errors.append(f"Product {product_id}: Lỗi phân bổ - {str(exc)}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
return {
|
||||
"status": "success" if not errors else "partial_failure",
|
||||
"updated_schedules": updated_schedules,
|
||||
"updated_entries": updated_entries,
|
||||
"errors": errors,
|
||||
"rule_used": allocation_rule
|
||||
"rule_used": allocation_rule,
|
||||
"total_principal_allocated": float(total_principal_allocated),
|
||||
"total_penalty_allocated": float(total_penalty_allocated)
|
||||
}
|
||||
|
||||
# ==========================================================================================
|
||||
# HÀM MIỄN LÃI
|
||||
# HÀM MIỄN LÃI - XỬ LÝ ENTRY TỪ TÀI KHOẢN MIỄN LÃI (ID=5)
|
||||
# ==========================================================================================
|
||||
def allocate_penalty_reduction(entries):
|
||||
if not entries:
|
||||
return {"status": "no_entries", "message": "Không có bút toán"}
|
||||
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.
|
||||
"""
|
||||
if not product_id:
|
||||
return {"status": "no_product", "message": "Không có product_id"}
|
||||
|
||||
updated_schedules = []
|
||||
updated_entries = []
|
||||
errors = []
|
||||
|
||||
for entry in entries:
|
||||
if entry.type.code != 'CR' or entry.account.id != 5:
|
||||
continue # Chỉ xử lý bút toán từ tài khoản miễn lãi
|
||||
with transaction.atomic():
|
||||
try:
|
||||
# Lấy product
|
||||
product = Product.objects.get(id=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
|
||||
}
|
||||
|
||||
amount = Decimal(str(entry.amount))
|
||||
if amount <= 0:
|
||||
continue
|
||||
txn = booked.transaction
|
||||
|
||||
with transaction.atomic():
|
||||
txn_detail = None
|
||||
try:
|
||||
booked = Product_Booked.objects.filter(product=entry.product).first()
|
||||
if not booked or not booked.transaction:
|
||||
errors.append(f"Entry {entry.code}: Không tìm thấy Transaction")
|
||||
current = Transaction_Current.objects.get(transaction=txn)
|
||||
txn_detail = current.detail
|
||||
except (Transaction_Current.DoesNotExist, AttributeError):
|
||||
txn_detail = Transaction_Detail.objects.filter(
|
||||
transaction=txn
|
||||
).order_by('-create_time').first()
|
||||
|
||||
if not txn_detail:
|
||||
errors.append(f"Product {product_id}: Không tìm thấy Transaction_Detail")
|
||||
return {
|
||||
"status": "error",
|
||||
"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(
|
||||
product=product,
|
||||
type__code='CR',
|
||||
account__id=5,
|
||||
allocation_remain__gt=0
|
||||
).order_by('date', 'create_time')
|
||||
|
||||
if not reduction_entries.exists():
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Không có entry miễn lãi cần xử lý",
|
||||
"updated_schedules": [],
|
||||
"updated_entries": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# Lấy các lịch chưa thanh toán (status=1)
|
||||
schedules = Payment_Schedule.objects.select_for_update().filter(
|
||||
txn_detail=txn_detail,
|
||||
status=1
|
||||
).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": []
|
||||
}
|
||||
|
||||
# Xử lý từng entry miễn lãi
|
||||
for entry in reduction_entries:
|
||||
remaining_reduce = Decimal(str(entry.allocation_remain))
|
||||
|
||||
if remaining_reduce <= 0:
|
||||
continue
|
||||
|
||||
txn = booked.transaction
|
||||
|
||||
txn_detail = None
|
||||
try:
|
||||
current = Transaction_Current.objects.get(transaction=txn)
|
||||
txn_detail = current.detail
|
||||
except (Transaction_Current.DoesNotExist, AttributeError):
|
||||
txn_detail = Transaction_Detail.objects.filter(
|
||||
transaction=txn
|
||||
).order_by('-create_time').first()
|
||||
|
||||
if not txn_detail:
|
||||
errors.append(f"Entry {entry.code}: Không tìm thấy Transaction_Detail")
|
||||
continue
|
||||
|
||||
# Lấy các lịch chưa thanh toán (status=1)
|
||||
schedules = Payment_Schedule.objects.filter(
|
||||
txn_detail=txn_detail,
|
||||
status=1
|
||||
).order_by('cycle', 'from_date')
|
||||
|
||||
if not schedules.exists():
|
||||
continue
|
||||
|
||||
remaining_reduce = amount # số tiền miễn lãi còn lại để phân bổ
|
||||
entry_allocation_detail = entry.allocation_detail or []
|
||||
entry_reduction_allocated = Decimal('0')
|
||||
|
||||
for schedule in schedules:
|
||||
if remaining_reduce <= 0:
|
||||
@@ -317,7 +472,12 @@ def allocate_penalty_reduction(entries):
|
||||
|
||||
# Chỉ miễn tối đa bằng số phạt còn lại
|
||||
to_reduce = min(remaining_reduce, current_penalty_remain)
|
||||
|
||||
if to_reduce <= 0:
|
||||
continue
|
||||
|
||||
remaining_reduce -= to_reduce
|
||||
entry_reduction_allocated += to_reduce
|
||||
|
||||
# Cập nhật các trường
|
||||
schedule.penalty_reduce = current_penalty_reduce + to_reduce
|
||||
@@ -328,8 +488,8 @@ def allocate_penalty_reduction(entries):
|
||||
|
||||
# KHÔNG ĐỘNG ĐẾN amount_remain (nợ gốc còn lại)
|
||||
|
||||
# Ghi trace bút toán miễn lãi
|
||||
entry_list = schedule.entry or []
|
||||
# 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'):
|
||||
@@ -337,50 +497,82 @@ def allocate_penalty_reduction(entries):
|
||||
else:
|
||||
date_value = str(date_value)
|
||||
|
||||
entry_list.append({
|
||||
schedule_entry_list.append({
|
||||
"code": entry.code,
|
||||
"amount": float(to_reduce),
|
||||
"date": date_value,
|
||||
"type": "REDUCTION",
|
||||
"note": "Miễn lãi phạt quá hạn"
|
||||
})
|
||||
schedule.entry = entry_list
|
||||
schedule.entry = schedule_entry_list
|
||||
|
||||
# Lưu lại
|
||||
# Lưu lại schedule
|
||||
schedule.save(update_fields=[
|
||||
'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry'
|
||||
])
|
||||
|
||||
updated_schedules.append(schedule.id)
|
||||
if schedule.id not in updated_schedules:
|
||||
updated_schedules.append(schedule.id)
|
||||
|
||||
except Exception as exc:
|
||||
errors.append(f"Entry {entry.code}: Lỗi miễn lãi - {str(exc)}")
|
||||
# Lưu chi tiết vào entry
|
||||
entry_allocation_detail.append({
|
||||
"schedule_id": schedule.id,
|
||||
"schedule_code": schedule.code,
|
||||
"amount": float(to_reduce),
|
||||
"type": "REDUCTION",
|
||||
"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_remain = remaining_reduce
|
||||
entry.allocation_detail = entry_allocation_detail
|
||||
|
||||
entry.save(update_fields=['allocation_amount', 'allocation_remain', 'allocation_detail'])
|
||||
|
||||
if entry.id not in updated_entries:
|
||||
updated_entries.append(entry.id)
|
||||
|
||||
except Product.DoesNotExist:
|
||||
errors.append(f"Product {product_id}: Không tồn tại")
|
||||
except Exception as exc:
|
||||
errors.append(f"Product {product_id}: Lỗi miễn lãi - {str(exc)}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
return {
|
||||
"status": "success" if not errors else "partial_failure",
|
||||
"updated_schedules": updated_schedules,
|
||||
"updated_entries": updated_entries,
|
||||
"errors": errors,
|
||||
"message": f"Đã miễn lãi cho {len(updated_schedules)} lịch thanh toán (chỉ giảm phạt + tổng còn lại)"
|
||||
"message": f"Đã miễn lãi cho {len(updated_schedules)} lịch thanh toán"
|
||||
}
|
||||
|
||||
# ==========================================================================================
|
||||
# BACKGROUND FUNCTION
|
||||
# BACKGROUND FUNCTION - NHẬN PRODUCT_ID
|
||||
# ==========================================================================================
|
||||
def background_allocate(entries_created):
|
||||
def background_allocate(product_id):
|
||||
"""
|
||||
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:
|
||||
# Debug type date
|
||||
for e in entries_created:
|
||||
print(f"Debug entry {e.code}: date type = {type(e.date)}, value = {e.date}")
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation started for product_id={product_id}")
|
||||
|
||||
normal_result = allocate_payment_to_schedules(entries_created)
|
||||
reduction_result = allocate_penalty_reduction(entries_created)
|
||||
# Phân bổ thanh toán thông thường
|
||||
normal_result = allocate_payment_to_schedules(product_id)
|
||||
|
||||
# Phân bổ miễn lãi
|
||||
reduction_result = allocate_penalty_reduction(product_id)
|
||||
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation completed:")
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation error: {str(e)}")
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation error for product_id={product_id}: {str(e)}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
# ==========================================================================================
|
||||
# API TẠO MỘT BÚT TOÁN
|
||||
@@ -398,27 +590,37 @@ def account_entry(request):
|
||||
category=request.data['category'],
|
||||
userid=request.data['user'],
|
||||
ref=ref,
|
||||
product=request.data['product'],
|
||||
customer=request.data['customer'],
|
||||
product=request.data.get('product'),
|
||||
customer=request.data.get('customer'),
|
||||
date=request.data.get('date')
|
||||
)
|
||||
|
||||
if 'error' in response_data:
|
||||
return Response(response_data, status=400)
|
||||
|
||||
if created_entry:
|
||||
thread = threading.Thread(
|
||||
target=background_allocate,
|
||||
args=([created_entry],),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
# Lưu product_id để chạy sau khi response
|
||||
product_id_to_allocate = created_entry.product_id if created_entry else None
|
||||
|
||||
return Response({
|
||||
# Tạo response trước
|
||||
response = Response({
|
||||
**response_data,
|
||||
"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_to_allocate:
|
||||
def run_allocation():
|
||||
thread = threading.Thread(
|
||||
target=background_allocate,
|
||||
args=(product_id_to_allocate,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
transaction.on_commit(run_allocation)
|
||||
|
||||
return response
|
||||
|
||||
# ==========================================================================================
|
||||
# API TẠO NHIỀU BÚT TOÁN
|
||||
# ==========================================================================================
|
||||
@@ -426,7 +628,7 @@ def account_entry(request):
|
||||
def account_multi_entry(request):
|
||||
try:
|
||||
result = []
|
||||
entries_created = []
|
||||
product_ids = set() # Thu thập các product_id cần phân bổ
|
||||
data_list = request.data.get('data', [])
|
||||
|
||||
with transaction.atomic():
|
||||
@@ -438,30 +640,38 @@ def account_multi_entry(request):
|
||||
type='CR',
|
||||
category=obj['category'],
|
||||
userid=request.data.get('user'),
|
||||
ref=obj['ref'],
|
||||
product=obj['product'],
|
||||
customer=obj['customer'],
|
||||
date=obj['date']
|
||||
ref=obj.get('ref'),
|
||||
product=obj.get('product'),
|
||||
customer=obj.get('customer'),
|
||||
date=obj.get('date')
|
||||
)
|
||||
|
||||
result.append(response_data)
|
||||
|
||||
if created_entry:
|
||||
entries_created.append(created_entry)
|
||||
if created_entry and created_entry.product_id:
|
||||
product_ids.add(created_entry.product_id)
|
||||
|
||||
if entries_created:
|
||||
thread = threading.Thread(
|
||||
target=background_allocate,
|
||||
args=(entries_created,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return Response({
|
||||
# Tạo response
|
||||
response = Response({
|
||||
"entries": result,
|
||||
"message": "Bút toán đã tạo thành công. Phân bổ thanh toán đang chạy ngầ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:
|
||||
def run_allocations():
|
||||
for product_id in product_ids:
|
||||
thread = threading.Thread(
|
||||
target=background_allocate,
|
||||
args=(product_id,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
transaction.on_commit(run_allocations)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print({'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"})
|
||||
return Response({'error': str(e)}, status=400)
|
||||
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.
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.
Reference in New Issue
Block a user