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
|
||
|