complete first version of main proccess
This commit is contained in:
parent
6ff4740d04
commit
f2fc2362a7
61 changed files with 3280 additions and 28 deletions
0
certificates/__init__.py
Normal file
0
certificates/__init__.py
Normal file
20
certificates/admin.py
Normal file
20
certificates/admin.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.contrib import admin
|
||||
from .models import CertificateTemplate, CertificateInstance
|
||||
|
||||
|
||||
@admin.register(CertificateTemplate)
|
||||
class CertificateTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'company', 'is_active', 'created')
|
||||
list_filter = ('is_active', 'company')
|
||||
search_fields = ('title', 'company__name')
|
||||
autocomplete_fields = ('company',)
|
||||
|
||||
|
||||
@admin.register(CertificateInstance)
|
||||
class CertificateInstanceAdmin(admin.ModelAdmin):
|
||||
list_display = ('process_instance', 'rendered_title', 'issued_at', 'approved')
|
||||
list_filter = ('approved', 'issued_at')
|
||||
search_fields = ('process_instance__code', 'rendered_title')
|
||||
autocomplete_fields = ('process_instance', 'template')
|
||||
|
||||
|
10
certificates/apps.py
Normal file
10
certificates/apps.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CertificatesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'certificates'
|
||||
verbose_name = 'گواهیها'
|
||||
|
||||
|
||||
|
58
certificates/migrations/0001_initial.py
Normal file
58
certificates/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-22 09:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('processes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CertificateTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('title', models.CharField(max_length=200, verbose_name='عنوان')),
|
||||
('body', models.TextField(verbose_name='متن قالب (با جایگزین\u200cها)')),
|
||||
('company_logo', models.ImageField(blank=True, null=True, upload_to='certificates/logos/%Y/%m/%d/', verbose_name='لوگو')),
|
||||
('company_name', models.CharField(blank=True, max_length=200, verbose_name='نام شرکت')),
|
||||
('company_seal_signature', models.ImageField(blank=True, null=True, upload_to='certificates/seals/%Y/%m/%d/', verbose_name='مهر و امضا')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'قالب گواهی',
|
||||
'verbose_name_plural': 'قالب\u200cهای گواهی',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CertificateInstance',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('rendered_title', models.CharField(max_length=250, verbose_name='عنوان رندر شده')),
|
||||
('rendered_body', models.TextField(verbose_name='متن رندر شده')),
|
||||
('issued_at', models.DateField(auto_now_add=True, verbose_name='تاریخ صدور')),
|
||||
('approved', models.BooleanField(default=False, verbose_name='تایید شده')),
|
||||
('approved_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تایید')),
|
||||
('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='certificates', to='processes.processinstance', verbose_name='نمونه فرآیند')),
|
||||
('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='certificates.certificatetemplate', verbose_name='قالب')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'گواهی',
|
||||
'verbose_name_plural': 'گواهی\u200cها',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-22 10:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_historicalprofile_bank_name_profile_bank_name'),
|
||||
('certificates', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='certificatetemplate',
|
||||
name='company_logo',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='certificatetemplate',
|
||||
name='company_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='certificatetemplate',
|
||||
name='company_seal_signature',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='certificatetemplate',
|
||||
name='company',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت صادر کننده'),
|
||||
),
|
||||
]
|
0
certificates/migrations/__init__.py
Normal file
0
certificates/migrations/__init__.py
Normal file
38
certificates/models.py
Normal file
38
certificates/models.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from common.models import BaseModel
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CertificateTemplate(BaseModel):
|
||||
title = models.CharField(max_length=200, verbose_name='عنوان')
|
||||
body = models.TextField(verbose_name='متن قالب (با جایگزینها)')
|
||||
company = models.ForeignKey('accounts.Company', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='شرکت صادر کننده')
|
||||
is_active = models.BooleanField(default=True, verbose_name='فعال')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'قالب گواهی'
|
||||
verbose_name_plural = 'قالبهای گواهی'
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class CertificateInstance(BaseModel):
|
||||
process_instance = models.ForeignKey('processes.ProcessInstance', on_delete=models.CASCADE, related_name='certificates', verbose_name='نمونه فرآیند')
|
||||
template = models.ForeignKey(CertificateTemplate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='قالب')
|
||||
rendered_title = models.CharField(max_length=250, verbose_name='عنوان رندر شده')
|
||||
rendered_body = models.TextField(verbose_name='متن رندر شده')
|
||||
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='تاریخ تایید')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'گواهی'
|
||||
verbose_name_plural = 'گواهیها'
|
||||
|
||||
def __str__(self):
|
||||
return f"گواهی {self.process_instance.code}"
|
||||
|
||||
|
28
certificates/templates/certificates/print.html
Normal file
28
certificates/templates/certificates/print.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends '_base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="text-center mb-4">
|
||||
{% 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>
|
||||
{% if template.company %}<div class="text-muted">{{ template.company.name }}</div>{% endif %}
|
||||
</div>
|
||||
<div style="white-space:pre-line; line-height:1.9;">
|
||||
{{ cert.rendered_body|safe }}
|
||||
</div>
|
||||
<div class="mt-5 d-flex justify-content-between">
|
||||
<div>تاریخ: {{ cert.issued_at }}</div>
|
||||
<div class="text-center">
|
||||
{% if template.company and template.company.signature %}
|
||||
<img src="{{ template.company.signature.url }}" alt="seal" style="max-height:120px">
|
||||
{% endif %}
|
||||
<div>مهر و امضای شرکت</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.print()</script>
|
||||
{% endblock %}
|
||||
|
||||
|
53
certificates/templates/certificates/step.html
Normal file
53
certificates/templates/certificates/step.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1">گواهی نهایی</h4>
|
||||
<small class="text-muted d-block">کد درخواست: {{ instance.code }}</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" target="_blank" href="{% url 'certificates:certificate_print' instance.id %}"><i class="bx bx-printer"></i> پرینت</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<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 %}
|
||||
<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;">
|
||||
{{ cert.rendered_body|safe }}
|
||||
</div>
|
||||
<div class="mt-4 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<div>تاریخ صدور: {{ cert.issued_at }}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
{% if template.company and template.company.signature %}
|
||||
<img src="{{ template.company.signature.url }}" alt="seal" style="max-height:100px">
|
||||
{% endif %}
|
||||
<div>مهر و امضای شرکت</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
{% if previous_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
|
||||
{% else %}<span></span>{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-primary" type="submit">تایید و پایان</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
11
certificates/urls.py
Normal file
11
certificates/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'certificates'
|
||||
|
||||
urlpatterns = [
|
||||
path('instance/<int:instance_id>/step/<int:step_id>/', views.certificate_step, name='certificate_step'),
|
||||
path('instance/<int:instance_id>/print/', views.certificate_print, name='certificate_print'),
|
||||
]
|
||||
|
||||
|
114
certificates/views.py
Normal file
114
certificates/views.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from processes.models import ProcessInstance, StepInstance
|
||||
from invoices.models import Invoice
|
||||
from installations.models import InstallationReport
|
||||
from .models import CertificateTemplate, CertificateInstance
|
||||
|
||||
from _helpers.jalali import Gregorian
|
||||
|
||||
|
||||
def _to_jalali(date_obj):
|
||||
try:
|
||||
g = Gregorian(date_obj)
|
||||
y, m, d = g.persian_tuple()
|
||||
return f"{y}/{m:02d}/{d:02d}"
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
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()
|
||||
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, 'address', '') or '',
|
||||
'visit_date_jalali': _to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else '',
|
||||
}
|
||||
title = (template.title or '').format(**ctx)
|
||||
body = (template.body or '')
|
||||
for k, v in ctx.items():
|
||||
body = body.replace(f"{{{{ {k} }}}}", str(v))
|
||||
return title, body
|
||||
|
||||
|
||||
@login_required
|
||||
def certificate_step(request, instance_id, step_id):
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_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()
|
||||
if incomplete:
|
||||
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')
|
||||
|
||||
template = CertificateTemplate.objects.filter(is_active=True).order_by('-created').first()
|
||||
if not template:
|
||||
return render(request, 'certificates/missing.html', {})
|
||||
|
||||
title, body = _render_template(template, instance)
|
||||
cert, _ = CertificateInstance.objects.get_or_create(
|
||||
process_instance=instance,
|
||||
defaults={'template': template, 'rendered_title': title, 'rendered_body': body}
|
||||
)
|
||||
# keep rendered up-to-date
|
||||
cert.template = template
|
||||
cert.rendered_title = title
|
||||
cert.rendered_body = body
|
||||
cert.save()
|
||||
|
||||
previous_step = instance.process.steps.filter(order__lt=instance.current_step.order).last() if instance.current_step else None
|
||||
next_step = instance.process.steps.filter(order__gt=instance.current_step.order).first() if instance.current_step else None
|
||||
|
||||
if request.method == 'POST':
|
||||
cert.approved = True
|
||||
cert.approved_at = timezone.now()
|
||||
cert.save()
|
||||
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step_id=step_id)
|
||||
step_instance.status = 'completed'
|
||||
step_instance.completed_at = timezone.now()
|
||||
step_instance.save()
|
||||
if next_step:
|
||||
instance.current_step = next_step
|
||||
instance.save()
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
|
||||
return redirect('processes:request_list')
|
||||
|
||||
return render(request, 'certificates/step.html', {
|
||||
'instance': instance,
|
||||
'template': template,
|
||||
'cert': cert,
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def certificate_print(request, instance_id):
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||
cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
|
||||
template = cert.template if cert else None
|
||||
return render(request, 'certificates/print.html', {
|
||||
'instance': instance,
|
||||
'cert': cert,
|
||||
'template': template,
|
||||
})
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue