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

@ -39,7 +39,8 @@ class CustomerForm(forms.ModelForm):
}), }),
'phone_number_1': forms.TextInput(attrs={ 'phone_number_1': forms.TextInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': '09123456789' 'placeholder': '09123456789',
'required': True
}), }),
'phone_number_2': forms.TextInput(attrs={ 'phone_number_2': forms.TextInput(attrs={
'class': 'form-control', 'class': 'form-control',

View file

@ -0,0 +1,25 @@
# Generated by Django 5.2.4 on 2025-10-02 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_historicalprofile_company_name_and_more'),
]
operations = [
migrations.AlterField(
model_name='historicalprofile',
name='phone_number_1',
field=models.CharField(default=1, max_length=11, verbose_name='شماره تماس ۱'),
preserve_default=False,
),
migrations.AlterField(
model_name='profile',
name='phone_number_1',
field=models.CharField(default=1, max_length=11, verbose_name='شماره تماس ۱'),
preserve_default=False,
),
]

View file

@ -78,9 +78,7 @@ class Profile(BaseModel):
) )
phone_number_1 = models.CharField( phone_number_1 = models.CharField(
max_length=11, max_length=11,
null=True,
verbose_name="شماره تماس ۱", verbose_name="شماره تماس ۱",
blank=True
) )
phone_number_2 = models.CharField( phone_number_2 = models.CharField(
max_length=11, max_length=11,

View file

@ -4,6 +4,8 @@ from django.contrib import messages
from django.http import JsonResponse from django.http import JsonResponse
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.template import Template, Context
from django.utils.safestring import mark_safe
from processes.models import ProcessInstance, StepInstance from processes.models import ProcessInstance, StepInstance
from invoices.models import Invoice from invoices.models import Invoice
@ -28,20 +30,33 @@ def _render_template(template: CertificateTemplate, instance: ProcessInstance):
well = instance.well well = instance.well
rep = instance.representative rep = instance.representative
latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first() latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
individual = True if rep.profile and rep.profile.user_type == 'individual' else False
customer_company_name = rep.profile.company_name if rep.profile and rep.profile.user_type == 'legal' else None
city = template.company.broker.affairs.county.city.name if template.company and template.company.broker and template.company.broker.affairs and template.company.broker.affairs.county and template.company.broker.affairs.county.city else None
county = template.company.broker.affairs.county.name if template.company and template.company.broker and template.company.broker.affairs and template.company.broker.affairs.county else None
ctx = { ctx = {
'today_jalali': _to_jalali(timezone.now().date()), 'today_jalali': mark_safe(f"<span class=\"fw-bold\">{_to_jalali(timezone.now().date())}</span>"),
'request_code': instance.code, 'request_code': mark_safe(f"<span class=\"fw-bold\">{instance.code}</span>"),
'company_name': (template.company.name if template.company else '') or '', 'company_name': mark_safe(f"<span class=\"fw-bold\">{(template.company.name if template.company else '') or ''}</span>"),
'customer_full_name': rep.get_full_name() if rep else '', 'customer_full_name': mark_safe(f"<span class=\"fw-bold\">{rep.get_full_name() if rep else ''}</span>"),
'water_subscription_number': getattr(well, 'water_subscription_number', '') or '', 'water_subscription_number': mark_safe(f"<span class=\"fw-bold\">{getattr(well, 'water_subscription_number', '') or ''}</span>"),
'address': getattr(well, 'county', '') or '', 'address': mark_safe(f"<span class=\"fw-bold\">{getattr(well, 'county', '') or ''}</span>"),
'visit_date_jalali': _to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else '', 'visit_date_jalali': mark_safe(f"<span class=\"fw-bold\">{_to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else ''}</span>"),
'city': mark_safe(f"<span class=\"fw-bold\">{city or ''}</span>"),
'county': mark_safe(f"<span class=\"fw-bold\">{county or ''}</span>"),
'customer_company_name': mark_safe(f"<span class=\"fw-bold\">{customer_company_name or ''}</span>"),
'individual': individual,
} }
title = (template.title or '').format(**ctx)
body = (template.body or '') # Render title using Django template engine
# Render body placeholders with bold values title_template = Template(template.title or '')
for k, v in ctx.items(): title = title_template.render(Context(ctx))
body = body.replace(f"{{{{ {k} }}}}", f"<strong>{str(v)}</strong>")
# Render body using Django template engine
body_template = Template(template.body or '')
body = body_template.render(Context(ctx))
return title, body return title, body

Binary file not shown.

View file

@ -99,9 +99,9 @@
<p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ report.utm_y|default:'-' }}</p> <p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ report.utm_y|default:'-' }}</p>
<p class="text-nowrap mb-2"><i class="bx bx-category bx-sm me-2"></i>نوع مصرف: {{ report.get_usage_type_display|default:'-' }}</p> <p class="text-nowrap mb-2"><i class="bx bx-category bx-sm me-2"></i>نوع مصرف: {{ report.get_usage_type_display|default:'-' }}</p>
<p class="text-nowrap mb-2"><i class="bx bx-id-card bx-sm me-2"></i>شماره پروانه بهره‌برداری: {{ report.exploitation_license_number|default:'-' }}</p> <p class="text-nowrap mb-2"><i class="bx bx-id-card bx-sm me-2"></i>شماره پروانه بهره‌برداری: {{ report.exploitation_license_number|default:'-' }}</p>
<p class="text-nowrap mb-2"><i class="bx bx-bolt-circle bx-sm me-2"></i>(کیلووات ساعت)قدرت موتور: {{ report.motor_power|default:'-' }}</p> <p class="text-nowrap mb-2"><i class="bx bx-bolt-circle bx-sm me-2"></i>قدرت موتور(کیلووات ساعت): {{ report.motor_power|default:'-' }}</p>
<p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>(لیتر/ثانیه) دبی قبل کالیبراسیون: {{ report.pre_calibration_flow_rate|default:'-' }}</p> <p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>دبی قبل کالیبراسیون(لیتر/ثانیه): {{ report.pre_calibration_flow_rate|default:'-' }}</p>
<p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>(لیتر/ثانیه) دبی بعد کالیبراسیون: {{ report.post_calibration_flow_rate|default:'-' }}</p> <p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>دبی بعد کالیبراسیون(لیتر/ثانیه): {{ report.post_calibration_flow_rate|default:'-' }}</p>
</div> </div>
</div> </div>
@ -157,7 +157,7 @@
<tr><td colspan="5" class="text-center text-muted">تغییری ثبت نشده است</td></tr> <tr><td colspan="5" class="text-center text-muted">تغییری ثبت نشده است</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>nvoices/instance/69/step/3/payments/
</div> </div>
</div> </div>
</div> </div>

