complete first version of main proccess

This commit is contained in:
aminhashemi92 2025-08-27 07:11:26 +03:30
parent 6ff4740d04
commit f2fc2362a7
61 changed files with 3280 additions and 28 deletions

0
certificates/__init__.py Normal file
View file

20
certificates/admin.py Normal file
View 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
View file

@ -0,0 +1,10 @@
from django.apps import AppConfig
class CertificatesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'certificates'
verbose_name = 'گواهی‌ها'

View 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ها',
},
),
]

View file

@ -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='شرکت صادر کننده'),
),
]

View file

38
certificates/models.py Normal file
View 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}"

View 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 %}

View 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
View 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
View 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,
})