main flow checked
This commit is contained in:
parent
b5bf3a5dbe
commit
f853ad9784
21 changed files with 365 additions and 89 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"<span class=\"fw-bold\">{_to_jalali(timezone.now().date())}</span>"),
|
||||
'request_code': mark_safe(f"<span class=\"fw-bold\">{instance.code}</span>"),
|
||||
'company_name': mark_safe(f"<span class=\"fw-bold\">{(template.company.name if template.company else '') or ''}</span>"),
|
||||
'customer_full_name': mark_safe(f"<span class=\"fw-bold\">{rep.get_full_name() if rep else ''}</span>"),
|
||||
'water_subscription_number': mark_safe(f"<span class=\"fw-bold\">{getattr(well, 'water_subscription_number', '') or ''}</span>"),
|
||||
'address': mark_safe(f"<span class=\"fw-bold\">{getattr(well, 'county', '') or ''}</span>"),
|
||||
'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 body placeholders with bold values
|
||||
for k, v in ctx.items():
|
||||
body = body.replace(f"{{{{ {k} }}}}", f"<strong>{str(v)}</strong>")
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
|
|
|||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
|
@ -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-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-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.post_calibration_flow_rate|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.post_calibration_flow_rate|default:'-' }}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
<tr><td colspan="5" class="text-center text-muted">تغییری ثبت نشده است</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>nvoices/instance/69/step/3/payments/
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -320,15 +320,29 @@ def installation_report_step(request, instance_id, step_id):
|
|||
can_approve_reject = False
|
||||
user_can_approve = can_approve_reject
|
||||
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}
|
||||
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,
|
||||
'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,
|
||||
})
|
||||
|
||||
# Determine if current user has already approved/rejected (to disable buttons)
|
||||
current_user_has_decided = False
|
||||
|
|
@ -356,10 +370,11 @@ def installation_report_step(request, instance_id, step_id):
|
|||
|
||||
if action == 'approve':
|
||||
# Record this user's approval for their role
|
||||
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=''
|
||||
)
|
||||
# Only mark report approved when ALL required roles have approved
|
||||
if step_instance.is_fully_approved():
|
||||
|
|
@ -386,12 +401,8 @@ def installation_report_step(request, instance_id, step_id):
|
|||
if not reason:
|
||||
messages.error(request, 'لطفاً علت رد شدن را وارد کنید.')
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
|
||||
StepApproval.objects.update_or_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)
|
||||
# Only create StepRejection for rejections, not StepApproval
|
||||
StepRejection.objects.create(step_instance=step_instance, role=matching_role, rejected_by=request.user, reason=reason)
|
||||
existing_report.approved = False
|
||||
existing_report.save()
|
||||
# If current step moved ahead of this step, reset it back for correction (align with invoices)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,17 @@
|
|||
<!-- Customer & Well Info (compact to match preview) -->
|
||||
<div class="row mb-3">
|
||||
<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>
|
||||
{% 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>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
<div class="alert alert-info">
|
||||
<h6>پیشفاکتور موجود</h6>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
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
|
||||
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}
|
||||
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)
|
||||
try:
|
||||
if instance.current_step and instance.current_step.order > step.order:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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='نقش'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='توضیحات'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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} - تایید شده"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ id="layout-navbar">
|
|||
<!-- /Language -->
|
||||
|
||||
<!-- Quick links -->
|
||||
|
||||
<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"
|
||||
data-bs-auto-close="outside" aria-expanded="false">
|
||||
|
|
@ -144,6 +145,11 @@ id="layout-navbar">
|
|||
</li>
|
||||
<!-- / 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 -->
|
||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1 d-none">
|
||||
|
|
|
|||
|
|
@ -83,10 +83,12 @@ class WellForm(forms.ModelForm):
|
|||
'utm_x': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'X UTM',
|
||||
'required': 'required',
|
||||
}),
|
||||
'utm_y': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Y UTM',
|
||||
'required': 'required',
|
||||
}),
|
||||
'utm_zone': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
|
|
|
|||
35
wells/migrations/0005_alter_historicalwell_utm_x_and_more.py
Normal file
35
wells/migrations/0005_alter_historicalwell_utm_x_and_more.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -80,15 +80,11 @@ class Well(SluggedModel):
|
|||
max_digits=10,
|
||||
decimal_places=0,
|
||||
verbose_name="X UTM",
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
utm_y = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=0,
|
||||
verbose_name="Y UTM",
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
utm_zone = models.PositiveIntegerField(
|
||||
verbose_name="زون UTM",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue