diff --git a/accounts/forms.py b/accounts/forms.py
index a5d493b..e5f3b90 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -39,7 +39,8 @@ class CustomerForm(forms.ModelForm):
}),
'phone_number_1': forms.TextInput(attrs={
'class': 'form-control',
- 'placeholder': '09123456789'
+ 'placeholder': '09123456789',
+ 'required': True
}),
'phone_number_2': forms.TextInput(attrs={
'class': 'form-control',
diff --git a/accounts/migrations/0008_alter_historicalprofile_phone_number_1_and_more.py b/accounts/migrations/0008_alter_historicalprofile_phone_number_1_and_more.py
new file mode 100644
index 0000000..a005ff8
--- /dev/null
+++ b/accounts/migrations/0008_alter_historicalprofile_phone_number_1_and_more.py
@@ -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,
+ ),
+ ]
diff --git a/accounts/models.py b/accounts/models.py
index 348304e..f6ccf3d 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -78,9 +78,7 @@ class Profile(BaseModel):
)
phone_number_1 = models.CharField(
max_length=11,
- null=True,
verbose_name="شماره تماس ۱",
- blank=True
)
phone_number_2 = models.CharField(
max_length=11,
diff --git a/certificates/views.py b/certificates/views.py
index 9a8f1de..5bdcade 100644
--- a/certificates/views.py
+++ b/certificates/views.py
@@ -4,6 +4,8 @@ from django.contrib import messages
from django.http import JsonResponse
from django.urls import reverse
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 invoices.models import Invoice
@@ -28,20 +30,33 @@ def _render_template(template: CertificateTemplate, instance: ProcessInstance):
well = instance.well
rep = instance.representative
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 = {
- 'today_jalali': _to_jalali(timezone.now().date()),
- 'request_code': instance.code,
- 'company_name': (template.company.name if template.company else '') or '',
- 'customer_full_name': rep.get_full_name() if rep else '',
- 'water_subscription_number': getattr(well, 'water_subscription_number', '') or '',
- 'address': getattr(well, 'county', '') or '',
- 'visit_date_jalali': _to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else '',
+ 'today_jalali': mark_safe(f"{_to_jalali(timezone.now().date())}"),
+ 'request_code': mark_safe(f"{instance.code}"),
+ 'company_name': mark_safe(f"{(template.company.name if template.company else '') or ''}"),
+ 'customer_full_name': mark_safe(f"{rep.get_full_name() if rep else ''}"),
+ 'water_subscription_number': mark_safe(f"{getattr(well, 'water_subscription_number', '') or ''}"),
+ 'address': mark_safe(f"{getattr(well, 'county', '') or ''}"),
+ 'visit_date_jalali': mark_safe(f"{_to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else ''}"),
+ 'city': mark_safe(f"{city or ''}"),
+ 'county': mark_safe(f"{county or ''}"),
+ 'customer_company_name': mark_safe(f"{customer_company_name or ''}"),
+ 'individual': individual,
}
- title = (template.title or '').format(**ctx)
- body = (template.body or '')
- # Render body placeholders with bold values
- for k, v in ctx.items():
- body = body.replace(f"{{{{ {k} }}}}", f"{str(v)}")
+
+ # Render title using Django template engine
+ title_template = Template(template.title or '')
+ title = title_template.render(Context(ctx))
+
+ # Render body using Django template engine
+ body_template = Template(template.body or '')
+ body = body_template.render(Context(ctx))
+
return title, body
diff --git a/db.sqlite3 b/db.sqlite3
index 194dde7..d2eb7a1 100644
Binary files a/db.sqlite3 and b/db.sqlite3 differ
diff --git a/installations/templates/installations/installation_report_step.html b/installations/templates/installations/installation_report_step.html
index df12a29..9ff6c20 100644
--- a/installations/templates/installations/installation_report_step.html
+++ b/installations/templates/installations/installation_report_step.html
@@ -99,9 +99,9 @@
-
اطلاعات مشترک
+
+ {% if instance.representative.profile.user_type == 'legal' %}
+ اطلاعات مشترک (حقوقی)
+ {% else %}
+ اطلاعات مشترک (حقیقی)
+ {% endif %}
+
+ {% if instance.representative.profile.user_type == 'legal' %}
+
نام شرکت: {{ instance.representative.profile.company_name|default:"-" }}
+
شناسه ملی: {{ instance.representative.profile.company_national_id|default:"-" }}
+ {% endif %}
نام: {{ quote.customer.get_full_name }}
{% if instance.representative.profile and instance.representative.profile.national_code %}
کد ملی: {{ instance.representative.profile.national_code }}
diff --git a/invoices/templates/invoices/quote_step.html b/invoices/templates/invoices/quote_step.html
index 404cf14..853f250 100644
--- a/invoices/templates/invoices/quote_step.html
+++ b/invoices/templates/invoices/quote_step.html
@@ -57,7 +57,7 @@
پیشفاکتور موجود
{{ existing_quote.name }} |
- مبلغ کل: {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان |
+ مبلغ کل (با احتساب مالیات): {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان |
وضعیت: {{ existing_quote.get_status_display_with_color|safe }}
diff --git a/invoices/views.py b/invoices/views.py
index 6e467b9..0df8191 100644
--- a/invoices/views.py
+++ b/invoices/views.py
@@ -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 = 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))
+ 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}
+ 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,
- 'status': (appr.decision if appr else None),
- 'reason': (appr.reason if appr else ''),
+ 'status': status,
+ 'reason': reason,
})
# 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')
if action == 'approve':
- StepApproval.objects.update_or_create(
+ StepApproval.objects.create(
step_instance=step_instance,
role=matching_role,
- defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
+ approved_by=request.user,
+ reason=''
)
if step_instance.is_fully_approved():
step_instance.status = 'completed'
@@ -422,12 +437,12 @@ def quote_payment_step(request, instance_id, step_id):
if not reason:
messages.error(request, 'علت رد شدن را وارد کنید')
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,
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)
+ rejected_by=request.user,
+ reason=reason
+ )
# If current step is ahead of this step, reset it back to this step
try:
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)
reqs = list(step.approver_requirements.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}
- 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,
- 'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
- 'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
- }
- for r in reqs
- ]
+ 'status': status,
+ 'reason': reason,
+ })
# dynamic permission to control approve/reject UI
try:
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:
messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})")
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,
role=matching_role,
- defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
+ approved_by=request.user,
+ reason=''
)
if step_instance.is_fully_approved():
step_instance.status = 'completed'
@@ -993,12 +1023,12 @@ def final_settlement_step(request, instance_id, step_id):
if not reason:
messages.error(request, 'علت رد شدن را وارد کنید')
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,
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)
+ 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)
try:
if instance.current_step and instance.current_step.order > step.order:
diff --git a/processes/admin.py b/processes/admin.py
index 4739742..42d4bca 100644
--- a/processes/admin.py
+++ b/processes/admin.py
@@ -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")
diff --git a/processes/migrations/0006_alter_stepapproval_unique_together_and_more.py b/processes/migrations/0006_alter_stepapproval_unique_together_and_more.py
new file mode 100644
index 0000000..f4cb896
--- /dev/null
+++ b/processes/migrations/0006_alter_stepapproval_unique_together_and_more.py
@@ -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='نقش'),
+ ),
+ ]
diff --git a/processes/migrations/0007_remove_stepapproval_decision_and_more.py b/processes/migrations/0007_remove_stepapproval_decision_and_more.py
new file mode 100644
index 0000000..d97d16b
--- /dev/null
+++ b/processes/migrations/0007_remove_stepapproval_decision_and_more.py
@@ -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='توضیحات'),
+ ),
+ ]
diff --git a/processes/models.py b/processes/models.py
index 391fd26..19b612d 100644
--- a/processes/models.py
+++ b/processes/models.py
@@ -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} - تایید شده"
diff --git a/processes/templates/processes/instance_summary.html b/processes/templates/processes/instance_summary.html
index ad84b59..a8529c6 100644
--- a/processes/templates/processes/instance_summary.html
+++ b/processes/templates/processes/instance_summary.html
@@ -95,32 +95,113 @@
{% if latest_report %}
-
-
-
تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}
+
+
+
+
تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}
+
+ {% if installation_assignment.scheduled_date %}
+
+
تاریخ برنامهریزی: {{ installation_assignment.scheduled_date|to_jalali }}
+
+ {% endif %}
+
سریال کنتور جدید: {{ latest_report.new_water_meter_serial|default:'-' }}
+
+
شماره پلمپ: {{ latest_report.seal_number|default:'-' }}
-
+
کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}
+
+ {% if latest_report.sim_number %}
+
+
شماره سیمکارت: {{ latest_report.sim_number }}
+
+ {% endif %}
+ {% if latest_report.meter_type %}
+
+
نوع کنتور: {{ latest_report.get_meter_type_display }}
+
+ {% endif %}
+ {% if latest_report.meter_size %}
+
+
سایز کنتور: {{ latest_report.meter_size }}
+
+ {% endif %}
+ {% if latest_report.water_meter_manufacturer %}
+
+
سازنده: {{ latest_report.water_meter_manufacturer.name }}
+
+ {% endif %}
+ {% if latest_report.discharge_pipe_diameter %}
+
+
قطر لوله آبده: {{ latest_report.discharge_pipe_diameter }} اینچ
+
+ {% endif %}
+ {% if latest_report.usage_type %}
+
+
نوع مصرف: {{ latest_report.get_usage_type_display }}
+
+ {% endif %}
+ {% if latest_report.driving_force %}
+
+
نیرو محرکه: {{ latest_report.driving_force }}
+
+ {% endif %}
+ {% if latest_report.motor_power %}
+
+
قدرت موتور: {{ latest_report.motor_power }} کیلووات ساعت
+
+ {% endif %}
+ {% if latest_report.exploitation_license_number %}
+
+
شماره پروانه: {{ latest_report.exploitation_license_number }}
+
+ {% endif %}
+ {% if latest_report.pre_calibration_flow_rate %}
+
+
دبی قبل از کالیبراسیون: {{ latest_report.pre_calibration_flow_rate }} لیتر/ثانیه
+
+ {% endif %}
+ {% if latest_report.post_calibration_flow_rate %}
+
+
دبی بعد از کالیبراسیون: {{ latest_report.post_calibration_flow_rate }} لیتر/ثانیه
+
+ {% endif %}
+
UTM X: {{ latest_report.utm_x|default:'-' }}
+
+
UTM Y: {{ latest_report.utm_y|default:'-' }}
+
{% if latest_report.description %}
-
-
توضیحات:
+
+
توضیحات
{{ latest_report.description }}
{% endif %}
-
-
عکسها
+
+
عکسها
{% for p in latest_report.photos.all %}
diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html
index 5640425..9ec7e92 100644
--- a/processes/templates/processes/request_list.html
+++ b/processes/templates/processes/request_list.html
@@ -37,12 +37,14 @@