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 # Tất cả entry CR đều có allocation_remain ban đầu = amount 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), 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,allocation_amount,allocation_remain' 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Ổ THEO PRODUCT_ID - QUÉT LẠI TOÀN BỘ ENTRY CŨ CÓ TIỀN THỪA # ========================================================================================== 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 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)") 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 } txn = booked.transaction # Lấy transaction detail 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"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 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 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') # Á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 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) # Lưu trace vào schedule schedule_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) schedule_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 = 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 sch.save(update_fields=[ 'paid_amount', 'penalty_paid', 'amount_remain', 'penalty_remain', 'remain_amount', 'entry', 'status' ]) if sch.id not in updated_schedules: updated_schedules.append(sch.id) # 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 - 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 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, "total_principal_allocated": float(total_principal_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): """ 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 = [] 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 } 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"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 entry_allocation_detail = entry.allocation_detail or [] entry_reduction_allocated = Decimal('0') 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) 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 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 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", "note": "Miễn lãi phạt quá hạn" }) schedule.entry = schedule_entry_list # Lưu lại schedule schedule.save(update_fields=[ 'penalty_reduce', 'penalty_remain', 'remain_amount', 'entry' ]) if schedule.id not in updated_schedules: updated_schedules.append(schedule.id) # 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" } # ========================================================================================== # BACKGROUND FUNCTION - NHẬN PRODUCT_ID # ========================================================================================== 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: 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) # 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 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 for product_id={product_id}: {str(e)}") import traceback print(traceback.format_exc()) # ========================================================================================== # 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.get('product'), customer=request.data.get('customer'), date=request.data.get('date') ) if 'error' in response_data: return Response(response_data, status=400) # Lưu product_id để chạy sau khi response product_id_to_allocate = created_entry.product_id if created_entry else None # 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 # ========================================================================================== @api_view(['POST']) def account_multi_entry(request): try: result = [] product_ids = set() # Thu thập các product_id cần phân bổ 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.get('ref'), product=obj.get('product'), customer=obj.get('customer'), date=obj.get('date') ) result.append(response_data) if created_entry and created_entry.product_id: product_ids.add(created_entry.product_id) # Tạo response response = Response({ "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..." }) # 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)