Add confirmation and summary
This commit is contained in:
parent
9b3973805e
commit
35799b7754
25 changed files with 1419 additions and 265 deletions
|
@ -6,8 +6,8 @@ from .models import Item, Quote, QuoteItem, Invoice, InvoiceItem, Payment
|
|||
|
||||
@admin.register(Item)
|
||||
class ItemAdmin(SimpleHistoryAdmin):
|
||||
list_display = ['name', 'unit_price', 'default_quantity', 'is_default_in_quotes', 'is_active', 'created_by']
|
||||
list_filter = ['is_default_in_quotes', 'is_active', 'created_by']
|
||||
list_display = ['name', 'unit_price', 'default_quantity', 'is_default_in_quotes', 'is_special', 'is_active', 'created_by']
|
||||
list_filter = ['is_default_in_quotes', 'is_special', 'is_active', 'created_by']
|
||||
search_fields = ['name', 'description']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
readonly_fields = ['deleted_at', 'created', 'updated']
|
||||
|
|
|
@ -50,7 +50,9 @@
|
|||
<div class="card border">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">فاکتور نهایی</h5>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="openSpecialChargeModal()"><i class="bx bx-plus"></i> افزودن هزینه تعمیر/تعویض</button>
|
||||
{% if is_manager %}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="openSpecialChargeModal()"><i class="bx bx-plus"></i> افزودن هزینه تعمیر/تعویض</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
@ -127,7 +129,9 @@
|
|||
<td class="text-end">{{ si.unit_price|floatformat:0|intcomma:False }}</td>
|
||||
<td class="text-end">
|
||||
{{ si.total_price|floatformat:0|intcomma:False }}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="deleteSpecial('{{ si.id }}')" title="حذف"><i class="bx bx-trash"></i></button>
|
||||
{% if is_manager %}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="deleteSpecial('{{ si.id }}')" title="حذف"><i class="bx bx-trash"></i></button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -164,7 +168,11 @@
|
|||
<span></span>
|
||||
{% endif %}
|
||||
{% if next_step %}
|
||||
<button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">تایید و ادامه</button>
|
||||
{% if is_manager %}
|
||||
<button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">تایید و ادامه</button>
|
||||
{% else %}
|
||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
{% load static %}
|
||||
{% load processes_tags %}
|
||||
{% load common_tags %}
|
||||
{% load accounts_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block sidebar %}
|
||||
|
@ -46,6 +47,7 @@
|
|||
<div class="bs-stepper-content">
|
||||
|
||||
<div class="row g-3">
|
||||
{% if is_broker %}
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card border h-100">
|
||||
<div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div>
|
||||
|
@ -78,11 +80,11 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">شماره مرجع</label>
|
||||
<label class="form-label">شماره مرجع/چک</label>
|
||||
<input type="text" class="form-control" name="reference_number" id="id_reference_number" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">تصویر فیش</label>
|
||||
<label class="form-label">تصویر فیش/چک</label>
|
||||
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
|
@ -92,23 +94,39 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-7">
|
||||
{% endif %}
|
||||
<div class="col-12 {% if is_broker %}col-lg-7{% else %}col-lg-12{% endif %}">
|
||||
<div class="card mb-3 border">
|
||||
<div class="card-header"><h5 class="mb-0">وضعیت فاکتور</h5></div>
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<h5 class="mb-0">وضعیت فاکتور</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<div class="border rounded p-3">
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="small text-muted">مبلغ نهایی</div>
|
||||
<div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="border rounded p-3">
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="small text-muted">پرداختیها</div>
|
||||
<div class="h5 mt-1 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="small text-muted">مانده</div>
|
||||
<div class="h5 mt-1 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
{% if invoice.remaining_amount <= 0 %}
|
||||
<span class="badge bg-success">تسویه کامل</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">باقیمانده دارد</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -123,8 +141,8 @@
|
|||
<th>مبلغ</th>
|
||||
<th>تاریخ</th>
|
||||
<th>روش</th>
|
||||
<th>شماره مرجع</th>
|
||||
<th style="width:150px">عملیات</th>
|
||||
<th class="text-nowrap">شماره مرجع/چک</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -132,7 +150,7 @@
|
|||
<tr>
|
||||
<td>{% if p.direction == 'in' %}<span class="badge bg-success">دریافتی{% else %}<span class="badge bg-warning text-dark">پرداختی{% endif %}</span></td>
|
||||
<td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
|
||||
<td>{{ p.payment_date|to_jalali }}</td>
|
||||
<td>{{ p.payment_date|date:'Y/m/d' }}</td>
|
||||
<td>{{ p.get_payment_method_display }}</td>
|
||||
<td>{{ p.reference_number|default:'-' }}</td>
|
||||
<td>
|
||||
|
@ -142,7 +160,9 @@
|
|||
<i class="bx bx-show"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFinalPayment({{ p.id }})" title="حذف" aria-label="حذف"><i class="bx bx-trash"></i></button>
|
||||
{% if is_broker %}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal('{{ p.id }}')" title="حذف" aria-label="حذف"><i class="bx bx-trash"></i></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -152,20 +172,141 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
<button type="button" id="btnApproveFinalSettlement" class="btn btn-primary">تایید و ادامه</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if approver_statuses %}
|
||||
<div class="card border mt-2">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<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-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectFinalSettleModal">رد</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body py-3">
|
||||
<div class="row g-2">
|
||||
{% for st in approver_statuses %}
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="d-flex flex-column border rounded px-2 py-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-light text-dark">{{ st.role.name }}</span>
|
||||
{% if st.status == 'approved' %}
|
||||
<span class="badge bg-success">تایید شد</span>
|
||||
{% elif st.status == 'rejected' %}
|
||||
<span class="badge bg-danger">رد شد</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">در انتظار</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if st.status == 'rejected' and st.reason %}
|
||||
<div class="mt-1 small text-danger">علت: {{ st.reason }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% else %}
|
||||
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Delete Confirmation Modal (final settlement payments) -->
|
||||
<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف تراکنش</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
آیا از حذف این تراکنش مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approve Final Settlement Modal -->
|
||||
<div class="modal fade" id="approveFinalSettleModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">تایید تسویه نهایی</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if invoice.remaining_amount != 0 %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
مانده فاکتور صفر نیست: <strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</strong><br>
|
||||
تا صفر نشود امکان تایید نیست.
|
||||
</div>
|
||||
{% else %}
|
||||
آیا از تایید این مرحله اطمینان دارید؟
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="submit" class="btn btn-success" {% if invoice.remaining_amount != 0 %}disabled{% endif %}>تایید</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reject Final Settlement Modal -->
|
||||
<div class="modal fade" id="rejectFinalSettleModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">رد تسویه نهایی</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">علت رد</label>
|
||||
<textarea class="form-control" name="reject_reason" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="submit" class="btn btn-danger">ثبت رد</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
|
@ -191,8 +332,11 @@
|
|||
if (g) { fd.set('payment_date', g); }
|
||||
return fd;
|
||||
}
|
||||
document.getElementById('btnAddFinalPayment').addEventListener('click', function(){
|
||||
const fd = buildForm();
|
||||
(function(){
|
||||
const btn = document.getElementById('btnAddFinalPayment');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function(){
|
||||
const fd = buildForm();
|
||||
// Frontend validation
|
||||
const amount = document.getElementById('id_amount').value.trim();
|
||||
const payDate = document.getElementById('id_payment_date').value.trim();
|
||||
|
@ -204,7 +348,7 @@
|
|||
showToast('همه فیلدها الزامی است', 'danger');
|
||||
return;
|
||||
}
|
||||
fetch('{% url "invoices:add_final_payment" instance.id step.id %}', { method:'POST', body: fd })
|
||||
fetch('{% url "invoices:add_final_payment" instance.id step.id %}', { method:'POST', body: fd })
|
||||
.then(r=>r.json()).then(resp=>{
|
||||
if (resp.success) {
|
||||
showToast('تراکنش ثبت شد', 'success');
|
||||
|
@ -213,12 +357,20 @@
|
|||
showToast(resp.message || 'خطا در ثبت تراکنش', 'danger');
|
||||
}
|
||||
}).catch(()=> showToast('خطا در ارتباط با سرور', 'danger'));
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function deleteFinalPayment(id){
|
||||
let deleteTargetId = null;
|
||||
function openDeleteModal(id){
|
||||
deleteTargetId = id;
|
||||
const modal = new bootstrap.Modal(document.getElementById('deletePaymentModal'));
|
||||
modal.show();
|
||||
}
|
||||
function confirmDeletePayment(){
|
||||
if (!deleteTargetId) return;
|
||||
const fd = new FormData();
|
||||
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
||||
fetch(`{% url "invoices:delete_final_payment" instance.id step.id 0 %}`.replace('/0/', `/${id}/`), { method:'POST', body: fd })
|
||||
fetch(`{% url "invoices:delete_final_payment" instance.id step.id 0 %}`.replace('/0/', `/${deleteTargetId}/`), { method:'POST', body: fd })
|
||||
.then(r=>r.json()).then(resp=>{
|
||||
if (resp.success) {
|
||||
showToast('حذف شد', 'success');
|
||||
|
@ -229,20 +381,7 @@
|
|||
}).catch(()=> showToast('خطا در ارتباط با سرور', 'danger'));
|
||||
}
|
||||
|
||||
document.getElementById('btnApproveFinalSettlement').addEventListener('click', function(){
|
||||
const fd = new FormData();
|
||||
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
||||
fetch('{% url "invoices:approve_final_settlement" instance.id step.id %}', { method:'POST', body: fd })
|
||||
.then(r=>r.json()).then(resp=>{
|
||||
if (resp.success) {
|
||||
showToast(resp.message || 'تایید شد', 'success');
|
||||
if (resp.redirect) setTimeout(()=>{ window.location.href = resp.redirect; }, 600);
|
||||
} else {
|
||||
showToast(resp.message || 'خطا در تایید', 'danger');
|
||||
}
|
||||
}).catch(()=> showToast('خطا در ارتباط با سرور', 'danger'));
|
||||
});
|
||||
// Legacy approve button removed; using modal forms below
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load processes_tags %}
|
||||
{% load accounts_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block sidebar %}
|
||||
|
@ -55,14 +56,15 @@
|
|||
<div class="content active dstepper-block">
|
||||
<div class="content-header mb-3">
|
||||
<h6 class="mb-0">{{ step.name }}</h6>
|
||||
<small>ثبت فیشهای واریزی برای پیشفاکتور</small>
|
||||
<small>ثبت فیشها/چکهای واریزی برای پیشفاکتور</small>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
{% if can_manage_payments %}
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card h-100 border">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">ثبت فیش جدید</h5>
|
||||
<h5 class="card-title mb-0">ثبت فیش/چک جدید</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
|
@ -84,11 +86,11 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">شماره مرجع</label>
|
||||
<label class="form-label">شماره مرجع/چک</label>
|
||||
<input type="text" class="form-control" name="reference_number" id="id_reference_number" placeholder="..." required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">تصویر فیش</label>
|
||||
<label class="form-label">تصویر فیش/چک</label>
|
||||
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
@ -96,16 +98,16 @@
|
|||
<textarea class="form-control" rows="2" name="notes" id="id_notes"></textarea>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="button" id="btnAddPayment" class="btn btn-primary">افزودن فیش</button>
|
||||
<button type="button" id="btnAddPayment" class="btn btn-primary">افزودن فیش/چک</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-7">
|
||||
{% endif %}
|
||||
<div class="col-12 {% if can_manage_payments %}col-lg-7{% else %}col-lg-12{% endif %}">
|
||||
<div class="card mb-3 border">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
|
||||
<a href="{% url 'invoices:quote_preview_step' instance.id step.id|add:'-1' %}" class="btn btn-sm btn-label-secondary">مشاهده پیشفاکتور</a>
|
||||
<h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
|
@ -139,8 +141,10 @@
|
|||
</div>
|
||||
|
||||
<div class="card border">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">فیشهای ثبت شده</h5>
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="card-title mb-0">فیشها/چکهای ثبت شده</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped mb-0">
|
||||
|
@ -149,9 +153,8 @@
|
|||
<th>مبلغ</th>
|
||||
<th>تاریخ</th>
|
||||
<th>روش</th>
|
||||
<th>شماره مرجع</th>
|
||||
<th>تصویر</th>
|
||||
<th style="width:120px">عملیات</th>
|
||||
<th>شماره مرجع/چک</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -162,28 +165,23 @@
|
|||
<td>{{ p.get_payment_method_display }}</td>
|
||||
<td>{{ p.reference_number|default:'-' }}</td>
|
||||
<td>
|
||||
{% if p.receipt_image %}
|
||||
<div class="btn-group">
|
||||
{% if p.receipt_image %}
|
||||
<a href="{{ p.receipt_image.url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="مشاهده" aria-label="مشاهده">
|
||||
<i class="bx bx-show"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="editPayment({{ p.id }})" title="ویرایش" aria-label="ویرایش">
|
||||
<i class="bx bx-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal({{ p.id }})" title="حذف" aria-label="حذف">
|
||||
{% endif %}
|
||||
{% if can_manage_payments %}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal('{{ p.id }}')" title="حذف" aria-label="حذف">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">تا کنون فیشی ثبت نشده است</td>
|
||||
<td colspan="6" class="text-center text-muted">تا کنون فیش/چکی ثبت نشده است</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -191,6 +189,42 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if approver_statuses %}
|
||||
<div class="card border mt-2">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<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="#approvePaymentsModal2" {% if step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectPaymentsModal">رد</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body py-3">
|
||||
<div class="row g-2">
|
||||
{% for st in approver_statuses %}
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="d-flex flex-column border rounded px-2 py-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-light text-dark">{{ st.role.name }}</span>
|
||||
{% if st.status == 'approved' %}
|
||||
<span class="badge bg-success">تایید شد</span>
|
||||
{% elif st.status == 'rejected' %}
|
||||
<span class="badge bg-danger">رد شد</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">در انتظار</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if st.status == 'rejected' and st.reason %}
|
||||
<div class="mt-1 small text-danger">علت: {{ st.reason }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 d-flex justify-content-between mt-3">
|
||||
{% if previous_step %}
|
||||
|
@ -201,30 +235,114 @@
|
|||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
<button type="button" id="btnApprovePayments" class="btn btn-primary">
|
||||
تایید پرداختها
|
||||
<i class="bx bx-chevron-left bx-sm ms-sm-2"></i>
|
||||
</button>
|
||||
{% if step_instance.status == 'completed' %}
|
||||
{% if next_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
|
||||
<span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
|
||||
<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 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف فیش</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
آیا از حذف این فیش مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Removed legacy approvePaymentsModal; using approvePaymentsModal2 with form POST -->
|
||||
<!-- Approve Modal 2 (direct approve button in header) -->
|
||||
<div class="modal fade" id="approvePaymentsModal2" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">تایید پرداختها</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if not totals.is_fully_paid %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
مبلغی از پیشفاکتور هنوز پرداخت نشده است.
|
||||
<div class="mt-1">مانده: <strong>{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان</strong></div>
|
||||
</div>
|
||||
آیا مطمئن هستید که میخواهید مرحله را تایید کنید؟
|
||||
{% else %}
|
||||
آیا از تایید این مرحله اطمینان دارید؟
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="submit" class="btn btn-success">تایید</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Reject Modal for payments step -->
|
||||
<div class="modal fade" id="rejectPaymentsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">رد مرحله پرداختها</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">علت رد</label>
|
||||
<textarea class="form-control" name="reject_reason" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="submit" class="btn btn-danger">ثبت رد</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
const isFullyPaid = {{ totals.is_fully_paid|yesno:'true,false' }};
|
||||
// Removed legacy isFullyPaid-driven approve flow; approval now via modal submit
|
||||
function buildFormData(form) {
|
||||
const fd = new FormData(form);
|
||||
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
||||
return fd;
|
||||
}
|
||||
document.getElementById('btnAddPayment').addEventListener('click', function() {
|
||||
const btnAddPayment = document.getElementById('btnAddPayment');
|
||||
if (btnAddPayment) btnAddPayment.addEventListener('click', function() {
|
||||
// Front-end validation
|
||||
const amount = document.getElementById('id_amount').value.trim();
|
||||
const payDate = document.getElementById('id_payment_date').value.trim();
|
||||
|
@ -283,51 +401,7 @@
|
|||
alert('ویرایش فیش را بعدا با مدال تکمیل میکنیم. فعلا حذف و افزودن مجدد انجام دهید.');
|
||||
}
|
||||
|
||||
function performApprovePayments() {
|
||||
const fd = new FormData();
|
||||
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
||||
fetch('{% url "invoices:approve_payments" instance.id step.id %}', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
}).then(r => r.json()).then(resp => {
|
||||
if (resp.success) {
|
||||
showToast(resp.message || 'پرداختها تایید شد', 'success');
|
||||
if (resp.redirect) {
|
||||
setTimeout(() => { window.location.href = resp.redirect; }, 600);
|
||||
}
|
||||
} else {
|
||||
showToast(resp.message || resp.error || 'خطا در تایید پرداختها', 'danger');
|
||||
}
|
||||
}).catch(() => showToast('خطا در ارتباط با سرور', 'danger'));
|
||||
}
|
||||
|
||||
function openApproveModal() {
|
||||
const el = document.getElementById('approvePaymentsModal');
|
||||
const remEl = document.getElementById('remainingAmountText');
|
||||
if (remEl) {
|
||||
remEl.textContent = '{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان';
|
||||
}
|
||||
// Prefer jQuery plugin if available to avoid namespace issues
|
||||
if (window.$ && typeof $(el).modal === 'function') {
|
||||
$(el).modal('show');
|
||||
} else if (window.bootstrap && window.bootstrap.Modal) {
|
||||
const modal = new window.bootstrap.Modal(el);
|
||||
modal.show();
|
||||
} else {
|
||||
// fallback: force display
|
||||
el.classList.add('show');
|
||||
el.style.display = 'block';
|
||||
el.removeAttribute('aria-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btnApprovePayments').addEventListener('click', function() {
|
||||
if (isFullyPaid) {
|
||||
performApprovePayments();
|
||||
} else {
|
||||
openApproveModal();
|
||||
}
|
||||
});
|
||||
// Legacy approve JS removed; approval handled by modal forms in header
|
||||
</script>
|
||||
|
||||
<!-- Persian Date Picker JS -->
|
||||
|
@ -365,42 +439,4 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف فیش</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
آیا از حذف این فیش مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Approve Confirmation Modal (shown when remaining amount > 0) -->
|
||||
<div class="modal fade" id="approvePaymentsModal" tabindex="-1" aria-labelledby="approvePaymentsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="approvePaymentsModalLabel">تایید نهایی پرداختها</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
مبلغی از پیشفاکتور هنوز پرداخت نشده است.
|
||||
<div class="mt-2">مانده: <strong id="remainingAmountText"></strong></div>
|
||||
آیا مطمئن هستید که میخواهید مرحله را تایید و به مرحله بعد بروید؟
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="performApprovePayments()">بله، تایید</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -221,20 +221,32 @@
|
|||
<span></span>
|
||||
{% endif %}
|
||||
|
||||
{% if step_instance.status == 'completed' %}
|
||||
{% if is_broker %}
|
||||
{% if step_instance.status == 'completed' %}
|
||||
{% if next_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}"
|
||||
class="btn btn-primary">
|
||||
<span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
|
||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="button">اتمام</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" id="btnApproveQuote">
|
||||
تایید پیشفاکتور
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if next_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}"
|
||||
class="btn btn-primary">
|
||||
<span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
|
||||
class="btn btn-label-primary">
|
||||
مرحله بعد
|
||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="button">اتمام</button>
|
||||
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" id="btnApproveQuote">
|
||||
تایید پیشفاکتور
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="col-12">
|
||||
{% if is_broker or existing_quote %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
|
@ -77,7 +78,8 @@
|
|||
data-item-id="{{ item.id }}"
|
||||
data-is-default="{% if item.is_default_in_quotes %}1{% else %}0{% endif %}"
|
||||
{% if selected_qty %}checked{% elif item.is_default_in_quotes %}checked{% endif %}
|
||||
{% if item.is_default_in_quotes %}disabled title="آیتم پیشفرض است و قابل حذف نیست"{% endif %}>
|
||||
{% if item.is_default_in_quotes or not is_broker %}disabled{% endif %}
|
||||
{% if item.is_default_in_quotes %}title="آیتم پیشفرض است و قابل حذف نیست"{% elif not is_broker %}title="فقط کارگزار مجاز به تغییر اقلام است"{% endif %}>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
|
@ -86,15 +88,15 @@
|
|||
<span class="badge bg-label-primary me-2">پیشفرض</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% if item.description %}<small class="text-muted">{{ item.description }}</small>{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.unit_price|floatformat:0|intcomma:False }} تومان</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm quote-item-qty" min="1"
|
||||
data-item-id="{{ item.id }}"
|
||||
value="{% if selected_qty %}{{ selected_qty }}{% else %}{{ item.default_quantity }}{% endif %}">
|
||||
<input type="number" class="form-control form-control-sm quote-item-qty" min="1"
|
||||
data-item-id="{{ item.id }}"
|
||||
value="{% if selected_qty %}{{ selected_qty }}{% else %}{{ item.default_quantity }}{% endif %}"
|
||||
{% if not is_broker %}disabled title="فقط کارگزار مجاز به تغییر تعداد است"{% endif %}>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
|
@ -102,8 +104,9 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-warning mb-0">شما دسترسی به ثبت اقلام ندارید.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-between">
|
||||
|
@ -118,27 +121,35 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% if step_instance.status == 'completed' %}
|
||||
{% if next_step %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
||||
|
||||
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
||||
و بعدی
|
||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if is_broker %}
|
||||
{% if step_instance.status == 'completed' %}
|
||||
{% if next_step %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
||||
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
||||
و بعدی
|
||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="button">اتمام</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="button">اتمام</button>
|
||||
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
||||
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
||||
و بعدی
|
||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
||||
|
||||
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
||||
و بعدی
|
||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||
</button>
|
||||
{% if next_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-label-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 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,9 @@ from django.urls import reverse
|
|||
from decimal import Decimal, InvalidOperation
|
||||
import json
|
||||
|
||||
from processes.models import ProcessInstance, ProcessStep, StepInstance
|
||||
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 installations.models import InstallationReport, InstallationItemChange
|
||||
|
||||
|
@ -28,7 +30,7 @@ def quote_step(request, instance_id, step_id):
|
|||
return redirect('processes:request_list')
|
||||
|
||||
# دریافت آیتمها
|
||||
items = Item.objects.all().order_by('name')
|
||||
items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name')
|
||||
existing_quote = Quote.objects.filter(process_instance=instance).first()
|
||||
existing_quote_items = {}
|
||||
if existing_quote:
|
||||
|
@ -40,6 +42,14 @@ def quote_step(request, instance_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()
|
||||
|
||||
# determine if current user is broker
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
is_broker = False
|
||||
try:
|
||||
is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
|
||||
except Exception:
|
||||
is_broker = False
|
||||
|
||||
return render(request, 'invoices/quote_step.html', {
|
||||
'instance': instance,
|
||||
'step': step,
|
||||
|
@ -49,6 +59,7 @@ def quote_step(request, instance_id, step_id):
|
|||
'existing_quote': existing_quote,
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
'is_broker': is_broker,
|
||||
})
|
||||
|
||||
@require_POST
|
||||
|
@ -57,6 +68,13 @@ def create_quote(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)
|
||||
# enforce permission: only BROKER can create/update quote
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
try:
|
||||
if not (profile and profile.has_role(UserRoles.BROKER)):
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز ثبت/ویرایش پیشفاکتور را ندارید'})
|
||||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز ثبت/ویرایش پیشفاکتور را ندارید'})
|
||||
|
||||
try:
|
||||
items_payload = json.loads(request.POST.get('items') or '[]')
|
||||
|
@ -72,7 +90,7 @@ def create_quote(request, instance_id, step_id):
|
|||
except Exception:
|
||||
continue
|
||||
|
||||
default_item_ids = set(Item.objects.filter(is_default_in_quotes=True).values_list('id', flat=True))
|
||||
default_item_ids = set(Item.objects.filter(is_default_in_quotes=True, is_deleted=False).values_list('id', flat=True))
|
||||
if default_item_ids:
|
||||
for default_id in default_item_ids:
|
||||
if default_id not in payload_by_id:
|
||||
|
@ -163,6 +181,14 @@ def quote_preview_step(request, instance_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()
|
||||
|
||||
# determine if current user is broker for UI controls
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
is_broker = False
|
||||
try:
|
||||
is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
|
||||
except Exception:
|
||||
is_broker = False
|
||||
|
||||
return render(request, 'invoices/quote_preview_step.html', {
|
||||
'instance': instance,
|
||||
'step': step,
|
||||
|
@ -170,6 +196,7 @@ def quote_preview_step(request, instance_id, step_id):
|
|||
'quote': quote,
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
'is_broker': is_broker,
|
||||
})
|
||||
|
||||
@login_required
|
||||
|
@ -190,6 +217,13 @@ def approve_quote(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)
|
||||
quote = get_object_or_404(Quote, process_instance=instance)
|
||||
# enforce permission: only BROKER can approve
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
try:
|
||||
if not (profile and profile.has_role(UserRoles.BROKER)):
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز تایید پیشفاکتور را ندارید'})
|
||||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز تایید پیشفاکتور را ندارید'})
|
||||
|
||||
# تایید پیشفاکتور
|
||||
quote.status = 'sent'
|
||||
|
@ -247,7 +281,97 @@ def quote_payment_step(request, instance_id, step_id):
|
|||
'is_fully_paid': quote.get_remaining_amount() <= 0,
|
||||
}
|
||||
|
||||
step_instance = instance.step_instances.filter(step=step).first()
|
||||
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
|
||||
reqs = list(step.approver_requirements.select_related('role').all())
|
||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
|
||||
user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
|
||||
approvals_list = list(step_instance.approvals.select_related('role').all())
|
||||
approvals_by_role = {a.role_id: a for a in approvals_list}
|
||||
approver_statuses = [
|
||||
{
|
||||
'role': r.role,
|
||||
'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
|
||||
'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
|
||||
}
|
||||
for r in reqs
|
||||
]
|
||||
# dynamic permission: who can approve/reject this step (based on requirements)
|
||||
try:
|
||||
req_role_ids = {r.role_id for r in reqs}
|
||||
user_role_ids = {ur.id for ur in user_roles}
|
||||
can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
|
||||
except Exception:
|
||||
can_approve_reject = False
|
||||
# approver status map for template
|
||||
reqs = list(step.approver_requirements.select_related('role').all())
|
||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
|
||||
user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
|
||||
approvals_list = list(step_instance.approvals.select_related('role').all())
|
||||
approvals_by_role = {a.role_id: a for a in approvals_list}
|
||||
approver_statuses = [
|
||||
{
|
||||
'role': r.role,
|
||||
'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
|
||||
'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
|
||||
}
|
||||
for r in reqs
|
||||
]
|
||||
|
||||
# Accountant/Admin approval and rejection via POST (multi-role)
|
||||
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
|
||||
# match user's role against step required approver roles
|
||||
req_roles = [req.role for req in step.approver_requirements.select_related('role').all()]
|
||||
user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all())
|
||||
matching_role = next((r for r in user_roles if r in req_roles), None)
|
||||
if matching_role is None:
|
||||
messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
|
||||
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
action = request.POST.get('action')
|
||||
if action == 'approve':
|
||||
StepApproval.objects.update_or_create(
|
||||
step_instance=step_instance,
|
||||
role=matching_role,
|
||||
defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
|
||||
)
|
||||
if step_instance.is_fully_approved():
|
||||
step_instance.status = 'completed'
|
||||
step_instance.completed_at = timezone.now()
|
||||
step_instance.save()
|
||||
# move to next step
|
||||
redirect_url = 'processes:request_list'
|
||||
if next_step:
|
||||
instance.current_step = next_step
|
||||
instance.save()
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
|
||||
return redirect(redirect_url)
|
||||
messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقشها.')
|
||||
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
if action == 'reject':
|
||||
reason = (request.POST.get('reject_reason') or '').strip()
|
||||
if not reason:
|
||||
messages.error(request, 'علت رد شدن را وارد کنید')
|
||||
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
||||
StepApproval.objects.update_or_create(
|
||||
step_instance=step_instance,
|
||||
role=matching_role,
|
||||
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
|
||||
)
|
||||
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
|
||||
messages.success(request, 'مرحله پرداختها رد شد و برای اصلاح بازگشت.')
|
||||
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
# role flags for permissions (legacy flags kept for compatibility)
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
is_broker = False
|
||||
is_accountant = False
|
||||
try:
|
||||
is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
|
||||
is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
|
||||
except Exception:
|
||||
is_broker = False
|
||||
is_accountant = False
|
||||
|
||||
return render(request, 'invoices/quote_payment_step.html', {
|
||||
'instance': instance,
|
||||
|
@ -258,6 +382,12 @@ def quote_payment_step(request, instance_id, step_id):
|
|||
'totals': totals,
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
'approver_statuses': approver_statuses,
|
||||
'is_broker': is_broker,
|
||||
'is_accountant': is_accountant,
|
||||
# dynamic permissions: any role required to approve can also manage payments
|
||||
'can_manage_payments': can_approve_reject,
|
||||
'can_approve_reject': can_approve_reject,
|
||||
})
|
||||
|
||||
|
||||
|
@ -279,6 +409,16 @@ def add_quote_payment(request, instance_id, step_id):
|
|||
}
|
||||
)
|
||||
|
||||
# dynamic permission: users whose roles are among required approvers can add payments
|
||||
try:
|
||||
req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
|
||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
|
||||
user_role_ids = set(user_roles_qs.values_list('id', flat=True))
|
||||
if len(req_role_ids.intersection(user_role_ids)) == 0:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
|
||||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
amount = (request.POST.get('amount') or '').strip()
|
||||
|
@ -325,6 +465,15 @@ def add_quote_payment(request, instance_id, step_id):
|
|||
logger.exception('Error adding quote payment (instance=%s, step=%s)', instance_id, step_id)
|
||||
return JsonResponse({'success': False, 'message': 'خطا در ثبت فیش', 'error': str(e)})
|
||||
|
||||
# After modifying payments, set step back to in_progress (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
|
||||
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
||||
|
||||
|
@ -360,6 +509,15 @@ def update_quote_payment(request, instance_id, step_id, payment_id):
|
|||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'})
|
||||
|
||||
# On update, 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
|
||||
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
||||
|
||||
|
@ -374,11 +532,30 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
|
|||
if not invoice:
|
||||
return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
|
||||
payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
|
||||
# dynamic permission: users whose roles are among required approvers can delete payments
|
||||
try:
|
||||
req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
|
||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
|
||||
user_role_ids = set(user_roles_qs.values_list('id', flat=True))
|
||||
if len(req_role_ids.intersection(user_role_ids)) == 0:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
|
||||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
|
||||
|
||||
try:
|
||||
# soft delete using project's BaseModel delete override
|
||||
payment.delete()
|
||||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
|
||||
# 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
|
||||
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
||||
|
||||
|
@ -534,6 +711,14 @@ def final_invoice_step(request, instance_id, step_id):
|
|||
# Choices for special items from DB
|
||||
special_choices = list(Item.objects.filter(is_special=True).values('id', 'name'))
|
||||
|
||||
# role flag for manager-only actions
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
is_manager = False
|
||||
try:
|
||||
is_manager = bool(profile and profile.has_role(UserRoles.MANAGER))
|
||||
except Exception:
|
||||
is_manager = False
|
||||
|
||||
return render(request, 'invoices/final_invoice_step.html', {
|
||||
'instance': instance,
|
||||
'step': step,
|
||||
|
@ -543,6 +728,7 @@ def final_invoice_step(request, instance_id, step_id):
|
|||
'invoice_specials': invoice.items.select_related('item').filter(item__is_special=True, is_deleted=False).all(),
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
'is_manager': is_manager,
|
||||
})
|
||||
|
||||
|
||||
|
@ -564,6 +750,12 @@ def approve_final_invoice(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)
|
||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
||||
# only MANAGER can approve
|
||||
try:
|
||||
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
|
||||
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:
|
||||
|
@ -592,6 +784,12 @@ def add_special_charge(request, instance_id, step_id):
|
|||
"""افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی بهصورت آیتم جداگانه"""
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
||||
# only MANAGER can add special charges
|
||||
try:
|
||||
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
|
||||
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()
|
||||
|
@ -623,6 +821,12 @@ def add_special_charge(request, instance_id, step_id):
|
|||
def delete_special_charge(request, instance_id, step_id, item_id):
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
||||
# only MANAGER can delete special charges
|
||||
try:
|
||||
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
|
||||
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
|
||||
|
@ -648,13 +852,87 @@ def final_settlement_step(request, instance_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()
|
||||
|
||||
# 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()}
|
||||
approver_statuses = [{'role': r.role, 'status': approvals_map.get(r.role_id)} for r in reqs]
|
||||
# dynamic permission to control approve/reject UI
|
||||
try:
|
||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
|
||||
user_role_ids = set(user_roles_qs.values_list('id', flat=True))
|
||||
req_role_ids = {r.role_id for r in reqs}
|
||||
can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
|
||||
except Exception:
|
||||
can_approve_reject = False
|
||||
|
||||
# Accountant/Admin approval and rejection (multi-role)
|
||||
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
|
||||
req_roles = [req.role for req in step.approver_requirements.select_related('role').all()]
|
||||
user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all())
|
||||
matching_role = next((r for r in user_roles if r in req_roles), None)
|
||||
if matching_role is None:
|
||||
messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
|
||||
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
action = request.POST.get('action')
|
||||
if action == 'approve':
|
||||
# enforce zero remaining
|
||||
invoice.calculate_totals()
|
||||
if invoice.remaining_amount != 0:
|
||||
messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})")
|
||||
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
|
||||
StepApproval.objects.update_or_create(
|
||||
step_instance=step_instance,
|
||||
role=matching_role,
|
||||
defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
|
||||
)
|
||||
if step_instance.is_fully_approved():
|
||||
step_instance.status = 'completed'
|
||||
step_instance.completed_at = timezone.now()
|
||||
step_instance.save()
|
||||
if next_step:
|
||||
instance.current_step = next_step
|
||||
instance.save()
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
|
||||
return redirect('processes:request_list')
|
||||
messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقشها.')
|
||||
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
if action == 'reject':
|
||||
reason = (request.POST.get('reject_reason') or '').strip()
|
||||
if not reason:
|
||||
messages.error(request, 'علت رد شدن را وارد کنید')
|
||||
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
|
||||
StepApproval.objects.update_or_create(
|
||||
step_instance=step_instance,
|
||||
role=matching_role,
|
||||
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
|
||||
)
|
||||
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
|
||||
messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
|
||||
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
# broker flag for payment management permission
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
is_broker = False
|
||||
try:
|
||||
is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
|
||||
except Exception:
|
||||
is_broker = False
|
||||
|
||||
return render(request, 'invoices/final_settlement_step.html', {
|
||||
'instance': instance,
|
||||
'step': step,
|
||||
'invoice': invoice,
|
||||
'payments': invoice.payments.filter(is_deleted=False).all(),
|
||||
'step_instance': step_instance,
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
'approver_statuses': approver_statuses,
|
||||
'can_approve_reject': can_approve_reject,
|
||||
'is_broker': is_broker,
|
||||
})
|
||||
|
||||
|
||||
|
@ -662,7 +940,14 @@ def final_settlement_step(request, instance_id, step_id):
|
|||
@login_required
|
||||
def add_final_payment(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)
|
||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
||||
# Only BROKER can add final settlement payments
|
||||
try:
|
||||
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
|
||||
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()
|
||||
|
@ -717,6 +1002,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
|
||||
try:
|
||||
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
|
||||
si.status = 'in_progress'
|
||||
si.completed_at = None
|
||||
si.save()
|
||||
except Exception:
|
||||
pass
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
|
||||
|
@ -732,10 +1025,25 @@ def add_final_payment(request, instance_id, step_id):
|
|||
@login_required
|
||||
def delete_final_payment(request, instance_id, step_id, payment_id):
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
||||
payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
|
||||
# Only BROKER can delete final settlement payments
|
||||
try:
|
||||
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
|
||||
except Exception:
|
||||
return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
|
||||
payment.delete()
|
||||
invoice.refresh_from_db()
|
||||
# After payment change, set step back to in_progress
|
||||
try:
|
||||
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
|
||||
si.status = 'in_progress'
|
||||
si.completed_at = None
|
||||
si.save()
|
||||
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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue