from django.shortcuts import render, get_object_or_404, redirect import logging from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import JsonResponse from django.views.decorators.http import require_POST from django.utils import timezone from django.urls import reverse from decimal import Decimal, InvalidOperation import json from processes.models import ProcessInstance, ProcessStep, StepInstance from .models import Item, Quote, QuoteItem, Payment, Invoice from installations.models import InstallationReport, InstallationItemChange @login_required def quote_step(request, instance_id, step_id): """مرحله انتخاب اقلام و ساخت پیش‌فاکتور""" instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id ) step = get_object_or_404(instance.process.steps, id=step_id) # بررسی دسترسی به مرحله if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') # دریافت آیتم‌ها items = Item.objects.all().order_by('name') existing_quote = Quote.objects.filter(process_instance=instance).first() existing_quote_items = {} if existing_quote: existing_quote_items = {qi.item_id: qi.quantity for qi in existing_quote.items.all()} step_instance = instance.step_instances.filter(step=step).first() # Navigation logic previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() return render(request, 'invoices/quote_step.html', { 'instance': instance, 'step': step, 'step_instance': step_instance, 'items': items, 'existing_quote_items': existing_quote_items, 'existing_quote': existing_quote, 'previous_step': previous_step, 'next_step': next_step, }) @require_POST @login_required def create_quote(request, instance_id, step_id): """ساخت/بروزرسانی پیش‌فاکتور از اقلام انتخابی""" instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) try: items_payload = json.loads(request.POST.get('items') or '[]') except json.JSONDecodeError: return JsonResponse({'success': False, 'message': 'داده‌های اقلام نامعتبر است'}) # اطمینان از حضور اقلام پیش‌فرض حتی اگر کلاینت ارسال نکرده باشد payload_by_id = {} for entry in items_payload: try: iid = int(entry.get('id')) payload_by_id[iid] = int(entry.get('qty') or 1) except Exception: continue default_item_ids = set(Item.objects.filter(is_default_in_quotes=True).values_list('id', flat=True)) if default_item_ids: for default_id in default_item_ids: if default_id not in payload_by_id: # مقدار پیش فرض را قرار بده default_qty = Item.objects.filter(id=default_id).values_list('default_quantity', flat=True).first() or 1 payload_by_id[default_id] = int(default_qty) # بازسازی payload نهایی معتبر items_payload = [{'id': iid, 'qty': qty} for iid, qty in payload_by_id.items() if qty and qty > 0] if not items_payload: return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'}) # Create or reuse quote quote, _ = Quote.objects.get_or_create( process_instance=instance, defaults={ 'name': f"پیش‌فاکتور {instance.code}", 'customer': instance.representative or request.user, 'valid_until': timezone.now().date(), 'created_by': request.user, } ) # Replace quote items with submitted ones quote.items.all().delete() for entry in items_payload: try: item_id = int(entry.get('id')) qty = int(entry.get('qty') or 1) except (TypeError, ValueError): continue if qty <= 0: continue item = Item.objects.filter(id=item_id).first() if not item: continue QuoteItem.objects.create( quote=quote, item=item, quantity=qty, unit_price=item.unit_price, total_price=item.unit_price * qty, ) quote.calculate_totals() # تکمیل مرحله step_instance, created = StepInstance.objects.get_or_create( process_instance=instance, step=step ) step_instance.status = 'completed' step_instance.completed_at = timezone.now() step_instance.save() # انتقال به مرحله بعدی next_step = instance.process.steps.filter(order__gt=step.order).first() redirect_url = None if next_step: instance.current_step = next_step instance.save() # هدایت مستقیم به مرحله پیش‌نمایش پیش‌فاکتور redirect_url = reverse('invoices:quote_preview_step', args=[instance.id, next_step.id]) return JsonResponse({'success': True, 'quote_id': quote.id, 'redirect': redirect_url}) @login_required def quote_preview_step(request, instance_id, step_id): """مرحله صدور پیش‌فاکتور - نمایش و تایید فاکتور""" instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id ) step = get_object_or_404(instance.process.steps, id=step_id) # بررسی دسترسی به مرحله if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') # دریافت پیش‌فاکتور quote = get_object_or_404(Quote, process_instance=instance) step_instance = instance.step_instances.filter(step=step).first() # Navigation logic previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() return render(request, 'invoices/quote_preview_step.html', { 'instance': instance, 'step': step, 'step_instance': step_instance, 'quote': quote, 'previous_step': previous_step, 'next_step': next_step, }) @login_required def quote_print(request, instance_id): """صفحه پرینت پیش‌فاکتور""" instance = get_object_or_404(ProcessInstance, id=instance_id) quote = get_object_or_404(Quote, process_instance=instance) return render(request, 'invoices/quote_print.html', { 'instance': instance, 'quote': quote, }) @require_POST @login_required def approve_quote(request, instance_id, step_id): """تایید پیش‌فاکتور و انتقال به مرحله بعدی""" instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) # تایید پیش‌فاکتور quote.status = 'sent' quote.save() # تکمیل مرحله step_instance, created = StepInstance.objects.get_or_create( process_instance=instance, step=step ) step_instance.status = 'completed' step_instance.completed_at = timezone.now() step_instance.save() # انتقال به مرحله بعدی next_step = instance.process.steps.filter(order__gt=step.order).first() redirect_url = None if next_step: instance.current_step = next_step instance.save() redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id]) else: # در صورت نبود مرحله بعدی، بازگشت به لیست درخواست‌ها redirect_url = reverse('processes:request_list') messages.success(request, 'پیش‌فاکتور با موفقیت تایید شد.') return JsonResponse({'success': True, 'message': 'پیش‌فاکتور با موفقیت تایید شد.', 'redirect': redirect_url}) @login_required def quote_payment_step(request, instance_id, step_id): """مرحله سوم: ثبت فیش‌های واریزی پیش‌فاکتور""" instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id ) step = get_object_or_404(instance.process.steps, id=step_id) # بررسی دسترسی if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') quote = get_object_or_404(Quote, process_instance=instance) invoice = Invoice.objects.filter(quote=quote).first() payments = invoice.payments.select_related('created_by').filter(is_deleted=False).all() if invoice else [] previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() totals = { 'final_amount': quote.final_amount, 'paid_amount': quote.get_paid_amount(), 'remaining_amount': quote.get_remaining_amount(), 'is_fully_paid': quote.get_remaining_amount() <= 0, } step_instance = instance.step_instances.filter(step=step).first() return render(request, 'invoices/quote_payment_step.html', { 'instance': instance, 'step': step, 'step_instance': step_instance, 'quote': quote, 'payments': payments, 'totals': totals, 'previous_step': previous_step, 'next_step': next_step, }) @require_POST @login_required def add_quote_payment(request, instance_id, step_id): """افزودن فیش واریزی جدید برای پیش‌فاکتور""" instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) invoice, _ = Invoice.objects.get_or_create( process_instance=instance, quote=quote, defaults={ 'name': f"Invoice {quote.name}", 'customer': quote.customer, 'due_date': timezone.now().date(), 'created_by': request.user, } ) logger = logging.getLogger(__name__) try: amount = (request.POST.get('amount') or '').strip() payment_date = (request.POST.get('payment_date') or '').strip() payment_method = (request.POST.get('payment_method') or '').strip() reference_number = (request.POST.get('reference_number') or '').strip() notes = (request.POST.get('notes') or '').strip() receipt_image = request.FILES.get('receipt_image') # Server-side validation for required fields if not amount: return JsonResponse({'success': False, 'message': 'مبلغ را وارد کنید'}) if not payment_date: return JsonResponse({'success': False, 'message': 'تاریخ پرداخت را وارد کنید'}) if not payment_method: return JsonResponse({'success': False, 'message': 'روش پرداخت را انتخاب کنید'}) if not reference_number: return JsonResponse({'success': False, 'message': 'شماره مرجع را وارد کنید'}) if not receipt_image: return JsonResponse({'success': False, 'message': 'تصویر فیش را بارگذاری کنید'}) # Normalize date to YYYY-MM-DD (accept YYYY/MM/DD from Persian datepicker) if '/' in payment_date: payment_date = payment_date.replace('/', '-') # Prevent overpayment try: amount_dec = Decimal(amount) except InvalidOperation: return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'}) remaining = quote.get_remaining_amount() if amount_dec > remaining: return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده پیش‌فاکتور است'}) Payment.objects.create( invoice=invoice, amount=amount_dec, payment_date=payment_date, payment_method=payment_method, reference_number=reference_number, receipt_image=receipt_image, notes=notes, created_by=request.user, ) except Exception as e: logger.exception('Error adding quote payment (instance=%s, step=%s)', instance_id, step_id) return JsonResponse({'success': False, 'message': 'خطا در ثبت فیش', 'error': str(e)}) redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id]) return JsonResponse({'success': True, 'redirect': redirect_url}) @require_POST @login_required def update_quote_payment(request, instance_id, step_id, payment_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) invoice = Invoice.objects.filter(quote=quote).first() if not invoice: return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'}) payment = get_object_or_404(Payment, id=payment_id, invoice=invoice) try: amount = request.POST.get('amount') payment_date = request.POST.get('payment_date') or payment.payment_date payment_method = request.POST.get('payment_method') or payment.payment_method reference_number = request.POST.get('reference_number') or '' notes = request.POST.get('notes') or '' receipt_image = request.FILES.get('receipt_image') if amount: payment.amount = amount payment.payment_date = payment_date payment.payment_method = payment_method payment.reference_number = reference_number payment.notes = notes # اگر نیاز به ذخیره عکس در Payment دارید، فیلد آن اضافه شده است if receipt_image: payment.receipt_image = receipt_image payment.save() except Exception: return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'}) redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id]) return JsonResponse({'success': True, 'redirect': redirect_url}) @require_POST @login_required def delete_quote_payment(request, instance_id, step_id, payment_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) invoice = Invoice.objects.filter(quote=quote).first() if not invoice: return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'}) payment = get_object_or_404(Payment, id=payment_id, invoice=invoice) try: # soft delete using project's BaseModel delete override payment.delete() except Exception: return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'}) redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id]) return JsonResponse({'success': True, 'redirect': redirect_url}) @require_POST @login_required def approve_payments(request, instance_id, step_id): """تایید مرحله پرداخت فیش‌ها و انتقال به مرحله بعد""" instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) is_fully_paid = quote.get_remaining_amount() <= 0 # تکمیل مرحله step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) step_instance.status = 'completed' step_instance.completed_at = timezone.now() step_instance.save() # حرکت به مرحله بعد next_step = instance.process.steps.filter(order__gt=step.order).first() redirect_url = reverse('processes:request_list') if next_step: instance.current_step = next_step instance.save() redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id]) msg = 'پرداخت‌ها تایید شد' if is_fully_paid: msg += ' - مبلغ پیش‌فاکتور به طور کامل پرداخت شده است.' else: msg += ' - توجه: مبلغ پیش‌فاکتور به طور کامل پرداخت نشده است.' return JsonResponse({'success': True, 'message': msg, 'redirect': redirect_url, 'is_fully_paid': is_fully_paid}) @login_required def final_invoice_step(request, instance_id, step_id): """تجمیع اقلام پیش‌فاکتور با تغییرات نصب و صدور فاکتور نهایی""" instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id ) step = get_object_or_404(instance.process.steps, id=step_id) if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') quote = get_object_or_404(Quote, process_instance=instance) # Helper to make safe Decimal from various inputs (handles commas/persian digits) def _to_decimal(value): if isinstance(value, Decimal): return value try: if isinstance(value, (int, float)): return Decimal(str(value)) s = str(value or '').strip() if not s: return Decimal('0') # normalize commas and Persian digits persian = '۰۱۲۳۴۵۶۷۸۹' latin = '0123456789' tbl = str.maketrans({persian[i]: latin[i] for i in range(10)}) s = s.translate(tbl).replace(',', '') return Decimal(s) except Exception: return Decimal('0') # Build initial map from quote item_id_to_row = {} for qi in quote.items.all(): item_id_to_row[qi.item_id] = { 'item': qi.item, 'base_qty': qi.quantity, 'base_price': _to_decimal(qi.unit_price), 'added_qty': 0, 'removed_qty': 0, } # Read installation changes from latest report (if any) latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first() if latest_report: for ch in latest_report.item_changes.all(): row = item_id_to_row.setdefault(ch.item_id, { 'item': ch.item, 'base_qty': 0, 'base_price': _to_decimal(ch.unit_price or ch.item.unit_price), 'added_qty': 0, 'removed_qty': 0, }) if ch.change_type == 'add': row['added_qty'] += ch.quantity if ch.unit_price: row['base_price'] = _to_decimal(ch.unit_price) else: row['removed_qty'] += ch.quantity if ch.unit_price: row['base_price'] = _to_decimal(ch.unit_price) # Compute final invoice lines rows = [] total_amount = Decimal('0') for _, r in item_id_to_row.items(): final_qty = max(0, (r['base_qty'] + r['added_qty'] - r['removed_qty'])) if final_qty == 0: continue unit_price_dec = _to_decimal(r['base_price']) line_total = Decimal(final_qty) * unit_price_dec total_amount += line_total rows.append({ 'item': r['item'], 'quantity': final_qty, 'unit_price': unit_price_dec, 'total_price': line_total, 'base_qty': r['base_qty'], 'added_qty': r['added_qty'], 'removed_qty': r['removed_qty'], }) # Create or reuse final invoice invoice, _ = Invoice.objects.get_or_create( process_instance=instance, customer=quote.customer, quote=quote, defaults={ 'name': f"فاکتور نهایی {instance.code}", 'due_date': timezone.now().date(), 'created_by': request.user, } ) # Replace only non-special items (preserve special charges added by user) qs = invoice.items.select_related('item').filter(item__is_special=False) try: qs._raw_delete(qs.db) except Exception: qs.delete() for r in rows: from .models import InvoiceItem InvoiceItem.objects.create( invoice=invoice, item=r['item'], quantity=r['quantity'], unit_price=r['unit_price'], ) invoice.calculate_totals() previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() # Choices for special items from DB special_choices = list(Item.objects.filter(is_special=True).values('id', 'name')) return render(request, 'invoices/final_invoice_step.html', { 'instance': instance, 'step': step, 'invoice': invoice, 'rows': rows, 'special_choices': special_choices, 'invoice_specials': invoice.items.select_related('item').filter(item__is_special=True, is_deleted=False).all(), 'previous_step': previous_step, 'next_step': next_step, }) @login_required def final_invoice_print(request, instance_id): instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) items = invoice.items.select_related('item').filter(is_deleted=False).all() return render(request, 'invoices/final_invoice_print.html', { 'instance': instance, 'invoice': invoice, 'items': items, }) @require_POST @login_required def approve_final_invoice(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) # Block approval when there is any remaining (positive or negative) invoice.calculate_totals() if invoice.remaining_amount != 0: return JsonResponse({ 'success': False, 'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})" }) # mark step completed step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) step_instance.status = 'completed' step_instance.completed_at = timezone.now() step_instance.save() # move to next next_step = instance.process.steps.filter(order__gt=step.order).first() redirect_url = reverse('processes:request_list') if next_step: instance.current_step = next_step instance.save() redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id]) return JsonResponse({'success': True, 'message': 'فاکتور نهایی تایید شد', 'redirect': redirect_url}) @require_POST @login_required def add_special_charge(request, instance_id, step_id): """افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی به‌صورت آیتم جداگانه""" instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) # charge_type was removed from UI; we no longer require it item_id = request.POST.get('item_id') amount = (request.POST.get('amount') or '').strip() if not item_id: return JsonResponse({'success': False, 'message': 'آیتم را انتخاب کنید'}) try: amount_dec = Decimal(amount) except (InvalidOperation, TypeError): return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'}) if amount_dec <= 0: return JsonResponse({'success': False, 'message': 'مبلغ باید مثبت باشد'}) # Fetch existing special item from DB special_item = get_object_or_404(Item, id=item_id, is_special=True) from .models import InvoiceItem InvoiceItem.objects.create( invoice=invoice, item=special_item, quantity=1, unit_price=amount_dec, ) invoice.calculate_totals() return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])}) @require_POST @login_required def delete_special_charge(request, instance_id, step_id, item_id): instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) from .models import InvoiceItem inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice) # allow deletion only for special items try: if not getattr(inv_item.item, 'is_special', False): return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'}) except Exception: return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'}) inv_item.hard_delete() invoice.calculate_totals() return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])}) @login_required def final_settlement_step(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') invoice = get_object_or_404(Invoice, process_instance=instance) previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() return render(request, 'invoices/final_settlement_step.html', { 'instance': instance, 'step': step, 'invoice': invoice, 'payments': invoice.payments.filter(is_deleted=False).all(), 'previous_step': previous_step, 'next_step': next_step, }) @require_POST @login_required def add_final_payment(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) amount = (request.POST.get('amount') or '').strip() payment_date = (request.POST.get('payment_date') or '').strip() payment_method = (request.POST.get('payment_method') or '').strip() reference_number = (request.POST.get('reference_number') or '').strip() direction = (request.POST.get('direction') or 'in').strip() receipt_image = request.FILES.get('receipt_image') if not amount: return JsonResponse({'success': False, 'message': 'مبلغ را وارد کنید'}) if not payment_date: return JsonResponse({'success': False, 'message': 'تاریخ پرداخت را وارد کنید'}) if not payment_method: return JsonResponse({'success': False, 'message': 'روش پرداخت را انتخاب کنید'}) if not reference_number: return JsonResponse({'success': False, 'message': 'شماره مرجع را وارد کنید'}) if not receipt_image: return JsonResponse({'success': False, 'message': 'تصویر فیش الزامی است'}) if '/' in payment_date: payment_date = payment_date.replace('/', '-') try: amount_dec = Decimal(amount) except InvalidOperation: return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'}) # Only allow outgoing (پرداخت به مشتری) when current net due is negative # Compute net due explicitly from current items/payments try: current_paid = sum((p.amount if p.direction == 'in' else -p.amount) for p in invoice.payments.filter(is_deleted=False).all()) except Exception: current_paid = Decimal('0') # Ensure invoice totals are up-to-date for final_amount invoice.calculate_totals() net_due = invoice.final_amount - current_paid if direction == 'out' and net_due >= 0: return JsonResponse({'success': False, 'message': 'در حال حاضر مانده به نفع مشتری نیست'}) # Amount constraints by sign of net due if net_due > 0 and direction == 'in' and amount_dec > net_due: return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده فاکتور است'}) if net_due < 0 and direction == 'out' and amount_dec > abs(net_due): return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده بدهی شرکت به مشتری است'}) if net_due < 0 and direction == 'in': return JsonResponse({'success': False, 'message': 'در حال حاضر مانده به نفع مشتری است؛ دریافت از مشتری مجاز نیست'}) Payment.objects.create( invoice=invoice, amount=amount_dec, payment_date=payment_date, payment_method=payment_method, reference_number=reference_number, direction='in' if direction != 'out' else 'out', receipt_image=receipt_image, created_by=request.user, ) # After creation, totals auto-updated by model save. Respond with redirect and new totals for UX. invoice.refresh_from_db() return JsonResponse({ 'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': { 'final_amount': str(invoice.final_amount), 'paid_amount': str(invoice.paid_amount), 'remaining_amount': str(invoice.remaining_amount), } }) @require_POST @login_required def delete_final_payment(request, instance_id, step_id, payment_id): instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) payment = get_object_or_404(Payment, id=payment_id, invoice=invoice) payment.delete() invoice.refresh_from_db() return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': { 'final_amount': str(invoice.final_amount), 'paid_amount': str(invoice.paid_amount), 'remaining_amount': str(invoice.remaining_amount), }}) @require_POST @login_required def approve_final_settlement(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) # Block approval if any remaining exists (positive or negative) invoice.calculate_totals() if invoice.remaining_amount != 0: return JsonResponse({ 'success': False, 'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})" }) # complete step step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) step_instance.status = 'completed' step_instance.completed_at = timezone.now() step_instance.save() # move next next_step = instance.process.steps.filter(order__gt=step.order).first() redirect_url = reverse('processes:request_list') if next_step: instance.current_step = next_step instance.save() redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id]) return JsonResponse({'success': True, 'message': 'تسویه حساب نهایی ثبت شد', 'redirect': redirect_url})