fix final payment step.

This commit is contained in:
aminhashemi92 2025-09-09 15:59:41 +03:30
parent 93db2fe7f5
commit 9592c00565
6 changed files with 305 additions and 80 deletions

Binary file not shown.

View file

@ -1,55 +1,206 @@
{% extends '_base.html' %} <!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>فاکتور نهایی {{ invoice.name }} - {{ instance.code }}</title>
{% load static %}
{% load humanize %} {% load humanize %}
{% block content %} <!-- Fonts (match base) -->
<div class="container py-4"> <link rel="preconnect" href="https://fonts.googleapis.com">
<div class="mb-4 d-flex justify-content-between align-items-center"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<div> <link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<h4 class="mb-1">فاکتور نهایی</h4>
<small class="text-muted">کد درخواست: {{ instance.code }}</small> <!-- Icons (optional) -->
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/boxicons.css' %}">
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/fontawesome.css' %}">
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/flag-icons.css' %}">
<!-- Core CSS (same as preview) -->
<link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
<link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
<link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
<link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
<style>
@page {
size: A4;
margin: 1cm;
}
@media print {
body { print-color-adjust: exact; }
.page-break { page-break-before: always; }
.no-print { display: none !important; }
}
.invoice-header { border-bottom: 1px solid #dee2e6; padding-bottom: 20px; margin-bottom: 30px; }
.company-logo { font-size: 24px; font-weight: bold; color: #696cff; }
.invoice-title { font-size: 28px; font-weight: bold; color: #333; }
.info-table td { padding: 5px 10px; border: none; }
.items-table { border: 1px solid #dee2e6; }
.items-table th { background-color: #f8f9fa; border-bottom: 2px solid #dee2e6; font-weight: bold; text-align: center; }
.items-table td { border-bottom: 1px solid #dee2e6; text-align: center; }
.total-section { background-color: #f8f9fa; font-weight: bold; }
.signature-section { margin-top: 50px; border-top: 1px solid #dee2e6; padding-top: 30px; }
.signature-box { border: 1px dashed #ccc; height: 80px; text-align: center; display: flex; align-items: center; justify-content: center; color: #666; }
</style>
</head>
<body>
<div class="container-fluid">
<!-- Header -->
<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>
<div> <div>
<!-- Placeholders for logo/signature --> {% if instance.broker.company %}
<div class="text-end">لوگو</div> {{ 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> </div>
<div class="table-responsive"> <div class="col-6 text-end">
<table class="table table-bordered"> <div class="mt-2">
<div><strong>#فاکتور نهایی {{ instance.code }}</strong></div>
<div class="text-muted 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">اطلاعات مشترک</h6>
<div class="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>
{% 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>
{% endif %}
{% if instance.representative.profile and instance.representative.profile.address %}
<div class="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">
<table class="table border-top m-0 items-table">
<thead> <thead>
<tr> <tr>
<th>آیتم</th> <th style="width: 5%">ردیف</th>
<th>تعداد</th> <th style="width: 30%">شرح کالا/خدمات</th>
<th>قیمت واحد</th> <th style="width: 30%">توضیحات</th>
<th>قیمت کل</th> <th style="width: 10%">تعداد</th>
<th style="width: 12.5%">قیمت واحد(تومان)</th>
<th style="width: 12.5%">قیمت کل(تومان)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for it in items %} {% for it in items %}
<tr> <tr>
<td>{{ it.item.name }}</td> <td>{{ forloop.counter }}</td>
<td class="text-nowrap">{{ it.item.name }}</td>
<td class="text-nowrap">{{ it.item.description|default:"-" }}</td>
<td>{{ it.quantity }}</td> <td>{{ it.quantity }}</td>
<td>{{ it.unit_price|floatformat:0|intcomma:False }}</td> <td>{{ it.unit_price|floatformat:0|intcomma:False }}</td>
<td>{{ it.total_price|floatformat:0|intcomma:False }}</td> <td>{{ it.total_price|floatformat:0|intcomma:False }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="4" class="text-center text-muted">آیتمی ندارد</td></tr> <tr><td colspan="6" class="text-center text-muted">آیتمی ندارد</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot> <tfoot>
<tr><th colspan="3" class="text-end">مبلغ کل</th><th>{{ invoice.total_amount|floatformat:0|intcomma:False }}</th></tr> <tr class="total-section">
<tr><th colspan="3" class="text-end">تخفیف</th><th>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</th></tr> <td colspan="5" class="text-end"><strong>جمع کل(تومان):</strong></td>
<tr><th colspan="3" class="text-end">مبلغ نهایی</th><th>{{ invoice.final_amount|floatformat:0|intcomma:False }}</th></tr> <td><strong>{{ invoice.total_amount|floatformat:0|intcomma:False }}</strong></td>
<tr><th colspan="3" class="text-end">پرداختی‌ها</th><th>{{ invoice.paid_amount|floatformat:0|intcomma:False }}</th></tr> </tr>
<tr><th colspan="3" class="text-end">مانده</th><th>{{ invoice.remaining_amount|floatformat:0|intcomma:False }}</th></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>
</tr>
{% endif %}
<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>
</tr>
<tr class="total-section">
<td colspan="5" class="text-end"><strong>پرداختی‌ها(تومان):</strong></td>
<td><strong">{{ invoice.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.remaining_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
<div class="mt-5 d-flex justify-content-between">
<div>امضا مشتری</div>
<div>امضا شرکت</div>
</div>
</div>
<script>window.print()</script>
{% endblock %}
<!-- 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">
<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>
{% endif %}
{% if instance.broker.company.account_number %}
<div class="small mb-1"><span class="text-muted">شماره حساب:</span> {{ instance.broker.company.account_number }}</div>
{% endif %}
{% if instance.broker.company.sheba_number %}
<div class="small mb-1"><span class="text-muted">شماره شبا:</span> {{ instance.broker.company.sheba_number }}</div>
{% endif %}
{% if instance.broker.company.bank_name %}
<div class="small"><span class="text-muted">بانک:</span> {{ instance.broker.company.get_bank_name_display }}</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<script>
window.onload = function() {
window.print();
setTimeout(function(){ window.close(); }, 200);
};
</script>
</body>
</html>

View file

@ -24,6 +24,10 @@
{% block content %} {% block content %}
{% include '_toasts.html' %} {% include '_toasts.html' %}
<!-- Instance Info Modal -->
{% instance_info_modal instance %}
{% csrf_token %} {% csrf_token %}
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
<div class="row"> <div class="row">
@ -32,14 +36,18 @@
<div> <div>
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4> <h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
<small class="text-muted d-block"> <small class="text-muted d-block">
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} {% instance_info instance %}
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
</small> </small>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت</a> <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
<i class="bx bx-printer me-2"></i> پرینت
</a>
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a> <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
بازگشت
</a>
</div> </div>
</div> </div>
@ -163,15 +171,24 @@
</div> </div>
<div class="card-footer d-flex justify-content-between"> <div class="card-footer d-flex justify-content-between">
{% if previous_step %} {% if previous_step %}
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a> <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
قبلی
</a>
{% else %} {% else %}
<span></span> <span></span>
{% endif %} {% endif %}
{% if next_step %} {% if next_step %}
{% if is_manager %} {% if is_manager %}
<button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">تایید و ادامه</button> <button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">
تایید و ادامه
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
</button>
{% else %} {% else %}
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a> <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>
</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View file

@ -23,6 +23,10 @@
{% block content %} {% block content %}
{% include '_toasts.html' %} {% include '_toasts.html' %}
<!-- Instance Info Modal -->
{% instance_info_modal instance %}
{% csrf_token %} {% csrf_token %}
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
<div class="row"> <div class="row">
@ -31,14 +35,18 @@
<div> <div>
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4> <h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
<small class="text-muted d-block"> <small class="text-muted d-block">
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} {% instance_info instance %}
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
</small> </small>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت</a> <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
<i class="bx bx-printer me-2"></i> پرینت
</a>
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a> <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
بازگشت
</a>
</div> </div>
</div> </div>
@ -88,7 +96,7 @@
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required> <input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="button" id="btnAddFinalPayment" class="btn btn-primary">افزودن</button> <button type="button" id="btnAddFinalPayment" class="btn btn-primary">افزودن فیش/چک</button>
</div> </div>
</form> </form>
</div> </div>
@ -182,7 +190,7 @@
<h6 class="mb-0">وضعیت تاییدها</h6> <h6 class="mb-0">وضعیت تاییدها</h6>
{% if can_approve_reject %} {% if can_approve_reject %}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveFinalSettleModal" {% if step_instance.status == 'completed' %}disabled{% endif %}>تایید</button> <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveFinalSettleModal">تایید</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectFinalSettleModal">رد</button> <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectFinalSettleModal">رد</button>
</div> </div>
{% endif %} {% endif %}
@ -214,13 +222,19 @@
{% endif %} {% endif %}
<div class="col-12 d-flex justify-content-between mt-3"> <div class="col-12 d-flex justify-content-between mt-3">
{% if previous_step %} {% if previous_step %}
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a> <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
قبلی
</a>
{% else %} {% else %}
<span></span> <span></span>
{% endif %} {% endif %}
{% if step_instance.status == 'completed' %} {% if step_instance.status == 'completed' %}
{% if next_step %} {% if next_step %}
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a> <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>
</a>
{% else %} {% else %}
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a> <a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
{% endif %} {% endif %}

View file

@ -31,5 +31,4 @@ urlpatterns = [
path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/', views.final_settlement_step, name='final_settlement_step'), path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/', views.final_settlement_step, name='final_settlement_step'),
path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/add/', views.add_final_payment, name='add_final_payment'), path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/add/', views.add_final_payment, name='add_final_payment'),
path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/<int:payment_id>/delete/', views.delete_final_payment, name='delete_final_payment'), path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/<int:payment_id>/delete/', views.delete_final_payment, name='delete_final_payment'),
path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/approve/', views.approve_final_settlement, name='approve_final_settlement'),
] ]

View file

@ -12,7 +12,7 @@ import json
from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval
from accounts.models import Role from accounts.models import Role
from common.consts import UserRoles 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 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) return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
except Exception: except Exception:
return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) 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, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
step_instance.status = 'completed' step_instance.status = 'completed'
step_instance.completed_at = timezone.now() 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) return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403)
except Exception: except Exception:
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403) 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') item_id = request.POST.get('item_id')
amount = (request.POST.get('amount') or '').strip() amount = (request.POST.get('amount') or '').strip()
if not item_id: if not item_id:
@ -841,7 +834,7 @@ def add_special_charge(request, instance_id, step_id):
# Fetch existing special item from DB # Fetch existing special item from DB
special_item = get_object_or_404(Item, id=item_id, is_special=True) special_item = get_object_or_404(Item, id=item_id, is_special=True)
from .models import InvoiceItem
InvoiceItem.objects.create( InvoiceItem.objects.create(
invoice=invoice, invoice=invoice,
item=special_item, 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) return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403)
except Exception: except Exception:
return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403) return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403)
from .models import InvoiceItem
inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice) inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice)
# allow deletion only for special items # allow deletion only for special items
try: 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): def final_settlement_step(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id) instance = get_object_or_404(ProcessInstance, id=instance_id)
step = get_object_or_404(instance.process.steps, id=step_id) step = get_object_or_404(instance.process.steps, id=step_id)
if not instance.can_access_step(step): if not instance.can_access_step(step):
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.') messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
return redirect('processes:request_list') return redirect('processes:request_list')
@ -890,6 +883,7 @@ def final_settlement_step(request, instance_id, step_id):
# Ensure step instance exists # Ensure step instance exists
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'}) step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
# Build approver statuses for template # Build approver statuses for template
reqs = list(step.approver_requirements.select_related('role').all()) 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()} 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} defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
) )
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, 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, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.') messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) 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) return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403)
except Exception: except Exception:
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403)
amount = (request.POST.get('amount') or '').strip() amount = (request.POST.get('amount') or '').strip()
payment_date = (request.POST.get('payment_date') or '').strip() payment_date = (request.POST.get('payment_date') or '').strip()
payment_method = (request.POST.get('payment_method') 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. # After creation, totals auto-updated by model save. Respond with redirect and new totals for UX.
invoice.refresh_from_db() invoice.refresh_from_db()
# After payment change, set step back to in_progress
# On delete, return to awaiting approval
try: try:
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
si.status = 'in_progress' si.status = 'in_progress'
si.completed_at = None si.completed_at = None
si.save() si.save()
si.approvals.all().delete()
except Exception: except Exception:
pass pass
@ -1065,6 +1069,16 @@ def add_final_payment(request, instance_id, step_id):
pass pass
except Exception: except Exception:
pass 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({ return JsonResponse({
'success': True, 'success': True,
'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), '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) return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
payment.delete() payment.delete()
invoice.refresh_from_db() invoice.refresh_from_db()
# After payment change, set step back to in_progress
# On delete, return to awaiting approval
try: try:
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
si.status = 'in_progress' si.status = 'in_progress'
si.completed_at = None si.completed_at = None
si.save() si.save()
si.approvals.all().delete()
except Exception: except Exception:
pass 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': { return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': {
'final_amount': str(invoice.final_amount), 'final_amount': str(invoice.final_amount),
'paid_amount': str(invoice.paid_amount), 'paid_amount': str(invoice.paid_amount),