902 lines
		
	
	
		
			No EOL
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			902 lines
		
	
	
		
			No EOL
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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, HttpResponse
 | 
						||
from django.views.decorators.http import require_POST, require_GET
 | 
						||
from django.utils import timezone
 | 
						||
from django.db import transaction
 | 
						||
from django.contrib.auth import get_user_model
 | 
						||
import openpyxl
 | 
						||
from openpyxl.styles import Font, Alignment, PatternFill
 | 
						||
from openpyxl.utils import get_column_letter
 | 
						||
from datetime import datetime
 | 
						||
from _helpers.utils import persian_converter3
 | 
						||
from .models import Process, ProcessInstance, StepInstance, ProcessStep
 | 
						||
from .utils import scope_instances_queryset, get_scoped_instance_or_404
 | 
						||
from installations.models import InstallationAssignment, InstallationReport
 | 
						||
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')
 | 
						||
    
 | 
						||
    # Prepare installation assignments map (scheduled date by instance)
 | 
						||
    try:
 | 
						||
        instance_ids = list(instances.values_list('id', flat=True))
 | 
						||
    except Exception:
 | 
						||
        instance_ids = []
 | 
						||
    assignments_map = {}
 | 
						||
    reports_map = {}
 | 
						||
    if instance_ids:
 | 
						||
        try:
 | 
						||
            ass_qs = InstallationAssignment.objects.filter(process_instance_id__in=instance_ids).values('process_instance_id', 'scheduled_date')
 | 
						||
            for row in ass_qs:
 | 
						||
                assignments_map[row['process_instance_id']] = row['scheduled_date']
 | 
						||
        except Exception:
 | 
						||
            assignments_map = {}
 | 
						||
        # latest report per instance (visited_date)
 | 
						||
        try:
 | 
						||
            rep_qs = InstallationReport.objects.filter(assignment__process_instance_id__in=instance_ids).order_by('-created').values('assignment__process_instance_id', 'visited_date')
 | 
						||
            for row in rep_qs:
 | 
						||
                pid = row['assignment__process_instance_id']
 | 
						||
                if pid not in reports_map:
 | 
						||
                    reports_map[pid] = row['visited_date']
 | 
						||
        except Exception:
 | 
						||
            reports_map = {}
 | 
						||
 | 
						||
    # Calculate progress for each instance and attach install schedule info
 | 
						||
    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
 | 
						||
        sched_date = assignments_map.get(instance.id)
 | 
						||
        overdue_days = 0
 | 
						||
        reference_date = None
 | 
						||
        if sched_date:
 | 
						||
            # Reference date: until installer submits a report, use today; otherwise use visited_date
 | 
						||
            try:
 | 
						||
                visited_date = reports_map.get(instance.id)
 | 
						||
                if visited_date:
 | 
						||
                    reference_date = visited_date
 | 
						||
                else:
 | 
						||
                    try:
 | 
						||
                        reference_date = timezone.localdate()
 | 
						||
                    except Exception:
 | 
						||
                        from datetime import date as _date
 | 
						||
                        reference_date = _date.today()
 | 
						||
                if reference_date > sched_date:
 | 
						||
                    overdue_days = (reference_date - sched_date).days
 | 
						||
            except Exception:
 | 
						||
                overdue_days = 0
 | 
						||
                reference_date = None
 | 
						||
 | 
						||
        installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date
 | 
						||
        instances_with_progress.append({
 | 
						||
            'instance': instance,
 | 
						||
            'progress_percentage': round(progress_percentage),
 | 
						||
            'completed_steps': completed_steps,
 | 
						||
            'total_steps': total_steps,
 | 
						||
            'installation_scheduled_date': installation_scheduled_date,
 | 
						||
            'installation_overdue_days': overdue_days,
 | 
						||
        })
 | 
						||
    
 | 
						||
    # 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': {
 | 
						||
                'user_type': profile.user_type,
 | 
						||
                'national_code': profile.national_code,
 | 
						||
                'company_name': profile.company_name,
 | 
						||
                'company_national_id': profile.company_national_id,
 | 
						||
                '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()
 | 
						||
    print(well_data)
 | 
						||
    # 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, InstallationAssignment
 | 
						||
    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()
 | 
						||
 | 
						||
    # Calculate installation delay
 | 
						||
    installation_assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
 | 
						||
    installation_delay_days = 0
 | 
						||
    if installation_assignment and latest_report:
 | 
						||
        scheduled_date = installation_assignment.scheduled_date
 | 
						||
        visited_date = latest_report.visited_date
 | 
						||
        if scheduled_date and visited_date and visited_date > scheduled_date:
 | 
						||
            installation_delay_days = (visited_date - scheduled_date).days
 | 
						||
 | 
						||
    # 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,
 | 
						||
        'installation_assignment': installation_assignment,
 | 
						||
        'installation_delay_days': installation_delay_days,
 | 
						||
    })
 | 
						||
 | 
						||
 | 
						||
def format_date_jalali(date_obj):
 | 
						||
    """Convert date to Jalali format without time"""
 | 
						||
    if not date_obj:
 | 
						||
        return ""
 | 
						||
    try:
 | 
						||
        # If it's a datetime, get just the date part
 | 
						||
        if hasattr(date_obj, 'date'):
 | 
						||
            date_obj = date_obj.date()
 | 
						||
        return persian_converter3(date_obj)
 | 
						||
    except Exception:
 | 
						||
        return ""
 | 
						||
 | 
						||
def format_datetime_jalali(datetime_obj):
 | 
						||
    """Convert datetime to Jalali format without time"""
 | 
						||
    if not datetime_obj:
 | 
						||
        return ""
 | 
						||
    try:
 | 
						||
        # Get just the date part
 | 
						||
        date_part = datetime_obj.date() if hasattr(datetime_obj, 'date') else datetime_obj
 | 
						||
        return persian_converter3(date_part)
 | 
						||
    except Exception:
 | 
						||
        return ""
 | 
						||
 | 
						||
@login_required
 | 
						||
def export_requests_excel(request):
 | 
						||
    """Export filtered requests to Excel"""
 | 
						||
    
 | 
						||
    # Get the same queryset as request_list view (with filters)
 | 
						||
    instances = ProcessInstance.objects.select_related(
 | 
						||
        'process', 'current_step', 'representative', 'well', 'well__county', 'well__affairs'
 | 
						||
    ).prefetch_related('step_instances')
 | 
						||
    
 | 
						||
    # Apply scoping
 | 
						||
    instances = scope_instances_queryset(request.user, instances)
 | 
						||
    
 | 
						||
    # Apply filters (same logic as request_list view)
 | 
						||
    filter_status = request.GET.get('status', '').strip()
 | 
						||
    if filter_status:
 | 
						||
        instances = instances.filter(status=filter_status)
 | 
						||
    
 | 
						||
    filter_affairs = request.GET.get('affairs', '').strip()
 | 
						||
    if filter_affairs and filter_affairs.isdigit():
 | 
						||
        instances = instances.filter(well__affairs_id=filter_affairs)
 | 
						||
    
 | 
						||
    filter_broker = request.GET.get('broker', '').strip()
 | 
						||
    if filter_broker and filter_broker.isdigit():
 | 
						||
        instances = instances.filter(well__broker_id=filter_broker)
 | 
						||
    
 | 
						||
    filter_step = request.GET.get('step', '').strip()
 | 
						||
    if filter_step and filter_step.isdigit():
 | 
						||
        instances = instances.filter(current_step_id=filter_step)
 | 
						||
    
 | 
						||
    # Get installation data
 | 
						||
    assignment_ids = list(instances.values_list('id', flat=True))
 | 
						||
    assignments_map = {}
 | 
						||
    reports_map = {}
 | 
						||
    installers_map = {}
 | 
						||
    
 | 
						||
    if assignment_ids:
 | 
						||
        assignments = InstallationAssignment.objects.filter(
 | 
						||
            process_instance_id__in=assignment_ids
 | 
						||
        ).select_related('process_instance', 'installer')
 | 
						||
        assignments_map = {a.process_instance_id: a.scheduled_date for a in assignments}
 | 
						||
        installers_map = {a.process_instance_id: a.installer for a in assignments}
 | 
						||
        
 | 
						||
        reports = InstallationReport.objects.filter(
 | 
						||
            assignment__process_instance_id__in=assignment_ids
 | 
						||
        ).select_related('assignment')
 | 
						||
        reports_map = {r.assignment.process_instance_id: r for r in reports}
 | 
						||
    
 | 
						||
    # Get quotes and payments data
 | 
						||
    from invoices.models import Quote, Payment, Invoice
 | 
						||
    quotes_map = {}
 | 
						||
    payments_map = {}
 | 
						||
    settlement_dates_map = {}
 | 
						||
    approval_dates_map = {}
 | 
						||
    approval_users_map = {}
 | 
						||
    
 | 
						||
    if assignment_ids:
 | 
						||
        # Get quotes
 | 
						||
        quotes = Quote.objects.filter(
 | 
						||
            process_instance_id__in=assignment_ids
 | 
						||
        ).select_related('process_instance')
 | 
						||
        quotes_map = {q.process_instance_id: q for q in quotes}
 | 
						||
        
 | 
						||
        # Get payments with reference numbers
 | 
						||
        payments = Payment.objects.filter(
 | 
						||
            invoice__process_instance_id__in=assignment_ids, 
 | 
						||
            is_deleted=False
 | 
						||
        ).select_related('invoice__process_instance').order_by('created')
 | 
						||
        
 | 
						||
        for payment in payments:
 | 
						||
            if payment.invoice.process_instance_id not in payments_map:
 | 
						||
                payments_map[payment.invoice.process_instance_id] = []
 | 
						||
            payments_map[payment.invoice.process_instance_id].append(payment)
 | 
						||
        
 | 
						||
        # Get final invoices to check settlement dates
 | 
						||
        invoices = Invoice.objects.filter(
 | 
						||
            process_instance_id__in=assignment_ids
 | 
						||
        ).select_related('process_instance')
 | 
						||
        
 | 
						||
        for invoice in invoices:
 | 
						||
            if invoice.remaining_amount == 0:  # Fully settled
 | 
						||
                # Find the last payment date for this invoice
 | 
						||
                last_payment = Payment.objects.filter(
 | 
						||
                    invoice__process_instance=invoice.process_instance,
 | 
						||
                    is_deleted=False
 | 
						||
                ).order_by('-created').first()
 | 
						||
                if last_payment:
 | 
						||
                    settlement_dates_map[invoice.process_instance_id] = last_payment.created
 | 
						||
        
 | 
						||
        # Get installation approval data
 | 
						||
        from processes.models import StepInstance, StepApproval
 | 
						||
        installation_steps = StepInstance.objects.filter(
 | 
						||
            process_instance_id__in=assignment_ids,
 | 
						||
            step__slug='installation_report',  # Assuming this is the slug for installation step
 | 
						||
            status='completed'
 | 
						||
        ).select_related('process_instance')
 | 
						||
        
 | 
						||
        for step_instance in installation_steps:
 | 
						||
            # Get the approval that completed this step
 | 
						||
            approval = StepApproval.objects.filter(
 | 
						||
                step_instance=step_instance,
 | 
						||
                is_deleted=False
 | 
						||
            ).select_related('approved_by').order_by('-created_at').first()
 | 
						||
            
 | 
						||
            if approval:
 | 
						||
                approval_dates_map[step_instance.process_instance_id] = approval.created_at
 | 
						||
                approval_users_map[step_instance.process_instance_id] = approval.approved_by
 | 
						||
    
 | 
						||
    # Calculate progress and installation data
 | 
						||
    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
 | 
						||
        
 | 
						||
        sched_date = assignments_map.get(instance.id)
 | 
						||
        overdue_days = 0
 | 
						||
        reference_date = None
 | 
						||
        
 | 
						||
        if sched_date:
 | 
						||
            try:
 | 
						||
                report = reports_map.get(instance.id)
 | 
						||
                if report and report.visited_date:
 | 
						||
                    reference_date = report.visited_date
 | 
						||
                else:
 | 
						||
                    try:
 | 
						||
                        reference_date = timezone.localdate()
 | 
						||
                    except Exception:
 | 
						||
                        from datetime import date as _date
 | 
						||
                        reference_date = _date.today()
 | 
						||
                if reference_date > sched_date:
 | 
						||
                    overdue_days = (reference_date - sched_date).days
 | 
						||
            except Exception:
 | 
						||
                overdue_days = 0
 | 
						||
        
 | 
						||
        installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date
 | 
						||
        
 | 
						||
        instances_with_progress.append({
 | 
						||
            'instance': instance,
 | 
						||
            'progress_percentage': round(progress_percentage),
 | 
						||
            'completed_steps': completed_steps,
 | 
						||
            'total_steps': total_steps,
 | 
						||
            'installation_scheduled_date': installation_scheduled_date,
 | 
						||
            'installation_overdue_days': overdue_days,
 | 
						||
        })
 | 
						||
    
 | 
						||
    # Create Excel workbook
 | 
						||
    wb = openpyxl.Workbook()
 | 
						||
    ws = wb.active
 | 
						||
    ws.title = "لیست درخواستها"
 | 
						||
    
 | 
						||
    # Set RTL (Right-to-Left) direction
 | 
						||
    ws.sheet_view.rightToLeft = True
 | 
						||
    
 | 
						||
    # Define column headers
 | 
						||
    headers = [
 | 
						||
        'شناسه',
 | 
						||
        'تاریخ ایجاد درخواست',
 | 
						||
        'نام نماینده',
 | 
						||
        'نام خانوادگی نماینده',
 | 
						||
        'کد ملی نماینده',
 | 
						||
        'نام شرکت',
 | 
						||
        'شناسه شرکت',
 | 
						||
        'سریال کنتور',
 | 
						||
        'سریال کنتور جدید',
 | 
						||
        'شماره اشتراک آب',
 | 
						||
        'شماره اشتراک برق',
 | 
						||
        'قدرت چاه',
 | 
						||
        'شماره تماس ۱',
 | 
						||
        'شماره تماس ۲',
 | 
						||
        'آدرس',
 | 
						||
        'مبلغ پیشفاکتور',
 | 
						||
        'تاریخ واریزیها و کدهای رهگیری',
 | 
						||
        'تاریخ مراجعه نصاب',
 | 
						||
        'تاخیر نصاب',
 | 
						||
        'نام نصاب',
 | 
						||
        'تاریخ تایید نصب توسط مدیر',
 | 
						||
        'نام تایید کننده نصب',
 | 
						||
        'تاریخ تسویه'
 | 
						||
    ]
 | 
						||
    
 | 
						||
    # Write headers
 | 
						||
    for col, header in enumerate(headers, 1):
 | 
						||
        cell = ws.cell(row=1, column=col, value=header)
 | 
						||
        cell.font = Font(bold=True)
 | 
						||
        cell.alignment = Alignment(horizontal='center')
 | 
						||
        cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid")
 | 
						||
    
 | 
						||
    # Write data rows
 | 
						||
    for row_num, item in enumerate(instances_with_progress, 2):
 | 
						||
        instance = item['instance']
 | 
						||
        
 | 
						||
        # Get representative info
 | 
						||
        rep_first_name = ""
 | 
						||
        rep_last_name = ""
 | 
						||
        rep_national_code = ""
 | 
						||
        rep_phone_1 = ""
 | 
						||
        rep_phone_2 = ""
 | 
						||
        rep_address = ""
 | 
						||
        company_name = ""
 | 
						||
        company_national_id = ""
 | 
						||
        
 | 
						||
        if instance.representative:
 | 
						||
            rep_first_name = instance.representative.first_name or ""
 | 
						||
            rep_last_name = instance.representative.last_name or ""
 | 
						||
            if hasattr(instance.representative, 'profile') and instance.representative.profile:
 | 
						||
                profile = instance.representative.profile
 | 
						||
                rep_national_code = profile.national_code or ""
 | 
						||
                rep_phone_1 = profile.phone_number_1 or ""
 | 
						||
                rep_phone_2 = profile.phone_number_2 or ""
 | 
						||
                rep_address = profile.address or ""
 | 
						||
                if profile.user_type == 'legal':
 | 
						||
                    company_name = profile.company_name or ""
 | 
						||
                    company_national_id = profile.company_national_id or ""
 | 
						||
        
 | 
						||
        # Get well info
 | 
						||
        water_subscription = ""
 | 
						||
        electricity_subscription = ""
 | 
						||
        well_power = ""
 | 
						||
        old_meter_serial = ""
 | 
						||
        if instance.well:
 | 
						||
            water_subscription = instance.well.water_subscription_number or ""
 | 
						||
            electricity_subscription = instance.well.electricity_subscription_number or ""
 | 
						||
            well_power = str(instance.well.well_power) if instance.well.well_power else ""
 | 
						||
            old_meter_serial = instance.well.water_meter_serial_number or ""
 | 
						||
        
 | 
						||
        # Get new meter serial from installation report
 | 
						||
        new_meter_serial = ""
 | 
						||
        installer_visit_date = ""
 | 
						||
        report = reports_map.get(instance.id)
 | 
						||
        if report:
 | 
						||
            new_meter_serial = report.new_water_meter_serial or ""
 | 
						||
            installer_visit_date = format_date_jalali(report.visited_date)
 | 
						||
        
 | 
						||
        # Get quote amount
 | 
						||
        quote_amount = ""
 | 
						||
        quote = quotes_map.get(instance.id)
 | 
						||
        if quote:
 | 
						||
            quote_amount = str(quote.final_amount) if quote.final_amount else ""
 | 
						||
        
 | 
						||
        # Get payments info
 | 
						||
        payments_info = ""
 | 
						||
        payments = payments_map.get(instance.id, [])
 | 
						||
        if payments:
 | 
						||
            payment_strings = []
 | 
						||
            for payment in payments:
 | 
						||
                date_str = format_datetime_jalali(payment.created)
 | 
						||
                reference_number = payment.reference_number or "بدون کد"
 | 
						||
                payment_strings.append(f"{date_str} - {reference_number}")
 | 
						||
            payments_info = " | ".join(payment_strings)
 | 
						||
        
 | 
						||
        # Get installer name
 | 
						||
        installer_name = ""
 | 
						||
        installer = installers_map.get(instance.id)
 | 
						||
        if installer:
 | 
						||
            installer_name = installer.get_full_name() or str(installer)
 | 
						||
        
 | 
						||
        # Get overdue days
 | 
						||
        overdue_days = ""
 | 
						||
        if item['installation_overdue_days'] and item['installation_overdue_days'] > 0:
 | 
						||
            overdue_days = str(item['installation_overdue_days'])
 | 
						||
        
 | 
						||
        # Get approval info
 | 
						||
        approval_date = ""
 | 
						||
        approval_user = ""
 | 
						||
        approval_date_obj = approval_dates_map.get(instance.id)
 | 
						||
        approval_user_obj = approval_users_map.get(instance.id)
 | 
						||
        if approval_date_obj:
 | 
						||
            approval_date = format_datetime_jalali(approval_date_obj)
 | 
						||
        if approval_user_obj:
 | 
						||
            approval_user = approval_user_obj.get_full_name() or str(approval_user_obj)
 | 
						||
        
 | 
						||
        # Get settlement date
 | 
						||
        settlement_date = ""
 | 
						||
        settlement_date_obj = settlement_dates_map.get(instance.id)
 | 
						||
        if settlement_date_obj:
 | 
						||
            settlement_date = format_datetime_jalali(settlement_date_obj)
 | 
						||
        
 | 
						||
        row_data = [
 | 
						||
            instance.code,                    # شناسه
 | 
						||
            format_datetime_jalali(instance.created),  # تاریخ ایجاد درخواست
 | 
						||
            rep_first_name,                   # نام نماینده
 | 
						||
            rep_last_name,                    # نام خانوادگی نماینده
 | 
						||
            rep_national_code,                # کد ملی نماینده
 | 
						||
            company_name,                     # نام شرکت
 | 
						||
            company_national_id,              # شناسه شرکت
 | 
						||
            old_meter_serial,                 # سریال کنتور
 | 
						||
            new_meter_serial,                 # سریال کنتور جدید
 | 
						||
            water_subscription,               # شماره اشتراک آب
 | 
						||
            electricity_subscription,         # شماره اشتراک برق
 | 
						||
            well_power,                       # قدرت چاه
 | 
						||
            rep_phone_1,                      # شماره تماس ۱
 | 
						||
            rep_phone_2,                      # شماره تماس ۲
 | 
						||
            rep_address,                      # آدرس
 | 
						||
            quote_amount,                     # مبلغ پیشفاکتور
 | 
						||
            payments_info,                    # تاریخ واریزیها و کدهای رهگیری
 | 
						||
            installer_visit_date,             # تاریخ مراجعه نصاب
 | 
						||
            overdue_days,                     # تاخیر نصاب
 | 
						||
            installer_name,                   # نام نصاب
 | 
						||
            approval_date,                    # تاریخ تایید نصب توسط مدیر
 | 
						||
            approval_user,                    # نام تایید کننده نصب
 | 
						||
            settlement_date                   # تاریخ تسویه
 | 
						||
        ]
 | 
						||
        
 | 
						||
        for col, value in enumerate(row_data, 1):
 | 
						||
            cell = ws.cell(row=row_num, column=col, value=value)
 | 
						||
            # Set right alignment for Persian text
 | 
						||
            cell.alignment = Alignment(horizontal='right')
 | 
						||
    
 | 
						||
    # Auto-adjust column widths
 | 
						||
    for col in range(1, len(headers) + 1):
 | 
						||
        column_letter = get_column_letter(col)
 | 
						||
        max_length = 0
 | 
						||
        for row in ws[column_letter]:
 | 
						||
            try:
 | 
						||
                if len(str(row.value)) > max_length:
 | 
						||
                    max_length = len(str(row.value))
 | 
						||
            except:
 | 
						||
                pass
 | 
						||
        adjusted_width = min(max_length + 2, 50)
 | 
						||
        ws.column_dimensions[column_letter].width = adjusted_width
 | 
						||
    
 | 
						||
    # Prepare response
 | 
						||
    response = HttpResponse(
 | 
						||
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
 | 
						||
    )
 | 
						||
    
 | 
						||
    # Generate filename with current date
 | 
						||
    current_date = datetime.now().strftime('%Y%m%d_%H%M')
 | 
						||
    filename = f'requests_export_{current_date}.xlsx'
 | 
						||
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
 | 
						||
    
 | 
						||
    # Save workbook to response
 | 
						||
    wb.save(response)
 | 
						||
    
 | 
						||
    return response
 | 
						||
     |