diff --git a/accounts/admin.py b/accounts/admin.py index 5530abc..d741357 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -33,7 +33,7 @@ class ProfileAdmin(admin.ModelAdmin): @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): - list_display = ['name', 'logo', 'signature', 'address', 'phone', 'broker'] + list_display = ['name', 'logo', 'signature', 'address', 'phone', 'broker', 'registration_number'] prepopulated_fields = {'slug': ('name',)} search_fields = ['name', 'address', 'phone'] list_filter = ['is_active', 'broker'] diff --git a/accounts/migrations/0005_company_registration_number.py b/accounts/migrations/0005_company_registration_number.py new file mode 100644 index 0000000..b38ab10 --- /dev/null +++ b/accounts/migrations/0005_company_registration_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-09-08 10:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_company_branch_name'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='registration_number', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شماره ثبت شرکت'), + ), + ] diff --git a/accounts/migrations/0006_company_card_holder_name.py b/accounts/migrations/0006_company_card_holder_name.py new file mode 100644 index 0000000..9228034 --- /dev/null +++ b/accounts/migrations/0006_company_card_holder_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-09-08 10:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_company_registration_number'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='card_holder_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='نام دارنده کارت'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index c1e78fd..937c329 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -204,6 +204,12 @@ class Company(NameSlugModel): blank=True, verbose_name='شماره تماس' ) + registration_number = models.CharField( + max_length=255, + null=True, + blank=True, + verbose_name='شماره ثبت شرکت' + ) broker = models.OneToOneField( Broker, on_delete=models.SET_NULL, @@ -238,6 +244,12 @@ class Company(NameSlugModel): ) ] ) + card_holder_name = models.CharField( + max_length=255, + null=True, + verbose_name="نام دارنده کارت", + blank=True, + ) sheba_number = models.CharField( max_length=30, null=True, diff --git a/contracts/templates/contracts/contract_print.html b/contracts/templates/contracts/contract_print.html index bc47f05..d6a6b85 100644 --- a/contracts/templates/contracts/contract_print.html +++ b/contracts/templates/contracts/contract_print.html @@ -2,45 +2,71 @@ - + چاپ قرارداد {{ instance.code }} - + {% load static %} + + + + + + + + + + + +
-
-
-
{{ contract.template.company.name }}
-
{{ contract.template.name }}
-
کد درخواست: {{ instance.code }} | تاریخ: {{ contract.jcreated }}
-
- {% if contract.template.company.logo %} - - {% endif %} - + +
+
تاریخ: {{ contract.jcreated_date }} | کد درخواست: {{ instance.code }}
+
+ {% if instance.broker and instance.broker.company %} + {{ instance.broker.company.name }} + {% elif template.company %} + {{ template.company.name }} + {% else %} + شرکت آب منطقه‌ای + {% endif %} +
+

{{ contract.template.name }}

-
+ +
{{ contract.rendered_body|safe }}

+ +
امضای مشترک
-
+
امضای شرکت
-
- {% if contract.template.company.signature %} - امضای شرکت +
+ {% if instance.broker and instance.broker.company and instance.broker.company.signature %} + امضای شرکت + {% elif contract.template.company and contract.template.company.signature %} + امضای شرکت {% endif %}
diff --git a/contracts/templates/contracts/contract_step.html b/contracts/templates/contracts/contract_step.html index df4fdfc..700d44b 100644 --- a/contracts/templates/contracts/contract_step.html +++ b/contracts/templates/contracts/contract_step.html @@ -19,6 +19,10 @@ {% block content %} {% include '_toasts.html' %} + + +{% instance_info_modal instance %} +
@@ -26,13 +30,18 @@

{{ step.name }}: {{ instance.process.name }}

- اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} + {% instance_info instance %}
@@ -41,29 +50,32 @@
+
تاریخ: {{ contract.jcreated_date }} | کد درخواست: {{ instance.code }}
+
+ {% if instance.broker and instance.broker.company %} + {{ instance.broker.company.name }} + {% elif template.company %} + {{ template.company.name }} + {% else %} + شرکت آب منطقه‌ای + {% endif %}
+

{{ contract.template.name }}

{% if can_view_contract_body %} - {% if template.company.logo %} -
- لوگوی شرکت -

{{ contract.template.company.name }}

-
{{ contract.template.name }}
-
- {% endif %} - -
تاریخ: {{ contract.jcreated }}

{{ contract.rendered_body|safe }}

امضای مشترک
-
+
امضای شرکت
-
- {% if template.company.signature %} - امضای شرکت +
+ {% if instance.broker and instance.broker.company and instance.broker.company.signature %} + امضای شرکت + {% elif template.company and template.company.signature %} + امضای شرکت {% endif %}
@@ -76,15 +88,23 @@
{% csrf_token %} {% if previous_step %} - قبلی + + + قبلی + {% else %} {% endif %} {% if next_step %} {% if is_broker %} - + {% else %} - بعدی + + بعدی + + {% endif %} {% else %} {% if is_broker %} diff --git a/contracts/views.py b/contracts/views.py index 1949665..7a1788b 100644 --- a/contracts/views.py +++ b/contracts/views.py @@ -2,29 +2,51 @@ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.urls import reverse from django.utils import timezone +from decimal import Decimal from django.template import Template, Context +from django.utils.safestring import mark_safe from processes.models import ProcessInstance, StepInstance from common.consts import UserRoles from .models import ContractTemplate, ContractInstance +from invoices.models import Invoice, Quote from _helpers.utils import jalali_converter2 +from django.http import JsonResponse def build_contract_context(instance: ProcessInstance) -> dict: representative = instance.representative profile = getattr(representative, 'profile', None) well = instance.well + # Compute prepayment from Quote-linked invoice payments + quote = Quote.objects.filter(process_instance=instance).first() + invoice = Invoice.objects.filter(quote=quote).first() if quote else None + payments_qs = invoice.payments.filter(is_deleted=False, direction='in').all() if invoice else [] + total_paid = sum((p.amount for p in payments_qs), Decimal('0')) + try: + latest_payment_date = max((p.payment_date for p in payments_qs)) if payments_qs else None + except Exception: + latest_payment_date = None + return { - 'customer_full_name': representative.get_full_name() if representative else '', - 'national_code': profile.national_code if profile else '', - 'address': profile.address if profile else '', - 'phone': profile.phone_number_1 if profile else '', - 'phone2': profile.phone_number_2 if profile else '', - 'water_subscription_number': well.water_subscription_number if well else '', - 'electricity_subscription_number': well.electricity_subscription_number if well else '', - 'water_meter_serial_number': well.water_meter_serial_number if well else '', - 'well_power': well.well_power if well else '', - 'request_code': instance.code, - 'today': jalali_converter2(timezone.now()), + 'customer_full_name': mark_safe(f"{representative.get_full_name() if representative else ''}"), + 'registration_number': mark_safe(f"{instance.broker.company.registration_number if instance.broker and instance.broker.company else ''}"), + 'national_code': mark_safe(f"{profile.national_code if profile else ''}"), + 'address': mark_safe(f"{profile.address if profile else ''}"), + 'phone': mark_safe(f"{profile.phone_number_1 if profile else ''}"), + 'phone2': mark_safe(f"{profile.phone_number_2 if profile else ''}"), + 'water_subscription_number': mark_safe(f"{well.water_subscription_number if well else ''}"), + 'electricity_subscription_number': mark_safe(f"{well.electricity_subscription_number if well else ''}"), + 'water_meter_serial_number': mark_safe(f"{well.water_meter_serial_number if well else ''}"), + 'well_power': mark_safe(f"{well.well_power if well else ''}"), + 'request_code': mark_safe(f"{instance.code}"), + 'today': mark_safe(f"{jalali_converter2(timezone.now())}"), + 'company_name': mark_safe(f"{instance.broker.company.name if instance.broker and instance.broker.company else ''}"), + 'city_name': mark_safe(f"{instance.broker.affairs.county.city.name if instance.broker and instance.broker.affairs and instance.broker.affairs.county and instance.broker.affairs.county.city else ''}"), + 'card_number': mark_safe(f"{instance.representative.profile.card_number if instance.representative else ''}"), + 'account_number': mark_safe(f"{instance.representative.profile.account_number if instance.representative else ''}"), + 'bank_name': mark_safe(f"{instance.representative.profile.get_bank_name_display() if instance.representative else ''}"), + 'prepayment_amount': mark_safe(f"{int(total_paid):,}"), + 'prepayment_date': mark_safe(f"{jalali_converter2(latest_payment_date)}") if latest_payment_date else '', } @@ -35,10 +57,7 @@ def contract_step(request, instance_id, step_id): step = get_object_or_404(instance.process.steps, id=step_id) previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() - # Access control: - # - INSTALLER: can open step but cannot view contract body (show inline message) - # - Others: can view - # - Only BROKER can submit/complete this step + profile = getattr(request.user, 'profile', None) is_broker = False can_view_contract_body = True @@ -72,7 +91,6 @@ def contract_step(request, instance_id, step_id): # If user submits to go next, only broker can complete and go to next if request.method == 'POST': if not is_broker: - from django.http import JsonResponse return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) StepInstance.objects.update_or_create( process_instance=instance, diff --git a/db.sqlite3 b/db.sqlite3 index 700863f..a04ea6c 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/invoices/models.py b/invoices/models.py index 53059d8..ddf1334 100644 --- a/invoices/models.py +++ b/invoices/models.py @@ -7,6 +7,7 @@ from decimal import Decimal from django.utils import timezone from django.core.validators import MinValueValidator from django.conf import settings +from _helpers.utils import jalali_converter2 User = get_user_model() @@ -372,3 +373,6 @@ class Payment(BaseModel): except Exception: pass return result + + def jpayment_date(self): + return jalali_converter2(self.payment_date) diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html index ca11dec..2335298 100644 --- a/invoices/templates/invoices/final_settlement_step.html +++ b/invoices/templates/invoices/final_settlement_step.html @@ -150,7 +150,7 @@ {% if p.direction == 'in' %}دریافتی{% else %}پرداختی{% endif %} {{ p.amount|floatformat:0|intcomma:False }} تومان - {{ p.payment_date|date:'Y/m/d' }} + {{ p.jpayment_date }} {{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }} @@ -316,11 +316,32 @@ (function initPersianDatePicker(){ if (window.$ && $.fn.persianDatepicker && $('#id_payment_date').length) { $('#id_payment_date').persianDatepicker({ - format: 'YYYY/MM/DD', initialValue: false, autoClose: true, persianDigit: false, observer: true, + format: 'YYYY/MM/DD', + initialValue: false, + autoClose: true, + persianDigit: false, + observer: true, calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } }, onSelect: function(unix){ - const g = new window.persianDate(unix).toCalendar('gregorian').format('YYYY-MM-DD'); - $('#id_payment_date').attr('data-gregorian', g); + // تبدیل تاریخ شمسی به میلادی برای ارسال به سرور + const gregorianDate = new Date(unix); + const year = gregorianDate.getFullYear(); + const month = String(gregorianDate.getMonth() + 1).padStart(2, '0'); + const day = String(gregorianDate.getDate()).padStart(2, '0'); + const gregorianDateString = `${year}-${month}-${day}`; + + // نمایش تاریخ شمسی در فیلد + if (window.persianDate) { + const persianDate = new window.persianDate(unix); + const persianDateString = persianDate.format('YYYY/MM/DD'); + $('#id_payment_date').val(persianDateString); + } else { + // اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده + $('#id_payment_date').val(gregorianDateString); + } + + // ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور + $('#id_payment_date').attr('data-gregorian', gregorianDateString); } }); } @@ -328,8 +349,14 @@ function buildForm(){ const fd = new FormData(document.getElementById('formFinalPayment')); - const g = document.getElementById('id_payment_date').getAttribute('data-gregorian'); - if (g) { fd.set('payment_date', g); } + + // تبدیل تاریخ شمسی به میلادی برای ارسال + const persianDateValue = $('#id_payment_date').val(); + const gregorianDateValue = $('#id_payment_date').attr('data-gregorian'); + if (persianDateValue && gregorianDateValue) { + fd.set('payment_date', gregorianDateValue); + } + return fd; } (function(){ diff --git a/invoices/templates/invoices/quote_payment_step.html b/invoices/templates/invoices/quote_payment_step.html index e73e0d3..551420b 100644 --- a/invoices/templates/invoices/quote_payment_step.html +++ b/invoices/templates/invoices/quote_payment_step.html @@ -164,7 +164,7 @@ {% for p in payments %} {{ p.amount|floatformat:0|intcomma:False }} تومان - {{ p.payment_date|date:'Y/m/d' }} + {{ p.jpayment_date }} {{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }} @@ -359,6 +359,13 @@ } const form = document.getElementById('formAddPayment'); const fd = buildFormData(form); + + // تبدیل تاریخ شمسی به میلادی برای ارسال + const persianDateValue = $('#id_payment_date').val(); + const gregorianDateValue = $('#id_payment_date').attr('data-gregorian'); + if (persianDateValue && gregorianDateValue) { + fd.set('payment_date', gregorianDateValue); + } fetch('{% url "invoices:add_quote_payment" instance.id step.id %}', { method: 'POST', body: fd @@ -422,18 +429,24 @@ observer: true, calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } }, onSelect: function(unix) { + // تبدیل تاریخ شمسی به میلادی برای ارسال به سرور const gregorianDate = new Date(unix); const year = gregorianDate.getFullYear(); const month = String(gregorianDate.getMonth() + 1).padStart(2, '0'); const day = String(gregorianDate.getDate()).padStart(2, '0'); const gregorianDateString = `${year}-${month}-${day}`; + + // نمایش تاریخ شمسی در فیلد if (window.persianDate) { const persianDate = new window.persianDate(unix); const persianDateString = persianDate.format('YYYY/MM/DD'); $('#id_payment_date').val(persianDateString); } else { + // اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده $('#id_payment_date').val(gregorianDateString); } + + // ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور $('#id_payment_date').attr('data-gregorian', gregorianDateString); } }); diff --git a/invoices/templates/invoices/quote_preview_step.html b/invoices/templates/invoices/quote_preview_step.html index 5a2fbbd..7243f4b 100644 --- a/invoices/templates/invoices/quote_preview_step.html +++ b/invoices/templates/invoices/quote_preview_step.html @@ -323,7 +323,7 @@ {% else %} {% if next_step %} + class="btn btn-primary"> مرحله بعد diff --git a/invoices/templates/invoices/quote_step.html b/invoices/templates/invoices/quote_step.html index a12c9c0..404cf14 100644 --- a/invoices/templates/invoices/quote_step.html +++ b/invoices/templates/invoices/quote_step.html @@ -149,7 +149,7 @@ {% endif %} {% else %} {% if next_step %} - + مرحله بعد diff --git a/invoices/views.py b/invoices/views.py index cc1f925..b271a29 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -172,18 +172,24 @@ def create_quote(request, instance_id, step_id): quote.status = 'draft' quote.save(update_fields=['status']) - if next_step: - next_step_instance = instance.step_instances.filter(step=next_step).first() - if next_step_instance and next_step_instance.status == 'completed': - next_step_instance.status = 'in_progress' - next_step_instance.completed_at = None - next_step_instance.save(update_fields=['status', 'completed_at']) + # Reset ALL subsequent completed steps to in_progress + 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 and subsequent_step_instance.status == 'completed': + # Bypass validation by using update() instead of save() + instance.step_instances.filter(step=subsequent_step).update( + status='in_progress', + completed_at=None + ) # Clear previous approvals if the step requires re-approval try: - next_step_instance.approvals.all().delete() + subsequent_step_instance.approvals.all().delete() except Exception: pass + # Set current step to the next step + if next_step: instance.current_step = next_step instance.save(update_fields=['current_step']) @@ -524,6 +530,26 @@ def add_quote_payment(request, instance_id, step_id): si.approvals.all().delete() 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 and subsequent_step_instance.status == 'completed': + # Bypass validation by using update() instead of save() + instance.step_instances.filter(step=subsequent_step).update( + status='in_progress', + completed_at=None + ) + # Clear previous approvals if the step requires re-approval + try: + subsequent_step_instance.approvals.all().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: @@ -572,6 +598,26 @@ def delete_quote_payment(request, instance_id, step_id, payment_id): si.approvals.all().delete() 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 and subsequent_step_instance.status == 'completed': + # Bypass validation by using update() instead of save() + instance.step_instances.filter(step=subsequent_step).update( + status='in_progress', + completed_at=None + ) + # Clear previous approvals if the step requires re-approval + try: + subsequent_step_instance.approvals.all().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: @@ -1000,6 +1046,25 @@ def add_final_payment(request, instance_id, step_id): 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 and subsequent_step_instance.status == 'completed': + # Bypass validation by using update() instead of save() + instance.step_instances.filter(step=subsequent_step).update( + status='in_progress', + completed_at=None + ) + # Clear previous approvals if the step requires re-approval + try: + subsequent_step_instance.approvals.all().delete() + except Exception: + pass + except Exception: + pass return JsonResponse({ 'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),