This commit is contained in:
aminhashemi92 2025-09-29 17:38:11 +03:30
parent 810c87e2e0
commit b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-09-27 15:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('certificates', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='certificateinstance',
name='hologram_code',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='کد یکتا هولوگرام'),
),
]

View file

@ -28,6 +28,7 @@ class CertificateInstance(BaseModel):
issued_at = models.DateField(auto_now_add=True, verbose_name='تاریخ صدور')
approved = models.BooleanField(default=False, verbose_name='تایید شده')
approved_at = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تایید')
hologram_code = models.CharField(max_length=50, null=True, blank=True, verbose_name='کد یکتا هولوگرام')
class Meta:
verbose_name = 'گواهی'

View file

@ -18,19 +18,20 @@
<link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
<style>
@page { size: A4; margin: 1cm; }
@page { size: A4 landscape; margin: 1cm; }
@media print { body { print-color-adjust: exact; } .no-print { display: none !important; } }
.header { border-bottom: 1px solid #dee2e6; padding-bottom: 16px; margin-bottom: 24px; }
.header { border-bottom: 0px solid #dee2e6; padding-bottom: 10px; margin-bottom: 10px; }
.company-name { font-weight: 600; }
.body-text { white-space: pre-line; line-height: 1.9; }
.signature-section { margin-top: 40px; border-top: 1px solid #dee2e6; padding-top: 24px; }
.signature-section { margin-top: 40px; border-top: 0px solid #dee2e6; padding-top: 24px; }
</style>
</head>
<body>
<div class="container-fluid py-3">
<!-- Top-left request info -->
<div class="d-flex mb-2">
<div class="d-flex">
<div class="ms-auto text-end">
<div class="">کد یکتا هولوگرام: {{ cert.hologram_code|default:'-' }}</div>
<div class="">شماره درخواست: {{ instance.code }}</div>
<div class="">تاریخ: {{ cert.jissued_at }}</div>
</div>
@ -38,10 +39,7 @@
<!-- Header with logo and company -->
<div class="header text-center">
{% if template.company and template.company.logo %}
<img src="{{ template.company.logo.url }}" alt="logo" style="max-height:90px">
{% endif %}
<h4 class="mt-2">{{ cert.rendered_title }}</h4>
<h4 class="">{{ cert.rendered_title }}</h4>
{% if template.company %}
<div class="text-muted company-name">{{ template.company.name }}</div>
{% endif %}
@ -51,17 +49,41 @@
<div class="body-text">
{{ cert.rendered_body|safe }}
</div>
<!-- Signature -->
<div class="signature-section d-flex justify-content-end">
<div class="text-center">
<div>مهر و امضای تایید کننده</div>
<div class="text-muted">{{ template.company.name }}</div>
{% if template.company and template.company.signature %}
<img src="{{ template.company.signature.url }}" alt="seal" style="max-height:200px">
{% endif %}
<h6 class="my-2">مشخصات چاه و کنتور هوشمند</h6>
<div class="row" style="font-size: 14px;">
<div class="col-4">
<div>موقعیت مکانی (UTM): {{ latest_report.utm_x|default:'-' }} , {{ latest_report.utm_y|default:'-' }}</div>
<div>نیرو محرکه چاه: {{ latest_report.driving_force|default:'-' }}</div>
<div>نوع کنتور: {{ latest_report.get_meter_type_display|default:'-' }}</div>
<div>قطر لوله آبده (اینچ): {{ latest_report.discharge_pipe_diameter|default:'-' }}</div>
<div>نوع مصرف: {{ latest_report.get_usage_type_display|default:'-' }}</div>
<div>شماره سیم‌کارت: {{ latest_report.sim_number|default:'-' }}</div>
</div>
<div class="col-4">
<div>سایز کنتور: {{ latest_report.meter_size|default:'-' }}</div>
<div>شماره پروانه بهره‌برداری چاه: {{ latest_report.exploitation_license_number|default:'-' }}</div>
<div>قدرت موتور: {{ latest_report.motor_power|default:'-' }}</div>
<div>دبی قبل از کالیبراسیون: {{ latest_report.pre_calibration_flow_rate|default:'-' }}</div>
<div>دبی بعد از کالیبراسیون: {{ latest_report.post_calibration_flow_rate|default:'-' }}</div>
<div>نام شرکت کنتورساز: {{ latest_report.water_meter_manufacturer.name|default:'-' }}</div>
<div>شماره سریال کنتور: {{ instance.well.water_meter_serial_number|default:'-' }}</div>
</div>
<div class="col-4">
<!-- Signature -->
<div class="signature-section d-flex justify-content-end">
<div class="text-center">
<div>مهر و امضای تایید کننده</div>
<div class="text-muted">{{ template.company.name }}</div>
{% if template.company and template.company.signature %}
<img src="{{ template.company.signature.url }}" alt="seal" style="max-height:200px">
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script>

View file

@ -38,9 +38,9 @@
</small>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary" target="_blank" href="{% url 'certificates:certificate_print' instance.id %}">
<button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#printHologramModal">
<i class="bx bx-printer me-2"></i> پرینت
</a>
</button>
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
@ -61,16 +61,33 @@
<div>تاریخ: {{ cert.jissued_at }}</div>
</div>
</div>
<div class="text-center mb-3">
{% if template.company and template.company.logo %}
<img src="{{ template.company.logo.url }}" alt="logo" style="max-height:80px">
{% endif %}
<div class="text-center">
<h5 class="mt-2">{{ cert.rendered_title }}</h5>
{% if template.company %}<div class="text-muted">{{ template.company.name }}</div>{% endif %}
</div>
<div class="mt-3" style="white-space:pre-line; line-height:1.9;">
<div class="mb-3" style="white-space:pre-line; line-height:1.9;">
{{ cert.rendered_body|safe }}
</div>
<h6 class="mb-2">مشخصات چاه و کنتور هوشمند</h6>
<div class="row g-2 small">
<div class="col-12 col-md-6">
<div class="d-flex gap-2"><span class="text-muted">موقعیت مکانی (UTM):</span><span class="fw-medium">{{ latest_report.utm_x|default:'-' }} , {{ latest_report.utm_y|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">نیرو محرکه چاه:</span><span class="fw-medium">{{ latest_report.driving_force|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">نوع کنتور:</span><span class="fw-medium">{{ latest_report.get_meter_type_display|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">قطر لوله آبده (اینچ):</span><span class="fw-medium">{{ latest_report.discharge_pipe_diameter|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">نوع مصرف:</span><span class="fw-medium">{{ latest_report.get_usage_type_display|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">شماره سیم‌کارت:</span><span class="fw-medium">{{ latest_report.sim_number|default:'-' }}</span></div>
</div>
<div class="col-12 col-md-6">
<div class="d-flex gap-2"><span class="text-muted">سایز کنتور:</span><span class="fw-medium">{{ latest_report.meter_size|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">شماره پروانه بهره‌برداری چاه:</span><span class="fw-medium">{{ latest_report.exploitation_license_number|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">قدرت موتور:</span><span class="fw-medium">{{ latest_report.motor_power|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">دبی قبل از کالیبراسیون:</span><span class="fw-medium">{{ latest_report.pre_calibration_flow_rate|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">دبی بعد از کالیبراسیون:</span><span class="fw-medium">{{ latest_report.post_calibration_flow_rate|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">نام شرکت کنتورساز:</span><span class="fw-medium">{{ latest_report.water_meter_manufacturer.name|default:'-' }}</span></div>
<div class="d-flex gap-2"><span class="text-muted">شماره سریال کنتور:</span><span class="fw-medium">{{ instance.well.water_meter_serial_number|default:'-' }}</span></div>
</div>
</div>
<div class="signature-section d-flex justify-content-end">
<div class="text-center">
<div>مهر و امضای تایید کننده</div>
@ -103,6 +120,29 @@
</div>
</div>
</div>
<!-- Print Hologram Modal -->
<div class="modal fade" id="printHologramModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{% url 'certificates:certificate_print' instance.id %}" target="_blank">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title">کد یکتا هولوگرام</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<label class="form-label">کد هولوگرام</label>
<input type="text" class="form-control" name="hologram_code" value="{{ cert.hologram_code|default:'' }}" placeholder="مثال: 123456" required>
<div class="form-text">این کد باید با کد هولوگرام روی گواهی یکسان باشد.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
<button type="submit" class="btn btn-primary">ثبت و پرینت</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -52,16 +52,20 @@ def certificate_step(request, instance_id, step_id):
# Ensure all previous steps are completed and invoice settled
prior_steps = instance.process.steps.filter(order__lt=instance.current_step.order if instance.current_step else 9999)
incomplete = StepInstance.objects.filter(process_instance=instance, step__in=prior_steps).exclude(status='completed').exists()
previous_step = instance.process.steps.filter(order__lt=instance.current_step.order).last() if instance.current_step else None
prev_si = StepInstance.objects.filter(process_instance=instance, step=previous_step).first() if previous_step else None
if incomplete:
if incomplete and not prev_si.status == 'approved':
messages.error(request, 'ابتدا همه مراحل قبلی را تکمیل کنید')
return redirect('processes:request_list')
inv = Invoice.objects.filter(process_instance=instance).first()
if inv:
inv.calculate_totals()
if inv.remaining_amount != 0:
messages.error(request, 'مانده فاکتور باید صفر باشد')
return redirect('processes:request_list')
if prev_si and not prev_si.status == 'approved':
inv.calculate_totals()
if inv.remaining_amount != 0:
messages.error(request, 'مانده فاکتور باید صفر باشد')
return redirect('processes:request_list')
template = CertificateTemplate.objects.filter(is_active=True).order_by('-created').first()
if not template:
@ -117,6 +121,8 @@ def certificate_step(request, instance_id, step_id):
instance.save()
return redirect('processes:instance_summary', instance_id=instance.id)
# latest installation report for details
latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
return render(request, 'certificates/step.html', {
'instance': instance,
'template': template,
@ -124,6 +130,7 @@ def certificate_step(request, instance_id, step_id):
'previous_step': previous_step,
'next_step': next_step,
'step': step,
'latest_report': latest_report,
})
@ -131,11 +138,32 @@ def certificate_step(request, instance_id, step_id):
def certificate_print(request, instance_id):
instance = get_scoped_instance_or_404(request, instance_id)
cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
if request.method == 'POST':
# Save/update hologram code then print
code = (request.POST.get('hologram_code') or '').strip()
if cert:
if code:
cert.hologram_code = code
cert.save(update_fields=['hologram_code'])
else:
template = CertificateTemplate.objects.filter(is_active=True).order_by('-created').first()
if template:
title, body = _render_template(template, instance)
cert = CertificateInstance.objects.create(process_instance=instance, template=template, rendered_title=title, rendered_body=body, hologram_code=code or None)
# proceed to rendering page after saving code
return render(request, 'certificates/print.html', {
'instance': instance,
'cert': cert,
'template': cert.template if cert else None,
'latest_report': latest_report,
})
template = cert.template if cert else None
return render(request, 'certificates/print.html', {
'instance': instance,
'cert': cert,
'template': template,
'latest_report': latest_report,
})