diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index 369ef950..aa5f5b52 100644 Binary files a/api/__pycache__/settings.cpython-313.pyc and b/api/__pycache__/settings.cpython-313.pyc differ diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc index 9791e674..bf1fdc7d 100644 Binary files a/app/__pycache__/models.cpython-313.pyc and b/app/__pycache__/models.cpython-313.pyc differ diff --git a/app/__pycache__/payment.cpython-313.pyc b/app/__pycache__/payment.cpython-313.pyc index e9e6ac4d..16e5de7b 100644 Binary files a/app/__pycache__/payment.cpython-313.pyc and b/app/__pycache__/payment.cpython-313.pyc differ diff --git a/app/migrations/0373_remove_transaction_ovd_days_and_more.py b/app/migrations/0373_remove_transaction_ovd_days_and_more.py new file mode 100644 index 00000000..955aa275 --- /dev/null +++ b/app/migrations/0373_remove_transaction_ovd_days_and_more.py @@ -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), + ), + ] diff --git a/app/models.py b/app/models.py index 753f14d6..b9a9348f 100644 --- a/app/models.py +++ b/app/models.py @@ -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) diff --git a/app/payment.py b/app/payment.py index a5c26a50..99e298be 100644 --- a/app/payment.py +++ b/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) \ No newline at end of file diff --git a/static/files/20260103061030-entry.xlsx b/static/files/20260103061030-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103061030-entry.xlsx and /dev/null differ diff --git a/static/files/20260103061529-entry.xlsx b/static/files/20260103061529-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103061529-entry.xlsx and /dev/null differ diff --git a/static/files/20260103061642-entry.xlsx b/static/files/20260103061642-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103061642-entry.xlsx and /dev/null differ diff --git a/static/files/20260103061746-entry.xlsx b/static/files/20260103061746-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103061746-entry.xlsx and /dev/null differ diff --git a/static/files/20260103061956-entry.xlsx b/static/files/20260103061956-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103061956-entry.xlsx and /dev/null differ diff --git a/static/files/20260103062121-entry.xlsx b/static/files/20260103062121-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103062121-entry.xlsx and /dev/null differ diff --git a/static/files/20260103062455-entry.xlsx b/static/files/20260103062455-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103062455-entry.xlsx and /dev/null differ diff --git a/static/files/20260103062658-entry.xlsx b/static/files/20260103062658-entry.xlsx deleted file mode 100644 index a0d25bea..00000000 Binary files a/static/files/20260103062658-entry.xlsx and /dev/null differ diff --git a/static/files/20260103063334-entry.xlsx b/static/files/20260103063334-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103063334-entry.xlsx and /dev/null differ diff --git a/static/files/20260103063504-entry.xlsx b/static/files/20260103063504-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103063504-entry.xlsx and /dev/null differ diff --git a/static/files/20260103063614-entry.xlsx b/static/files/20260103063614-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103063614-entry.xlsx and /dev/null differ diff --git a/static/files/20260103063714-entry.xlsx b/static/files/20260103063714-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103063714-entry.xlsx and /dev/null differ diff --git a/static/files/20260103063913-entry.xlsx b/static/files/20260103063913-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103063913-entry.xlsx and /dev/null differ diff --git a/static/files/20260103064318-entry.xlsx b/static/files/20260103064318-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103064318-entry.xlsx and /dev/null differ diff --git a/static/files/20260103064416-entry.xlsx b/static/files/20260103064416-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103064416-entry.xlsx and /dev/null differ diff --git a/static/files/20260103064558-entry.xlsx b/static/files/20260103064558-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103064558-entry.xlsx and /dev/null differ diff --git a/static/files/20260103064653-entry.xlsx b/static/files/20260103064653-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103064653-entry.xlsx and /dev/null differ diff --git a/static/files/20260103064915-entry.xlsx b/static/files/20260103064915-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103064915-entry.xlsx and /dev/null differ diff --git a/static/files/20260103065009-entry.xlsx b/static/files/20260103065009-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103065009-entry.xlsx and /dev/null differ diff --git a/static/files/20260103065140-entry.xlsx b/static/files/20260103065140-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103065140-entry.xlsx and /dev/null differ diff --git a/static/files/20260103065246-entry.xlsx b/static/files/20260103065246-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260103065246-entry.xlsx and /dev/null differ diff --git a/static/files/20260104101131-entry.xlsx b/static/files/20260104101131-entry.xlsx deleted file mode 100644 index c2cbb80d..00000000 Binary files a/static/files/20260104101131-entry.xlsx and /dev/null differ diff --git a/static/files/20260104102310-entry.xlsx b/static/files/20260104102310-entry.xlsx deleted file mode 100644 index 0a07ca0c..00000000 Binary files a/static/files/20260104102310-entry.xlsx and /dev/null differ