View file

@ -320,15 +320,29 @@ def installation_report_step(request, instance_id, step_id):
can_approve_reject = False can_approve_reject = False
user_can_approve = can_approve_reject user_can_approve = can_approve_reject
approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False)) approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
rejections_list = list(step_instance.rejections.select_related('role', 'rejected_by').filter(is_deleted=False))
approvals_by_role = {a.role_id: a for a in approvals_list} approvals_by_role = {a.role_id: a for a in approvals_list}
approver_statuses = [ rejections_by_role = {r.role_id: r for r in rejections_list}
{ approver_statuses = []
for r in reqs:
appr = approvals_by_role.get(r.role_id)
rejection = rejections_by_role.get(r.role_id)
if appr:
status = 'approved'
reason = appr.reason
elif rejection:
status = 'rejected'
reason = rejection.reason
else:
status = None
reason = ''
approver_statuses.append({
'role': r.role, 'role': r.role,
'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None), 'status': status,
'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''), 'reason': reason,
} })
for r in reqs
]
# Determine if current user has already approved/rejected (to disable buttons) # Determine if current user has already approved/rejected (to disable buttons)
current_user_has_decided = False current_user_has_decided = False
@ -356,10 +370,11 @@ def installation_report_step(request, instance_id, step_id):
if action == 'approve': if action == 'approve':
# Record this user's approval for their role # Record this user's approval for their role
StepApproval.objects.update_or_create( StepApproval.objects.create(
step_instance=step_instance, step_instance=step_instance,
role=matching_role, role=matching_role,
defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''} approved_by=request.user,
reason=''
) )
# Only mark report approved when ALL required roles have approved # Only mark report approved when ALL required roles have approved
if step_instance.is_fully_approved(): if step_instance.is_fully_approved():
@ -386,12 +401,8 @@ def installation_report_step(request, instance_id, step_id):
if not reason: if not reason:
messages.error(request, 'لطفاً علت رد شدن را وارد کنید.') messages.error(request, 'لطفاً علت رد شدن را وارد کنید.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
StepApproval.objects.update_or_create( # Only create StepRejection for rejections, not StepApproval
step_instance=step_instance, StepRejection.objects.create(step_instance=step_instance, role=matching_role, rejected_by=request.user, reason=reason)
role=matching_role,
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
)
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
existing_report.approved = False existing_report.approved = False
existing_report.save() existing_report.save()
# If current step moved ahead of this step, reset it back for correction (align with invoices) # If current step moved ahead of this step, reset it back for correction (align with invoices)

View file

@ -145,7 +145,17 @@
<!-- Customer & Well Info (compact to match preview) --> <!-- Customer & Well Info (compact to match preview) -->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-6"> <div class="col-6">
<h6 class="fw-bold mb-2">اطلاعات مشترک</h6> <h6 class="fw-bold mb-2">
{% if instance.representative.profile.user_type == 'legal' %}
اطلاعات مشترک (حقوقی)
{% else %}
اطلاعات مشترک (حقیقی)
{% endif %}
</h6>
{% if instance.representative.profile.user_type == 'legal' %}
<div class="small mb-1"><span class="text-muted">نام شرکت:</span> {{ instance.representative.profile.company_name|default:"-" }}</div>
<div class="small mb-1"><span class="text-muted">شناسه ملی:</span> {{ instance.representative.profile.company_national_id|default:"-" }}</div>
{% endif %}
<div class="small mb-1"><span class="text-muted">نام:</span> {{ quote.customer.get_full_name }}</div> <div class="small mb-1"><span class="text-muted">نام:</span> {{ quote.customer.get_full_name }}</div>
{% if instance.representative.profile and instance.representative.profile.national_code %} {% if instance.representative.profile and instance.representative.profile.national_code %}
<div class="small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div> <div class="small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>

View file

@ -57,7 +57,7 @@
<div class="alert alert-info"> <div class="alert alert-info">
<h6>پیش‌فاکتور موجود</h6> <h6>پیش‌فاکتور موجود</h6>
<span class="mb-1">{{ existing_quote.name }} | </span> <span class="mb-1">{{ existing_quote.name }} | </span>
<span class="mb-1">مبلغ کل: {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span> <span class="mb-1">مبلغ کل (با احتساب مالیات): {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span>
<span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span> <span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span>
</div> </div>
</div> </div>

View file

@ -358,14 +358,28 @@ def quote_payment_step(request, instance_id, step_id):
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None) user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else [] user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False)) approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
rejections_list = list(step_instance.rejections.select_related('role', 'rejected_by').filter(is_deleted=False))
approvals_by_role = {a.role_id: a for a in approvals_list} approvals_by_role = {a.role_id: a for a in approvals_list}
rejections_by_role = {r.role_id: r for r in rejections_list}
approver_statuses = [] approver_statuses = []
for r in reqs: for r in reqs:
appr = approvals_by_role.get(r.role_id) appr = approvals_by_role.get(r.role_id)
rejection = rejections_by_role.get(r.role_id)
if appr:
status = 'approved'
reason = appr.reason
elif rejection:
status = 'rejected'
reason = rejection.reason
else:
status = None
reason = ''
approver_statuses.append({ approver_statuses.append({
'role': r.role, 'role': r.role,
'status': (appr.decision if appr else None), 'status': status,
'reason': (appr.reason if appr else ''), 'reason': reason,
}) })
# dynamic permission: who can approve/reject this step (based on requirements) # dynamic permission: who can approve/reject this step (based on requirements)
@ -398,10 +412,11 @@ def quote_payment_step(request, instance_id, step_id):
action = request.POST.get('action') action = request.POST.get('action')
if action == 'approve': if action == 'approve':
StepApproval.objects.update_or_create( StepApproval.objects.create(
step_instance=step_instance, step_instance=step_instance,
role=matching_role, role=matching_role,
defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''} approved_by=request.user,
reason=''
) )
if step_instance.is_fully_approved(): if step_instance.is_fully_approved():
step_instance.status = 'completed' step_instance.status = 'completed'
@ -422,12 +437,12 @@ def quote_payment_step(request, instance_id, step_id):
if not reason: if not reason:
messages.error(request, 'علت رد شدن را وارد کنید') messages.error(request, 'علت رد شدن را وارد کنید')
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id) return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
StepApproval.objects.update_or_create( StepRejection.objects.create(
step_instance=step_instance, step_instance=step_instance,
role=matching_role, role=matching_role,
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} rejected_by=request.user,
reason=reason
) )
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
# If current step is ahead of this step, reset it back to this step # If current step is ahead of this step, reset it back to this step
try: try:
if instance.current_step and instance.current_step.order > step.order: if instance.current_step and instance.current_step.order > step.order:
@ -928,15 +943,29 @@ def final_settlement_step(request, instance_id, step_id):
# Build approver statuses for template (include reason to display in UI) # Build approver statuses for template (include reason to display in UI)
reqs = list(step.approver_requirements.select_related('role').all()) reqs = list(step.approver_requirements.select_related('role').all())
approvals = list(step_instance.approvals.select_related('role').all()) approvals = list(step_instance.approvals.select_related('role').all())
rejections = list(step_instance.rejections.select_related('role').all())
approvals_by_role = {a.role_id: a for a in approvals} approvals_by_role = {a.role_id: a for a in approvals}
approver_statuses = [ rejections_by_role = {r.role_id: r for r in rejections}
{ approver_statuses = []
for r in reqs:
appr = approvals_by_role.get(r.role_id)
rejection = rejections_by_role.get(r.role_id)
if appr:
status = 'approved'
reason = appr.reason
elif rejection:
status = 'rejected'
reason = rejection.reason
else:
status = None
reason = ''
approver_statuses.append({
'role': r.role, 'role': r.role,
'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None), 'status': status,
'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''), 'reason': reason,
} })
for r in reqs
]
# dynamic permission to control approve/reject UI # dynamic permission to control approve/reject UI
try: try:
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()) user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
@ -971,10 +1000,11 @@ def final_settlement_step(request, instance_id, step_id):
if invoice.remaining_amount != 0: if invoice.remaining_amount != 0:
messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})") messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})")
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
StepApproval.objects.update_or_create( StepApproval.objects.create(
step_instance=step_instance, step_instance=step_instance,
role=matching_role, role=matching_role,
defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''} approved_by=request.user,
reason=''
) )
if step_instance.is_fully_approved(): if step_instance.is_fully_approved():
step_instance.status = 'completed' step_instance.status = 'completed'
@ -993,12 +1023,12 @@ def final_settlement_step(request, instance_id, step_id):
if not reason: if not reason:
messages.error(request, 'علت رد شدن را وارد کنید') messages.error(request, 'علت رد شدن را وارد کنید')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
StepApproval.objects.update_or_create( StepRejection.objects.create(
step_instance=step_instance, step_instance=step_instance,
role=matching_role, role=matching_role,
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} rejected_by=request.user,
reason=reason
) )
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
# If current step is ahead of this step, reset it back to this step (align behavior with other steps) # If current step is ahead of this step, reset it back to this step (align behavior with other steps)
try: try:
if instance.current_step and instance.current_step.order > step.order: if instance.current_step and instance.current_step.order > step.order:

