Compare commits
10 commits
a195e0b6fc
...
e2e4a6dad8
| Author | SHA1 | Date | |
|---|---|---|---|
| e2e4a6dad8 | |||
| 10810f8700 | |||
| 6367d34f0c | |||
| 23f50aacd4 | |||
| 7c2a1ebc7a | |||
| 129500bc1d | |||
| ef0779de6a | |||
| 328c657e0f | |||
| a819e841f9 | |||
| db61f35711 |
34 changed files with 948 additions and 367 deletions
|
|
@ -157,15 +157,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)
|
||||
|
|
|
|||
|
|
@ -193,3 +193,121 @@ def normalize_size(size: int) -> str:
|
|||
else:
|
||||
size_gb = size / (1024 * 1024 * 1024)
|
||||
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 ""
|
||||
|
|
@ -64,7 +64,7 @@ layout-wide customizer-hide
|
|||
</svg>
|
||||
|
||||
</span>
|
||||
<span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
|
||||
<span class="app-brand-text demo text-body fw-bold">کنتور پلاس</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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='کد یکتا هولوگرام'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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 = 'گواهی'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 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:
|
||||
if code:
|
||||
# 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:
|
||||
# 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 or None)
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
@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)}"
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='قطر لوله آبده (اینچ)'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='نیرو محرکه چاه')
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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='مرحله پرداخت'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
{% load common_tags %}
|
||||
|
||||
<!-- Fonts (match base) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
|
@ -48,72 +49,39 @@
|
|||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<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><strong>#فاکتور نهایی {{ instance.code }}</strong></div>
|
||||
<div class="text-muted small">تاریخ صدور: {{ invoice.jcreated_date }}</div>
|
||||
<div>شماره : {{ instance.code }}</div>
|
||||
<div class="small">تاریخ صدور: {{ invoice.jcreated_date }}</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 -->
|
||||
<div class="mb-4">
|
||||
|
|
@ -144,30 +112,34 @@
|
|||
</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>
|
||||
|
|
@ -175,16 +147,8 @@
|
|||
|
||||
<!-- 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
امکان تایید تا تسویه کامل فاکتور وجود ندارد.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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="d-flex gap-2 mb-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.company_name|default:"-" }}</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-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.company_national_id|default:"-" }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-flex gap-2 mb-1">
|
||||
<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="d-flex gap-2 mb-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.national_code }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if instance.representative.profile.phone_number_1 %}
|
||||
<div class="d-flex gap-2 mb-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="d-flex gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
|
||||
</div>
|
||||
<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,17 +254,11 @@
|
|||
<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>
|
||||
</div>
|
||||
{% if instance.broker.company %}
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-3">اطلاعات پرداخت:</h6>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<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>
|
||||
|
|
@ -286,9 +289,18 @@
|
|||
<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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,7 +120,6 @@
|
|||
|
||||
<!-- 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' %}
|
||||
اطلاعات مشترک (حقوقی)
|
||||
|
|
@ -152,29 +127,22 @@
|
|||
اطلاعات مشترک (حقیقی)
|
||||
{% 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="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> {{ quote.customer.get_full_name }}</div>
|
||||
<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="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 -->
|
||||
<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>
|
||||
<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,9 +221,23 @@
|
|||
{% 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>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section (optional, compact) -->
|
||||
{% if quote.notes %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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=next_step.id)
|
||||
return redirect(redirect_url)
|
||||
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, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
|
||||
si.status = 'in_progress'
|
||||
si.completed_at = None
|
||||
si.save(update_fields=['status', 'completed_at'])
|
||||
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, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
|
||||
si.status = 'in_progress'
|
||||
si.completed_at = None
|
||||
si.save(update_fields=['status', 'completed_at'])
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
بازگشت
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,17 @@ def request_list(request):
|
|||
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 = []
|
||||
for instance in instances:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
BIN
static/assets/img/logo/fav.png
Normal file
BIN
static/assets/img/logo/fav.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
static/assets/img/logo/logo.png
Normal file
BIN
static/assets/img/logo/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
13
static/assets/img/logo/logo.svg
Normal file
13
static/assets/img/logo/logo.svg
Normal 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 |
6
static/assets/vendor/css/rtl/core-dark.css
vendored
6
static/assets/vendor/css/rtl/core-dark.css
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
6
static/assets/vendor/css/rtl/core.css
vendored
6
static/assets/vendor/css/rtl/core.css
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -17,14 +17,10 @@ layout-navbar-fixed layout-menu-fixed layout-compact
|
|||
</title>
|
||||
|
||||
|
||||
<meta name="description" content="Most Powerful & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue