changes
This commit is contained in:
466
app/payment.py
466
app/payment.py
@@ -3,24 +3,22 @@ from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from django.db.models import F
|
||||
import threading
|
||||
|
||||
|
||||
decimal = 0
|
||||
#==========================================================================================
|
||||
# ==========================================================================================
|
||||
def getcode(code, Model):
|
||||
try:
|
||||
obj = Model.objects.latest('id')
|
||||
val = 1 if obj == None else obj.id + 1
|
||||
except Exception as e:
|
||||
val = 1
|
||||
length = len(str(val))
|
||||
text = ''
|
||||
for i in range(0, 6-length):
|
||||
text += '0'
|
||||
return '{}{}{}'.format(code, text, val)
|
||||
try:
|
||||
obj = Model.objects.latest('id')
|
||||
val = 1 if obj is None else obj.id + 1
|
||||
except Exception:
|
||||
val = 1
|
||||
length = len(str(val))
|
||||
text = '0' * (6 - length)
|
||||
return f"{code}{text}{val}"
|
||||
|
||||
|
||||
#==========================================================================================
|
||||
# ==========================================================================================
|
||||
def account_entry_api(code, amount, content, type, category, userid, ref=None, product=None, customer=None, date=None):
|
||||
try:
|
||||
user = User.objects.get(id=userid)
|
||||
@@ -34,10 +32,9 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p
|
||||
start_balance = account.balance or 0
|
||||
|
||||
if entry_type.code == 'DR' and start_balance < amount:
|
||||
return {'error': 'Số dư không đủ để thực hiện giao dịch.'}
|
||||
return {'error': 'Số dư không đủ để thực hiện giao dịch.'}, None
|
||||
|
||||
if entry_type.code == 'CR':
|
||||
# account.balance = F('balance') + amount
|
||||
account.balance += amount
|
||||
else:
|
||||
account.balance -= amount
|
||||
@@ -58,72 +55,413 @@ def account_entry_api(code, amount, content, type, category, userid, ref=None, p
|
||||
account=account,
|
||||
date=system_date,
|
||||
ref=ref,
|
||||
product=None if product==None else Product.objects.get(id=product),
|
||||
customer=None if customer==None else Customer.objects.get(id=customer)
|
||||
product=None if product is None else Product.objects.get(id=product),
|
||||
customer=None if customer is None else Customer.objects.get(id=customer)
|
||||
)
|
||||
|
||||
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'
|
||||
fields = text.split(',')
|
||||
return Internal_Entry.objects.filter(id=entry.id).values(*fields).first()
|
||||
response_data = Internal_Entry.objects.filter(id=entry.id).values(*fields).first()
|
||||
return response_data, entry
|
||||
|
||||
except User.DoesNotExist:
|
||||
return {'error': f"Người dùng với ID {userid} không tồn tại."}
|
||||
return {'error': f"Người dùng với ID {userid} không tồn tại."}, None
|
||||
except Internal_Account.DoesNotExist:
|
||||
return {'error': f"Tài khoản nội bộ với mã '{code}' không tồn tại."}
|
||||
return {'error': f"Tài khoản nội bộ với mã '{code}' không tồn tại."}, None
|
||||
except Entry_Type.DoesNotExist:
|
||||
return {'error': f"Loại bút toán với mã '{type}' không tồn tại."}
|
||||
return {'error': f"Loại bút toán với mã '{type}' không tồn tại."}, None
|
||||
except Entry_Category.DoesNotExist:
|
||||
return {'error': f"Danh mục bút toán với ID '{category}' không tồn tại."}
|
||||
return {'error': f"Danh mục bút toán với ID '{category}' không tồn tại."}, None
|
||||
except Exception as e:
|
||||
return {'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"}
|
||||
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():
|
||||
try:
|
||||
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Ổ THÔNG THƯỜNG
|
||||
# ==========================================================================================
|
||||
def allocate_payment_to_schedules(entries):
|
||||
if not entries:
|
||||
return {"status": "no_entries", "message": "Không có bút toán"}
|
||||
|
||||
allocation_rule = get_allocation_rule()
|
||||
updated_schedules = []
|
||||
errors = []
|
||||
|
||||
# Lấy status "đã thanh toán" một lần
|
||||
paid_payment_status = None
|
||||
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)")
|
||||
|
||||
for entry in entries:
|
||||
if entry.type.code != 'CR' or entry.account.id == 5:
|
||||
continue
|
||||
|
||||
amount = Decimal(str(entry.amount))
|
||||
if amount <= 0:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
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")
|
||||
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')
|
||||
|
||||
for sch in schedules:
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
penalty_remain = Decimal(str(sch.penalty_remain or 0))
|
||||
amount_remain = Decimal(str(sch.amount_remain or 0))
|
||||
paid_amount = Decimal(str(sch.paid_amount or 0))
|
||||
penalty_paid = Decimal(str(sch.penalty_paid or 0))
|
||||
remain_amount = Decimal(str(sch.remain_amount or 0))
|
||||
|
||||
to_penalty = Decimal('0')
|
||||
to_principal = Decimal('0')
|
||||
|
||||
if allocation_rule == 'fee-principal':
|
||||
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:
|
||||
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
|
||||
|
||||
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 []
|
||||
|
||||
date_value = entry.date
|
||||
if hasattr(date_value, 'isoformat'):
|
||||
date_value = date_value.isoformat()
|
||||
else:
|
||||
date_value = str(date_value)
|
||||
|
||||
entry_list.append({
|
||||
"code": entry.code,
|
||||
"amount": float(allocated_here),
|
||||
"date": date_value,
|
||||
"type": "CR",
|
||||
"principal": float(to_principal),
|
||||
"penalty": float(to_penalty),
|
||||
"rule": allocation_rule
|
||||
})
|
||||
sch.entry = entry_list
|
||||
|
||||
if sch.amount_remain <= 0 and sch.penalty_remain <= 0 and paid_payment_status:
|
||||
sch.status = paid_payment_status
|
||||
|
||||
sch.save(update_fields=[
|
||||
'paid_amount', 'penalty_paid', 'amount_remain',
|
||||
'penalty_remain', 'remain_amount', 'entry', 'status'
|
||||
])
|
||||
|
||||
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()
|
||||
txn_detail.refresh_from_db()
|
||||
|
||||
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()
|
||||
txn.refresh_from_db()
|
||||
|
||||
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)}")
|
||||
|
||||
return {
|
||||
"status": "success" if not errors else "partial_failure",
|
||||
"updated_schedules": updated_schedules,
|
||||
"errors": errors,
|
||||
"rule_used": allocation_rule
|
||||
}
|
||||
|
||||
# ==========================================================================================
|
||||
# HÀM MIỄN LÃI
|
||||
# ==========================================================================================
|
||||
def allocate_penalty_reduction(entries):
|
||||
if not entries:
|
||||
return {"status": "no_entries", "message": "Không có bút toán"}
|
||||
|
||||
updated_schedules = []
|
||||
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
|
||||
|
||||
amount = Decimal(str(entry.amount))
|
||||
if amount <= 0:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
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")
|
||||
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ổ
|
||||
|
||||
for schedule in schedules:
|
||||
if remaining_reduce <= 0:
|
||||
break
|
||||
|
||||
current_penalty_remain = Decimal(str(schedule.penalty_remain or 0))
|
||||
current_penalty_reduce = Decimal(str(schedule.penalty_reduce or 0))
|
||||
current_remain_amount = Decimal(str(schedule.remain_amount or 0))
|
||||
|
||||
# Chỉ miễn tối đa bằng số phạt còn lại
|
||||
to_reduce = min(remaining_reduce, current_penalty_remain)
|
||||
remaining_reduce -= to_reduce
|
||||
|
||||
# Cập nhật các trường
|
||||
schedule.penalty_reduce = current_penalty_reduce + to_reduce
|
||||
schedule.penalty_remain = current_penalty_remain - to_reduce
|
||||
|
||||
# GIẢM TỔNG CÒN LẠI (remain_amount)
|
||||
schedule.remain_amount = max(Decimal('0'), current_remain_amount - to_reduce)
|
||||
|
||||
# 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 []
|
||||
|
||||
date_value = entry.date
|
||||
if hasattr(date_value, 'isoformat'):
|
||||
date_value = date_value.isoformat()
|
||||
else:
|
||||
date_value = str(date_value)
|
||||
|
||||
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
|
||||
|
||||
# Lưu lại
|
||||
schedule.save(update_fields=[
|
||||
'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry'
|
||||
])
|
||||
|
||||
updated_schedules.append(schedule.id)
|
||||
|
||||
except Exception as exc:
|
||||
errors.append(f"Entry {entry.code}: Lỗi miễn lãi - {str(exc)}")
|
||||
|
||||
return {
|
||||
"status": "success" if not errors else "partial_failure",
|
||||
"updated_schedules": updated_schedules,
|
||||
"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)"
|
||||
}
|
||||
|
||||
# ==========================================================================================
|
||||
# BACKGROUND FUNCTION
|
||||
# ==========================================================================================
|
||||
def background_allocate(entries_created):
|
||||
try:
|
||||
# Debug type date
|
||||
for e in entries_created:
|
||||
print(f"Debug entry {e.code}: date type = {type(e.date)}, value = {e.date}")
|
||||
|
||||
normal_result = allocate_payment_to_schedules(entries_created)
|
||||
reduction_result = allocate_penalty_reduction(entries_created)
|
||||
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation completed:")
|
||||
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)}")
|
||||
|
||||
# ==========================================================================================
|
||||
# API TẠO MỘT BÚT TOÁN
|
||||
# ==========================================================================================
|
||||
@api_view(['POST'])
|
||||
def account_entry(request):
|
||||
print(request.data.get('date'))
|
||||
ref = request.data.get('ref')
|
||||
data = account_entry_api(
|
||||
code=request.data['code'],
|
||||
amount=request.data['amount'],
|
||||
content=request.data['content'],
|
||||
type=request.data['type'],
|
||||
category=request.data['category'],
|
||||
userid=request.data['user'],
|
||||
ref=ref,
|
||||
product=request.data['product'],
|
||||
customer=request.data['customer'],
|
||||
date=request.data.get('date')
|
||||
)
|
||||
|
||||
if 'error' in data:
|
||||
return Response(data, status=400)
|
||||
|
||||
return Response(data)
|
||||
print(request.data.get('date'))
|
||||
ref = request.data.get('ref')
|
||||
|
||||
response_data, created_entry = account_entry_api(
|
||||
code=request.data['code'],
|
||||
amount=request.data['amount'],
|
||||
content=request.data['content'],
|
||||
type=request.data['type'],
|
||||
category=request.data['category'],
|
||||
userid=request.data['user'],
|
||||
ref=ref,
|
||||
product=request.data['product'],
|
||||
customer=request.data['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()
|
||||
|
||||
return 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..."
|
||||
})
|
||||
|
||||
# ==========================================================================================
|
||||
# API TẠO NHIỀU BÚT TOÁN
|
||||
# ==========================================================================================
|
||||
@api_view(['POST'])
|
||||
def account_multi_entry(request):
|
||||
try:
|
||||
result = []
|
||||
data = request.data.get('data')
|
||||
for obj in data:
|
||||
row = account_entry_api(
|
||||
code=obj['Tài khoản'],
|
||||
amount=obj['amount'],
|
||||
content=obj['content'],
|
||||
type='CR',
|
||||
category=obj['category'],
|
||||
userid=request.data.get('user'),
|
||||
ref=obj['ref'],
|
||||
product=obj['product'],
|
||||
customer=obj['customer'],
|
||||
date=obj['date']
|
||||
)
|
||||
result.append(row)
|
||||
entries_created = []
|
||||
data_list = request.data.get('data', [])
|
||||
|
||||
with transaction.atomic():
|
||||
for obj in data_list:
|
||||
response_data, created_entry = account_entry_api(
|
||||
code=obj['Tài khoản'],
|
||||
amount=obj['amount'],
|
||||
content=obj['content'],
|
||||
type='CR',
|
||||
category=obj['category'],
|
||||
userid=request.data.get('user'),
|
||||
ref=obj['ref'],
|
||||
product=obj['product'],
|
||||
customer=obj['customer'],
|
||||
date=obj['date']
|
||||
)
|
||||
|
||||
result.append(response_data)
|
||||
|
||||
if created_entry:
|
||||
entries_created.append(created_entry)
|
||||
|
||||
if entries_created:
|
||||
thread = threading.Thread(
|
||||
target=background_allocate,
|
||||
args=(entries_created,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return 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..."
|
||||
})
|
||||
|
||||
return Response(result)
|
||||
|
||||
except Exception as e:
|
||||
print({'error': f"Đã xảy ra lỗi không mong muốn: {str(e)}"})
|
||||
return Response(data, status=400)
|
||||
return Response({'error': str(e)}, status=400)
|
||||
Reference in New Issue
Block a user