View file

@ -162,9 +162,9 @@ class StepInstanceAdmin(SimpleHistoryAdmin):
@admin.register(StepRejection) @admin.register(StepRejection)
class StepRejectionAdmin(SimpleHistoryAdmin): class StepRejectionAdmin(SimpleHistoryAdmin):
list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at', 'is_deleted'] list_display = ['step_instance', 'role', 'rejected_by', 'reason_short', 'created_at', 'is_deleted']
list_filter = ['rejected_by', 'created_at', 'step_instance__step__process'] list_filter = ['role', 'rejected_by', 'created_at', 'step_instance__step__process']
search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason'] search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason', 'role__name']
readonly_fields = ['created_at'] readonly_fields = ['created_at']
ordering = ['-created_at'] ordering = ['-created_at']
@ -182,6 +182,6 @@ class StepApproverRequirementAdmin(admin.ModelAdmin):
@admin.register(StepApproval) @admin.register(StepApproval)
class StepApprovalAdmin(admin.ModelAdmin): class StepApprovalAdmin(admin.ModelAdmin):
list_display = ("step_instance", "role", "decision", "approved_by", "created_at", "is_deleted") list_display = ("step_instance", "role", "approved_by", "created_at", "is_deleted")
list_filter = ("decision", "role", "step_instance__step__process") list_filter = ("role", "step_instance__step__process")
search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username") 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): def approvals_by_role(self):
decisions = {} decisions = {}
for a in self.approvals.select_related('role').order_by('created_at'): 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 return decisions
def is_fully_approved(self) -> bool: def is_fully_approved(self) -> bool:
@ -409,6 +409,7 @@ class StepRejection(models.Model):
related_name='rejections', related_name='rejections',
verbose_name="نمونه مرحله" verbose_name="نمونه مرحله"
) )
role = models.ForeignKey(Role, on_delete=models.SET_NULL, blank=True, null=True, verbose_name="نقش")
rejected_by = models.ForeignKey( rejected_by = models.ForeignKey(
User, User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -431,7 +432,7 @@ class StepRejection(models.Model):
ordering = ['-created_at'] ordering = ['-created_at']
def __str__(self): 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): def save(self, *args, **kwargs):
"""ذخیره با تغییر وضعیت مرحله""" """ذخیره با تغییر وضعیت مرحله"""
@ -447,7 +448,6 @@ class StepRejection(models.Model):
self.save() self.save()
class StepApproverRequirement(models.Model): class StepApproverRequirement(models.Model):
"""Required approver roles for a step.""" """Required approver roles for a step."""
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله") 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): class StepApproval(models.Model):
"""Approvals per role for a concrete step instance.""" """Approvals per role for a concrete step instance."""
step_instance = models.ForeignKey(StepInstance, on_delete=models.CASCADE, related_name='approvals', verbose_name="نمونه مرحله") 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="تاییدکننده") 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='تاریخ') created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')
is_deleted = models.BooleanField(default=False, verbose_name='حذف شده') is_deleted = models.BooleanField(default=False, verbose_name='حذف شده')
class Meta: class Meta:
unique_together = ('step_instance', 'role')
verbose_name = 'تایید مرحله' verbose_name = 'تایید مرحله'
verbose_name_plural = 'تاییدهای مرحله' verbose_name_plural = 'تاییدهای مرحله'
@ -487,4 +485,4 @@ class StepApproval(models.Model):
def __str__(self): 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 border">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">گزارش نصب</h6> <h6 class="mb-0">گزارش نصب</h6>
<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 %} {% 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> <span class="small text-muted">نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }}</span>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
{% if latest_report %} {% if latest_report %}
<div class="row g-3"> <!-- اطلاعات گزارش نصب -->
<div class="col-12 col-md-6"> <div class="row g-3 mb-3">
<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="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> <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> <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>
<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> <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> <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> <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>
</div> </div>
{% if latest_report.description %} {% if latest_report.description %}
<div class="mt-2"> <div class="mb-3">
<p class="mb-0"><i class="bx bx-text bx-sm me-2"></i><strong>توضیحات:</strong></p> <h6 class="text-primary mb-2"><i class="bx bx-text me-1"></i>توضیحات</h6>
<div class="text-muted">{{ latest_report.description }}</div> <div class="text-muted">{{ latest_report.description }}</div>
</div> </div>
{% endif %} {% endif %}
<hr>
<h6>عکس‌ها</h6> <h6 class="text-primary mb-2"><i class="bx bx-image me-1"></i>عکس‌ها</h6>
<div class="row"> <div class="row">
{% for p in latest_report.photos.all %} {% 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> <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="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="dt-buttons btn-group flex-wrap mb-0">
<div class="btn-group"> <div class="btn-group">
{% if not request.user|is_installer %}
<button class="btn btn-label-success me-2" type="button" onclick="exportToExcel()"> <button class="btn btn-label-success me-2" type="button" onclick="exportToExcel()">
<span class="d-flex align-items-center gap-2"> <span class="d-flex align-items-center gap-2">
<i class="bx bx-export me-sm-1"></i> <i class="bx bx-export me-sm-1"></i>
<span class="d-none d-sm-inline-block">خروجی اکسل</span> <span class="d-none d-sm-inline-block">خروجی اکسل</span>
</span> </span>
</button> </button>
{% endif %}
{% if request.user|is_broker %} {% if request.user|is_broker %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal"> <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
<i class="bx bx-plus me-1"></i> <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 # Collect final invoice, payments, and certificate if any
from invoices.models import Invoice from invoices.models import Invoice
from installations.models import InstallationReport from installations.models import InstallationReport, InstallationAssignment
from certificates.models import CertificateInstance from certificates.models import CertificateInstance
invoice = Invoice.objects.filter(process_instance=instance).first() invoice = Invoice.objects.filter(process_instance=instance).first()
payments = invoice.payments.filter(is_deleted=False).all() if invoice else [] payments = invoice.payments.filter(is_deleted=False).all() if invoice else []
latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first() latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
certificate = CertificateInstance.objects.filter(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 # Build rows like final invoice step
rows = [] rows = []
if invoice: if invoice:
@ -527,6 +536,8 @@ def instance_summary(request, instance_id):
'rows': rows, 'rows': rows,
'latest_report': latest_report, 'latest_report': latest_report,
'certificate': certificate, '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 # Get the approval that completed this step
approval = StepApproval.objects.filter( approval = StepApproval.objects.filter(
step_instance=step_instance, step_instance=step_instance,
decision='approved',
is_deleted=False is_deleted=False
).select_related('approved_by').order_by('-created').first() ).select_related('approved_by').order_by('-created_at').first()
if approval: 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 approval_users_map[step_instance.process_instance_id] = approval.approved_by
# Calculate progress and installation data # Calculate progress and installation data

View file

@ -35,6 +35,7 @@ id="layout-navbar">
<!-- /Language --> <!-- /Language -->
<!-- Quick links --> <!-- Quick links -->
<li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0 d-none"> <li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0 d-none">
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown" <a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
data-bs-auto-close="outside" aria-expanded="false"> data-bs-auto-close="outside" aria-expanded="false">
@ -144,6 +145,11 @@ id="layout-navbar">
</li> </li>
<!-- / Style Switcher--> <!-- / Style Switcher-->
<li class="nav-item align-items-center">
{% if request.user.profile %}
<p class="text-muted badge bg-label-primary m-0">{{ request.user.profile.roles_str }}</p>
{% endif %}
</li>
<!-- Notification --> <!-- Notification -->
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1 d-none"> <li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1 d-none">

View file

@ -83,10 +83,12 @@ class WellForm(forms.ModelForm):
'utm_x': forms.NumberInput(attrs={ 'utm_x': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'X UTM', 'placeholder': 'X UTM',
'required': 'required',
}), }),
'utm_y': forms.NumberInput(attrs={ 'utm_y': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Y UTM', 'placeholder': 'Y UTM',
'required': 'required',
}), }),
'utm_zone': forms.NumberInput(attrs={ 'utm_zone': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',

View file

@ -0,0 +1,35 @@
# Generated by Django 5.2.4 on 2025-10-02 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wells', '0004_remove_historicalwell_discharge_pipe_diameter_and_more'),
]
operations = [
migrations.AlterField(
model_name='historicalwell',
name='utm_x',
field=models.DecimalField(decimal_places=0, default=11, max_digits=10, verbose_name='X UTM'),
preserve_default=False,
),
migrations.AlterField(
model_name='historicalwell',
name='utm_y',
field=models.DecimalField(decimal_places=0, default=2, max_digits=10, verbose_name='Y UTM'),
preserve_default=False,
),
migrations.AlterField(
model_name='well',
name='utm_x',
field=models.DecimalField(decimal_places=0, max_digits=10, verbose_name='X UTM'),
),
migrations.AlterField(
model_name='well',
name='utm_y',
field=models.DecimalField(decimal_places=0, max_digits=10, verbose_name='Y UTM'),
),
]

View file

@ -80,15 +80,11 @@ class Well(SluggedModel):
max_digits=10, max_digits=10,
decimal_places=0, decimal_places=0,
verbose_name="X UTM", verbose_name="X UTM",
null=True,
blank=True
) )
utm_y = models.DecimalField( utm_y = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=0, decimal_places=0,
verbose_name="Y UTM", verbose_name="Y UTM",
null=True,
blank=True
) )
utm_zone = models.PositiveIntegerField( utm_zone = models.PositiveIntegerField(
verbose_name="زون UTM", verbose_name="زون UTM",