from app.models import * 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 # ========================================================================================== def getcode(code, Model): 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) entry_type = Entry_Type.objects.get(code=type) entry_category = Entry_Category.objects.get(id=category) system_date = date if date else datetime.now().strftime("%Y-%m-%d") amount = float(amount) with transaction.atomic(): account = Internal_Account.objects.select_for_update().get(code=code) 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.'}, None if entry_type.code == 'CR': account.balance += amount else: account.balance -= amount account.save() account.refresh_from_db() new_balance = account.balance entry = Internal_Entry.objects.create( category=entry_category, content=content, amount=amount, inputer=user, approver=user, type=entry_type, balance_before=start_balance, balance_after=new_balance, account=account, 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) ) 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(',') 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."}, 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."}, None except Entry_Type.DoesNotExist: 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."}, None except Exception as 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') 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 = [] 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..." }) 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)