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, date from decimal import Decimal from django.db.models import F import threading import json # ========================================================================================== # HELPER FUNCTIONS # ========================================================================================== 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): 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), 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 # ========================================================================================== def get_allocation_rule(): return 'principal-fee' # ========================================================================================== DAILY_PENALTY_RATE = Decimal('0.0005') # 0.05% mỗi ngày 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 allocate_payment_to_schedules(product_id): if not product_id: return {"status": "no_product", "message": "Không có product_id"} updated_schedules = [] updated_entries = [] paid_payment_status = Payment_Status.objects.filter(id=2).first() paid_txn_status = Transaction_Status.objects.filter(id=2).first() today = datetime.now().date() DAILY_PENALTY_RATE = Decimal('0.0005') # Giả định 0.05% with transaction.atomic(): try: product = Product.objects.get(id=product_id) booked = Product_Booked.objects.filter(product=product).first() if not booked or not booked.transaction: return {"status": "error", "errors": ["Không tìm thấy Transaction"]} txn = booked.transaction txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by('-create_time').first() # Lấy các bút toán CR còn dư tiền (Sắp xếp chính xác để trừ theo thứ tự thời gian) entries_with_remain = Internal_Entry.objects.select_for_update().filter( product=product, type__code='CR', allocation_remain__gt=0 ).exclude(account__id=5).order_by('date', 'create_time') if not entries_with_remain.exists(): return {"status": "success", "message": "Không có tiền để phân bổ"} # Lấy lịch nợ (chỉ lấy status=1) schedules = Payment_Schedule.objects.select_for_update().filter( txn_detail=txn_detail, status__id=1 ).order_by('cycle', 'from_date') total_principal_allocated = Decimal('0') total_penalty_allocated = Decimal('0') for entry in entries_with_remain: entry_date = entry.date if isinstance(entry_date, str): entry_date = datetime.strptime(entry_date, "%Y-%m-%d").date() 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') for sch in schedules: if remaining <= 0: break current_amount_remain = Decimal(str(sch.amount_remain or 0)) # --- BƯỚC 1: LẤY LÃI TÍCH LŨY TỪ TRACE --- last_entry_date = None accumulated_penalty_to_last = Decimal('0') if sch.entry: for e in sch.entry: if e.get('type') == 'PAYMENT': e_date = datetime.strptime(e['date'], "%Y-%m-%d").date() # Dùng <= để lấy được cả bút toán cùng ngày nộp trước đó if e_date <= entry_date: if e.get('penalty_to_this_entry') is not None: # Luôn cập nhật để lấy con số lũy kế MỚI NHẤT accumulated_penalty_to_last = Decimal(str(e['penalty_to_this_entry'])) if not last_entry_date or e_date > last_entry_date: last_entry_date = e_date # --- BƯỚC 2: TÍNH LÃI PHÁT SINH MỚI --- penalty_added_to_entry = Decimal('0') days_for_trace = 0 if current_amount_remain > 0: if last_entry_date: days_between = max(0, (entry_date - last_entry_date).days) penalty_added_to_entry = current_amount_remain * Decimal(days_between) * DAILY_PENALTY_RATE days_for_trace = days_between else: days_overdue_to_entry = max(0, (entry_date - sch.to_date).days) penalty_added_to_entry = current_amount_remain * Decimal(days_overdue_to_entry) * DAILY_PENALTY_RATE days_for_trace = days_overdue_to_entry # Tổng nợ lãi tại thời điểm này penalty_to_this_entry = accumulated_penalty_to_last + penalty_added_to_entry # QUAN TRỌNG: Nợ lãi thực tế cần trả ngay bây giờ penalty_to_pay_now = max(Decimal('0'), penalty_to_this_entry - Decimal(str(sch.penalty_paid or 0))) # --- BƯỚC 3: PHÂN BỔ TIỀN --- to_principal = min(remaining, current_amount_remain) remaining -= to_principal amount_remain_after = current_amount_remain - to_principal to_penalty = min(remaining, penalty_to_pay_now) remaining -= to_penalty allocated_here = to_principal + to_penalty # Nếu vẫn không có gì để phân bổ cho kỳ này, bỏ qua sang kỳ sau if allocated_here <= 0: continue entry_principal_allocated += to_principal entry_penalty_allocated += to_penalty # --- BƯỚC 4: LÃI DỰ PHÒNG ĐẾN NAY --- days_from_entry_to_today = max(0, (today - entry_date).days) print(f" - Lai du phong: {days_from_entry_to_today} , ngay nhap: {entry_date}, ngay hien tai: {today}") additional_penalty_to_today = Decimal('0') if amount_remain_after > 0: additional_penalty_to_today = amount_remain_after * Decimal(days_from_entry_to_today) * DAILY_PENALTY_RATE print(f"lai du phong la : {additional_penalty_to_today}") # --- CẬP NHẬT DỮ LIỆU --- sch.paid_amount = Decimal(str(sch.paid_amount or 0)) + to_principal sch.penalty_paid = Decimal(str(sch.penalty_paid or 0)) + to_penalty sch.amount_remain = amount_remain_after sch.penalty_amount = penalty_to_this_entry + additional_penalty_to_today sch.penalty_remain = max(Decimal('0'), sch.penalty_amount - sch.penalty_paid) sch.remain_amount = sch.amount_remain + sch.penalty_remain if amount_remain_after > 0: sch.ovd_days = max(0, (today - sch.to_date).days) else : sch.ovd_days = days_for_trace print(f"Lai la : {penalty_to_this_entry + additional_penalty_to_today} = {sch.penalty_amount}") # Ghi Trace sch_entry_list = sch.entry or [] sch_entry_list.append({ "type": "PAYMENT", "code": entry.code, "date": entry_date.strftime("%Y-%m-%d"), "amount": float(allocated_here), "principal": float(to_principal), "penalty": float(to_penalty), "penalty_added_to_entry": float(penalty_added_to_entry), "penalty_to_this_entry": float(penalty_to_this_entry), "amount_remain_after_allocation": float(amount_remain_after), }) sch.entry = sch_entry_list # Lưu lại list if sch.amount_remain <= 0 and sch.penalty_remain <= 0: sch.status = paid_payment_status sch.save() if sch.id not in updated_schedules: updated_schedules.append(sch.id) entry_allocation_detail.append({ "schedule_id": sch.id, "amount": float(allocated_here), "principal": float(to_principal), "penalty": float(to_penalty) }) # Cập nhật Entry nguồn entry.allocation_amount = (entry.allocation_amount or Decimal('0')) + (entry_principal_allocated + entry_penalty_allocated) entry.allocation_remain = remaining entry.allocation_detail = entry_allocation_detail entry.save() total_principal_allocated += entry_principal_allocated total_penalty_allocated += entry_penalty_allocated ## Cập nhật Transaction (Giữ nguyên gốc) if total_principal_allocated > 0 or total_penalty_allocated > 0: # Cập nhật tiền gốc đã nhận txn_detail.amount_received = F('amount_received') + total_principal_allocated txn_detail.amount_remaining = F('amount_remaining') - total_principal_allocated txn_detail.save() txn.amount_received = F('amount_received') + total_principal_allocated txn.amount_remain = F('amount_remain') - total_principal_allocated txn.save() # QUAN TRỌNG: Kiểm tra để đóng Status txn_detail.refresh_from_db() txn.refresh_from_db() # Nếu gốc đã hết (amount_remaining <= 0) # VÀ không còn kỳ lịch thanh toán nào chưa hoàn thành (đã sạch nợ lãi) has_pending_sch = Payment_Schedule.objects.filter(txn_detail=txn_detail, status__id=1).exists() if txn_detail.amount_remaining <= 0 and not has_pending_sch: if paid_txn_status: txn_detail.status = paid_txn_status txn_detail.save() txn.status = paid_txn_status txn.save() return {"status": "success", "updated_schedules": updated_schedules} except Exception as exc: import traceback print(traceback.format_exc()) return {"status": "error", "errors": [str(exc)]} # ========================================================================================== 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. Sau khi miễn giảm, kiểm tra và đóng lịch/giao dịch nếu đủ điều kiện. """ if not product_id: return {"status": "no_product", "message": "Không có product_id"} updated_schedules = [] updated_entries = [] errors = [] with transaction.atomic(): try: 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} 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": [] } schedules = Payment_Schedule.objects.select_for_update().filter( txn_detail=txn_detail, status__id=1 # Chỉ xử lý các lịch chưa thanh toán ).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": [] } 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 # Chỉ miễn giảm cho các lịch còn nợ phạt current_penalty_remain = Decimal(str(schedule.penalty_remain or 0)) if current_penalty_remain <= 0: continue to_reduce = min(remaining_reduce, current_penalty_remain) if to_reduce <= 0: continue remaining_reduce -= to_reduce entry_reduction_allocated += to_reduce schedule.penalty_reduce = (schedule.penalty_reduce or Decimal('0')) + to_reduce schedule.penalty_remain -= to_reduce schedule.remain_amount -= to_reduce sch_entry_list = schedule.entry or [] sch_entry_list.append({ "type": "REDUCTION", "code": entry.code, "date": datetime.now().strftime("%Y-%m-%d"), "amount": float(to_reduce) }) schedule.entry = safe_json_serialize(sch_entry_list) schedule.save(update_fields=['penalty_reduce', 'penalty_remain', 'remain_amount', 'entry']) # KIỂM TRA ĐỂ ĐÓNG LỊCH if schedule.remain_amount <= 0 and schedule.amount_remain <= 0: try: paid_status = Payment_Status.objects.get(id=2) schedule.status = paid_status schedule.save(update_fields=['status']) except Payment_Status.DoesNotExist: errors.append("Không tìm thấy Payment_Status id=2") if schedule.id not in updated_schedules: updated_schedules.append(schedule.id) 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") }) 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) # KIỂM TRA ĐỂ ĐÓNG TOÀN BỘ GIAO DỊCH try: txn_detail.refresh_from_db() has_pending_sch = Payment_Schedule.objects.filter(txn_detail=txn_detail, status__id=1).exists() if txn_detail.amount_remaining <= 0 and not has_pending_sch: paid_txn_status = Transaction_Status.objects.get(id=2) txn_detail.status = paid_txn_status txn.status = paid_txn_status txn_detail.save(update_fields=['status']) txn.save(update_fields=['status']) print(f"Transaction for product {product_id} closed after penalty reduction.") except Transaction_Status.DoesNotExist: errors.append("Không tìm thấy Transaction_Status id=2") except Exception as e: errors.append(f"Lỗi khi đóng transaction sau miễn giảm: {str(e)}") except Exception as exc: errors.append(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 } # ========================================================================================== def reset_product_state_before_allocation(product_id): """ Reset toàn bộ trạng thái công nợ của sản phẩm về ban đầu TRƯỚC KHI chạy phân bổ. Sử dụng logic tính toán giống delete_entry nhưng không xóa entry và không hoàn tác tài khoản. """ with transaction.atomic(): try: product = Product.objects.get(id=product_id) booked = Product_Booked.objects.filter(product=product).first() if not (booked and booked.transaction): print(f"Reset: Không tìm thấy transaction cho product {product_id}") return 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 not txn_detail: print(f"Reset: Không tìm thấy txn_detail cho product {product_id}") return # ================================================================= # Bước 1: Reset các lịch thanh toán theo công thức (GIỐNG delete_entry) # ================================================================= all_schedules = Payment_Schedule.objects.select_for_update().filter(txn_detail=txn_detail) unpaid_status = Payment_Status.objects.get(id=1) for schedule in all_schedules: current_amount_remain = Decimal(str(schedule.amount_remain or 0)) # Tính tổng principal đã phân bổ vào đúng lịch này từ tất cả entry CR principal_allocated_to_schedule = Decimal('0') for e in Internal_Entry.objects.filter(product_id=product_id, type__code='CR'): for alloc in (e.allocation_detail or []): if alloc.get('schedule_id') == schedule.id: principal_allocated_to_schedule += Decimal(str(alloc.get('principal', 0))) # Reset theo công thức - GIỐNG delete_entry schedule.entry = [] schedule.amount_remain = current_amount_remain + principal_allocated_to_schedule schedule.remain_amount = schedule.amount_remain schedule.paid_amount = schedule.amount - schedule.amount_remain schedule.penalty_paid = Decimal('0') schedule.penalty_reduce = Decimal('0') schedule.penalty_amount = Decimal('0') schedule.penalty_remain = Decimal('0') schedule.ovd_days = 0 schedule.status = unpaid_status schedule.save(update_fields=[ 'entry', 'paid_amount', 'amount_remain', 'remain_amount', 'penalty_paid', 'penalty_reduce', 'penalty_remain', 'ovd_days', 'status', 'penalty_amount' ]) # ================================================================= # Bước 2: Reset allocation của tất cả entry CR của sản phẩm (GIỐNG delete_entry) # ================================================================= all_cr_entries = Internal_Entry.objects.filter( product_id=product_id, type__code='CR' ) for e in all_cr_entries: e.allocation_detail = [] e.allocation_amount = Decimal('0') e.allocation_remain = Decimal(str(e.amount)) e.save(update_fields=[ 'allocation_detail', 'allocation_amount', 'allocation_remain' ]) # ================================================================= # Bước 3: Đặt lại Transaction_Detail từ các lịch và LƯU TRƯỚC (GIỐNG delete_entry) # ================================================================= if txn_detail: # Lấy tổng paid của các lịch THUỘC detail này schedules_of_this_detail = Payment_Schedule.objects.filter(txn_detail=txn_detail) detail_total_paid = Decimal('0') for sch in schedules_of_this_detail: paid = Decimal(str(sch.paid_amount or 0)) detail_total_paid += paid # Cập nhật Transaction_Detail txn_detail.amount_received = detail_total_paid txn_detail.amount_remaining = Decimal(str(txn_detail.amount or 0)) - detail_total_paid # ===== LƯU NGAY TRANSACTION_DETAIL ===== txn_detail.save(update_fields=['amount_received', 'amount_remaining']) txn_detail.refresh_from_db() # Đặt lại status về unpaid nếu còn nợ if txn_detail.amount_remaining > 0: try: unpaid_txn_status = Transaction_Status.objects.get(id=1) txn_detail.status = unpaid_txn_status txn_detail.save(update_fields=['status']) except: pass # ================================================================= # Bước 4: SAU KHI LƯU TXN_DETAIL, Query lại TẤT CẢ DETAIL và tính Transaction (GIỐNG delete_entry) # ================================================================= # Query lại TẤT CẢ detail sau khi đã lưu all_details = Transaction_Detail.objects.filter(transaction=txn) txn_total_received = Decimal('0') txn_total_deposit_received = Decimal('0') for detail in all_details: # Refresh để lấy giá trị mới nhất từ DB detail.refresh_from_db() txn_total_received += Decimal(str(detail.amount_received or 0)) # Tính deposit từ các lịch deposit thuộc detail này deposit_schedules = Payment_Schedule.objects.filter( txn_detail=detail, type_id=1 ) for dep_sch in deposit_schedules: txn_total_deposit_received += Decimal(str(dep_sch.paid_amount or 0)) # Cập nhật Transaction - SỬA: amount -> sale_price txn.amount_received = txn_total_received txn.amount_remain = Decimal(str(txn.sale_price or 0)) - txn_total_received # ← SỬA ĐÂY if hasattr(txn, 'deposit_received'): txn.deposit_received = txn_total_deposit_received if hasattr(txn, 'deposit_remaining') and hasattr(txn, 'deposit_amount'): txn.deposit_remaining = Decimal(str(txn.deposit_amount or 0)) - txn_total_deposit_received # ===== LƯU TRANSACTION ===== txn.save(update_fields=['amount_received', 'amount_remain', 'deposit_received', 'deposit_remaining']) txn.refresh_from_db() # Đặt lại status về unpaid nếu còn nợ if txn.amount_remain > 0: try: unpaid_txn_status = Transaction_Status.objects.get(id=1) txn.status = unpaid_txn_status txn.save(update_fields=['status']) except: pass print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Đã reset thành công trạng thái cho product_id={product_id}") except Exception as e: print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Lỗi khi reset product state for product_id={product_id}: {str(e)}") import traceback print(traceback.format_exc()) def background_allocate(product_id): """ Background task để chạy allocation. Luồng xử lý: Reset toàn bộ -> Phân bổ tiền trả nợ -> Phân bổ miễn giảm. """ try: print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Background allocation started for product_id={product_id}") # 1. Reset toàn bộ trạng thái công nợ của sản phẩm về gốc reset_product_state_before_allocation(product_id) # 2. Chạy phân bổ thanh toán (tiền khách trả) normal_result = allocate_payment_to_schedules(product_id) # 3. Chạy phân bổ miễn giảm (tiền công ty giảm cho khách) 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 allocation result:", normal_result) print("Penalty reduction result:", 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 VIEWS # ========================================================================================== @api_view(['POST']) def account_entry(request): """View function để tạo bút toán (được gọi từ urls.py)""" 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) product_id = created_entry.product_id if created_entry else None 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..." }) if product_id: def run_allocation(): thread = threading.Thread(target=background_allocate, args=(product_id,), daemon=True) thread.start() transaction.on_commit(run_allocation) return response @api_view(['POST']) def account_multi_entry(request): """Tạo nhiều bút toán cùng lúc""" try: result = [] product_ids = set() 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) 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..." }) 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) @api_view(['POST']) def delete_entry(request): """Xóa bút toán - reset sạch entry = [], lưu hết trước, xóa entry sau, đặt lại txn/txndetail, rồi phân bổ lại""" entry_id = request.data.get('id') if not entry_id: return Response({'error': 'Thiếu id bút toán'}, status=400) 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) } if entry.type.code != 'CR': return Response({'error': 'Hiện chỉ hỗ trợ xóa bút toán thu tiền (CR)'}, status=400) product_id = entry.product_id if not product_id: return Response({'error': 'Bút toán không gắn với product nào'}, status=400) allocation_detail = entry.allocation_detail or [] schedules_reversed = [] # ================================================================= # Bước 1: Reset các lịch bị ảnh hưởng theo công thức & lưu # ================================================================= 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) current_amount_remain = Decimal(str(schedule.amount_remain or 0)) # Tính tổng principal đã phân bổ vào đúng lịch này từ tất cả entry CR principal_allocated_to_schedule = Decimal('0') for e in Internal_Entry.objects.filter(product_id=product_id, type__code='CR'): for alloc in (e.allocation_detail or []): if alloc.get('schedule_id') == schedule.id: principal_allocated_to_schedule += Decimal(str(alloc.get('principal', 0))) 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 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 # Reset theo công thức schedule.entry = [] schedule.amount_remain = current_amount_remain + principal_allocated_to_schedule schedule.remain_amount = schedule.amount_remain schedule.paid_amount = schedule.amount - schedule.amount_remain schedule.penalty_paid = Decimal('0') schedule.penalty_reduce = Decimal('0') schedule.penalty_amount = Decimal('0') schedule.penalty_remain = Decimal('0') schedule.ovd_days = 0 schedule.status = Payment_Status.objects.get(id=1) schedule.save(update_fields=[ 'entry', 'paid_amount', 'amount_remain', 'remain_amount', 'penalty_paid', 'penalty_reduce', 'penalty_remain', 'ovd_days', 'status','penalty_amount' ]) 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 # ================================================================= # Bước 2: Reset allocation của tất cả entry CR của sản phẩm & lưu # ================================================================= all_cr_entries = Internal_Entry.objects.filter( product_id=product_id, type__code='CR' ) for e in all_cr_entries: e.allocation_detail = [] e.allocation_amount = Decimal('0') e.allocation_remain = Decimal(str(e.amount)) e.save(update_fields=[ 'allocation_detail', 'allocation_amount', 'allocation_remain' ]) # ================================================================= # Bước 3: Hoàn tác số dư tài khoản (lưu trước khi xóa entry) # ================================================================= 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']) # ================================================================= # Bước 4: XÓA ENTRY (sau khi reset và lưu hết) # ================================================================= entry.delete() # ================================================================= # Bước 5: ĐẶT LẠI Transaction & Transaction_Detail TRƯỚC KHI PHÂN BỔ LẠI # ================================================================= 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 # Tính lại Transaction_Detail hiện tại TRƯỚC 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: # Lấy tổng paid của các lịch THUỘC detail này schedules_of_this_detail = Payment_Schedule.objects.filter(txn_detail=txn_detail) detail_total_paid = Decimal('0') detail_deposit_paid = Decimal('0') for sch in schedules_of_this_detail: paid = Decimal(str(sch.paid_amount or 0)) detail_total_paid += paid if sch.type_id == 1: # deposit detail_deposit_paid += paid # Cập nhật Transaction_Detail txn_detail.amount_received = detail_total_paid txn_detail.amount_remaining = Decimal(str(txn_detail.amount or 0)) - detail_total_paid # ===== LƯU NGAY ===== txn_detail.save(update_fields=['amount_received', 'amount_remaining']) 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 # ====== SAU KHI LƯU HẾT TXN_DETAIL, MỚI TÍNH TRANSACTION ====== # Query lại TẤT CẢ detail sau khi đã lưu all_details = Transaction_Detail.objects.filter(transaction=txn) txn_total_received = Decimal('0') txn_total_deposit_received = Decimal('0') for detail in all_details: # Lấy giá trị mới nhất từ DB (đã refresh) detail.refresh_from_db() txn_total_received += Decimal(str(detail.amount_received or 0)) # Tính deposit từ các lịch deposit thuộc detail này deposit_schedules = Payment_Schedule.objects.filter( txn_detail=detail, type_id=1 ) for dep_sch in deposit_schedules: txn_total_deposit_received += Decimal(str(dep_sch.paid_amount or 0)) # Cập nhật Transaction txn.amount_received = txn_total_received txn.amount_remain = Decimal(str(txn.sale_price or 0)) - txn_total_received if hasattr(txn, 'deposit_received'): txn.deposit_received = txn_total_deposit_received if hasattr(txn, 'deposit_remaining') and hasattr(txn, 'deposit_amount'): txn.deposit_remaining = Decimal(str(txn.deposit_amount or 0)) - txn_total_deposit_received txn.save(update_fields=['amount_received', 'amount_remain', 'deposit_received', 'deposit_remaining']) 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 đặt lại Transaction trước phân bổ: {str(e)}") # ================================================================= # Bước 6: Phân bổ lại toàn bộ sản phẩm (sẽ tự tính lại txn/txndetail đúng) # ================================================================= def trigger_reallocate(): if product_id: try: allocate_payment_to_schedules(product_id) allocate_penalty_reduction(product_id) except Exception as exc: print(f"Lỗi khi re-allocate sau xóa: {exc}") traceback.print_exc() transaction.on_commit(trigger_reallocate) return Response({ 'success': True, 'message': 'Đã xóa bút toán, reset sạch entry = [], lưu hết trước, xóa entry sau, đặt lại txn/txndetail trước phân bổ, đang phân bổ lại toàn bộ...', 'entry': entry_info, 'reversed': { 'schedules_count': len(schedules_reversed), 'schedules': schedules_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)