Merge branch 'main' of ssh://git.poraab.com/aminhashemi92/shafafiyat into production

This commit is contained in:
Haydo 2025-10-10 11:58:34 +03:30
commit d3857d37c5
34 changed files with 952 additions and 405 deletions

View file

@ -179,15 +179,15 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JAZZMIN_SETTINGS = {
# title of the window (Will default to current_admin_site.site_title if absent or None)
"site_title": "سامانه شفافیت",
"site_title": "کنتور پلاس",
# Title on the login screen (19 chars max) (defaults to current_admin_site.site_header if absent or None)
"site_header": "سامانه شفافیت",
"site_header": "کنتور پلاس",
# Title on the brand (19 chars max) (defaults to current_admin_site.site_header if absent or None)
"site_brand": "سامانه شفافیت",
"site_brand": "کنتور پلاس",
# Welcome text on the login screen
"welcome_sign": "به سامانه شفافیت خوش آمدید",
"welcome_sign": "به کنتور پلاس خوش آمدید",
# Copyright on the footer
"copyright": "سامانه شفافیت",
"copyright": "کنتور پلاس",
# Logo to use for your site, must be present in static files, used for brand on top left
# "site_logo": "../static/dist/img/iconlogo.png",
# Relative paths to custom CSS/JS scripts (must be present in static files)

View file

@ -192,4 +192,122 @@ def normalize_size(size: int) -> str:
return f"{int(size_mb)} MB" if size_mb.is_integer() else f"{size_mb:.1f} MB"
else:
size_gb = size / (1024 * 1024 * 1024)
return f"{int(size_gb)} GB" if size_gb.is_integer() else f"{size_gb:.1f} GB"
return f"{int(size_gb)} GB" if size_gb.is_integer() else f"{size_gb:.1f} GB"
def number_to_persian_words(number):
"""
تبدیل عدد به حروف فارسی
مثال: 12345 -> دوازده هزار و سیصد و چهل و پنج
"""
try:
# تبدیل به عدد صحیح (در صورت نیاز)
from decimal import Decimal
if isinstance(number, Decimal):
number = int(number)
elif isinstance(number, float):
number = int(number)
elif isinstance(number, str):
number = int(float(number.replace(',', '')))
if number == 0:
return "صفر"
if number < 0:
return "منفی " + number_to_persian_words(abs(number))
# اعداد یک رقمی
ones = [
"", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه"
]
# اعداد ده تا نوزده
teens = [
"ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده",
"شانزده", "هفده", "هجده", "نوزده"
]
# اعداد بیست تا نود
tens = [
"", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود"
]
# اعداد صد تا نهصد
hundreds = [
"", "یکصد", "دویست", "سیصد", "چهارصد", "پانصد",
"ششصد", "هفتصد", "هشتصد", "نهصد"
]
# مراتب بزرگتر
scale = [
"", "هزار", "میلیون", "میلیارد", "بیلیون", "بیلیارد"
]
def convert_group(num):
"""تبدیل گروه سه رقمی به حروف"""
if num == 0:
return ""
result = []
# صدها
h = num // 100
if h > 0:
result.append(hundreds[h])
# دهگان و یکان
remainder = num % 100
if remainder >= 10 and remainder < 20:
# اعداد 10 تا 19
result.append(teens[remainder - 10])
else:
# دهگان
t = remainder // 10
if t > 0:
result.append(tens[t])
# یکان
o = remainder % 10
if o > 0:
result.append(ones[o])
return " و ".join(result)
# تقسیم عدد به گروه‌های سه رقمی
groups = []
scale_index = 0
while number > 0:
group = number % 1000
if group != 0:
group_text = convert_group(group)
if scale_index > 0:
group_text += " " + scale[scale_index]
groups.append(group_text)
number //= 1000
scale_index += 1
# معکوس کردن و ترکیب گروه‌ها
groups.reverse()
result = " و ".join(groups)
return result
except Exception:
return ""
def amount_to_persian_words(amount):
"""
تبدیل مبلغ به حروف فارسی با واحد ریال
مثال: 12345 -> دوازده هزار و سیصد و چهل و پنج ریال
"""
try:
words = number_to_persian_words(amount)
if words:
return words + " ریال"
return ""
except Exception:
return ""

View file

@ -27,44 +27,8 @@ layout-wide customizer-hide
<div class="card-body">
<!-- Logo -->
<div class="app-brand justify-content-center">
<a href="index.html" class="app-brand-link gap-2">
<span class="app-brand-logo demo">
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z" id="path-1"></path>
<path d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z" id="path-3"></path>
<path d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z" id="path-4"></path>
<path d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z" id="path-5"></path>
</defs>
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
<g id="Icon" transform="translate(27.000000, 15.000000)">
<g id="Mask" transform="translate(0.000000, 8.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use fill="#696cff" xlink:href="#path-1"></use>
<g id="Path-3" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-3"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
</g>
<g id="Path-4" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-4"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
</g>
</g>
<g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
<use fill="#696cff" xlink:href="#path-5"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
</g>
</g>
</g>
</g>
</svg>
</span>
<span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
<a href="./" class="app-brand-link gap-2">
<img src="{% static 'assets/img/logo/logo.png' %}" alt="logo" class="img-fluid" width="100">
</a>
</div>

View file

@ -12,9 +12,7 @@ class CertificateTemplateAdmin(admin.ModelAdmin):
@admin.register(CertificateInstance)
class CertificateInstanceAdmin(admin.ModelAdmin):
list_display = ('process_instance', 'rendered_title', 'issued_at', 'approved')
list_display = ('process_instance', 'rendered_title', 'hologram_code', 'issued_at', 'approved')
list_filter = ('approved', 'issued_at')
search_fields = ('process_instance__code', 'rendered_title')
search_fields = ('process_instance__code', 'rendered_title', 'hologram_code')
autocomplete_fields = ('process_instance', 'template')

View file

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

View file

@ -28,7 +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='کد یکتا هولوگرام')
hologram_code = models.CharField(max_length=50, null=True, blank=True, verbose_name='کد یکتا هولوگرام', unique=True)
class Meta:
verbose_name = 'گواهی'

View file

@ -38,9 +38,11 @@
</small>
</div>
<div class="d-flex gap-2">
{% if request.user|is_broker or request.user|is_manager %}
<button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#printHologramModal">
<i class="bx bx-printer me-2"></i> پرینت
</button>
{% endif %}
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
@ -52,7 +54,7 @@
<div class="bs-stepper wizard-vertical vertical mt-2">
{% stepper_header instance step %}
<div class="bs-stepper-content">
{% if request.user|is_broker or request.user|is_manager or request.user|is_water_resource_manager %}
<div class="card">
<div class="card-body">
<div class="d-flex mb-2">
@ -115,6 +117,21 @@
</form>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body">
<div class="text-center py-5">
<div class="mb-4">
<i class="bx bx-lock-alt text-warning" style="font-size: 80px;"></i>
</div>
<h4 class="mb-3">دسترسی محدود</h4>
<p class="text-muted mb-4">
متأسفانه شما دسترسی لازم برای مشاهده این صفحه را ندارید.<br>
</p>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>

View file

@ -6,13 +6,14 @@ from django.urls import reverse
from django.utils import timezone
from django.template import Template, Context
from django.utils.safestring import mark_safe
from django.db import IntegrityError
from processes.models import ProcessInstance, StepInstance
from invoices.models import Invoice
from installations.models import InstallationReport
from .models import CertificateTemplate, CertificateInstance
from common.consts import UserRoles
from common.decorators import allowed_roles
from _helpers.jalali import Gregorian
from processes.utils import get_scoped_instance_or_404
@ -150,6 +151,7 @@ def certificate_step(request, instance_id, step_id):
@login_required
@allowed_roles([UserRoles.BROKER, UserRoles.MANAGER])
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()
@ -157,15 +159,56 @@ def certificate_print(request, instance_id):
if request.method == 'POST':
# Save/update hologram code then print
code = (request.POST.get('hologram_code') or '').strip()
if cert:
if code:
if not code:
messages.error(request, 'کد یکتای هولوگرام الزامی است')
# Find certificate step to redirect back
certificate_step = instance.process.steps.filter(order=9).first()
if certificate_step and instance.current_step:
return redirect('processes:step_detail', instance_id=instance.id, step_id=certificate_step.id)
return redirect('processes:instance_summary', instance_id=instance.id)
try:
if cert:
# Check if hologram code is already used by another certificate
if CertificateInstance.objects.filter(hologram_code=code).exclude(id=cert.id).exists():
messages.error(request, 'این کد هولوگرام قبلاً استفاده شده است. لطفاً کد دیگری وارد کنید')
# Find certificate step to redirect back
certificate_step = instance.process.steps.filter(order=9).first()
if certificate_step and instance.current_step:
return redirect('processes:step_detail', instance_id=instance.id, step_id=certificate_step.id)
return redirect('processes:instance_summary', instance_id=instance.id)
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)
else:
# Check if hologram code is already used
if CertificateInstance.objects.filter(hologram_code=code).exists():
messages.error(request, 'این کد هولوگرام قبلاً استفاده شده است. لطفاً کد دیگری وارد کنید')
# Find certificate step to redirect back
certificate_step = instance.process.steps.filter(order=9).first()
if certificate_step and instance.current_step:
return redirect('processes:step_detail', instance_id=instance.id, step_id=certificate_step.id)
return redirect('processes:instance_summary', instance_id=instance.id)
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
)
except IntegrityError:
messages.error(request, 'این کد هولوگرام قبلاً استفاده شده است. لطفاً کد دیگری وارد کنید')
# Find certificate step to redirect back
certificate_step = instance.process.steps.filter(order=9).first()
if certificate_step and instance.current_step:
return redirect('processes:step_detail', instance_id=instance.id, step_id=certificate_step.id)
return redirect('processes:instance_summary', instance_id=instance.id)
# proceed to rendering page after saving code
return render(request, 'certificates/print.html', {
'instance': instance,

View file

@ -1,7 +1,23 @@
from django import template
from _helpers.utils import jalali_converter2
from _helpers.utils import jalali_converter2, amount_to_persian_words
register = template.Library()
@register.filter(name='to_jalali')
def to_jalali(value):
return jalali_converter2(value)
return jalali_converter2(value)
@register.filter(name='amount_to_words')
def amount_to_words(value):
"""تبدیل مبلغ به حروف فارسی"""
try:
if value is None or value == '':
return ""
# تبدیل Decimal به int
from decimal import Decimal
if isinstance(value, Decimal):
value = int(value)
result = amount_to_persian_words(value)
return result if result else "صفر ریال"
except Exception as e:
return f"خطا: {str(e)}"

View file

@ -66,13 +66,11 @@ class InstallationReportForm(forms.ModelForm):
'class': 'form-select'
}, choices=[
('', 'انتخاب کنید'),
('A', 'A'),
('B', 'B')
('direct', 'مستقیم'),
('indirect', 'غیرمستقیم')
]),
'discharge_pipe_diameter': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '1',
'required': True
}),
'usage_type': forms.Select(attrs={
@ -90,20 +88,18 @@ class InstallationReportForm(forms.ModelForm):
}),
'motor_power': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '1',
'required': True
}),
'pre_calibration_flow_rate': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '0.01',
'step': '0.0001',
'required': True
}),
'post_calibration_flow_rate': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '0.01',
'step': '0.0001',
'required': True
}),
'water_meter_manufacturer': forms.Select(attrs={

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-10-09 08:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('installations', '0007_installationreport_meter_model'),
]
operations = [
migrations.AlterField(
model_name='installationreport',
name='post_calibration_flow_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
),
migrations.AlterField(
model_name='installationreport',
name='pre_calibration_flow_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 5.2.4 on 2025-10-09 12:28
import django.core.validators
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('installations', '0008_alter_installationreport_post_calibration_flow_rate_and_more'),
]
operations = [
migrations.AlterField(
model_name='installationreport',
name='meter_model',
field=models.CharField(blank=True, choices=[('direct', 'مستقیم'), ('indirect', 'غیرمستقیم')], max_length=20, null=True, verbose_name='مدل کنتور'),
),
migrations.AlterField(
model_name='installationreport',
name='post_calibration_flow_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))], verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
),
migrations.AlterField(
model_name='installationreport',
name='pre_calibration_flow_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))], verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-10-09 12:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('installations', '0009_alter_installationreport_meter_model_and_more'),
]
operations = [
migrations.AlterField(
model_name='installationreport',
name='motor_power',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='(کیلووات ساعت) قدرت موتور'),
),
migrations.AlterField(
model_name='installationreport',
name='post_calibration_flow_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
),
migrations.AlterField(
model_name='installationreport',
name='pre_calibration_flow_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-10-09 12:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('installations', '0010_alter_installationreport_motor_power_and_more'),
]
operations = [
migrations.AlterField(
model_name='installationreport',
name='discharge_pipe_diameter',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True, verbose_name='قطر لوله آبده (اینچ)'),
),
]

View file

@ -1,6 +1,8 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.core.validators import MinValueValidator
from decimal import Decimal
from common.models import BaseModel
User = get_user_model()
@ -48,12 +50,12 @@ class InstallationReport(BaseModel):
]
meter_type = models.CharField(max_length=20, choices=METER_TYPE_CHOICES, null=True, blank=True, verbose_name='نوع کنتور')
METER_MODEL_CHOICES = [
('A', 'A'),
('B', 'B'),
('direct', 'مستقیم'),
('indirect', 'غیرمستقیم'),
]
meter_model = models.CharField(max_length=20, choices=METER_MODEL_CHOICES, null=True, blank=True, verbose_name='مدل کنتور')
meter_size = models.CharField(max_length=50, null=True, blank=True, verbose_name='سایز کنتور')
discharge_pipe_diameter = models.PositiveIntegerField(null=True, blank=True, verbose_name='قطر لوله آبده (اینچ)')
discharge_pipe_diameter = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True, verbose_name='قطر لوله آبده (اینچ)')
USAGE_TYPE_CHOICES = [
('domestic', 'شرب و خدمات'),
('agriculture', 'کشاورزی'),
@ -61,9 +63,9 @@ class InstallationReport(BaseModel):
]
usage_type = models.CharField(max_length=20, choices=USAGE_TYPE_CHOICES, null=True, verbose_name='نوع مصرف')
exploitation_license_number = models.CharField(max_length=50, verbose_name='شماره پروانه بهره‌برداری چاه')
motor_power = models.PositiveIntegerField(null=True, blank=True, verbose_name='(کیلووات ساعت) قدرت موتور')
pre_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون')
post_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون')
motor_power = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True, verbose_name='(کیلووات ساعت) قدرت موتور')
pre_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون')
post_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون')
water_meter_manufacturer = models.ForeignKey('wells.WaterMeterManufacturer', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='شرکت سازنده کنتور آب')
sim_number = models.CharField(max_length=20, null=True, blank=True, verbose_name='شماره سیمکارت')
driving_force = models.CharField(max_length=50, null=True, blank=True, verbose_name='نیرو محرکه چاه')

View file

@ -31,6 +31,60 @@
.removal-checkbox:checked:focus {
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25) !important;
}
/* Upload Loader Overlay */
#uploadLoader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: none;
justify-content: center;
align-items: center;
}
#uploadLoader.active {
display: flex;
}
.loader-content {
background: white;
padding: 2rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 300px;
}
.loader-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #696cff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loader-text {
font-size: 1.1rem;
font-weight: 500;
color: #333;
margin-bottom: 0.5rem;
}
.loader-subtext {
font-size: 0.9rem;
color: #666;
}
</style>
{% endblock %}
@ -38,6 +92,15 @@
{% include '_toasts.html' %}
<!-- Upload Loader Overlay -->
<div id="uploadLoader">
<div class="loader-content">
<div class="loader-spinner"></div>
<div class="loader-text">در حال آپلود...</div>
<div class="loader-subtext">لطفا تا بارگذاری کامل گزارش منتظر بمانید.</div>
</div>
</div>
<!-- Instance Info Modal -->
{% instance_info_modal instance %}
@ -516,7 +579,7 @@
{% if user_is_installer %}
<button type="submit" class="btn btn-success" form="installation-report-form">ثبت گزارش</button>
{% endif %}
{% if next_step and not edit_mode %}
{% if next_step and not edit_mode and report %}
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
بعدی
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
@ -638,6 +701,9 @@
// Require date and show success toast on submit (persist across redirect)
(function(){
const form = document.querySelector('form[enctype]') || document.querySelector('form');
const loader = document.getElementById('uploadLoader');
const submitButton = document.querySelector('button[type="submit"][form="installation-report-form"]');
if (!form) return;
form.addEventListener('submit', function(ev){
const display = document.getElementById('id_visited_date_display');
@ -663,8 +729,32 @@
return false;
}
} catch(_) {}
// Show loader overlay when form is valid and submitting
if (loader) {
loader.classList.add('active');
}
// Disable submit button to prevent double submission
if (submitButton) {
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>در حال ارسال...';
}
try { sessionStorage.setItem('install_report_saved', '1'); } catch(_) {}
}, false);
// Hide loader on back navigation or page show (in case of errors)
window.addEventListener('pageshow', function(event) {
if (loader) {
loader.classList.remove('active');
}
if (submitButton) {
submitButton.disabled = false;
submitButton.innerHTML = 'ثبت گزارش';
}
});
// on load, if saved flag exists, show toast
try {
if (sessionStorage.getItem('install_report_saved') === '1') {

View file

@ -63,7 +63,9 @@ class InvoiceAdmin(SimpleHistoryAdmin):
def remaining_amount_display(self, obj):
amount = obj.get_remaining_amount()
color = "green" if amount <= 0 else "red"
return format_html('<span style="color: {};">{:,.0f} ریال</span>', color, amount)
# Pre-format the number to avoid applying a numeric format code to a SafeString
formatted_amount = f"{amount:,.0f}"
return format_html('<span style="color: {};">{} ریال</span>', color, formatted_amount)
remaining_amount_display.short_description = "مبلغ باقی‌مانده"
@admin.register(Payment)

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-10-09 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoices', '0002_remove_historicalinvoice_paid_amount_and_more'),
]
operations = [
migrations.AddField(
model_name='historicalpayment',
name='payment_stage',
field=models.CharField(choices=[('quote', 'پیش\u200cفاکتور'), ('final_settlement', 'تسویه نهایی')], default='quote', max_length=20, verbose_name='مرحله پرداخت'),
),
migrations.AddField(
model_name='payment',
name='payment_stage',
field=models.CharField(choices=[('quote', 'پیش\u200cفاکتور'), ('final_settlement', 'تسویه نهایی')], default='quote', max_length=20, verbose_name='مرحله پرداخت'),
),
]

View file

@ -350,6 +350,11 @@ class InvoiceItem(BaseModel):
class Payment(BaseModel):
"""مدل پرداخت‌ها"""
PAYMENT_STAGE_CHOICES = [
('quote', 'پیش‌فاکتور'),
('final_settlement', 'تسویه نهایی'),
]
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments', verbose_name="فاکتور")
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="مبلغ پرداخت")
direction = models.CharField(
@ -370,6 +375,12 @@ class Payment(BaseModel):
default='cash',
verbose_name="روش پرداخت"
)
payment_stage = models.CharField(
max_length=20,
choices=PAYMENT_STAGE_CHOICES,
default='quote',
verbose_name="مرحله پرداخت"
)
reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True, unique=True)
payment_date = models.DateField(verbose_name="تاریخ پرداخت")
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)

View file

@ -7,6 +7,7 @@
{% load static %}
{% load humanize %}
{% load common_tags %}
<!-- Fonts (match base) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -48,71 +49,38 @@
</head>
<body>
<div class="container-fluid">
<!-- Header -->
<!-- Invoice Header (compact, matches preview) -->
<div class="invoice-header">
<div class="row align-items-center">
<div class="col-6 d-flex align-items-center">
<div class="me-3" style="width:64px;height:64px;display:flex;align-items:center;justify-content:center;background:#eef2ff;border-radius:8px;">
{% if instance.broker.company and instance.broker.company.logo %}
<img src="{{ instance.broker.company.logo.url }}" alt="لوگو" style="max-height:58px;max-width:120px;">
{% else %}
<span class="company-logo">شرکت</span>
{% endif %}
</div>
<div>
{% if instance.broker.company %}
{{ instance.broker.company.name }}
{% endif %}
{% if instance.broker.company %}
<div class="text-muted small">
{% if instance.broker.company.address %}
<div>{{ instance.broker.company.address }}</div>
{% endif %}
{% if instance.broker.affairs.county.city.name %}
<div>{{ instance.broker.affairs.county.city.name }}، ایران</div>
{% endif %}
{% if instance.broker.company.phone %}
<div>تلفن: {{ instance.broker.company.phone }}</div>
{% endif %}
<div class="row align-items-start justify-content-end">
<h5 class="mb-0 text-center fw-bold">فاکتور</h5>
<div class="col-3 text-start">
<div class="mt-2">
<div>شماره : {{ instance.code }}</div>
<div class="small">تاریخ صدور: {{ invoice.jcreated_date }}</div>
</div>
{% endif %}
</div>
</div>
<div class="col-6 text-end">
<div class="mt-2">
<div><strong>#فاکتور نهایی {{ instance.code }}</strong></div>
<div class="text-muted small">تاریخ صدور: {{ invoice.jcreated_date }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Customer & Well Info -->
<div class="row mb-3">
<div class="col-6">
<h6 class="fw-bold mb-2">اطلاعات مشترک {% if instance.representative.profile and instance.representative.profile.user_type == 'legal' %}(حقوقی){% else %}(حقیقی){% endif %}</h6>
<div class="col-4 small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
{% if instance.representative.profile and 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>
<div class="col-4 small mb-1"><span class="text-muted">نام شرکت:</span> {{ instance.representative.profile.company_name|default:"-" }}</div>
<div class="col-4 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> {{ invoice.customer.get_full_name|default:instance.representative.get_full_name }}</div>
<div class="col-4 small mb-1"><span class="text-muted">نام و نام خانوادگی:</span> {{ invoice.customer.get_full_name|default:instance.representative.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>
<div class="col-4 small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.phone_number_1 %}
<div class="small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
<div class="col-4 small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.address %}
<div class="small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
<div class="col-12 small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
{% endif %}
</div>
<div class="col-6">
<h6 class="fw-bold mb-2">اطلاعات چاه</h6>
<div class="small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
<div class="small mb-1"><span class="text-muted">شماره اشتراک برق:</span> {{ instance.well.electricity_subscription_number|default:"-" }}</div>
<div class="small mb-1"><span class="text-muted">سریال کنتور:</span> {{ instance.well.water_meter_serial_number|default:"-" }}</div>
<div class="small"><span class="text-muted">قدرت چاه:</span> {{ instance.well.well_power|default:"-" }}</div>
</div>
</div>
<!-- Items Table -->
@ -144,47 +112,43 @@
</tbody>
<tfoot>
<tr class="total-section">
<td colspan="5" class="text-end"><strong>جمع کل(ریال):</strong></td>
<td><strong>{{ invoice.total_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>جمع کل(ریال):</strong></td>
<td colspan="6" class="text-end"><strong>{{ invoice.total_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
{% if invoice.discount_amount > 0 %}
<tr class="total-section">
<td colspan="5" class="text-end"><strong>تخفیف(ریال):</strong></td>
<td><strong>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>تخفیف(ریال):</strong></td>
<td colspan="6" class="text-end"><strong>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
{% endif %}
<tr class="total-section">
<td colspan="5" class="text-end"><strong>مالیات بر ارزش افزوده(ریال):</strong></td>
<td><strong>{{ invoice.get_vat_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>مالیات بر ارزش افزوده(ریال):</strong></td>
<td colspan="6" class="text-end"><strong>{{ invoice.get_vat_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section border-top border-2">
<td colspan="5" class="text-end"><strong>مبلغ نهایی (شامل مالیات)(ریال):</strong></td>
<td><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>مبلغ نهایی (شامل مالیات)(ریال):</strong></td>
<td colspan="6" class="text-end"><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section">
<td colspan="5" class="text-end"><strong>پرداختی‌ها(ریال):</strong></td>
<td><strong">{{ invoice.get_paid_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>پرداختی‌ها(ریال):</strong></td>
<td colspan="6" class="text-end"><strong">{{ invoice.get_paid_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section">
<td colspan="5" class="text-end"><strong>مانده(ریال):</strong></td>
<td><strong>{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>مانده(ریال):</strong></td>
<td colspan="6" class="text-end"><strong>{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section small border-top border-2">
<td colspan="2" class="text-start"><strong>مبلغ نهایی به حروف:</strong></td>
<td colspan="6" class="text-end"><strong>{{ invoice.final_amount|amount_to_words }}</strong></td>
</tr>
</tfoot>
</table>
</div>
<!-- Conditions & Payment -->
<div class="row">
<div class="col-8">
<h6 class="fw-bold">مهر و امضا:</h6>
<ul class="small mb-0">
{% if instance.broker.company and instance.broker.company.signature %}
<li class="mt-3" style="list-style:none;"><img src="{{ instance.broker.company.signature.url }}" alt="امضا" style="height: 200px;"></li>
{% endif %}
</ul>
</div>
{% if instance.broker.company %}
<div class="col-4">
<div class="col-8">
<h6 class="fw-bold mb-2">اطلاعات پرداخت</h6>
{% if instance.broker.company.card_number %}
<div class="small mb-1"><span class="text-muted">شماره کارت:</span> {{ instance.broker.company.card_number }}</div>
@ -200,6 +164,20 @@
{% endif %}
</div>
{% endif %}
<div class="col-4">
{% if instance.broker.company and instance.broker.company.signature %}
<div class="row d-flex justify-content-center">
<h6 class="mb-3 text-center">مهر و امضا
{% if instance.broker.company.signature %}
<img class="img-fluid" src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="">
{% endif %}
</h6>
</div>
{% endif %}
</div>
</div>
</div>

View file

@ -60,7 +60,7 @@
<div class="bs-stepper-content">
<div class="row g-3">
{% if is_broker and invoice.get_remaining_amount != 0 %}
{% if is_broker and needs_approval %}
<div class="col-12 col-lg-5">
<div class="card border h-100">
<div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div>
@ -193,7 +193,7 @@
</div>
</div>
</div>
{% if approver_statuses and invoice.get_remaining_amount != 0 and step_instance.status != 'completed' %}
{% if approver_statuses and needs_approval and step_instance.status != 'completed' %}
<div class="card border mt-2">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">وضعیت تاییدها</h6>
@ -318,7 +318,11 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% if invoice.get_remaining_amount != 0 %}
{% if not needs_approval %}
<div class="alert alert-info" role="alert">
فاکتور کاملاً تسویه شده است و نیازی به تایید ندارد.
</div>
{% elif invoice.get_remaining_amount != 0 %}
<div class="alert alert-warning" role="alert">
مانده فاکتور: <strong>{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} ریال</strong><br>
امکان تایید تا تسویه کامل فاکتور وجود ندارد.

View file

@ -2,6 +2,7 @@
{% load static %}
{% load processes_tags %}
{% load humanize %}
{% load common_tags %}
{% block sidebar %}
{% include 'sidebars/admin.html' %}
@ -56,8 +57,9 @@
<!-- Invoice Preview Card -->
<div class="card invoice-preview-card mt-4 border">
<div class="card-body">
<div class="d-flex justify-content-between flex-xl-row flex-md-column flex-sm-row flex-column p-sm-3 p-0 align-items-center">
<div class="mb-xl-0 mb-4">
<h5 class="mb-0 text-center fw-bold">پیش‌فاکتور</h5>
<div class="d-flex justify-content-end flex-xl-row flex-md-column flex-sm-row flex-column p-0 align-items-center">
<div class="mb-xl-0 mb-4 d-none">
<!-- Company Logo & Info -->
<div class="d-flex align-items-center">
<div class="avatar avatar-lg me-3">
@ -94,13 +96,13 @@
</div>
</div>
<!-- Invoice Details -->
<div class="text-center">
<div class="mb-3">
<h5 class="text-body">#{{ quote.name }}</h5>
<div class="text-start">
<div class="">
<h6 class="text-body">شماره : {{ quote.name }}</h6>
</div>
<div class="invoice-details">
<div class="d-flex justify-content-end align-items-center mb-2">
<span class="text-muted me-2">تاریخ صدور:</span>
<span class="me-2">تاریخ صدور:</span>
<span class="fw-medium text-body">{{ quote.jcreated_date }}</span>
</div>
</div>
@ -110,7 +112,7 @@
<hr class="my-0">
<div class="card-body py-1">
<div class="row">
<div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
<div class="col-xl-12 col-md-12 col-sm-12 col-12 mb-3">
<div class="">
<div class="card-body p-3">
<h6 class="card-title text-primary mb-2">
@ -121,43 +123,48 @@
اطلاعات مشترک (حقیقی)
{% endif %}
</h6>
{% if instance.representative.profile.user_type == 'legal' %}
<div class="d-flex gap-2 mb-1">
<span class="text-muted small">نام شرکت:</span>
<span class="fw-medium small">{{ instance.representative.profile.company_name|default:"-" }}</span>
<div class="row">
<div class="col-md-3 d-flex gap-2 mb-1">
<span class="text-muted small">شماره اشتراک آب:</span>
<span class="fw-medium small">{{ instance.well.water_subscription_number }}</span>
</div>
{% if instance.representative.profile.user_type == 'legal' %}
<div class="col-md-3 d-flex gap-2 mb-1">
<span class="text-muted small">نام شرکت:</span>
<span class="fw-medium small">{{ instance.representative.profile.company_name|default:"-" }}</span>
</div>
<div class="col-md-3 d-flex gap-2 mb-1">
<span class="text-muted small">شناسه ملی:</span>
<span class="fw-medium small">{{ instance.representative.profile.company_national_id|default:"-" }}</span>
</div>
{% endif %}
<div class="col-md-3 d-flex gap-2 mb-1">
<span class="text-muted small">نام:</span>
<span class="fw-medium small">{{ quote.customer.get_full_name }}</span>
</div>
{% if instance.representative.profile.national_code %}
<div class="col-md-3 d-flex gap-2 mb-1">
<span class="text-muted small">کد ملی:</span>
<span class="fw-medium small">{{ instance.representative.profile.national_code }}</span>
</div>
{% endif %}
{% if instance.representative.profile.phone_number_1 %}
<div class="col-md-3 d-flex gap-2 mb-1">
<span class="text-muted small">تلفن:</span>
<span class="fw-medium small">{{ instance.representative.profile.phone_number_1 }}</span>
</div>
{% endif %}
{% if instance.representative.profile.address %}
<div class="col-md-12 d-flex gap-2">
<span class="text-muted small">آدرس:</span>
<span class="fw-medium small">{{ instance.representative.profile.address }}</span>
</div>
{% endif %}
</div>
<div class="d-flex gap-2 mb-1">
<span class="text-muted small">شناسه ملی:</span>
<span class="fw-medium small">{{ instance.representative.profile.company_national_id|default:"-" }}</span>
</div>
{% endif %}
<div class="d-flex gap-2 mb-1">
<span class="text-muted small">نام:</span>
<span class="fw-medium small">{{ quote.customer.get_full_name }}</span>
</div>
{% if instance.representative.profile.national_code %}
<div class="d-flex gap-2 mb-1">
<span class="text-muted small">کد ملی:</span>
<span class="fw-medium small">{{ instance.representative.profile.national_code }}</span>
</div>
{% endif %}
{% if instance.representative.profile.phone_number_1 %}
<div class="d-flex gap-2 mb-1">
<span class="text-muted small">تلفن:</span>
<span class="fw-medium small">{{ instance.representative.profile.phone_number_1 }}</span>
</div>
{% endif %}
{% if instance.representative.profile.address %}
<div class="d-flex gap-2">
<span class="text-muted small">آدرس:</span>
<span class="fw-medium small">{{ instance.representative.profile.address }}</span>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
<div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3 d-none">
<div class="border-0 bg-light">
<div class="card-body p-3">
<h6 class="card-title text-primary mb-2">
@ -214,7 +221,8 @@
<p class="mb-2">تخفیف:</p>
{% endif %}
<p class="mb-2">مالیات بر ارزش افزوده:</p>
<p class="mb-0 fw-bold">مبلغ نهایی (شامل مالیات):</p>
<p class="mb-2 fw-bold">مبلغ نهایی (شامل مالیات):</p>
<p class="mb-0 small text-muted">مبلغ نهایی به حروف:</p>
</td>
<td class="px-4 py-5">
<p class="fw-medium mb-2">{{ quote.total_amount|floatformat:0|intcomma:False }} ریال</p>
@ -222,7 +230,8 @@
<p class="fw-medium mb-2">{{ quote.discount_amount|floatformat:0|intcomma:False }} ریال</p>
{% endif %}
<p class="fw-medium mb-2">{{ quote.get_vat_amount|floatformat:0|intcomma:False }} ریال</p>
<p class="fw-bold mb-0">{{ quote.final_amount|floatformat:0|intcomma:False }} ریال</p>
<p class="fw-bold mb-2">{{ quote.final_amount|floatformat:0|intcomma:False }} ریال</p>
<p class="mb-0 small text-muted">{{ quote.final_amount|amount_to_words }}</p>
</td>
</tr>
</tbody>
@ -245,50 +254,53 @@
<i class="bx bx-info-circle text-muted me-2"></i>
این برگه صرفاً جهت اعلام قیمت بوده و ارزش قانونی دیگری ندارد
</li>
{% if instance.broker.company.signature %}
<li class="mb-0 text-start mt-4 ms-5">
<img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="height: 200px;">
</li>
{% endif %}
</ul>
{% if instance.broker.company %}
<div class="col-md-4 mt-4">
<h6 class="mb-1">اطلاعات پرداخت:</h6>
<div class="d-flex flex-column gap-1">
{% if instance.broker.company.card_number %}
<div>
<small class="text-muted">شماره کارت:</small>
<div class="fw-medium">{{ instance.broker.company.card_number }}</div>
</div>
{% endif %}
{% if instance.broker.company.account_number %}
<div>
<small class="text-muted">شماره حساب:</small>
<div class="fw-medium">{{ instance.broker.company.account_number }}</div>
</div>
{% endif %}
{% if instance.broker.company.sheba_number %}
<div>
<small class="text-muted">شماره شبا:</small>
<div class="fw-medium">{{ instance.broker.company.sheba_number }}</div>
</div>
{% endif %}
{% if instance.broker.company.bank_name %}
<div>
<small class="text-muted">بانک:</small>
<div class="fw-medium">{{ instance.broker.company.get_bank_name_display }}</div>
</div>
{% endif %}
{% if instance.broker.company.branch_name %}
<div>
<small class="text-muted">شعبه:</small>
<div class="fw-medium">{{ instance.broker.company.branch_name }}</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% if instance.broker.company %}
<div class="col-md-4">
<h6 class="mb-3">اطلاعات پرداخت:</h6>
<div class="d-flex flex-column gap-2">
{% if instance.broker.company.card_number %}
<div>
<small class="text-muted">شماره کارت:</small>
<div class="fw-medium">{{ instance.broker.company.card_number }}</div>
</div>
<div class="col-md-4 mt-5">
<div class="row d-flex justify-content-center">
<h6 class="mb-3 text-center">مهر و امضا</h6>
{% if instance.broker.company.signature %}
<img class="img-fluid" src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="">
{% endif %}
{% if instance.broker.company.account_number %}
<div>
<small class="text-muted">شماره حساب:</small>
<div class="fw-medium">{{ instance.broker.company.account_number }}</div>
</div>
{% endif %}
{% if instance.broker.company.sheba_number %}
<div>
<small class="text-muted">شماره شبا:</small>
<div class="fw-medium">{{ instance.broker.company.sheba_number }}</div>
</div>
{% endif %}
{% if instance.broker.company.bank_name %}
<div>
<small class="text-muted">بانک:</small>
<div class="fw-medium">{{ instance.broker.company.get_bank_name_display }}</div>
</div>
{% endif %}
{% if instance.broker.company.branch_name %}
<div>
<small class="text-muted">شعبه:</small>
<div class="fw-medium">{{ instance.broker.company.branch_name }}</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>

View file

@ -7,6 +7,7 @@
{% load static %}
{% load humanize %}
{% load common_tags %}
<!-- Fonts (match base) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -75,6 +76,7 @@
.items-table td {
border-bottom: 1px solid #dee2e6;
text-align: center;
font-size: 8px;
}
.total-section {
@ -105,38 +107,12 @@
<!-- Invoice Header (compact, matches preview) -->
<div class="invoice-header">
<div class="row align-items-center">
<div class="col-6 d-flex align-items-center">
<div class="me-3" style="width:64px;height:64px;display:flex;align-items:center;justify-content:center;background:#eef2ff;border-radius:8px;">
{% if instance.broker.company and instance.broker.company.logo %}
<img src="{{ instance.broker.company.logo.url }}" alt="لوگو" style="max-height:58px;max-width:120px;">
{% else %}
<span class="company-logo">شرکت</span>
{% endif %}
</div>
<div>
{% if instance.broker.company %}
{{ instance.broker.company.name }}
{% endif %}
{% if instance.broker.company %}
<div class="text-muted small">
{% if instance.broker.company.address %}
<div>{{ instance.broker.company.address }}</div>
{% endif %}
{% if instance.broker.affairs.county.city.name %}
<div>{{ instance.broker.affairs.county.city.name }}، ایران</div>
{% endif %}
{% if instance.broker.company.phone %}
<div>تلفن: {{ instance.broker.company.phone }}</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="col-6 text-end">
<div class="row align-items-start justify-content-end">
<h5 class="mb-3 text-center fw-bold">پیش‌فاکتور</h5>
<div class="col-3 text-start">
<div class="mt-2">
<div><strong>#{{ quote.name }}</strong></div>
<div class="text-muted small">تاریخ صدور: {{ quote.jcreated_date }}</div>
<div>شماره : {{ quote.name }}</div>
<div class="small">تاریخ صدور: {{ quote.jcreated_date }}</div>
</div>
</div>
</div>
@ -144,37 +120,29 @@
<!-- Customer & Well Info (compact to match preview) -->
<div class="row mb-3">
<div class="col-6">
<h6 class="fw-bold mb-2">
{% if instance.representative.profile.user_type == 'legal' %}
اطلاعات مشترک (حقوقی)
{% else %}
اطلاعات مشترک (حقیقی)
{% endif %}
</h6>
<h6 class="fw-bold mb-2">
{% 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>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.phone_number_1 %}
<div class="small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.address %}
<div class="small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
اطلاعات مشترک (حقوقی)
{% else %}
اطلاعات مشترک (حقیقی)
{% endif %}
</h6>
<div class="col-4 small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
{% if instance.representative.profile.user_type == 'legal' %}
<div class="col-4 small mb-1"><span class="text-muted">نام شرکت:</span> {{ instance.representative.profile.company_name|default:"-" }}</div>
<div class="col-4 small mb-1"><span class="text-muted">شناسه ملی:</span> {{ instance.representative.profile.company_national_id|default:"-" }}</div>
{% endif %}
<div class="col-4 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="col-4 small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.phone_number_1 %}
<div class="col-4 small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.address %}
<div class="col-12 small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
{% endif %}
</div>
<div class="col-6">
<h6 class="fw-bold mb-2">اطلاعات چاه</h6>
<div class="small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
<div class="small mb-1"><span class="text-muted">شماره اشتراک برق:</span> {{ instance.well.electricity_subscription_number|default:"-" }}</div>
<div class="small mb-1"><span class="text-muted">سریال کنتور:</span> {{ instance.well.water_meter_serial_number|default:"-" }}</div>
<div class="small"><span class="text-muted">قدرت چاه:</span> {{ instance.well.well_power|default:"-" }}</div>
</div>
</div>
<!-- Items Table -->
<div class="mb-4">
@ -203,22 +171,26 @@
</tbody>
<tfoot>
<tr class="total-section">
<td colspan="5" class="text-end"><strong>جمع کل(ریال):</strong></td>
<td><strong>{{ quote.total_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>جمع کل(ریال):</strong></td>
<td colspan="5" class="text-end"><strong>{{ quote.total_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
{% if quote.discount_amount > 0 %}
<tr class="total-section">
<td colspan="5" class="text-end"><strong>تخفیف(ریال):</strong></td>
<td><strong>{{ quote.discount_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>تخفیف(ریال):</strong></td>
<td colspan="5" class="text-end"><strong>{{ quote.discount_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
{% endif %}
<tr class="total-section">
<td colspan="5" class="text-end"><strong>مالیات بر ارزش افزوده(ریال):</strong></td>
<td><strong>{{ quote.get_vat_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>مالیات بر ارزش افزوده(ریال):</strong></td>
<td colspan="5" class="text-end"><strong>{{ quote.get_vat_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section border-top border-2">
<td colspan="5" class="text-end"><strong>مبلغ نهایی (با مالیات)(ریال):</strong></td>
<td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>
<td colspan="3" class="text-start"><strong>مبلغ نهایی (با مالیات)(ریال):</strong></td>
<td colspan="5" class="text-end"><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section small border-top border-2">
<td colspan="2" class="text-start"><strong>مبلغ نهایی به حروف:</strong></td>
<td colspan="6" class="text-end"><strong>{{ quote.final_amount|amount_to_words }}</strong></td>
</tr>
</tfoot>
</table>
@ -232,14 +204,11 @@
<li class="mb-1">اعتبار پیش‌فاکتور صادر شده ۴۸ ساعت پس از تاریخ صدور می‌باشد</li>
<li class="mb-1">مبلغ فوق به صورت علی‌الحساب دریافت می‌گردد</li>
<li class="mb-1">این برگه صرفاً جهت اعلام قیمت بوده و ارزش قانونی دیگری ندارد</li>
{% if instance.broker.company and instance.broker.company.signature %}
<li class="mt-3" style="list-style:none;"><img src="{{ instance.broker.company.signature.url }}" alt="امضا" style="height: 200px;"></li>
{% endif %}
</ul>
</div>
{% if instance.broker.company %}
<div class="col-4">
<h6 class="fw-bold mb-2">اطلاعات پرداخت</h6>
{% if instance.broker.company %}
<h6 class="fw-bold mt-3">اطلاعات پرداخت</h6>
{% if instance.broker.company.card_number %}
<div class="small mb-1"><span class="text-muted">شماره کارت:</span> {{ instance.broker.company.card_number }}</div>
{% endif %}
@ -252,8 +221,22 @@
{% if instance.broker.company.bank_name %}
<div class="small"><span class="text-muted">بانک:</span> {{ instance.broker.company.get_bank_name_display }}</div>
{% endif %}
{% endif %}
</div>
<div class="col-4">
{% if instance.broker.company and instance.broker.company.signature %}
<div class="row d-flex justify-content-center">
<h6 class="mb-3 text-center">مهر و امضا
{% if instance.broker.company.signature %}
<img class="img-fluid" src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="">
{% endif %}
</h6>
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Signature Section (optional, compact) -->

View file

@ -115,7 +115,7 @@ def create_quote(request, instance_id, step_id):
quote, created_q = Quote.objects.get_or_create(
process_instance=instance,
defaults={
'name': f"پیش‌فاکتور {instance.code}",
'name': f"{instance.code}",
'customer': instance.representative or request.user,
'valid_until': timezone.now().date(),
'created_by': request.user,
@ -422,13 +422,29 @@ def quote_payment_step(request, instance_id, step_id):
step_instance.status = 'completed'
step_instance.completed_at = timezone.now()
step_instance.save()
# move to next step
redirect_url = 'processes:request_list'
# Auto-complete next step if it exists
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(redirect_url)
next_step_instance, _ = StepInstance.objects.get_or_create(
process_instance=instance,
step=next_step,
defaults={'status': 'in_progress'}
)
next_step_instance.status = 'completed'
next_step_instance.completed_at = timezone.now()
next_step_instance.save()
# Move to the step after next
step_after_next = instance.process.steps.filter(order__gt=next_step.order).first()
if step_after_next:
instance.current_step = step_after_next
instance.save()
return redirect('processes:step_detail', instance_id=instance.id, step_id=step_after_next.id)
else:
# No more steps, go to request list
return redirect('processes:request_list')
return redirect('processes:request_list')
messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقش‌ها.')
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
@ -548,6 +564,7 @@ def add_quote_payment(request, instance_id, step_id):
amount=amount_dec,
payment_date=payment_date,
payment_method=payment_method,
payment_stage='quote',
reference_number=reference_number,
receipt_image=receipt_image,
notes=notes,
@ -869,6 +886,8 @@ def add_special_charge(request, instance_id, step_id):
"""افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی به‌صورت آیتم جداگانه"""
instance = get_scoped_instance_or_404(request, instance_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
step = get_object_or_404(instance.process.steps, id=step_id)
# only MANAGER can add special charges
try:
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
@ -898,29 +917,50 @@ def add_special_charge(request, instance_id, step_id):
unit_price=amount_dec,
)
invoice.calculate_totals()
# If the next step was completed, reopen it (set to in_progress) due to invoice change
# After modifying payments, set step back to in_progress
try:
step = get_object_or_404(instance.process.steps, id=step_id)
next_step = instance.process.steps.filter(order__gt=step.order).first()
if next_step:
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=next_step)
if si.status in ['completed', 'approved']:
si.status = 'in_progress'
si.completed_at = None
si.save(update_fields=['status', 'completed_at'])
# Clear prior approvals/rejections as the underlying totals changed
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
si.status = 'in_progress'
si.completed_at = None
si.save()
except Exception:
pass
# Reset ALL subsequent completed steps to in_progress
try:
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
for subsequent_step in subsequent_steps:
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
if subsequent_step_instance:
# Bypass validation by using update() instead of save()
instance.step_instances.filter(step=subsequent_step).update(
status='in_progress',
completed_at=None
)
# Clear prior approvals/rejections as the underlying totals changed
try:
for appr in list(si.approvals.all()):
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
try:
for rej in list(si.rejections.all()):
for rej in list(subsequent_step_instance.rejections.all()):
rej.delete()
except Exception:
pass
except Exception:
pass
# 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:
instance.current_step = step
instance.save(update_fields=['current_step'])
except Exception:
pass
return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])})
@ -929,6 +969,8 @@ def add_special_charge(request, instance_id, step_id):
def delete_special_charge(request, instance_id, step_id, item_id):
instance = get_scoped_instance_or_404(request, instance_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
step = get_object_or_404(instance.process.steps, id=step_id)
# only MANAGER can delete special charges
try:
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
@ -944,29 +986,51 @@ def delete_special_charge(request, instance_id, step_id, item_id):
return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'})
inv_item.hard_delete()
invoice.calculate_totals()
# If the next step was completed, reopen it (set to in_progress) due to invoice change
# After modifying payments, set step back to in_progress
try:
step = get_object_or_404(instance.process.steps, id=step_id)
next_step = instance.process.steps.filter(order__gt=step.order).first()
if next_step:
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=next_step)
if si.status in ['completed', 'approved']:
si.status = 'in_progress'
si.completed_at = None
si.save(update_fields=['status', 'completed_at'])
# Clear prior approvals/rejections as the underlying totals changed
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
si.status = 'in_progress'
si.completed_at = None
si.save()
except Exception:
pass
# Reset ALL subsequent completed steps to in_progress
try:
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
for subsequent_step in subsequent_steps:
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
if subsequent_step_instance:
# Bypass validation by using update() instead of save()
instance.step_instances.filter(step=subsequent_step).update(
status='in_progress',
completed_at=None
)
# Clear prior approvals/rejections as the underlying totals changed
try:
for appr in list(si.approvals.all()):
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
try:
for rej in list(si.rejections.all()):
for rej in list(subsequent_step_instance.rejections.all()):
rej.delete()
except Exception:
pass
except Exception:
pass
# 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:
instance.current_step = step
instance.save(update_fields=['current_step'])
except Exception:
pass
return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])})
@ -986,17 +1050,63 @@ def final_settlement_step(request, instance_id, step_id):
# Ensure step instance exists
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
# Check if there are changes that require approval
# (used for both auto-complete and UI display)
has_special_charges = False
has_installation_changes = False
has_final_settlement_payments = False
try:
has_special_charges = invoice.items.filter(item__is_special=True, is_deleted=False).exists()
except Exception:
pass
try:
from installations.models import InstallationAssignment
assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
if assignment:
reports = assignment.reports.all()
for report in reports:
if report.item_changes.filter(is_deleted=False).exists():
has_installation_changes = True
break
except Exception:
pass
# Check if there are payments added during final settlement step
# using the payment_stage field
try:
final_settlement_payments = invoice.payments.filter(
is_deleted=False,
payment_stage='final_settlement'
)
if final_settlement_payments.exists():
has_final_settlement_payments = True
except Exception:
pass
# Auto-complete step when invoice is fully settled (no approvals needed)
# BUT only if no special charges were added in final_invoice step
# AND no installation item changes were made
# AND no payments were added in this final settlement step
# (meaning the remaining amount is from the original quote_payment step)
try:
invoice.calculate_totals()
if invoice.get_remaining_amount() == 0:
remaining = invoice.get_remaining_amount()
# Only auto-complete if:
# 1. Remaining amount is zero
# 2. No special charges were added (meaning this is settling the original quote)
# 3. No installation item changes (meaning no items added/removed in installation step)
# 4. No payments added in final settlement step (meaning no new receipts need approval)
if remaining == 0 and not has_special_charges and not has_installation_changes and not has_final_settlement_payments:
if step_instance.status != 'completed':
step_instance.status = 'completed'
step_instance.completed_at = timezone.now()
step_instance.save()
# if next_step:
# instance.current_step = next_step
# instance.save(update_fields=['current_step'])
if next_step:
instance.current_step = next_step
instance.save(update_fields=['current_step'])
# return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
# return redirect('processes:request_list')
except Exception:
@ -1136,6 +1246,21 @@ def final_settlement_step(request, instance_id, step_id):
except Exception:
is_broker = False
# Determine if approval is needed
# Approval is needed if:
# 1. Remaining amount is not zero, OR
# 2. Special charges were added (meaning new balance was created in final_invoice step), OR
# 3. Installation item changes were made (meaning items were added/removed in installation step), OR
# 4. Payments were added in final settlement step (new receipts need approval)
needs_approval = True
try:
remaining = invoice.get_remaining_amount()
# No approval needed only if: remaining is zero AND no special charges AND no installation changes AND no final settlement payments
if remaining == 0 and not has_special_charges and not has_installation_changes and not has_final_settlement_payments:
needs_approval = False
except Exception:
needs_approval = True
return render(request, 'invoices/final_settlement_step.html', {
'instance': instance,
'step': step,
@ -1148,6 +1273,7 @@ def final_settlement_step(request, instance_id, step_id):
'can_approve_reject': can_approve_reject,
'is_broker': is_broker,
'current_user_has_decided': current_user_has_decided,
'needs_approval': needs_approval,
'is_manager': bool(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).filter(slug=UserRoles.MANAGER.value).exists()) if getattr(request.user, 'profile', None) else False,
})
@ -1220,6 +1346,7 @@ def add_final_payment(request, instance_id, step_id):
amount=amount_dec,
payment_date=payment_date,
payment_method=payment_method,
payment_stage='final_settlement',
reference_number=reference_number,
direction='in' if direction != 'out' else 'out',
receipt_image=receipt_image,

View file

@ -3,6 +3,7 @@
{% load humanize %}
{% load common_tags %}
{% load processes_tags %}
{% load accounts_tags %}
{% block sidebar %}
{% include 'sidebars/admin.html' %}
@ -37,9 +38,11 @@
<i class="bx bx-printer me-2"></i> پرینت فاکتور
</a>
{% endif %}
{% if request.user|is_broker or request.user|is_manager %}
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#printHologramModal">
<i class="bx bx-printer me-2"></i> پرینت گواهی
</button>
{% endif %}
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
بازگشت

View file

@ -223,11 +223,24 @@
<tr>
<td>{{ item.instance.code }}</td>
<td>{{ item.instance.process.name }}</td>
<td class="text-primary">
<td>
{% if item.instance.status == 'completed' %}
<a href="{% url 'processes:instance_summary' item.instance.id %}" class="text-primary">{{ item.instance.current_step.name|default:"--" }}</a>
{% elif item.instance.current_step %}
<a href="{% url 'processes:instance_steps' item.instance.id %}" class="text-primary">{{ item.instance.current_step.name }}</a>
{% if item.current_step_approval_status %}
<br>
<small class="{% if item.current_step_approval_status.status == 'rejected' %}text-danger{% elif item.current_step_approval_status.status == 'approved' %}text-success{% else %}text-warning{% endif %}">
{% if item.current_step_approval_status.status == 'rejected' %}
<i class="bx bx-x-circle"></i>
{% elif item.current_step_approval_status.status == 'approved' %}
<i class="bx bx-check-circle"></i>
{% else %}
<i class="bx bx-time"></i>
{% endif %}
{{ item.current_step_approval_status.display }}
</small>
{% endif %}
{% else %}
--
{% endif %}

View file

@ -94,6 +94,17 @@ def request_list(request):
reports_map[pid] = row['visited_date']
except Exception:
reports_map = {}
# Build a map to check if installation reports exist (for approval status logic)
has_installation_report_map = {}
if instance_ids:
try:
report_exists_qs = InstallationReport.objects.filter(
assignment__process_instance_id__in=instance_ids
).values_list('assignment__process_instance_id', flat=True).distinct()
has_installation_report_map = {pid: True for pid in report_exists_qs}
except Exception:
has_installation_report_map = {}
# Calculate progress for each instance and attach install schedule info
instances_with_progress = []
@ -134,6 +145,60 @@ def request_list(request):
except Exception:
emergency_approved = False
# Get current step approval status
current_step_approval_status = None
if instance.current_step:
try:
current_step_instance = instance.step_instances.filter(step=instance.current_step).first()
if current_step_instance:
# Special check: For installation report step (order=6), only show approval status if report exists
should_show_approval_status = True
if instance.current_step.order == 6:
# Check if installation report exists
if not has_installation_report_map.get(instance.id, False):
should_show_approval_status = False
if should_show_approval_status:
# Check if this step requires approvals
required_roles = current_step_instance.required_roles()
if required_roles:
# Get approvals by role
approvals_by_role = current_step_instance.approvals_by_role()
# Check for rejections
latest_rejection = current_step_instance.get_latest_rejection()
if latest_rejection and current_step_instance.status == 'rejected':
role_name = latest_rejection.role.name if latest_rejection.role else 'نامشخص'
current_step_approval_status = {
'status': 'rejected',
'role': role_name,
'display': f'رد شده توسط {role_name}'
}
else:
# Check approval status
pending_roles = []
approved_roles = []
for role in required_roles:
if approvals_by_role.get(role.id) == 'approved':
approved_roles.append(role.name)
else:
pending_roles.append(role.name)
if pending_roles:
current_step_approval_status = {
'status': 'pending',
'roles': pending_roles,
'display': f'در انتظار تایید {" و ".join(pending_roles)}'
}
elif approved_roles and not pending_roles:
current_step_approval_status = {
'status': 'approved',
'roles': approved_roles,
'display': f'تایید شده توسط {" و ".join(approved_roles)}'
}
except Exception:
current_step_approval_status = None
instances_with_progress.append({
'instance': instance,
'progress_percentage': round(progress_percentage),
@ -142,6 +207,7 @@ def request_list(request):
'installation_scheduled_date': installation_scheduled_date,
'installation_overdue_days': overdue_days,
'emergency_approved': emergency_approved,
'current_step_approval_status': current_step_approval_status,
})
# Summary stats for header cards

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,13 @@
<svg width="623" height="389" viewBox="0 0 623 389" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M622.408 2L461.408 163V245L505.285 220.983C507.605 219.713 510.334 219.414 512.875 220.152L524.408 223.5L445.908 301L448.908 387.5L563.772 272.143C564.849 271.062 565.663 269.749 566.154 268.305L574.9 242.566C579.944 227.724 577.735 211.376 568.933 198.404L544.908 163L619.591 85.9085C621.398 84.0431 622.408 81.5478 622.408 78.9505V2Z" fill="#8889FF" stroke="#8889FF"/>
<path d="M286.408 253.895C286.408 247.944 289.059 242.301 293.64 238.502L350.908 191V321.588C350.908 327.26 348.499 332.666 344.281 336.459L286.408 388.5V253.895Z" fill="#8889FF"/>
<path d="M363.908 253.895C363.908 247.944 366.559 242.301 371.14 238.502L428.408 191V321.588C428.408 327.26 425.999 332.666 421.781 336.459L363.908 388.5V253.895Z" fill="#8889FF"/>
<path d="M368.65 179.938C368.65 173.677 371.582 167.777 376.572 163.996L424.083 128L421.982 180.206C421.754 185.873 419.13 191.176 414.765 194.796L368.65 233.043L368.65 179.938Z" fill="#6A6CFF"/>
<path d="M291.558 177.938C291.558 171.677 294.49 165.777 299.48 161.996L346.991 126L344.89 178.206C344.662 183.873 342.039 189.176 337.674 192.796L291.558 231.043L291.558 177.938Z" fill="#6A6CFF"/>
<path d="M291.558 108.938C291.558 102.677 294.49 96.7772 299.48 92.9963L346.991 56.9999L344.89 109.206C344.662 114.873 342.039 120.176 337.674 123.796L291.558 162.043L291.558 108.938Z" fill="#6A6CFF"/>
<path d="M170.908 314.5L101.408 387.5H158.427C163.216 387.5 167.807 385.592 171.185 382.198L270.408 282.5V212.227C270.408 209.128 269.608 206.082 268.086 203.383L265.374 198.575C261.769 192.184 254.644 188.621 247.368 189.57L244.75 189.912C239.249 190.629 233.957 192.483 229.211 195.356L217.408 202.5L199.908 217L184.908 232.5L174.417 246.738C172.138 249.831 170.908 253.573 170.908 257.415V314.5Z" fill="#8889FF"/>
<path d="M152.408 243L7.9082 387H62.2682C67.487 387 72.4991 384.96 76.2347 381.316L146.375 312.886C150.233 309.122 152.408 303.961 152.408 298.571V243Z" fill="#8889FF"/>
<path d="M63.5462 74C63.5462 74 2 161.461 2 195.454C2 229.445 29.5553 257 63.5462 257C97.5371 257 125.092 229.445 125.092 195.454C125.092 161.461 63.5462 74 63.5462 74ZM63.5462 229.92C46.3212 229.92 32.3595 215.956 32.3595 198.733C32.3595 181.508 63.5462 137.189 63.5462 137.189C63.5462 137.189 94.7329 181.508 94.7329 198.733C94.7329 215.956 80.7692 229.92 63.5462 229.92Z" fill="#60B8DF"/>
<path d="M52.9863 153.224C58.7827 143.957 63.5422 137.189 63.5422 137.189L43.5169 104.344C40.2713 109.509 36.8438 115.103 33.4043 120.912C39.8795 130.712 52.5509 152.498 52.9863 153.224Z" fill="#5696AC"/>
<path d="M42.0612 185.986H57.2421V170.301C57.2421 168.922 57.9482 168.233 59.3604 168.233L67.5812 168.334C67.6485 168.334 67.7157 168.334 67.783 168.334C69.027 168.334 69.6491 168.99 69.6491 170.301V185.986H84.9812C86.2589 185.986 86.9146 186.675 86.9482 188.054V196.426C86.9482 197.704 86.2926 198.359 84.9812 198.393H69.8004V214.179C69.8004 215.322 69.0943 215.978 67.6821 216.146H59.3604C58.0827 216.146 57.3766 215.49 57.2421 214.179V198.393H42.0612C40.6827 198.393 39.9934 197.737 39.9934 196.426V188.054C39.9934 186.675 40.6827 185.986 42.0612 185.986Z" fill="#56AE27"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -24340,8 +24340,8 @@ html:not(.layout-footer-fixed) .content-wrapper {
}
.menu-vertical .app-brand {
padding-right: 2rem;
padding-left: 2rem
padding-right: 1.5rem;
padding-left: 1.5rem
}
.menu-horizontal .app-brand, .menu-horizontal .app-brand + .menu-divider {
@ -24379,7 +24379,7 @@ html:not(.layout-footer-fixed) .content-wrapper {
@media (min-width: 1200px) {
.layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand {
width: 5.25rem
width: 6.75rem
}
.layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand-logo, .layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand-link, .layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand-text {

View file

@ -24375,8 +24375,8 @@ html:not(.layout-footer-fixed) .content-wrapper {
}
.menu-vertical .app-brand {
padding-right: 2rem;
padding-left: 2rem
padding-right: 1.5rem;
padding-left: 1.5rem
}
.menu-horizontal .app-brand, .menu-horizontal .app-brand + .menu-divider {
@ -24414,7 +24414,7 @@ html:not(.layout-footer-fixed) .content-wrapper {
@media (min-width: 1200px) {
.layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand {
width: 5.25rem
width: 6.75rem
}
.layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand-logo, .layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand-link, .layout-menu-collapsed:not(.layout-menu-hover):not(.layout-menu-offcanvas):not(.layout-menu-fixed-offcanvas) .layout-menu .app-brand-text {

View file

@ -17,14 +17,10 @@ layout-navbar-fixed layout-menu-fixed layout-compact
</title>
<meta name="description" content="Most Powerful &amp; Comprehensive Bootstrap 5 HTML Admin Dashboard Template built for developers!"/>
<meta name="keywords" content="dashboard, bootstrap 5 dashboard, bootstrap 5 design, bootstrap 5">
<!-- Canonical SEO -->
<link rel="canonical" href="https://themeselection.com/item/sneat-bootstrap-html-admin-template/">
<meta name="description" content="Meter Plus"/>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="{% static 'assets/img/favicon/favicon.ico' %}"/>
<link rel="icon" type="image/x-icon" href="{% static 'assets/img/logo/fav.png' %}"/ height="50">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -99,7 +95,7 @@ layout-navbar-fixed layout-menu-fixed layout-compact
<div class="container-xxl d-flex flex-wrap justify-content-between py-3 flex-md-row flex-column">
<div class="mb-2 mb-md-0">
<div class="d-flex flex-column">
<span class="fw-medium">© {{ current_year|default:"2024" }} تمامی حقوق متعلق به شرکت زیست آب است.</span>
<span class="fw-medium">© {{ current_year|default:"2024" }} تمامی حقوق متعلق به شرکت زیست‌آب پرآب است.</span>
<small class="text-muted mt-1">طراحی و توسعه با ❤️ در ایران</small>
</div>
</div>
@ -112,17 +108,17 @@ layout-navbar-fixed layout-menu-fixed layout-compact
<span class="text-muted">|</span>
<span class="text-muted">
<i class="bx bx-envelope me-1"></i>
پشتیبانی: info@zistab.com
پشتیبانی: info@poraab.com
</span>
<span class="text-muted">|</span>
<span class="text-muted">
<i class="bx bx-phone me-1"></i>
تلفن: 021-12345678
تلفن: 02188728477
</span>
<span class="text-muted">|</span>
<span class="text-muted">
<i class="bx bx-map me-1"></i>
تهران، خیابان ولیعصر
تهران، خیابان شهید بهشتی، پلاک ۴۳۶
</span>
</div>
</div>

View file

@ -5,55 +5,10 @@
<aside id="layout-menu" class="layout-menu menu-vertical menu bg-menu-theme">
<div class="app-brand demo ">
<a href="index.html" class="app-brand-link">
<span class="app-brand-logo demo">
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path
d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z"
id="path-1"></path>
<path
d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z"
id="path-3"></path>
<path
d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z"
id="path-4"></path>
<path
d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z"
id="path-5"></path>
</defs>
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
<g id="Icon" transform="translate(27.000000, 15.000000)">
<g id="Mask" transform="translate(0.000000, 8.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use fill="#696cff" xlink:href="#path-1"></use>
<g id="Path-3" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-3"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
</g>
<g id="Path-4" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-4"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
</g>
</g>
<g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
<use fill="#696cff" xlink:href="#path-5"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
</g>
</g>
</g>
</g>
</svg>
</span>
<span class="app-brand-text demo menu-text fw-bold ms-2 fs-4">سامانه شفافیت</span>
<div class="app-brand demo justify-content-center">
<a href="./" class="app-brand-link">
<img src="{% static 'assets/img/logo/logo.png' %}" alt="logo" class="img-fluid" width="100">
</a>
<a href="#" class="layout-menu-toggle menu-link text-large ms-auto">
<i class="bx bx-chevron-left bx-sm align-middle"></i>
</a>