from django.shortcuts import render, get_object_or_404, redirect from django.urls import reverse 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, require_GET from django.db import transaction from django.contrib.auth import get_user_model from .models import Process, ProcessInstance, StepInstance, ProcessStep from .utils import scope_instances_queryset, get_scoped_instance_or_404 from installations.models import InstallationAssignment from wells.models import Well from accounts.models import Profile, Broker from locations.models import Affairs from accounts.forms import CustomerForm from wells.forms import WellForm from wells.models import WaterMeterManufacturer from common.consts import UserRoles @login_required def request_list(request): """نمایش لیست درخواست‌ها با جدول و مدال ایجاد""" instances = ProcessInstance.objects.select_related('well', 'representative', 'requester', 'broker', 'current_step', 'process').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created') access_denied = False # filter by roles (scoped queryset) try: instances = scope_instances_queryset(request.user, instances) if not instances.exists() and not getattr(request.user, 'profile', None): access_denied = True instances = instances.none() except Exception: access_denied = True instances = instances.none() # Filters status_q = (request.GET.get('status') or '').strip() affairs_q = (request.GET.get('affairs') or '').strip() broker_q = (request.GET.get('broker') or '').strip() step_q = (request.GET.get('step') or '').strip() if status_q: instances = instances.filter(status=status_q) if affairs_q: try: instances = instances.filter(well__affairs_id=int(affairs_q)) except Exception: pass if broker_q: try: instances = instances.filter(broker_id=int(broker_q)) except Exception: pass if step_q: try: instances = instances.filter(current_step_id=int(step_q)) except Exception: pass processes = Process.objects.filter(is_active=True) status_choices = list(ProcessInstance.STATUS_CHOICES) affairs_list = Affairs.objects.all().order_by('name') brokers_list = Broker.objects.all().order_by('name') steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order') manufacturers = WaterMeterManufacturer.objects.all().order_by('name') # Calculate progress for each instance instances_with_progress = [] for instance in instances: total_steps = instance.process.steps.count() completed_steps = instance.step_instances.filter(status='completed').count() progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0 instances_with_progress.append({ 'instance': instance, 'progress_percentage': round(progress_percentage), 'completed_steps': completed_steps, 'total_steps': total_steps, }) # Summary stats for header cards total_count = instances.count() completed_count = instances.filter(status='completed').count() in_progress_count = instances.filter(status='in_progress').count() pending_count = instances.filter(status='pending').count() return render(request, 'processes/request_list.html', { 'instances_with_progress': instances_with_progress, 'customer_form': CustomerForm(), 'well_form': WellForm(), 'processes': processes, 'manufacturers': manufacturers, 'total_count': total_count, 'completed_count': completed_count, 'in_progress_count': in_progress_count, 'pending_count': pending_count, # filter context 'status_choices': status_choices, 'affairs_list': affairs_list, 'brokers_list': brokers_list, 'steps_list': steps_list, 'filter_status': status_q, 'filter_affairs': affairs_q, 'filter_broker': broker_q, 'filter_step': step_q, 'access_denied': access_denied, }) @require_GET @login_required def lookup_well_by_subscription(request): sub = request.GET.get('water_subscription_number', '').strip() if not sub: return JsonResponse({'ok': False, 'error': 'شماره اشتراک الزامی است'}, status=400) try: well = Well.objects.select_related('representative', 'water_meter_manufacturer').get(water_subscription_number=sub) data = { 'id': well.id, 'water_subscription_number': well.water_subscription_number, 'electricity_subscription_number': well.electricity_subscription_number, 'water_meter_serial_number': well.water_meter_serial_number, 'water_meter_old_serial_number': well.water_meter_old_serial_number, 'water_meter_manufacturer': well.water_meter_manufacturer.id if well.water_meter_manufacturer else None, 'utm_x': str(well.utm_x) if well.utm_x is not None else None, 'utm_y': str(well.utm_y) if well.utm_y is not None else None, 'utm_zone': well.utm_zone, 'utm_hemisphere': well.utm_hemisphere, 'well_power': well.well_power, 'reference_letter_number': well.reference_letter_number, 'reference_letter_date': well.reference_letter_date.isoformat() if well.reference_letter_date else None, 'representative_letter_file_url': well.representative_letter_file.url if well.representative_letter_file else '', 'representative_letter_file_name': well.representative_letter_file.name.split('/')[-1] if well.representative_letter_file else '', 'representative_id': well.representative.id if well.representative else None, 'representative_full_name': well.representative.get_full_name() if well.representative else None, } return JsonResponse({'ok': True, 'exists': True, 'well': data}) except Well.DoesNotExist: return JsonResponse({'ok': True, 'exists': False}) @require_GET @login_required def lookup_representative_by_national_code(request): national_code = request.GET.get('national_code', '').strip() if not national_code: return JsonResponse({'ok': False, 'error': 'کد ملی الزامی است'}, status=400) profile = Profile.objects.select_related('user').filter(national_code=national_code).first() if not profile: return JsonResponse({'ok': True, 'exists': False}) user = profile.user return JsonResponse({ 'ok': True, 'exists': True, 'user': { 'id': user.id, 'username': user.username, 'first_name': user.first_name, 'last_name': user.last_name, 'full_name': user.get_full_name(), 'profile': { 'national_code': profile.national_code, 'phone_number_1': profile.phone_number_1, 'phone_number_2': profile.phone_number_2, 'card_number': profile.card_number, 'account_number': profile.account_number, 'bank_name': profile.bank_name, 'address': profile.address, } } }) @require_POST @login_required @transaction.atomic def create_request_with_entities(request): """ایجاد/به‌روزرسانی چاه و نماینده و سپس ایجاد درخواست""" User = get_user_model() # Only BROKER can create requests try: if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)): return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403) except Exception: return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403) process_id = request.POST.get('process') process = Process.objects.get(id=process_id) description = request.POST.get('description', '') # Well fields water_subscription_number = request.POST.get('water_subscription_number') well_id = request.POST.get('well_id') # optional if existing # Representative fields representative_id = request.POST.get('representative_id') if not process_id: return JsonResponse({'ok': False, 'errors': {'request': {'process': ['فرآیند الزامی است']}}}, status=400) if not water_subscription_number: return JsonResponse({'ok': False, 'errors': {'well': {'water_subscription_number': ['شماره اشتراک آب الزامی است']}}}, status=400) if not representative_id and not request.POST.get('national_code'): return JsonResponse({'ok': False, 'errors': {'customer': {'national_code': ['کد ملی نماینده را وارد کنید یا دکمه بررسی/افزودن نماینده را بزنید']}}}, status=400) representative_user = None representative_profile = None if representative_id: representative_profile = Profile.objects.select_related('user').filter(user_id=representative_id).first() if not representative_profile: return JsonResponse({'ok': False, 'errors': {'customer': {'__all__': ['نماینده انتخاب‌شده یافت نشد']}}}, status=400) # Use CustomerForm with request.POST data, merging with existing values customer_form = CustomerForm(request.POST, instance=representative_profile) customer_form.request = request if not customer_form.is_valid(): return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400) representative_profile = customer_form.save() representative_user = representative_profile.user else: # Use CustomerForm to validate/create/update representative profile by national code profile_instance = None national_code = request.POST.get('national_code') if national_code: profile_instance = Profile.objects.filter(national_code=national_code).first() customer_form = CustomerForm(request.POST, instance=profile_instance) customer_form.request = request if not customer_form.is_valid(): return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400) representative_profile = customer_form.save() representative_user = representative_profile.user # Resolve/create/update well # Build WellForm data from POST well = None if well_id: well = Well.objects.filter(id=well_id).first() if not well: return JsonResponse({'ok': False, 'error': 'شناسه چاه نامعتبر است'}, status=400) else: existing = Well.objects.filter(water_subscription_number=water_subscription_number).first() if existing: well = existing well_data = request.POST.copy() # Ensure representative set from created/selected user if not provided if representative_user and not well_data.get('representative'): well_data['representative'] = str(representative_user.id) if not well_data.get('water_subscription_number'): well_data['water_subscription_number'] = water_subscription_number # Preserve existing values on partial updates if well: for field_name in WellForm.Meta.fields: if field_name in ('representative_letter_file',): # File field handled via request.FILES; skip if not provided continue incoming = well_data.get(field_name, None) if incoming is None or incoming == '': current_value = getattr(well, field_name, None) if current_value is None: continue # Convert FK to id if hasattr(current_value, 'pk'): well_data[field_name] = str(current_value.pk) else: # Convert dates/decimals/others to string try: well_data[field_name] = current_value.isoformat() # dates except AttributeError: well_data[field_name] = str(current_value) well_form = WellForm(well_data, request.FILES, instance=well) if not well_form.is_valid(): return JsonResponse({'ok': False, 'errors': {'well': well_form.errors}}, status=400) # Save with ability to remove existing file well = well_form.save(commit=False) try: if request.POST.get('remove_file') == 'true' and getattr(well, 'representative_letter_file', None): well.representative_letter_file.delete(save=False) well.representative_letter_file = None except Exception: pass well.save() # Auto fill geo ownership from current user profile if available current_profile = getattr(request.user, 'profile', None) if current_profile: if hasattr(well, 'affairs'): well.affairs = current_profile.affairs if hasattr(well, 'county'): well.county = current_profile.county if hasattr(well, 'broker'): well.broker = current_profile.broker well.save() # Ensure no active (non-deleted, non-completed) request exists for this well try: active_exists = ProcessInstance.objects.filter(well=well, is_deleted=False).exclude(status='completed').exists() if active_exists: return JsonResponse({'ok': False, 'error': 'برای این چاه یک درخواست جاری وجود دارد. ابتدا آن را تکمیل یا حذف کنید.'}, status=400) except Exception: return JsonResponse({'ok': False, 'error': 'خطا در بررسی وضعیت درخواست‌های قبلی این چاه'}, status=400) # Create request instance instance = ProcessInstance.objects.create( process=process, description=description, well=well, representative=representative_user, requester=request.user, broker=request.user.profile.broker if request.user.profile else None, status='pending', priority='medium', ) # ایجاد نمونه‌های مرحله بر اساس مراحل فرآیند و تنظیم مرحله فعلی for step in process.steps.all().order_by('order'): StepInstance.objects.create( process_instance=instance, step=step ) first_step = process.steps.all().order_by('order').first() if first_step: instance.current_step = first_step instance.status = 'in_progress' instance.save() redirect_url = reverse('processes:instance_steps', args=[instance.id]) return JsonResponse({'ok': True, 'instance_id': instance.id, 'redirect': redirect_url}) @require_POST @login_required def delete_request(request, instance_id): """حذف درخواست""" instance = get_scoped_instance_or_404(request, instance_id) # Only BROKER can delete requests and only within their scope try: profile = getattr(request.user, 'profile', None) if not (profile and profile.has_role(UserRoles.BROKER)): return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403) # Enforce ownership by broker (prevent deleting others' requests) if instance.broker_id and profile.broker and instance.broker_id != profile.broker.id: return JsonResponse({'success': False, 'message': 'شما مجاز به حذف این درخواست نیستید'}, status=403) except Exception: return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403) code = instance.code if instance.status == 'completed': return JsonResponse({ 'success': False, 'message': 'درخواست تکمیل شده نمی‌تواند حذف شود' }) instance.delete() return JsonResponse({ 'success': True, 'message': f'درخواست {code} با موفقیت حذف شد' }) @login_required def step_detail(request, instance_id, step_id): """نمایش جزئیات مرحله خاص""" # Enforce scoped access to prevent URL tampering instance = get_scoped_instance_or_404(request, instance_id) # Prefetch for performance instance = ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile').get(id=instance.id) step = get_object_or_404(instance.process.steps, id=step_id) # If the request is already completed, redirect to read-only summary page if instance.status == 'completed': return redirect('processes:instance_summary', instance_id=instance.id) # جلوگیری از پرش به مراحل آینده: فقط اجازه نمایش مرحله جاری یا مراحل تکمیل‌شده try: if instance.current_step and step.order > instance.current_step.order: messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id) except Exception: pass # بررسی دسترسی به مرحله if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') # هدایت به view مناسب بر اساس نوع مرحله if step.order == 1: # مرحله اول - انتخاب اقلام return redirect('invoices:quote_step', instance_id=instance.id, step_id=step.id) elif step.order == 2: # مرحله دوم - صدور پیش‌فاکتور return redirect('invoices:quote_preview_step', instance_id=instance.id, step_id=step.id) elif step.order == 3: # مرحله سوم - ثبت فیش‌های واریزی return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id) elif step.order == 4: # مرحله چهارم - قرارداد return redirect('contracts:contract_step', instance_id=instance.id, step_id=step.id) elif step.order == 5: # مرحله پنجم - انتخاب نصاب return redirect('installations:installation_assign_step', instance_id=instance.id, step_id=step.id) elif step.order == 6: # مرحله ششم - گزارش نصب return redirect('installations:installation_report_step', instance_id=instance.id, step_id=step.id) elif step.order == 7: # مرحله هفتم - فاکتور نهایی return redirect('invoices:final_invoice_step', instance_id=instance.id, step_id=step.id) elif step.order == 8: # مرحله هشتم - تسویه حساب نهایی return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) elif step.order == 9: # مرحله نهم - گواهی نهایی return redirect('certificates:certificate_step', instance_id=instance.id, step_id=step.id) # برای سایر مراحل، template عمومی نمایش داده می‌شود 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, 'processes/step_detail.html', { 'instance': instance, 'step': step, 'step_instance': step_instance, 'previous_step': previous_step, 'next_step': next_step, }) @login_required def instance_steps(request, instance_id): """هدایت به مرحله فعلی instance""" # Enforce scoped access to prevent URL tampering instance = get_scoped_instance_or_404(request, instance_id) if not instance.current_step: # اگر مرحله فعلی تعریف نشده، به اولین مرحله برو first_step = instance.process.steps.first() if first_step: instance.current_step = first_step instance.save() return redirect('processes:step_detail', instance_id=instance.id, step_id=first_step.id) else: messages.error(request, 'هیچ مرحله‌ای برای این فرآیند تعریف نشده است.') return redirect('processes:request_list') # If completed, go to summary instead of steps if instance.status == 'completed': return redirect('processes:instance_summary', instance_id=instance.id) return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id) @login_required def instance_summary(request, instance_id): """نمای خلاصهٔ فقط‌خواندنی برای درخواست‌های تکمیل‌شده.""" # Enforce scoped access to prevent URL tampering instance = get_scoped_instance_or_404(request, instance_id) instance = get_object_or_404(ProcessInstance.objects.select_related('well', 'representative'), id=instance_id) # Only show for completed requests; otherwise route to steps if instance.status != 'completed': return redirect('processes:instance_steps', instance_id=instance.id) # Collect final invoice, payments, and certificate if any from invoices.models import Invoice from installations.models import InstallationReport from certificates.models import CertificateInstance invoice = Invoice.objects.filter(process_instance=instance).first() payments = invoice.payments.filter(is_deleted=False).all() if invoice else [] latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first() certificate = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first() # Build rows like final invoice step rows = [] if invoice: items_qs = invoice.items.select_related('item').filter(is_deleted=False).all() rows = list(items_qs) return render(request, 'processes/instance_summary.html', { 'instance': instance, 'invoice': invoice, 'payments': payments, 'rows': rows, 'latest_report': latest_report, 'certificate': certificate, })