diff --git a/.gitignore b/.gitignore index 77e02cd..d975f31 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,8 @@ *.pyc __pycache__/ local_settings.py -# *.sqlite3 -# db.sqlite3 +*.sqlite3 +db.sqlite3 db.sqlite3-journal media #static diff --git a/installations/forms.py b/installations/forms.py index 86e7428..e877b2b 100644 --- a/installations/forms.py +++ b/installations/forms.py @@ -20,7 +20,7 @@ class InstallationReportForm(forms.ModelForm): model = InstallationReport fields = [ 'visited_date', 'new_water_meter_serial', 'seal_number', - 'utm_x', 'utm_y', 'meter_type', 'meter_size', + 'utm_x', 'utm_y', 'meter_type', 'meter_size', 'meter_model', 'discharge_pipe_diameter', 'usage_type', 'exploitation_license_number', 'motor_power', 'pre_calibration_flow_rate', 'post_calibration_flow_rate', 'water_meter_manufacturer', 'sim_number', 'driving_force', @@ -62,6 +62,13 @@ class InstallationReportForm(forms.ModelForm): 'meter_size': forms.TextInput(attrs={ 'class': 'form-control' }), + 'meter_model': forms.Select(attrs={ + 'class': 'form-select' + }, choices=[ + ('', 'انتخاب کنید'), + ('A', 'A'), + ('B', 'B') + ]), 'discharge_pipe_diameter': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0', diff --git a/installations/models.py b/installations/models.py index 6eed7a5..6cd48bd 100644 --- a/installations/models.py +++ b/installations/models.py @@ -47,6 +47,11 @@ class InstallationReport(BaseModel): ('volumetric', 'حجمی'), ] meter_type = models.CharField(max_length=20, choices=METER_TYPE_CHOICES, null=True, blank=True, verbose_name='نوع کنتور') + METER_MODEL_CHOICES = [ + ('A', 'A'), + ('B', 'B'), + ] + 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='قطر لوله آبده (اینچ)') USAGE_TYPE_CHOICES = [ diff --git a/installations/templates/installations/installation_report_step.html b/installations/templates/installations/installation_report_step.html index d6a7cfc..2d14128 100644 --- a/installations/templates/installations/installation_report_step.html +++ b/installations/templates/installations/installation_report_step.html @@ -86,7 +86,11 @@

سریال جدید: {{ report.new_water_meter_serial|default:'-' }}

شماره پلمپ: {{ report.seal_number|default:'-' }}

نوع کنتور: {{ report.get_meter_type_display|default:'-' }}

+ {% if report.meter_type == 'smart' %} +

مدل کنتور: {{ report.get_meter_model_display|default:'-' }}

+ {% else %}

سایز کنتور: {{ report.meter_size|default:'-' }}

+ {% endif %}

قطر لوله آبده (اینچ): {{ report.discharge_pipe_diameter|default:'-' }}

سازنده کنتور: {{ report.water_meter_manufacturer|default:'-' }}

شماره سیمکارت: {{ report.sim_number|default:'-' }}

@@ -279,13 +283,20 @@
{{ form.meter_type.errors.0 }}
{% endif %} -
+
{{ form.meter_size.label_tag }} {{ form.meter_size }} {% if form.meter_size.errors %}
{{ form.meter_size.errors.0 }}
{% endif %}
+
+ {{ form.meter_model.label_tag }} + {{ form.meter_model }} + {% if form.meter_model.errors %} +
{{ form.meter_size.errors.0 }}
+ {% endif %} +
{{ form.discharge_pipe_diameter.label_tag }} {{ form.discharge_pipe_diameter }} @@ -329,7 +340,7 @@ {% endif %}
- {{ form.water_meter_manufacturer.label_tag }} + {{ form.water_meter_manufacturer.label_tag }}حجمی
{{ form.water_meter_manufacturer }} {{ form.new_manufacturer }} @@ -435,7 +446,7 @@ {% if qi.item.description %}{{ qi.item.description }}{% endif %}
- {{ qi.unit_price|floatformat:0|intcomma:False }} تومان + {{ qi.unit_price|floatformat:0|intcomma:False }} ریال {% if removed_qty|get_item:qi.item.id %}{{ removed_qty|get_item:qi.item.id }}{% else %}{{ qi.quantity }}{% endif %} @@ -474,7 +485,7 @@ {% if it.description %}{{ it.description }}{% endif %}
- {{ it.unit_price|floatformat:0|intcomma:False }} تومان + {{ it.unit_price|floatformat:0|intcomma:False }} ریال {% with add_entry=added_map|get_item:it.id %} @@ -505,7 +516,7 @@ {% if user_is_installer %} {% endif %} - {% if next_step %} + {% if next_step and not edit_mode %} بعدی @@ -759,6 +770,47 @@ } } } + + // Dynamic meter field visibility based on meter type + (function() { + const meterTypeSelect = document.getElementById('{{ form.meter_type.id_for_label }}'); + const meterSizeWrapper = document.getElementById('meter_size_wrapper'); + const meterModelWrapper = document.getElementById('meter_model_wrapper'); + + function updateMeterFields() { + if (!meterTypeSelect) return; + + const selectedType = meterTypeSelect.value; + + if (selectedType === 'smart') { + // Show meter_model, hide meter_size + meterModelWrapper.style.display = ''; + meterSizeWrapper.style.display = 'none'; + // Clear meter_size value when hidden + const meterSizeInput = meterSizeWrapper.querySelector('input, select'); + if (meterSizeInput) meterSizeInput.value = ''; + } else if (selectedType === 'volumetric') { + // Show meter_size, hide meter_model + meterSizeWrapper.style.display = ''; + meterModelWrapper.style.display = 'none'; + // Clear meter_model value when hidden + const meterModelSelect = meterModelWrapper.querySelector('select'); + if (meterModelSelect) meterModelSelect.value = ''; + } else { + // No selection: hide both + meterSizeWrapper.style.display = 'none'; + meterModelWrapper.style.display = 'none'; + } + } + + // Initial update on page load + updateMeterFields(); + + // Update on change + if (meterTypeSelect) { + meterTypeSelect.addEventListener('change', updateMeterFields); + } + })(); {% endblock %} diff --git a/invoices/admin.py b/invoices/admin.py index 72df296..a53e692 100644 --- a/invoices/admin.py +++ b/invoices/admin.py @@ -57,13 +57,13 @@ class InvoiceAdmin(SimpleHistoryAdmin): status_display.short_description = "وضعیت" def paid_amount_display(self, obj): - return f"{obj.get_paid_amount():,.0f} تومان" + return f"{obj.get_paid_amount():,.0f} ریال" paid_amount_display.short_description = "مبلغ پرداخت شده" def remaining_amount_display(self, obj): amount = obj.get_remaining_amount() color = "green" if amount <= 0 else "red" - return format_html('{:,.0f} تومان', color, amount) + return format_html('{:,.0f} ریال', color, amount) remaining_amount_display.short_description = "مبلغ باقی‌مانده" @admin.register(Payment) diff --git a/invoices/models.py b/invoices/models.py index 4a48c8c..b93e4b4 100644 --- a/invoices/models.py +++ b/invoices/models.py @@ -38,7 +38,7 @@ class Item(NameSlugModel): ordering = ['name'] def __str__(self): - return f"{self.name} - {self.unit_price} تومان" + return f"{self.name} - {self.unit_price} ریال" class Quote(NameSlugModel): """مدل پیش‌فاکتور""" @@ -137,11 +137,11 @@ class Quote(NameSlugModel): return '{}'.format(color, self.get_status_display()) def get_paid_amount(self): - """مبلغ پرداخت شده برای این پیش‌فاکتور بر اساس پرداخت‌های فاکتور مرتبط""" + """خالص پرداختی (دریافتی از مشتری منهای پرداختی به مشتری) برای این پیش‌فاکتور بر اساس پرداخت‌های فاکتور مرتبط""" invoice = Invoice.objects.filter(quote=self).first() if not invoice: return Decimal('0') - return sum(p.amount for p in invoice.payments.filter(is_deleted=False).all()) + return sum((p.amount if p.direction == 'in' else -p.amount) for p in invoice.payments.filter(is_deleted=False).all()) def get_remaining_amount(self): """مبلغ باقی‌مانده بر اساس پرداخت‌ها""" @@ -151,6 +151,15 @@ class Quote(NameSlugModel): remaining = Decimal('0') return remaining + def get_vat_amount(self) -> Decimal: + """محاسبه مبلغ مالیات به صورت جداگانه بر اساس VAT_RATE.""" + base_amount = (self.total_amount or Decimal('0')) - (self.discount_amount or Decimal('0')) + try: + vat_rate = Decimal(str(getattr(settings, 'VAT_RATE', 0))) + except Exception: + vat_rate = Decimal('0') + return base_amount * vat_rate + class QuoteItem(BaseModel): """مدل آیتم‌های پیش‌فاکتور""" quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیش‌فاکتور") @@ -291,6 +300,15 @@ class Invoice(NameSlugModel): remaining = self.final_amount - paid return remaining + def get_vat_amount(self) -> Decimal: + """محاسبه مبلغ مالیات به صورت جداگانه بر اساس VAT_RATE.""" + base_amount = (self.total_amount or Decimal('0')) - (self.discount_amount or Decimal('0')) + try: + vat_rate = Decimal(str(getattr(settings, 'VAT_RATE', 0))) + except Exception: + vat_rate = Decimal('0') + return base_amount * vat_rate + def get_status_display_with_color(self): """نمایش وضعیت با رنگ""" @@ -365,7 +383,7 @@ class Payment(BaseModel): ordering = ['-payment_date'] def __str__(self): - return f"پرداخت {self.amount} تومان - {self.invoice.name}" + return f"پرداخت {self.amount} ریال - {self.invoice.name}" def save(self, *args, **kwargs): """بروزرسانی مبالغ فاکتور""" diff --git a/invoices/templates/invoices/final_invoice_print.html b/invoices/templates/invoices/final_invoice_print.html index d9c8333..36e23f0 100644 --- a/invoices/templates/invoices/final_invoice_print.html +++ b/invoices/templates/invoices/final_invoice_print.html @@ -124,8 +124,8 @@ شرح کالا/خدمات توضیحات تعداد - قیمت واحد(تومان) - قیمت کل(تومان) + قیمت واحد(ریال) + قیمت کل(ریال) @@ -144,25 +144,29 @@ - جمع کل(تومان): + جمع کل(ریال): {{ invoice.total_amount|floatformat:0|intcomma:False }} {% if invoice.discount_amount > 0 %} - تخفیف(تومان): + تخفیف(ریال): {{ invoice.discount_amount|floatformat:0|intcomma:False }} {% endif %} + + مالیات بر ارزش افزوده(ریال): + {{ invoice.get_vat_amount|floatformat:0|intcomma:False }} + - مبلغ نهایی (شامل مالیات)(تومان): + مبلغ نهایی (شامل مالیات)(ریال): {{ invoice.final_amount|floatformat:0|intcomma:False }} - پرداختی‌ها(تومان): + پرداختی‌ها(ریال): {{ invoice.get_paid_amount|floatformat:0|intcomma:False }} - مانده(تومان): + مانده(ریال): {{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} diff --git a/invoices/templates/invoices/final_invoice_step.html b/invoices/templates/invoices/final_invoice_step.html index 1d99072..c2e3b64 100644 --- a/invoices/templates/invoices/final_invoice_step.html +++ b/invoices/templates/invoices/final_invoice_step.html @@ -68,19 +68,19 @@
مبلغ نهایی (با مالیات)
-
{{ 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 }} ریال
@@ -100,8 +100,8 @@ افزوده حذف تعداد نهایی - قیمت واحد (تومان) - قیمت کل (تومان) + قیمت واحد (ریال) + قیمت کل (ریال) @@ -153,23 +153,27 @@ مبلغ کل - {{ invoice.total_amount|floatformat:0|intcomma:False }} تومان + {{ invoice.total_amount|floatformat:0|intcomma:False }} ریال تخفیف - {{ invoice.discount_amount|floatformat:0|intcomma:False }} تومان + {{ invoice.discount_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 }} ریال @@ -223,8 +227,8 @@
- - + +
@@ -246,8 +250,17 @@ else { el.classList.add('show'); el.style.display = 'block'; } } function submitSpecialCharge(){ - const fd = new FormData(document.getElementById('specialChargeForm')); + const form = document.getElementById('specialChargeForm'); + const fd = new FormData(form); fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value); + // Ensure raw numeric amount is sent + (function ensureRawAmount(){ + const amountInput = document.getElementById('id_charge_amount'); + if (amountInput){ + const raw = (amountInput.getAttribute('data-raw-value') || amountInput.value.replace(/\D/g, '')); + if (raw) fd.set('amount', raw); + } + })(); fetch('{% url "invoices:add_special_charge" instance.id step.id %}', { method: 'POST', body: fd }) .then(r=>r.json()).then(resp=>{ if (resp.success){ @@ -285,6 +298,8 @@ } }).catch(()=> showToast('خطا در ارتباط با سرور', 'danger')); }); + + // Number formatting is handled by number-formatter.js {% endblock %} diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html index a4767d4..0e7916b 100644 --- a/invoices/templates/invoices/final_settlement_step.html +++ b/invoices/templates/invoices/final_settlement_step.html @@ -60,7 +60,7 @@
- {% if is_broker %} + {% if is_broker and invoice.get_remaining_amount != 0 %}
ثبت تراکنش تسویه
@@ -75,8 +75,8 @@
- - + +
@@ -122,19 +122,19 @@
مبلغ نهایی (با مالیات)
-
{{ 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 }} ریال
@@ -166,7 +166,7 @@ {% for p in payments %} {% if p.direction == 'in' %}دریافتی{% else %}پرداختی{% endif %} - {{ p.amount|floatformat:0|intcomma:False }} تومان + {{ p.amount|floatformat:0|intcomma:False }} ریال {{ p.jpayment_date }} {{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }} @@ -193,7 +193,7 @@
- {% if approver_statuses %} + {% if approver_statuses and invoice.get_remaining_amount != 0 and step_instance.status != 'completed' %}
وضعیت تاییدها
@@ -320,7 +320,7 @@
- - + +
@@ -117,19 +117,19 @@
مبلغ نهایی پیش‌فاکتور (با مالیات)
-
{{ totals.final_amount|floatformat:0|intcomma:False }} تومان
+
{{ totals.final_amount|floatformat:0|intcomma:False }} ریال
مبلغ پرداخت‌شده
-
{{ totals.paid_amount|floatformat:0|intcomma:False }} تومان
+
{{ totals.paid_amount|floatformat:0|intcomma:False }} ریال
مانده
-
{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان
+
{{ totals.remaining_amount|floatformat:0|intcomma:False }} ریال
@@ -153,6 +153,7 @@ + @@ -163,7 +164,8 @@ {% for p in payments %} - + + @@ -175,9 +177,7 @@ {% endif %} {% if is_broker %} - + {% endif %} @@ -301,7 +301,7 @@ {% if not totals.is_fully_paid %} آیا مطمئن هستید که می‌خواهید مرحله را تایید کنید؟ {% else %} @@ -366,6 +366,12 @@ } const form = document.getElementById('formAddPayment'); const fd = buildFormData(form); + // Ensure raw numeric amount is sent + (function ensureRawAmount(){ + const amountInput = document.getElementById('id_amount'); + const raw = (amountInput.getAttribute('data-raw-value') || amountInput.value.replace(/\D/g, '')); + if (raw) fd.set('amount', raw); + })(); // تبدیل تاریخ شمسی به میلادی برای ارسال const persianDateValue = $('#id_payment_date').val(); @@ -383,7 +389,7 @@ setTimeout(() => { window.location.href = resp.redirect; }, 700); } } else { - showToast(resp.message + ':' + resp.error || 'خطا در ثبت فیش', 'danger'); + showToast((resp.message || resp.error || 'خطا در ثبت فیش'), 'danger'); } }).catch(() => showToast('خطا در ارتباط با سرور', 'danger')); }); @@ -460,6 +466,7 @@ } catch (e) { console.error('Error initializing Persian Date Picker:', e); } } })(); + {% endblock %} diff --git a/invoices/templates/invoices/quote_preview_step.html b/invoices/templates/invoices/quote_preview_step.html index eeead73..0742094 100644 --- a/invoices/templates/invoices/quote_preview_step.html +++ b/invoices/templates/invoices/quote_preview_step.html @@ -200,9 +200,9 @@ - + - + {% endfor %} @@ -213,14 +213,16 @@ {% if quote.discount_amount > 0 %}

تخفیف:

{% endif %} +

مالیات بر ارزش افزوده:

مبلغ نهایی (شامل مالیات):

diff --git a/invoices/templates/invoices/quote_print.html b/invoices/templates/invoices/quote_print.html index fc445a6..052f579 100644 --- a/invoices/templates/invoices/quote_print.html +++ b/invoices/templates/invoices/quote_print.html @@ -185,8 +185,8 @@ - - + + @@ -203,17 +203,21 @@ - + {% if quote.discount_amount > 0 %} - + {% endif %} + + + + - + diff --git a/invoices/templates/invoices/quote_step.html b/invoices/templates/invoices/quote_step.html index 853f250..82ec091 100644 --- a/invoices/templates/invoices/quote_step.html +++ b/invoices/templates/invoices/quote_step.html @@ -57,7 +57,7 @@
پیش‌فاکتور موجود
{{ existing_quote.name }} | - مبلغ کل (با احتساب مالیات): {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | + مبلغ کل (با احتساب مالیات): {{ existing_quote.final_amount|floatformat:0|intcomma:False }} ریال | وضعیت: {{ existing_quote.get_status_display_with_color|safe }}
@@ -97,7 +97,7 @@ {% if item.description %}{{ item.description }}{% endif %} - +
نوع مبلغ تاریخ پرداخت/سررسید چک روش
{{ p.amount|floatformat:0|intcomma:False }} تومان{% if p.direction == 'in' %}دریافتی{% else %}پرداختی{% endif %}{{ p.amount|floatformat:0|intcomma:False }} ریال {{ p.jpayment_date }} {{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }}
{{ quote_item.item.name }} {{ quote_item.item.description|default:"-" }}{{ quote_item.unit_price|floatformat:0|intcomma:False }} تومان{{ quote_item.unit_price|floatformat:0|intcomma:False }} ریال {{ quote_item.quantity }}{{ quote_item.total_price|floatformat:0|intcomma:False }} تومان{{ quote_item.total_price|floatformat:0|intcomma:False }} ریال
-

{{ quote.total_amount|floatformat:0|intcomma:False }} تومان

+

{{ quote.total_amount|floatformat:0|intcomma:False }} ریال

{% if quote.discount_amount > 0 %} -

{{ quote.discount_amount|floatformat:0|intcomma:False }} تومان

+

{{ quote.discount_amount|floatformat:0|intcomma:False }} ریال

{% endif %} -

{{ quote.final_amount|floatformat:0|intcomma:False }} تومان

+

{{ quote.get_vat_amount|floatformat:0|intcomma:False }} ریال

+

{{ quote.final_amount|floatformat:0|intcomma:False }} ریال

شرح کالا/خدمات توضیحات تعدادقیمت واحد(تومان)قیمت کل(تومان)قیمت واحد(ریال)قیمت کل(ریال)
جمع کل(تومان):جمع کل(ریال): {{ quote.total_amount|floatformat:0|intcomma:False }}
تخفیف(تومان):تخفیف(ریال): {{ quote.discount_amount|floatformat:0|intcomma:False }}
مالیات بر ارزش افزوده(ریال):{{ quote.get_vat_amount|floatformat:0|intcomma:False }}
مبلغ نهایی (با مالیات)(تومان):مبلغ نهایی (با مالیات)(ریال): {{ quote.final_amount|floatformat:0|intcomma:False }}
{{ item.unit_price|floatformat:0|intcomma:False }} تومان{{ item.unit_price|floatformat:0|intcomma:False }} ریال {% if invoice %}
-
مبلغ نهایی
{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان
-
پرداختی‌ها
{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان
-
مانده
{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان
+
مبلغ نهایی
{{ invoice.final_amount|floatformat:0|intcomma:False }} ریال
+
پرداختی‌ها
{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} ریال
+
مانده
{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} ریال
@@ -237,7 +237,7 @@ {% for p in payments %} - + diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html index 9ec7e92..eee1727 100644 --- a/processes/templates/processes/request_list.html +++ b/processes/templates/processes/request_list.html @@ -245,7 +245,12 @@ {{ item.progress_percentage }}% - +
{% if p.direction == 'in' %}دریافتی{% else %}پرداختی{% endif %}{{ p.amount|floatformat:0|intcomma:False }} تومان{{ p.amount|floatformat:0|intcomma:False }} ریال {{ p.payment_date|date:'Y/m/d' }} {{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }} {{ item.instance.get_status_display_with_color|safe }} + {{ item.instance.get_status_display_with_color|safe }} + {% if item.emergency_approved %} + تایید اضطراری + {% endif %} + {% if item.installation_scheduled_date %}
diff --git a/processes/views.py b/processes/views.py index e959b2e..372c8e7 100644 --- a/processes/views.py +++ b/processes/views.py @@ -123,6 +123,17 @@ def request_list(request): reference_date = None installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date + + # Emergency approved flag (final settlement step forced approval) + try: + final_settlement_step = instance.process.steps.filter(order=8).first() + emergency_approved = False + if final_settlement_step: + si = instance.step_instances.filter(step=final_settlement_step).first() + emergency_approved = bool(si and si.status == 'approved') + except Exception: + emergency_approved = False + instances_with_progress.append({ 'instance': instance, 'progress_percentage': round(progress_percentage), @@ -130,6 +141,7 @@ def request_list(request): 'total_steps': total_steps, 'installation_scheduled_date': installation_scheduled_date, 'installation_overdue_days': overdue_days, + 'emergency_approved': emergency_approved, }) # Summary stats for header cards diff --git a/static/assets/js/number-formatter.js b/static/assets/js/number-formatter.js new file mode 100644 index 0000000..47b2e88 --- /dev/null +++ b/static/assets/js/number-formatter.js @@ -0,0 +1,144 @@ +/** + * Number Formatter Utility + * Formats numbers with comma separators for better readability + */ + +// Format number with comma separators (e.g., 1234567 -> 1,234,567) +function formatNumber(num) { + if (!num) return ''; + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +// Remove comma separators from formatted number +function unformatNumber(str) { + if (!str) return ''; + return str.replace(/,/g, ''); +} + +// Extract only digits from any string +function extractDigits(str) { + if (!str) return ''; + return str.replace(/\D/g, ''); +} + +// Initialize number formatting for specified input selectors +function initNumberFormatting(selectors) { + if (typeof $ === 'undefined') { + console.warn('jQuery not found. Number formatting requires jQuery.'); + return; + } + + $(document).ready(function() { + selectors.forEach(function(selector) { + // Store cursor position to maintain it after formatting + function setCursorPosition(input, pos) { + if (input.setSelectionRange) { + input.setSelectionRange(pos, pos); + } + } + + $(selector).on('input', function(e) { + let input = $(this); + let inputElement = this; + let value = input.val(); + let cursorPos = inputElement.selectionStart; + + // Extract only digits + let digitsOnly = extractDigits(value); + + // Store raw value + input.attr('data-raw-value', digitsOnly); + + // Format and set the value + let formattedValue = formatNumber(digitsOnly); + input.val(formattedValue); + + // Adjust cursor position + let oldLength = value.length; + let newLength = formattedValue.length; + let newCursorPos = cursorPos + (newLength - oldLength); + + // Make sure cursor position is valid + if (newCursorPos < 0) newCursorPos = 0; + if (newCursorPos > newLength) newCursorPos = newLength; + + // Set cursor position after a short delay + setTimeout(function() { + setCursorPosition(inputElement, newCursorPos); + }, 1); + }); + + // Handle paste events + $(selector).on('paste', function(e) { + let input = $(this); + setTimeout(function() { + let value = input.val(); + let digitsOnly = extractDigits(value); + input.attr('data-raw-value', digitsOnly); + input.val(formatNumber(digitsOnly)); + }, 1); + }); + }); + + // Before form submission, replace formatted values with raw values + $('form').on('submit', function() { + selectors.forEach(function(selector) { + let input = $(selector); + let rawValue = input.attr('data-raw-value'); + if (rawValue) { + input.val(rawValue); + } + }); + }); + }); +} + +// Helper function to get raw value from formatted input +function getRawValue(input) { + return $(input).attr('data-raw-value') || unformatNumber($(input).val()); +} + +// Helper function to set raw value before AJAX submission +function setRawValuesForSubmission(selectors) { + selectors.forEach(function(selector) { + let input = $(selector); + let rawValue = input.attr('data-raw-value'); + if (rawValue) { + input.val(rawValue); + } + }); +} + +// Helper function to restore formatted values after AJAX submission +function restoreFormattedValues(selectors) { + selectors.forEach(function(selector) { + let input = $(selector); + let rawValue = input.attr('data-raw-value'); + if (rawValue) { + input.val(formatNumber(rawValue)); + } + }); +} + +// Auto-initialize for common amount input selectors +$(document).ready(function() { + const commonSelectors = [ + '#id_amount', + '#id_charge_amount', + 'input[name="amount"]', + 'input[name="unit_price"]', + 'input[name="price"]' + ]; + + initNumberFormatting(commonSelectors); + + // Make helper functions globally available for AJAX forms + window.formatNumber = formatNumber; + window.unformatNumber = unformatNumber; + window.getRawValue = getRawValue; + // Avoid name collision causing recursion by aliasing helpers + const __nf_setRawValuesForSubmission = setRawValuesForSubmission; + const __nf_restoreFormattedValues = restoreFormattedValues; + window.setRawValuesForSubmission = function() { __nf_setRawValuesForSubmission(commonSelectors); }; + window.restoreFormattedValues = function() { __nf_restoreFormattedValues(commonSelectors); }; +}); diff --git a/templates/_base.html b/templates/_base.html index 94ebdd1..0a41233 100644 --- a/templates/_base.html +++ b/templates/_base.html @@ -169,6 +169,8 @@ layout-navbar-fixed layout-menu-fixed layout-compact + +