This commit is contained in:
aminhashemi92 2025-09-29 17:38:11 +03:30
parent 810c87e2e0
commit b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions

View file

@ -162,7 +162,7 @@ class StepInstanceAdmin(SimpleHistoryAdmin):
@admin.register(StepRejection)
class StepRejectionAdmin(SimpleHistoryAdmin):
list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at']
list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at', 'is_deleted']
list_filter = ['rejected_by', 'created_at', 'step_instance__step__process']
search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason']
readonly_fields = ['created_at']
@ -182,6 +182,6 @@ class StepApproverRequirementAdmin(admin.ModelAdmin):
@admin.register(StepApproval)
class StepApprovalAdmin(admin.ModelAdmin):
list_display = ("step_instance", "role", "decision", "approved_by", "created_at")
list_display = ("step_instance", "role", "decision", "approved_by", "created_at", "is_deleted")
list_filter = ("decision", "role", "step_instance__step__process")
search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username")

View file

@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-09-27 06:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('processes', '0003_historicalstepinstance_edit_count_and_more'),
]
operations = [
migrations.AddField(
model_name='historicalsteprejection',
name='is_deleted',
field=models.BooleanField(default=False, verbose_name='حذف شده'),
),
migrations.AddField(
model_name='stepapproval',
name='is_deleted',
field=models.BooleanField(default=False, verbose_name='حذف شده'),
),
migrations.AddField(
model_name='steprejection',
name='is_deleted',
field=models.BooleanField(default=False, verbose_name='حذف شده'),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-09-27 15:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('processes', '0004_historicalsteprejection_is_deleted_and_more'),
]
operations = [
migrations.AlterField(
model_name='historicalstepinstance',
name='status',
field=models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح'), ('approved', 'تایید اضطراری')], default='pending', max_length=20, verbose_name='وضعیت'),
),
migrations.AlterField(
model_name='stepinstance',
name='status',
field=models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح'), ('approved', 'تایید اضطراری')], default='pending', max_length=20, verbose_name='وضعیت'),
),
]

View file

@ -290,6 +290,10 @@ class ProcessInstance(SluggedModel):
dependencies = step.get_dependencies()
for dependency_id in dependencies:
step_instance = self.step_instances.filter(step_id=dependency_id).first()
if step_instance and step_instance.status == 'in_progress' and step_instance.step.order == 3 and step.order == 4:
return True
if step_instance and step_instance.status == 'approved' and step_instance.step.order == 8 and step.order == 9:
return True
if not step_instance or step_instance.status != 'completed':
return False
return True
@ -320,6 +324,7 @@ class StepInstance(models.Model):
('skipped', 'رد شده'),
('blocked', 'مسدود شده'),
('rejected', 'رد شده و نیاز به اصلاح'),
('approved', 'تایید اضطراری'),
],
default='pending',
verbose_name="وضعیت"
@ -417,6 +422,7 @@ class StepRejection(models.Model):
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ رد شدن")
is_deleted = models.BooleanField(default=False, verbose_name='حذف شده')
history = HistoricalRecords()
class Meta:
@ -433,6 +439,14 @@ class StepRejection(models.Model):
self.step_instance.save()
super().save(*args, **kwargs)
def hard_delete(self):
super().delete()
def delete(self, *args, **kwargs):
self.is_deleted = True
self.save()
class StepApproverRequirement(models.Model):
"""Required approver roles for a step."""
@ -457,11 +471,20 @@ class StepApproval(models.Model):
decision = models.CharField(max_length=8, choices=[('approved', 'تایید'), ('rejected', 'رد')], verbose_name='نتیجه')
reason = models.TextField(blank=True, verbose_name='علت (برای رد)')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')
is_deleted = models.BooleanField(default=False, verbose_name='حذف شده')
class Meta:
unique_together = ('step_instance', 'role')
verbose_name = 'تایید مرحله'
verbose_name_plural = 'تاییدهای مرحله'
def delete(self, *args, **kwargs):
self.is_deleted = True
self.save()
def hard_delete(self):
super().delete()
def __str__(self):
return f"{self.step_instance} - {self.role} - {self.decision}"

View file

@ -1,6 +1,7 @@
{% extends '_base.html' %}
{% load static %}
{% load accounts_tags %}
{% load common_tags %}
{% block sidebar %}
{% include 'sidebars/admin.html' %}
@ -36,12 +37,10 @@
<div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
<div class="dt-buttons btn-group flex-wrap mb-0">
<div class="btn-group">
<button class="btn buttons-collection btn-label-primary dropdown-toggle me-4 d-none" type="button">
<span>
<span class="d-flex align-items-center gap-2">
<i class="icon-base bx bx-export me-sm-1"></i>
<span class="d-none d-sm-inline-block">خروجی</span>
</span>
<button class="btn btn-label-success me-2" type="button" onclick="exportToExcel()">
<span class="d-flex align-items-center gap-2">
<i class="bx bx-export me-sm-1"></i>
<span class="d-none d-sm-inline-block">خروجی اکسل</span>
</span>
</button>
{% if request.user|is_broker %}
@ -212,6 +211,7 @@
<th>امور</th>
<th>پیشرفت</th>
<th>وضعیت</th>
<th>تاریخ نصب/تاخیر</th>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
</tr>
@ -244,6 +244,20 @@
</div>
</td>
<td>{{ item.instance.get_status_display_with_color|safe }}</td>
<td>
{% if item.installation_scheduled_date %}
<div>
<span title="{{ item.installation_scheduled_date|date:'Y-m-d' }}">{{ item.installation_scheduled_date | to_jalali }}</span>
</div>
{% if item.installation_overdue_days and item.installation_overdue_days > 0 %}
<small class="text-danger d-block">{{ item.installation_overdue_days }} روز تاخیر</small>
{% else %}
<small class="text-muted d-block">بدون تاخیر</small>
{% endif %}
{% else %}
<span class="text-muted">تاریخ نصب تعیین نشده</span>
{% endif %}
</td>
<td>{{ item.instance.jcreated_date }}</td>
<td>
<div class="d-inline-block">
@ -287,6 +301,7 @@
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
@ -419,6 +434,10 @@
<div id="repNewFields" class="col-sm-12" style="display:none;">
<div class="row g-3">
<div class="col-sm-12">
<label class="form-label" for="id_user_type">{{ customer_form.user_type.label }}</label>
{{ customer_form.user_type }}
</div>
<div class="col-sm-6">
<label class="form-label" for="id_first_name">{{ customer_form.first_name.label }}</label>
{{ customer_form.first_name }}
@ -439,6 +458,15 @@
<label class="form-label" for="id_national_code">{{ customer_form.national_code.label }}</label>
{{ customer_form.national_code }}
</div>
<!-- Company fields for legal entities -->
<div class="col-sm-6 company-fields" style="display:none;">
<label class="form-label" for="id_company_name">{{ customer_form.company_name.label }}</label>
{{ customer_form.company_name }}
</div>
<div class="col-sm-6 company-fields" style="display:none;">
<label class="form-label" for="id_company_national_id">{{ customer_form.company_national_id.label }}</label>
{{ customer_form.company_national_id }}
</div>
<div class="col-sm-6">
<label class="form-label" for="id_card_number">{{ customer_form.card_number.label }}</label>
{{ customer_form.card_number }}
@ -717,6 +745,21 @@
.fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
});
function toggleRepCompanyFields() {
const userType = $('#user-type-select').val();
if (userType === 'legal') {
$('#repNewFields .company-fields').show();
$('input[name="company_name"]').attr('required', true);
$('input[name="company_national_id"]').attr('required', true);
} else {
$('#repNewFields .company-fields').hide();
$('input[name="company_name"]').removeAttr('required');
$('input[name="company_national_id"]').removeAttr('required');
}
}
$('#user-type-select').on('change', toggleRepCompanyFields);
$('#btnLookupRep').on('click', function() {
const nc = $('#rep_national_code').val().trim();
if (!nc) { setStatus('#repStatus', 'لطفا کد ملی نماینده را وارد کنید', 'danger'); return; }
@ -732,36 +775,47 @@
$('#id_first_name').val(resp.user.first_name || '');
$('#id_last_name').val(resp.user.last_name || '');
if (resp.user.profile) {
$('#user-type-select').val(resp.user.profile.user_type || 'individual');
$('#id_national_code').val(resp.user.profile.national_code || nc);
$('#id_phone_number_1').val(resp.user.profile.phone_number_1 || '');
$('#id_phone_number_2').val(resp.user.profile.phone_number_2 || '');
$('#id_company_name').val(resp.user.profile.company_name || '');
$('#id_company_national_id').val(resp.user.profile.company_national_id || '');
$('#id_card_number').val(resp.user.profile.card_number || '');
$('#id_account_number').val(resp.user.profile.account_number || '');
$('#id_bank_name').val(resp.user.profile.bank_name || '');
$('#id_address').val(resp.user.profile.address || '');
} else {
$('#user-type-select').val('individual');
$('#id_national_code').val(nc);
$('#id_phone_number_1').val('');
$('#id_phone_number_2').val('');
$('#id_company_name').val('');
$('#id_company_national_id').val('');
$('#id_card_number').val('');
$('#id_account_number').val('');
$('#id_bank_name').val('');
$('#id_address').val('');
}
toggleRepCompanyFields();
setStatus('#repStatus', 'نماینده یافت شد.', 'success');
} else {
currentRepId = null;
$('#repNewFields').show();
// Clear form and prefill national code
$('#user-type-select').val('individual');
$('#id_first_name').val('');
$('#id_last_name').val('');
$('#id_national_code').val(nc);
$('#id_phone_number_1').val('');
$('#id_phone_number_2').val('');
$('#id_company_name').val('');
$('#id_company_national_id').val('');
$('#id_card_number').val('');
$('#id_account_number').val('');
$('#id_bank_name').val('');
$('#id_address').val('');
toggleRepCompanyFields();
setStatus('#repStatus', 'نماینده یافت نشد. لطفا اطلاعات را تکمیل کنید.', 'danger');
}
})
@ -954,6 +1008,45 @@
});
};
};
// Export to Excel function
window.exportToExcel = function() {
// Get current filter parameters from the form
const form = document.querySelector('form[method="get"]');
const formData = new FormData(form);
// Build query string
const params = new URLSearchParams();
for (const [key, value] of formData.entries()) {
if (value.trim()) {
params.append(key, value);
}
}
// Create export URL with current filters
const exportUrl = '{% url "processes:export_requests_excel" %}' + '?' + params.toString();
// Show loading state
const btn = document.querySelector('button[onclick="exportToExcel()"]');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="bx bx-loader-circle bx-spin me-1"></i>در حال تولید...';
btn.disabled = true;
// Create invisible link and trigger download
const link = document.createElement('a');
link.href = exportUrl;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Reset button after a short delay
setTimeout(() => {
btn.innerHTML = originalText;
btn.disabled = false;
showToast('فایل اکسل آماده دانلود است', 'success');
}, 1000);
};
});
</script>
{% endblock %}

View file

@ -28,9 +28,11 @@ def stepper_header(instance, current_step=None):
status = step_id_to_status.get(step.id, 'pending')
# بررسی دسترسی به مرحله (UI navigation constraints):
# can_access = instance.can_access_step(step)
can_access = instance.can_access_step(step)
# فقط مراحل تکمیل‌شده یا مرحله جاری قابل کلیک هستند
can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id)
# can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id)
# مرحله انتخاب‌شده (نمایش فعلی)
is_selected = bool(current_step and step.id == current_step.id)
# مرحله‌ای که باید انجام شود (مرحله جاری در instance)

View file

@ -6,6 +6,7 @@ app_name = 'processes'
urlpatterns = [
# Requests UI
path('requests/', views.request_list, name='request_list'),
path('requests/export/excel/', views.export_requests_excel, name='export_requests_excel'),
path('requests/create/', views.create_request_with_entities, name='create_request_with_entities'),
path('requests/lookup/well/', views.lookup_well_by_subscription, name='lookup_well_by_subscription'),
path('requests/lookup/representative/', views.lookup_representative_by_national_code, name='lookup_representative_by_national_code'),

View file

@ -20,7 +20,7 @@ def scope_instances_queryset(user, queryset=None):
return qs.filter(id__in=assign_ids)
if profile.has_role(UserRoles.BROKER):
return qs.filter(broker=profile.broker)
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER):
return qs.filter(broker__affairs__county=profile.county)
if profile.has_role(UserRoles.ADMIN):
return qs
@ -69,7 +69,7 @@ def scope_wells_queryset(user, queryset=None):
return qs
if profile.has_role(UserRoles.BROKER):
return qs.filter(broker=profile.broker)
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER):
return qs.filter(broker__affairs__county=profile.county)
if profile.has_role(UserRoles.INSTALLER):
# Wells that have instances assigned to this installer
@ -102,7 +102,7 @@ def scope_customers_queryset(user, queryset=None):
return qs
if profile.has_role(UserRoles.BROKER):
return qs.filter(broker=profile.broker)
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER):
return qs.filter(county=profile.county)
if profile.has_role(UserRoles.INSTALLER):
# Customers that are representatives of instances assigned to this installer

View file

@ -3,13 +3,19 @@ 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.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
from installations.models import InstallationAssignment, InstallationReport
from wells.models import Well
from accounts.models import Profile, Broker
from locations.models import Affairs
@ -65,18 +71,65 @@ def request_list(request):
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
# 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
@ -160,7 +213,10 @@ def lookup_representative_by_national_code(request):
'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,
@ -240,6 +296,7 @@ def create_request_with_entities(request):
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)
@ -366,12 +423,12 @@ def step_detail(request, instance_id, step_id):
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
# 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):
@ -471,4 +528,365 @@ def instance_summary(request, instance_id):
'latest_report': latest_report,
'certificate': certificate,
})
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,
decision='approved',
is_deleted=False
).select_related('approved_by').order_by('-created').first()
if approval:
approval_dates_map[step_instance.process_instance_id] = approval.created
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