huge fix
This commit is contained in:
parent
810c87e2e0
commit
b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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='حذف شده'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='وضعیت'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue