Add confirmation and summary
This commit is contained in:
parent
9b3973805e
commit
35799b7754
25 changed files with 1419 additions and 265 deletions
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepRevision
|
||||
from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepRevision, StepApproverRequirement, StepApproval
|
||||
|
||||
@admin.register(Process)
|
||||
class ProcessAdmin(SimpleHistoryAdmin):
|
||||
|
@ -179,3 +179,17 @@ class StepRevisionAdmin(SimpleHistoryAdmin):
|
|||
def changes_short(self, obj):
|
||||
return obj.changes_description[:50] + "..." if len(obj.changes_description) > 50 else obj.changes_description
|
||||
changes_short.short_description = "تغییرات"
|
||||
|
||||
|
||||
@admin.register(StepApproverRequirement)
|
||||
class StepApproverRequirementAdmin(admin.ModelAdmin):
|
||||
list_display = ("step", "role", "required_count")
|
||||
list_filter = ("step__process", "role")
|
||||
search_fields = ("step__name", "role__name")
|
||||
|
||||
|
||||
@admin.register(StepApproval)
|
||||
class StepApprovalAdmin(admin.ModelAdmin):
|
||||
list_display = ("step_instance", "role", "decision", "approved_by", "created_at")
|
||||
list_filter = ("decision", "role", "step_instance__step__process")
|
||||
search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username")
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Generated by Django 5.2.4 on 2025-09-01 10:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_historicalprofile_bank_name_profile_bank_name'),
|
||||
('processes', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StepApproval',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('decision', models.CharField(choices=[('approved', 'تایید'), ('rejected', 'رد')], max_length=8, verbose_name='نتیجه')),
|
||||
('reason', models.TextField(blank=True, verbose_name='علت (برای رد)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')),
|
||||
('approved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='تاییدکننده')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش')),
|
||||
('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approvals', to='processes.stepinstance', verbose_name='نمونه مرحله')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'تایید مرحله',
|
||||
'verbose_name_plural': 'تاییدهای مرحله',
|
||||
'unique_together': {('step_instance', 'role')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StepApproverRequirement',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('required_count', models.PositiveIntegerField(default=1, verbose_name='تعداد موردنیاز')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش تاییدکننده')),
|
||||
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approver_requirements', to='processes.processstep', verbose_name='مرحله')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'نیازمندی تایید نقش',
|
||||
'verbose_name_plural': 'نیازمندی\u200cهای تایید نقش',
|
||||
'unique_together': {('step', 'role')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -4,6 +4,8 @@ from common.models import NameSlugModel, SluggedModel
|
|||
from simple_history.models import HistoricalRecords
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from accounts.models import Role
|
||||
from _helpers.utils import generate_unique_slug
|
||||
import random
|
||||
|
||||
|
@ -46,6 +48,9 @@ class ProcessStep(NameSlugModel):
|
|||
)
|
||||
history = HistoricalRecords()
|
||||
|
||||
# Note: approver requirements are defined via StepApproverRequirement through model
|
||||
# See StepApproverRequirement below
|
||||
|
||||
class Meta:
|
||||
verbose_name = "مرحله فرآیند"
|
||||
verbose_name_plural = "مراحل فرآیند"
|
||||
|
@ -353,6 +358,26 @@ class StepInstance(models.Model):
|
|||
"""دریافت آخرین رد شدن"""
|
||||
return self.rejections.order_by('-created_at').first()
|
||||
|
||||
# -------- Multi-role approval helpers --------
|
||||
def required_roles(self):
|
||||
return [req.role for req in self.step.approver_requirements.select_related('role').all()]
|
||||
|
||||
def approvals_by_role(self):
|
||||
decisions = {}
|
||||
for a in self.approvals.select_related('role').order_by('created_at'):
|
||||
decisions[a.role_id] = a.decision
|
||||
return decisions
|
||||
|
||||
def is_fully_approved(self) -> bool:
|
||||
req_roles = self.required_roles()
|
||||
if not req_roles:
|
||||
return True
|
||||
role_to_decision = self.approvals_by_role()
|
||||
for r in req_roles:
|
||||
if role_to_decision.get(r.id) != 'approved':
|
||||
return False
|
||||
return True
|
||||
|
||||
class StepRejection(models.Model):
|
||||
"""مدل رد شدن مرحله"""
|
||||
step_instance = models.ForeignKey(
|
||||
|
@ -424,3 +449,36 @@ class StepRevision(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return f"بازبینی {self.step_instance} توسط {self.revised_by.get_full_name()}"
|
||||
|
||||
|
||||
class StepApproverRequirement(models.Model):
|
||||
"""Required approver roles for a step."""
|
||||
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله")
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش تاییدکننده")
|
||||
required_count = models.PositiveIntegerField(default=1, verbose_name="تعداد موردنیاز")
|
||||
|
||||
class Meta:
|
||||
unique_together = ('step', 'role')
|
||||
verbose_name = "نیازمندی تایید نقش"
|
||||
verbose_name_plural = "نیازمندیهای تایید نقش"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.step} ← {self.role} (x{self.required_count})"
|
||||
|
||||
|
||||
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="نقش")
|
||||
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='علت (برای رد)')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')
|
||||
|
||||
class Meta:
|
||||
unique_together = ('step_instance', 'role')
|
||||
verbose_name = 'تایید مرحله'
|
||||
verbose_name_plural = 'تاییدهای مرحله'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.step_instance} - {self.role} - {self.decision}"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<div class="step
|
||||
{% if not can_access %}disabled
|
||||
{% elif status == 'completed' %}completed
|
||||
{% elif status == 'rejected' %}rejected
|
||||
{% elif is_todo %}active
|
||||
{% endif %}
|
||||
{% if is_selected %} selected{% endif %}"
|
||||
|
@ -19,9 +20,9 @@
|
|||
<span class="step-trigger">
|
||||
{% endif %}
|
||||
|
||||
<span class="bs-stepper-circle">{{ forloop.counter }}</span>
|
||||
<span class="bs-stepper-circle {% if status == 'rejected' %}bg-danger text-white{% endif %}">{{ forloop.counter }}</span>
|
||||
<span class="bs-stepper-label mt-1">
|
||||
<span class="bs-stepper-title">{{ step.name }}</span>
|
||||
<span class="bs-stepper-title {% if status == 'rejected' %}text-danger{% endif %}">{{ step.name }}</span>
|
||||
<span class="bs-stepper-subtitle">{{ step.description|default:' ' }}</span>
|
||||
</span>
|
||||
|
||||
|
|
168
processes/templates/processes/instance_summary.html
Normal file
168
processes/templates/processes/instance_summary.html
Normal file
|
@ -0,0 +1,168 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'sidebars/admin.html' %}
|
||||
{% endblock sidebar %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include 'navbars/admin.html' %}
|
||||
{% endblock navbar %}
|
||||
|
||||
{% block title %}گزارش نهایی - درخواست {{ instance.code }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_toasts.html' %}
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1">گزارش نهایی درخواست {{ instance.code }}</h4>
|
||||
<small class="text-muted d-block">
|
||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if invoice %}
|
||||
<a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت فاکتور</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'certificates:certificate_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت گواهی</a>
|
||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="card border">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">فاکتور نهایی</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if invoice %}
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مبلغ نهایی</div><div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">پرداختیها</div><div class="h5 mt-1 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مانده</div><div class="h5 mt-1 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>آیتم</th>
|
||||
<th class="text-center">تعداد</th>
|
||||
<th class="text-end">قیمت واحد</th>
|
||||
<th class="text-end">قیمت کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for it in rows %}
|
||||
<tr>
|
||||
<td>{{ it.item.name }}</td>
|
||||
<td class="text-center">{{ it.quantity }}</td>
|
||||
<td class="text-end">{{ it.unit_price|floatformat:0|intcomma:False }}</td>
|
||||
<td class="text-end">{{ it.total_price|floatformat:0|intcomma:False }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center text-muted">اطلاعاتی ندارد</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">فاکتور نهایی ثبت نشده است.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">گزارش نصب</h6>
|
||||
{% 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 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>
|
||||
<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>
|
||||
<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">
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ latest_report.utm_x|default:'-' }}</p>
|
||||
<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="text-muted">{{ latest_report.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<h6>عکسها</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>
|
||||
{% empty %}
|
||||
<div class="text-muted">بدون عکس</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">گزارش نصب ثبت نشده است.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">تراکنشها</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>نوع</th>
|
||||
<th>مبلغ</th>
|
||||
<th>تاریخ</th>
|
||||
<th>روش</th>
|
||||
<th>شماره مرجع/چک</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in payments %}
|
||||
<tr>
|
||||
<td>{% if p.direction == 'in' %}<span class="badge bg-success">دریافتی{% else %}<span class="badge bg-warning text-dark">پرداختی{% endif %}</span></td>
|
||||
<td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
|
||||
<td>{{ p.payment_date|date:'Y/m/d' }}</td>
|
||||
<td>{{ p.get_payment_method_display }}</td>
|
||||
<td>{{ p.reference_number|default:'-' }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5" class="text-center text-muted">بدون تراکنش</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -72,9 +72,15 @@
|
|||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end m-0">
|
||||
<li>
|
||||
<a href="{% url 'processes:instance_steps' inst.id %}" class="dropdown-item">
|
||||
<i class="bx bx-show me-1"></i>مشاهده جزئیات
|
||||
</a>
|
||||
{% if inst.status == 'completed' %}
|
||||
<a href="{% url 'processes:instance_summary' inst.id %}" class="dropdown-item">
|
||||
<i class="bx bx-show me-1"></i>مشاهده گزارش
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'processes:instance_steps' inst.id %}" class="dropdown-item">
|
||||
<i class="bx bx-show me-1"></i>مشاهده جزئیات
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<div class="dropdown-divider"></div>
|
||||
<li>
|
||||
|
|
|
@ -26,8 +26,10 @@ def stepper_header(instance, current_step=None):
|
|||
step_instance = next((si for si in step_instances if si.step_id == step.id), None)
|
||||
status = step_id_to_status.get(step.id, 'pending')
|
||||
|
||||
# بررسی دسترسی به مرحله
|
||||
can_access = instance.can_access_step(step)
|
||||
# بررسی دسترسی به مرحله (UI navigation constraints):
|
||||
# can_access = instance.can_access_step(step)
|
||||
# فقط مراحل تکمیلشده یا مرحله جاری قابل کلیک هستند
|
||||
can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id)
|
||||
# مرحله انتخابشده (نمایش فعلی)
|
||||
is_selected = bool(current_step and step.id == current_step.id)
|
||||
# مرحلهای که باید انجام شود (مرحله جاری در instance)
|
||||
|
|
|
@ -14,6 +14,7 @@ urlpatterns = [
|
|||
# New step-based architecture
|
||||
path('instance/<int:instance_id>/steps/', views.instance_steps, name='instance_steps'),
|
||||
path('instance/<int:instance_id>/step/<int:step_id>/', views.step_detail, name='step_detail'),
|
||||
path('instance/<int:instance_id>/summary/', views.instance_summary, name='instance_summary'),
|
||||
|
||||
# Legacy process views
|
||||
path('', views.process_list, name='process_list'),
|
||||
|
|
|
@ -357,7 +357,18 @@ def step_detail(request, instance_id, step_id):
|
|||
id=instance_id
|
||||
)
|
||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||
# If the request is already completed, redirect to read-only summary page
|
||||
if instance.status == 'completed':
|
||||
return redirect('processes:instance_summary', instance_id=instance.id)
|
||||
|
||||
# جلوگیری از پرش به مراحل آینده: فقط اجازه نمایش مرحله جاری یا مراحل تکمیلشده
|
||||
try:
|
||||
if instance.current_step and step.order > instance.current_step.order:
|
||||
messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.')
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# بررسی دسترسی به مرحله
|
||||
if not instance.can_access_step(step):
|
||||
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
||||
|
@ -414,8 +425,43 @@ def instance_steps(request, instance_id):
|
|||
messages.error(request, 'هیچ مرحلهای برای این فرآیند تعریف نشده است.')
|
||||
return redirect('processes:request_list')
|
||||
|
||||
# If completed, go to summary instead of steps
|
||||
if instance.status == 'completed':
|
||||
return redirect('processes:instance_summary', instance_id=instance.id)
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
|
||||
|
||||
@login_required
|
||||
def instance_summary(request, instance_id):
|
||||
"""نمای خلاصهٔ فقطخواندنی برای درخواستهای تکمیلشده."""
|
||||
instance = get_object_or_404(ProcessInstance.objects.select_related('well', 'representative'), id=instance_id)
|
||||
# Only show for completed requests; otherwise route to steps
|
||||
if instance.status != 'completed':
|
||||
return redirect('processes:instance_steps', instance_id=instance.id)
|
||||
|
||||
# Collect final invoice, payments, and certificate if any
|
||||
from invoices.models import Invoice
|
||||
from installations.models import InstallationReport
|
||||
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()
|
||||
|
||||
# Build rows like final invoice step
|
||||
rows = []
|
||||
if invoice:
|
||||
items_qs = invoice.items.select_related('item').filter(is_deleted=False).all()
|
||||
rows = list(items_qs)
|
||||
|
||||
return render(request, 'processes/instance_summary.html', {
|
||||
'instance': instance,
|
||||
'invoice': invoice,
|
||||
'payments': payments,
|
||||
'rows': rows,
|
||||
'latest_report': latest_report,
|
||||
'certificate': certificate,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def my_processes(request):
|
||||
"""نمایش فرآیندهای کاربر"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue