main flow checked

This commit is contained in:
aminhashemi92 2025-10-03 21:56:25 +03:30
parent b5bf3a5dbe
commit f853ad9784
21 changed files with 365 additions and 89 deletions

View file

@ -162,9 +162,9 @@ class StepInstanceAdmin(SimpleHistoryAdmin):
@admin.register(StepRejection)
class StepRejectionAdmin(SimpleHistoryAdmin):
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']
list_display = ['step_instance', 'role', 'rejected_by', 'reason_short', 'created_at', 'is_deleted']
list_filter = ['role', 'rejected_by', 'created_at', 'step_instance__step__process']
search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason', 'role__name']
readonly_fields = ['created_at']
ordering = ['-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", "is_deleted")
list_filter = ("decision", "role", "step_instance__step__process")
list_display = ("step_instance", "role", "approved_by", "created_at", "is_deleted")
list_filter = ("role", "step_instance__step__process")
search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username")

View file

@ -0,0 +1,34 @@
# Generated by Django 5.2.4 on 2025-10-02 09:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0008_alter_historicalprofile_phone_number_1_and_more'),
('processes', '0005_alter_historicalstepinstance_status_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='stepapproval',
unique_together=set(),
),
migrations.AddField(
model_name='historicalsteprejection',
name='role',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='accounts.role', verbose_name='نقش'),
),
migrations.AddField(
model_name='steprejection',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.role', verbose_name='نقش'),
),
migrations.AlterField(
model_name='stepapproval',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.role', verbose_name='نقش'),
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 5.2.4 on 2025-10-02 09:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('processes', '0006_alter_stepapproval_unique_together_and_more'),
]
operations = [
migrations.RemoveField(
model_name='stepapproval',
name='decision',
),
migrations.AlterField(
model_name='stepapproval',
name='reason',
field=models.TextField(blank=True, verbose_name='توضیحات'),
),
]

View file

@ -387,7 +387,7 @@ class StepInstance(models.Model):
def approvals_by_role(self):
decisions = {}
for a in self.approvals.select_related('role').order_by('created_at'):
decisions[a.role_id] = a.decision
decisions[a.role_id] = 'approved'
return decisions
def is_fully_approved(self) -> bool:
@ -409,6 +409,7 @@ class StepRejection(models.Model):
related_name='rejections',
verbose_name="نمونه مرحله"
)
role = models.ForeignKey(Role, on_delete=models.SET_NULL, blank=True, null=True, verbose_name="نقش")
rejected_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
@ -431,7 +432,7 @@ class StepRejection(models.Model):
ordering = ['-created_at']
def __str__(self):
return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()}"
return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()} ({self.role.name})"
def save(self, *args, **kwargs):
"""ذخیره با تغییر وضعیت مرحله"""
@ -447,7 +448,6 @@ class StepRejection(models.Model):
self.save()
class StepApproverRequirement(models.Model):
"""Required approver roles for a step."""
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله")
@ -466,15 +466,13 @@ class StepApproverRequirement(models.Model):
class StepApproval(models.Model):
"""Approvals per role for a concrete step instance."""
step_instance = models.ForeignKey(StepInstance, on_delete=models.CASCADE, related_name='approvals', verbose_name="نمونه مرحله")
role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش")
role = models.ForeignKey(Role, on_delete=models.SET_NULL, blank=True, null=True, verbose_name="نقش")
approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="تاییدکننده")
decision = models.CharField(max_length=8, choices=[('approved', 'تایید'), ('rejected', 'رد')], verbose_name='نتیجه')
reason = models.TextField(blank=True, 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 = 'تاییدهای مرحله'
@ -487,4 +485,4 @@ class StepApproval(models.Model):
def __str__(self):
return f"{self.step_instance} - {self.role} - {self.decision}"
return f"{self.step_instance} - {self.role} - تایید شده"

View file

@ -95,32 +95,113 @@
<div class="card border">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">گزارش نصب</h6>
{% if latest_report and latest_report.assignment and latest_report.assignment.installer %}
<span class="small text-muted">نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }}</span>
{% endif %}
<div class="d-flex align-items-center gap-3">
{% if installation_delay_days > 0 %}
<span class="badge bg-warning text-dark">
<i class="bx bx-time-five bx-xs"></i> {{ installation_delay_days }} روز تاخیر
</span>
{% elif installation_assignment and latest_report %}
<span class="badge bg-success">
<i class="bx bx-check bx-xs"></i> به موقع
</span>
{% endif %}
{% if latest_report and latest_report.assignment and latest_report.assignment.installer %}
<span class="small text-muted">نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }}</span>
{% endif %}
</div>
</div>
<div class="card-body">
{% if latest_report %}
<div class="row g-3">
<div class="col-12 col-md-6">
<p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}</p>
<!-- اطلاعات گزارش نصب -->
<div class="row g-3 mb-3">
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-calendar bx-sm me-2"></i>تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}</p>
</div>
{% if installation_assignment.scheduled_date %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-calendar-star bx-sm me-2"></i>تاریخ برنامه‌ریزی: {{ installation_assignment.scheduled_date|to_jalali }}</p>
</div>
{% endif %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال کنتور جدید: {{ latest_report.new_water_meter_serial|default:'-' }}</p>
</div>
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ latest_report.seal_number|default:'-' }}</p>
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
</div>
{% if latest_report.sim_number %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-mobile bx-sm me-2"></i>شماره سیمکارت: {{ latest_report.sim_number }}</p>
</div>
{% endif %}
{% if latest_report.meter_type %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-category bx-sm me-2"></i>نوع کنتور: {{ latest_report.get_meter_type_display }}</p>
</div>
{% endif %}
{% if latest_report.meter_size %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-ruler bx-sm me-2"></i>سایز کنتور: {{ latest_report.meter_size }}</p>
</div>
{% endif %}
{% if latest_report.water_meter_manufacturer %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-buildings bx-sm me-2"></i>سازنده: {{ latest_report.water_meter_manufacturer.name }}</p>
</div>
{% endif %}
{% if latest_report.discharge_pipe_diameter %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-shape-circle bx-sm me-2"></i>قطر لوله آبده: {{ latest_report.discharge_pipe_diameter }} اینچ</p>
</div>
{% endif %}
{% if latest_report.usage_type %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-droplet bx-sm me-2"></i>نوع مصرف: {{ latest_report.get_usage_type_display }}</p>
</div>
{% endif %}
{% if latest_report.driving_force %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-car bx-sm me-2"></i>نیرو محرکه: {{ latest_report.driving_force }}</p>
</div>
{% endif %}
{% if latest_report.motor_power %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-tag bx-sm me-2"></i>قدرت موتور: {{ latest_report.motor_power }} کیلووات ساعت</p>
</div>
{% endif %}
{% if latest_report.exploitation_license_number %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-id-card bx-sm me-2"></i>شماره پروانه: {{ latest_report.exploitation_license_number }}</p>
</div>
{% endif %}
{% if latest_report.pre_calibration_flow_rate %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>دبی قبل از کالیبراسیون: {{ latest_report.pre_calibration_flow_rate }} لیتر/ثانیه</p>
</div>
{% endif %}
{% if latest_report.post_calibration_flow_rate %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-tachometer bx-sm me-2"></i>دبی بعد از کالیبراسیون: {{ latest_report.post_calibration_flow_rate }} لیتر/ثانیه</p>
</div>
{% endif %}
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ latest_report.utm_x|default:'-' }}</p>
</div>
<div class="col-12 col-md-4">
<p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ latest_report.utm_y|default:'-' }}</p>
</div>
</div>
{% if latest_report.description %}
<div class="mt-2">
<p class="mb-0"><i class="bx bx-text bx-sm me-2"></i><strong>توضیحات:</strong></p>
<div class="mb-3">
<h6 class="text-primary mb-2"><i class="bx bx-text me-1"></i>توضیحات</h6>
<div class="text-muted">{{ latest_report.description }}</div>
</div>
{% endif %}
<hr>
<h6>عکس‌ها</h6>
<h6 class="text-primary mb-2"><i class="bx bx-image me-1"></i>عکس‌ها</h6>
<div class="row">
{% for p in latest_report.photos.all %}
<div class="col-6 col-md-3 mb-2"><img class="img-fluid rounded border" src="{{ p.image.url }}" alt="photo"></div>

View file

@ -37,12 +37,14 @@
<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">
{% if not request.user|is_installer %}
<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>
{% endif %}
{% if request.user|is_broker %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
<i class="bx bx-plus me-1"></i>

View file

@ -507,13 +507,22 @@ def instance_summary(request, instance_id):
# Collect final invoice, payments, and certificate if any
from invoices.models import Invoice
from installations.models import InstallationReport
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:
@ -527,6 +536,8 @@ def instance_summary(request, instance_id):
'rows': rows,
'latest_report': latest_report,
'certificate': certificate,
'installation_assignment': installation_assignment,
'installation_delay_days': installation_delay_days,
})
@ -653,12 +664,11 @@ def export_requests_excel(request):
# 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()
).select_related('approved_by').order_by('-created_at').first()
if approval:
approval_dates_map[step_instance.process_instance_id] = approval.created
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