diff --git a/db.sqlite3 b/db.sqlite3 index 2f70da4..1bba24d 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/invoices/templates/invoices/final_invoice_print.html b/invoices/templates/invoices/final_invoice_print.html index cfa58eb..6e37a0d 100644 --- a/invoices/templates/invoices/final_invoice_print.html +++ b/invoices/templates/invoices/final_invoice_print.html @@ -1,55 +1,206 @@ -{% extends '_base.html' %} -{% load humanize %} + + + + + + فاکتور نهایی {{ invoice.name }} - {{ instance.code }} + + {% load static %} + {% load humanize %} -{% block content %} -
-
-
-

فاکتور نهایی

- کد درخواست: {{ instance.code }} + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ {% 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 %} +
+ {% endif %} +
+
+
+
+
#فاکتور نهایی {{ instance.code }}
+
تاریخ صدور: {{ invoice.jcreated_date }}
+
+
+
-
- -
لوگو
+ + +
+
+
اطلاعات مشترک
+
نام: {{ 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 }}
+ {% endif %} + {% if instance.representative.profile and 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 }}
+ {% endif %} +
+
+
اطلاعات چاه
+
شماره اشتراک آب: {{ instance.well.water_subscription_number }}
+
شماره اشتراک برق: {{ instance.well.electricity_subscription_number|default:"-" }}
+
سریال کنتور: {{ instance.well.water_meter_serial_number|default:"-" }}
+
قدرت چاه: {{ instance.well.well_power|default:"-" }}
+
-
-
- - - - - - - - - - - {% for it in items %} - - - - - - - {% empty %} - - {% endfor %} - - - - - - - - -
آیتمتعدادقیمت واحدقیمت کل
{{ it.item.name }}{{ it.quantity }}{{ it.unit_price|floatformat:0|intcomma:False }}{{ it.total_price|floatformat:0|intcomma:False }}
آیتمی ندارد
مبلغ کل{{ invoice.total_amount|floatformat:0|intcomma:False }}
تخفیف{{ invoice.discount_amount|floatformat:0|intcomma:False }}
مبلغ نهایی{{ invoice.final_amount|floatformat:0|intcomma:False }}
پرداختی‌ها{{ invoice.paid_amount|floatformat:0|intcomma:False }}
مانده{{ invoice.remaining_amount|floatformat:0|intcomma:False }}
-
-
-
امضا مشتری
-
امضا شرکت
-
-
- -{% endblock %} + +
+ + + + + + + + + + + + + {% for it in items %} + + + + + + + + + {% empty %} + + {% endfor %} + + + + + + + {% if invoice.discount_amount > 0 %} + + + + + {% endif %} + + + + + + + + + + + + + +
ردیفشرح کالا/خدماتتوضیحاتتعدادقیمت واحد(تومان)قیمت کل(تومان)
{{ forloop.counter }}{{ it.item.name }}{{ it.item.description|default:"-" }}{{ it.quantity }}{{ it.unit_price|floatformat:0|intcomma:False }}{{ it.total_price|floatformat:0|intcomma:False }}
آیتمی ندارد
جمع کل(تومان):{{ invoice.total_amount|floatformat:0|intcomma:False }}
تخفیف(تومان):{{ invoice.discount_amount|floatformat:0|intcomma:False }}
مبلغ نهایی(تومان):{{ invoice.final_amount|floatformat:0|intcomma:False }}
پرداختی‌ها(تومان):{{ invoice.paid_amount|floatformat:0|intcomma:False }}
مانده(تومان):{{ invoice.remaining_amount|floatformat:0|intcomma:False }}
+
+ +
+
+
مهر و امضا:
+
    + {% 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 }}
+ {% endif %} + {% if instance.broker.company.account_number %} +
شماره حساب: {{ instance.broker.company.account_number }}
+ {% endif %} + {% if instance.broker.company.sheba_number %} +
شماره شبا: {{ instance.broker.company.sheba_number }}
+ {% endif %} + {% if instance.broker.company.bank_name %} +
بانک: {{ instance.broker.company.get_bank_name_display }}
+ {% endif %} +
+ {% endif %} +
+ +
+ + + + diff --git a/invoices/templates/invoices/final_invoice_step.html b/invoices/templates/invoices/final_invoice_step.html index dfee339..5fe05e4 100644 --- a/invoices/templates/invoices/final_invoice_step.html +++ b/invoices/templates/invoices/final_invoice_step.html @@ -24,6 +24,10 @@ {% block content %} {% include '_toasts.html' %} + + +{% instance_info_modal instance %} + {% csrf_token %}
@@ -32,14 +36,18 @@

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

- اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} + {% instance_info instance %}
@@ -163,15 +171,24 @@
diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html index 2335298..350d5b7 100644 --- a/invoices/templates/invoices/final_settlement_step.html +++ b/invoices/templates/invoices/final_settlement_step.html @@ -23,6 +23,10 @@ {% block content %} {% include '_toasts.html' %} + + +{% instance_info_modal instance %} + {% csrf_token %}
@@ -31,14 +35,18 @@

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

- اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} + {% instance_info instance %}
@@ -88,7 +96,7 @@
- +
@@ -182,7 +190,7 @@
وضعیت تاییدها
{% if can_approve_reject %}
- +
{% endif %} @@ -214,13 +222,19 @@ {% endif %}
{% if previous_step %} - قبلی + + + قبلی + {% else %} {% endif %} {% if step_instance.status == 'completed' %} {% if next_step %} - بعدی + + بعدی + + {% else %} اتمام {% endif %} diff --git a/invoices/urls.py b/invoices/urls.py index f959799..f40df30 100644 --- a/invoices/urls.py +++ b/invoices/urls.py @@ -31,5 +31,4 @@ urlpatterns = [ path('instance//step//final-settlement/', views.final_settlement_step, name='final_settlement_step'), path('instance//step//final-settlement/add/', views.add_final_payment, name='add_final_payment'), path('instance//step//final-settlement//delete/', views.delete_final_payment, name='delete_final_payment'), - path('instance//step//final-settlement/approve/', views.approve_final_settlement, name='approve_final_settlement'), ] diff --git a/invoices/views.py b/invoices/views.py index ea99eb7..b8a1eb2 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -12,7 +12,7 @@ import json from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval from accounts.models import Role from common.consts import UserRoles -from .models import Item, Quote, QuoteItem, Payment, Invoice +from .models import Item, Quote, QuoteItem, Payment, Invoice, InvoiceItem from installations.models import InstallationReport, InstallationItemChange @@ -792,14 +792,7 @@ def approve_final_invoice(request, instance_id, step_id): return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) except Exception: return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) - # Block approval when there is any remaining (positive or negative) - invoice.calculate_totals() - # if invoice.remaining_amount != 0: - # return JsonResponse({ - # 'success': False, - # 'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})" - # }) - # mark step completed + step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) step_instance.status = 'completed' step_instance.completed_at = timezone.now() @@ -826,7 +819,7 @@ def add_special_charge(request, instance_id, step_id): return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403) except Exception: return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403) - # charge_type was removed from UI; we no longer require it + item_id = request.POST.get('item_id') amount = (request.POST.get('amount') or '').strip() if not item_id: @@ -841,7 +834,7 @@ def add_special_charge(request, instance_id, step_id): # Fetch existing special item from DB special_item = get_object_or_404(Item, id=item_id, is_special=True) - from .models import InvoiceItem + InvoiceItem.objects.create( invoice=invoice, item=special_item, @@ -863,7 +856,6 @@ def delete_special_charge(request, instance_id, step_id, item_id): return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403) except Exception: return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403) - from .models import InvoiceItem inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice) # allow deletion only for special items try: @@ -880,6 +872,7 @@ def delete_special_charge(request, instance_id, step_id, item_id): def final_settlement_step(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) + if not instance.can_access_step(step): messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') return redirect('processes:request_list') @@ -890,6 +883,7 @@ 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'}) + # Build approver statuses for template reqs = list(step.approver_requirements.select_related('role').all()) approvals_map = {a.role_id: a.decision for a in step_instance.approvals.select_related('role').all()} @@ -947,6 +941,13 @@ def final_settlement_step(request, instance_id, step_id): defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} ) StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason) + # If current step is ahead of this step, reset it back to this step (align behavior with other steps) + 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 messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.') return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) @@ -984,6 +985,7 @@ def add_final_payment(request, instance_id, step_id): return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) except Exception: return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) + amount = (request.POST.get('amount') or '').strip() payment_date = (request.POST.get('payment_date') or '').strip() payment_method = (request.POST.get('payment_method') or '').strip() @@ -1038,12 +1040,14 @@ def add_final_payment(request, instance_id, step_id): ) # After creation, totals auto-updated by model save. Respond with redirect and new totals for UX. invoice.refresh_from_db() - # After payment change, set step back to in_progress + + # On delete, return to awaiting approval try: si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) si.status = 'in_progress' si.completed_at = None si.save() + si.approvals.all().delete() except Exception: pass @@ -1065,6 +1069,16 @@ def add_final_payment(request, instance_id, step_id): 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_settlement_step', args=[instance.id, step_id]), @@ -1091,14 +1105,44 @@ def delete_final_payment(request, instance_id, step_id, payment_id): return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403) payment.delete() invoice.refresh_from_db() - # After payment change, set step back to in_progress + + # On delete, return to awaiting approval try: si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) si.status = 'in_progress' si.completed_at = None si.save() + 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: + instance.current_step = step + instance.save(update_fields=['current_step']) + except Exception: + pass + return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': { 'final_amount': str(invoice.final_amount), 'paid_amount': str(invoice.paid_amount),