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 %}
{% block content %}
<div class="container py-4">
<div class="mb-4 d-flex justify-content-between align-items-center">
<div>
<h4 class="mb-1">فاکتور نهایی</h4>
<small class="text-muted">کد درخواست: {{ instance.code }}</small>
<!-- Fonts (match base) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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">
<!-- 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>
<!-- Placeholders for logo/signature -->
<div class="text-end">لوگو</div>
{% if instance.broker.company %}
{{ 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 class="table-responsive">
<table class="table table-bordered">
<div class="col-6 text-end">
<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>
<tr>
<th>آیتم</th>
<th>تعداد</th>
<th>قیمت واحد</th>
<th>قیمت کل</th>
<th style="width: 5%">ردیف</th>
<th style="width: 30%">شرح کالا/خدمات</th>
<th style="width: 30%">توضیحات</th>
<th style="width: 10%">تعداد</th>
<th style="width: 12.5%">قیمت واحد(تومان)</th>
<th style="width: 12.5%">قیمت کل(تومان)</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<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.unit_price|floatformat:0|intcomma:False }}</td>
<td>{{ it.total_price|floatformat:0|intcomma:False }}</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted">آیتمی ندارد</td></tr>
<tr><td colspan="6" class="text-center text-muted">آیتمی ندارد</td></tr>
{% endfor %}
</tbody>
<tfoot>
<tr><th colspan="3" class="text-end">مبلغ کل</th><th>{{ invoice.total_amount|floatformat:0|intcomma:False }}</th></tr>
<tr><th colspan="3" class="text-end">تخفیف</th><th>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</th></tr>
<tr><th colspan="3" class="text-end">مبلغ نهایی</th><th>{{ invoice.final_amount|floatformat:0|intcomma:False }}</th></tr>
<tr><th colspan="3" class="text-end">پرداختی‌ها</th><th>{{ invoice.paid_amount|floatformat:0|intcomma:False }}</th></tr>
<tr><th colspan="3" class="text-end">مانده</th><th>{{ invoice.remaining_amount|floatformat:0|intcomma:False }}</th></tr>
<tr class="total-section">
<td colspan="5" class="text-end"><strong>جمع کل(تومان):</strong></td>
<td><strong>{{ invoice.total_amount|floatformat:0|intcomma:False }}</strong></td>
</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>
</table>
</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 %}
{% include '_toasts.html' %}
<!-- Instance Info Modal -->
{% instance_info_modal instance %}
{% csrf_token %}
<div class="container-xxl flex-grow-1 container-p-y">
<div class="row">
@ -32,14 +36,18 @@
<div>
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
<small class="text-muted d-block">
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
{% instance_info instance %}
</small>
</div>
<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>
@ -163,15 +171,24 @@
</div>
<div class="card-footer d-flex justify-content-between">
{% 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 %}
<span></span>
{% endif %}
{% if next_step %}
{% 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 %}
<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 %}
</div>

View file

@ -23,6 +23,10 @@
{% block content %}
{% include '_toasts.html' %}
<!-- Instance Info Modal -->
{% instance_info_modal instance %}
{% csrf_token %}
<div class="container-xxl flex-grow-1 container-p-y">
<div class="row">
@ -31,14 +35,18 @@
<div>
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
<small class="text-muted d-block">
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
{% instance_info instance %}
</small>
</div>
<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>
@ -88,7 +96,7 @@
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
</div>
<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>
</form>
</div>
@ -182,7 +190,7 @@
<h6 class="mb-0">وضعیت تاییدها</h6>
{% if can_approve_reject %}
<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>
</div>
{% endif %}
@ -214,13 +222,19 @@
{% endif %}
<div class="col-12 d-flex justify-content-between mt-3">
{% 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 %}
<span></span>
{% endif %}
{% if step_instance.status == 'completed' %}
{% 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 %}
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
{% 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/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/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 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),