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 @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})