diff --git a/_base/settings.py b/_base/settings.py index e9079ca..6283ccf 100644 --- a/_base/settings.py +++ b/_base/settings.py @@ -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) diff --git a/_helpers/utils.py b/_helpers/utils.py index a7adccb..92f24b4 100644 --- a/_helpers/utils.py +++ b/_helpers/utils.py @@ -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" \ No newline at end of file + 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 "" \ No newline at end of file diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html index e43800d..f66c7b5 100644 --- a/accounts/templates/accounts/login.html +++ b/accounts/templates/accounts/login.html @@ -64,7 +64,7 @@ layout-wide customizer-hide - سامانه شفافیت + کنتور پلاس diff --git a/certificates/admin.py b/certificates/admin.py index de9ba72..f1eff8a 100644 --- a/certificates/admin.py +++ b/certificates/admin.py @@ -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') - - diff --git a/certificates/migrations/0003_alter_certificateinstance_hologram_code.py b/certificates/migrations/0003_alter_certificateinstance_hologram_code.py new file mode 100644 index 0000000..00fb9cd --- /dev/null +++ b/certificates/migrations/0003_alter_certificateinstance_hologram_code.py @@ -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='کد یکتا هولوگرام'), + ), + ] diff --git a/certificates/models.py b/certificates/models.py index a7afe72..c374035 100644 --- a/certificates/models.py +++ b/certificates/models.py @@ -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 = 'گواهی' diff --git a/certificates/templates/certificates/step.html b/certificates/templates/certificates/step.html index 2f7e089..f8249cb 100644 --- a/certificates/templates/certificates/step.html +++ b/certificates/templates/certificates/step.html @@ -38,9 +38,11 @@
+ {% if request.user|is_broker or request.user|is_manager %} + {% endif %} @@ -52,7 +54,7 @@
{% stepper_header instance step %}
- + {% if request.user|is_broker or request.user|is_manager or request.user|is_water_resource_manager %}
@@ -115,6 +117,21 @@
+ {% else %} +
+
+
+
+ +
+

دسترسی محدود

+

+ متأسفانه شما دسترسی لازم برای مشاهده این صفحه را ندارید.
+

+
+
+
+ {% endif %}
diff --git a/certificates/views.py b/certificates/views.py index 5149334..26a5d6f 100644 --- a/certificates/views.py +++ b/certificates/views.py @@ -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, diff --git a/common/templatetags/common_tags.py b/common/templatetags/common_tags.py index ef17d42..2373852 100644 --- a/common/templatetags/common_tags.py +++ b/common/templatetags/common_tags.py @@ -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) \ No newline at end of file + 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)}" \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 4408b15..312b49a 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/installations/forms.py b/installations/forms.py index e877b2b..b8be2b0 100644 --- a/installations/forms.py +++ b/installations/forms.py @@ -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={ diff --git a/installations/migrations/0008_alter_installationreport_post_calibration_flow_rate_and_more.py b/installations/migrations/0008_alter_installationreport_post_calibration_flow_rate_and_more.py new file mode 100644 index 0000000..b69ada5 --- /dev/null +++ b/installations/migrations/0008_alter_installationreport_post_calibration_flow_rate_and_more.py @@ -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='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'), + ), + ] diff --git a/installations/migrations/0009_alter_installationreport_meter_model_and_more.py b/installations/migrations/0009_alter_installationreport_meter_model_and_more.py new file mode 100644 index 0000000..e5e3e8b --- /dev/null +++ b/installations/migrations/0009_alter_installationreport_meter_model_and_more.py @@ -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='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'), + ), + ] diff --git a/installations/migrations/0010_alter_installationreport_motor_power_and_more.py b/installations/migrations/0010_alter_installationreport_motor_power_and_more.py new file mode 100644 index 0000000..3cc7f90 --- /dev/null +++ b/installations/migrations/0010_alter_installationreport_motor_power_and_more.py @@ -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='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'), + ), + ] diff --git a/installations/migrations/0011_alter_installationreport_discharge_pipe_diameter.py b/installations/migrations/0011_alter_installationreport_discharge_pipe_diameter.py new file mode 100644 index 0000000..ae5e2bb --- /dev/null +++ b/installations/migrations/0011_alter_installationreport_discharge_pipe_diameter.py @@ -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='قطر لوله آبده (اینچ)'), + ), + ] diff --git a/installations/models.py b/installations/models.py index 6cd48bd..c7d7e31 100644 --- a/installations/models.py +++ b/installations/models.py @@ -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='نیرو محرکه چاه') diff --git a/installations/templates/installations/installation_report_step.html b/installations/templates/installations/installation_report_step.html index 2d14128..3246dea 100644 --- a/installations/templates/installations/installation_report_step.html +++ b/installations/templates/installations/installation_report_step.html @@ -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; + } {% endblock %} @@ -38,6 +92,15 @@ {% include '_toasts.html' %} + +
+
+
+
در حال آپلود...
+
لطفا تا بارگذاری کامل گزارش منتظر بمانید.
+
+
+ {% instance_info_modal instance %} @@ -516,7 +579,7 @@ {% if user_is_installer %} {% endif %} - {% if next_step and not edit_mode %} + {% if next_step and not edit_mode and report %}
بعدی @@ -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 = 'در حال ارسال...'; + } + 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') { diff --git a/invoices/migrations/0003_historicalpayment_payment_stage_and_more.py b/invoices/migrations/0003_historicalpayment_payment_stage_and_more.py new file mode 100644 index 0000000..f0c30bb --- /dev/null +++ b/invoices/migrations/0003_historicalpayment_payment_stage_and_more.py @@ -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='مرحله پرداخت'), + ), + ] diff --git a/invoices/models.py b/invoices/models.py index b93e4b4..b61d487 100644 --- a/invoices/models.py +++ b/invoices/models.py @@ -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) diff --git a/invoices/templates/invoices/final_invoice_print.html b/invoices/templates/invoices/final_invoice_print.html index 36e23f0..167306f 100644 --- a/invoices/templates/invoices/final_invoice_print.html +++ b/invoices/templates/invoices/final_invoice_print.html @@ -7,6 +7,7 @@ {% load static %} {% load humanize %} + {% load common_tags %} @@ -48,71 +49,38 @@
- +
-
-
-
- {% if instance.broker.company and instance.broker.company.logo %} - لوگو - {% else %} - - {% endif %} -
-
- {% if instance.broker.company %} - {{ instance.broker.company.name }} - {% endif %} - {% if instance.broker.company %} -
- {% if instance.broker.company.address %} -
{{ instance.broker.company.address }}
- {% endif %} - {% if instance.broker.affairs.county.city.name %} -
{{ instance.broker.affairs.county.city.name }}، ایران
- {% endif %} - {% if instance.broker.company.phone %} -
تلفن: {{ instance.broker.company.phone }}
- {% endif %} +
+
فاکتور
+
+
+
شماره : {{ instance.code }}
+
تاریخ صدور: {{ invoice.jcreated_date }}
- {% endif %}
-
-
-
-
#فاکتور نهایی {{ instance.code }}
-
تاریخ صدور: {{ invoice.jcreated_date }}
-
-
-
+
+
-
اطلاعات مشترک {% if instance.representative.profile and instance.representative.profile.user_type == 'legal' %}(حقوقی){% else %}(حقیقی){% endif %}
+
شماره اشتراک آب: {{ instance.well.water_subscription_number }}
{% if instance.representative.profile and instance.representative.profile.user_type == 'legal' %} -
نام شرکت: {{ instance.representative.profile.company_name|default:"-" }}
-
شناسه ملی: {{ instance.representative.profile.company_national_id|default:"-" }}
+
نام شرکت: {{ instance.representative.profile.company_name|default:"-" }}
+
شناسه ملی: {{ instance.representative.profile.company_national_id|default:"-" }}
{% endif %} -
نام: {{ invoice.customer.get_full_name|default:instance.representative.get_full_name }}
+
نام و نام خانوادگی: {{ invoice.customer.get_full_name|default:instance.representative.get_full_name }}
{% if instance.representative.profile and instance.representative.profile.national_code %} -
کد ملی: {{ instance.representative.profile.national_code }}
+
کد ملی: {{ instance.representative.profile.national_code }}
{% endif %} {% if instance.representative.profile and instance.representative.profile.phone_number_1 %} -
تلفن: {{ instance.representative.profile.phone_number_1 }}
+
تلفن: {{ instance.representative.profile.phone_number_1 }}
{% endif %} {% if instance.representative.profile and instance.representative.profile.address %} -
آدرس: {{ instance.representative.profile.address }}
+
آدرس: {{ instance.representative.profile.address }}
{% endif %} -
-
-
اطلاعات چاه
-
شماره اشتراک آب: {{ instance.well.water_subscription_number }}
-
شماره اشتراک برق: {{ instance.well.electricity_subscription_number|default:"-" }}
-
سریال کنتور: {{ instance.well.water_meter_serial_number|default:"-" }}
-
قدرت چاه: {{ instance.well.well_power|default:"-" }}
-
@@ -144,47 +112,43 @@ - جمع کل(ریال): - {{ invoice.total_amount|floatformat:0|intcomma:False }} + جمع کل(ریال): + {{ invoice.total_amount|floatformat:0|intcomma:False }} {% if invoice.discount_amount > 0 %} - تخفیف(ریال): - {{ invoice.discount_amount|floatformat:0|intcomma:False }} + تخفیف(ریال): + {{ invoice.discount_amount|floatformat:0|intcomma:False }} {% endif %} - مالیات بر ارزش افزوده(ریال): - {{ invoice.get_vat_amount|floatformat:0|intcomma:False }} + مالیات بر ارزش افزوده(ریال): + {{ invoice.get_vat_amount|floatformat:0|intcomma:False }} - مبلغ نهایی (شامل مالیات)(ریال): - {{ invoice.final_amount|floatformat:0|intcomma:False }} + مبلغ نهایی (شامل مالیات)(ریال): + {{ invoice.final_amount|floatformat:0|intcomma:False }} - پرداختی‌ها(ریال): - {{ invoice.get_paid_amount|floatformat:0|intcomma:False }} + پرداختی‌ها(ریال): + {{ invoice.get_paid_amount|floatformat:0|intcomma:False }} - مانده(ریال): - {{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} + مانده(ریال): + {{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} + + مبلغ نهایی به حروف: + {{ invoice.final_amount|amount_to_words }} +
-
-
مهر و امضا:
-
    - {% if instance.broker.company and instance.broker.company.signature %} -
  • امضا
  • - {% endif %} -
-
{% if instance.broker.company %} -
+
اطلاعات پرداخت
{% if instance.broker.company.card_number %}
شماره کارت: {{ instance.broker.company.card_number }}
@@ -200,6 +164,20 @@ {% endif %}
{% endif %} + +
+ {% if instance.broker.company and instance.broker.company.signature %} +
+
مهر و امضا + {% if instance.broker.company.signature %} + امضای شرکت + {% endif %} +
+ +
+ {% endif %} +
+
diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html index 0e7916b..97a3861 100644 --- a/invoices/templates/invoices/final_settlement_step.html +++ b/invoices/templates/invoices/final_settlement_step.html @@ -60,7 +60,7 @@
- {% if is_broker and invoice.get_remaining_amount != 0 %} + {% if is_broker and needs_approval %}
ثبت تراکنش تسویه
@@ -193,7 +193,7 @@
- {% 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' %}
وضعیت تاییدها
@@ -318,7 +318,11 @@