diff --git a/certificates/views.py b/certificates/views.py
index 5bdcade..5149334 100644
--- a/certificates/views.py
+++ b/certificates/views.py
@@ -78,7 +78,7 @@ def certificate_step(request, instance_id, step_id):
if inv:
if prev_si and not prev_si.status == 'approved':
inv.calculate_totals()
- if inv.remaining_amount != 0:
+ if inv.get_remaining_amount() != 0:
messages.error(request, 'مانده فاکتور باید صفر باشد')
return redirect('processes:request_list')
diff --git a/db.sqlite3 b/db.sqlite3
index ac3e854..02dd5e4 100644
Binary files a/db.sqlite3 and b/db.sqlite3 differ
diff --git a/invoices/admin.py b/invoices/admin.py
index f8a46cb..72df296 100644
--- a/invoices/admin.py
+++ b/invoices/admin.py
@@ -44,11 +44,11 @@ class PaymentInline(admin.TabularInline):
@admin.register(Invoice)
class InvoiceAdmin(SimpleHistoryAdmin):
- list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount', 'remaining_amount', 'due_date']
+ list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount_display', 'remaining_amount_display', 'due_date']
list_filter = ['status', 'created', 'due_date', 'process_instance__process']
search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
prepopulated_fields = {'slug': ('name',)}
- readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount', 'remaining_amount']
+ readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount_display', 'remaining_amount_display']
inlines = [InvoiceItemInline, PaymentInline]
ordering = ['-created']
@@ -56,6 +56,16 @@ class InvoiceAdmin(SimpleHistoryAdmin):
return mark_safe(obj.get_status_display_with_color())
status_display.short_description = "وضعیت"
+ def paid_amount_display(self, obj):
+ return f"{obj.get_paid_amount():,.0f} تومان"
+ paid_amount_display.short_description = "مبلغ پرداخت شده"
+
+ def remaining_amount_display(self, obj):
+ amount = obj.get_remaining_amount()
+ color = "green" if amount <= 0 else "red"
+ return format_html('{:,.0f} تومان', color, amount)
+ remaining_amount_display.short_description = "مبلغ باقیمانده"
+
@admin.register(Payment)
class PaymentAdmin(SimpleHistoryAdmin):
list_display = ['invoice', 'amount', 'payment_method', 'payment_date', 'created_by']
diff --git a/invoices/migrations/0002_remove_historicalinvoice_paid_amount_and_more.py b/invoices/migrations/0002_remove_historicalinvoice_paid_amount_and_more.py
new file mode 100644
index 0000000..4617568
--- /dev/null
+++ b/invoices/migrations/0002_remove_historicalinvoice_paid_amount_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.2.4 on 2025-10-04 08:16
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('invoices', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='historicalinvoice',
+ name='paid_amount',
+ ),
+ migrations.RemoveField(
+ model_name='historicalinvoice',
+ name='remaining_amount',
+ ),
+ migrations.RemoveField(
+ model_name='invoice',
+ name='paid_amount',
+ ),
+ migrations.RemoveField(
+ model_name='invoice',
+ name='remaining_amount',
+ ),
+ ]
diff --git a/invoices/models.py b/invoices/models.py
index 854f46b..4a48c8c 100644
--- a/invoices/models.py
+++ b/invoices/models.py
@@ -228,18 +228,6 @@ class Invoice(NameSlugModel):
default=0,
verbose_name="مبلغ نهایی"
)
- paid_amount = models.DecimalField(
- max_digits=15,
- decimal_places=2,
- default=0,
- verbose_name="مبلغ پرداخت شده"
- )
- remaining_amount = models.DecimalField(
- max_digits=15,
- decimal_places=2,
- default=0,
- verbose_name="مبلغ باقیمانده"
- )
due_date = models.DateField(verbose_name="تاریخ سررسید")
notes = models.TextField(verbose_name="یادداشتها", blank=True)
created_by = models.ForeignKey(
@@ -278,22 +266,31 @@ class Invoice(NameSlugModel):
vat_amount = base_amount * vat_rate
self.final_amount = base_amount + vat_amount
- # خالص مانده به نفع شرکت (مثبت) یا به نفع مشتری (منفی)
- net_due = self.final_amount - self.paid_amount
- self.remaining_amount = net_due
-
- # وضعیت بر اساس مانده خالص
+ # وضعیت بر اساس مانده خالص (استفاده از تابعها)
+ paid = self.get_paid_amount()
+ net_due = self.final_amount - paid
+
if net_due == 0:
self.status = 'paid'
elif net_due > 0:
# مشتری هنوز باید پرداخت کند
- self.status = 'partially_paid' if self.paid_amount > 0 else 'sent'
+ self.status = 'partially_paid' if paid > 0 else 'sent'
else:
# شرکت باید به مشتری پرداخت کند
self.status = 'partially_paid'
self.save()
+ def get_paid_amount(self):
+ """مبلغ پرداخت شده بر اساس پرداختها (مثل Quote)"""
+ return sum((p.amount if p.direction == 'in' else -p.amount) for p in self.payments.filter(is_deleted=False).all())
+
+ def get_remaining_amount(self):
+ """مبلغ باقیمانده بر اساس پرداختها (مثل Quote)"""
+ paid = self.get_paid_amount()
+ remaining = self.final_amount - paid
+ return remaining
+
def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ"""
@@ -373,17 +370,13 @@ class Payment(BaseModel):
def save(self, *args, **kwargs):
"""بروزرسانی مبالغ فاکتور"""
super().save(*args, **kwargs)
- # بروزرسانی مبلغ پرداخت شده فاکتور
- total_paid = sum((p.amount if p.direction == 'in' else -p.amount) for p in self.invoice.payments.filter(is_deleted=False).all())
- self.invoice.paid_amount = total_paid
+ # فقط مجدداً calculate_totals را صدا کن (مثل Quote)
self.invoice.calculate_totals()
def delete(self, using=None, keep_parents=False):
"""حذف نرم و بروزرسانی مبالغ فاکتور پس از حذف"""
result = super().delete(using=using, keep_parents=keep_parents)
try:
- total_paid = sum((p.amount if p.direction == 'in' else -p.amount) for p in self.invoice.payments.filter(is_deleted=False).all())
- self.invoice.paid_amount = total_paid
self.invoice.calculate_totals()
except Exception:
pass
diff --git a/invoices/templates/invoices/final_invoice_print.html b/invoices/templates/invoices/final_invoice_print.html
index 36f0ffd..d9c8333 100644
--- a/invoices/templates/invoices/final_invoice_print.html
+++ b/invoices/templates/invoices/final_invoice_print.html
@@ -159,11 +159,11 @@
| پرداختیها(تومان): |
- {{ invoice.paid_amount|floatformat:0|intcomma:False }} |
+ {{ invoice.get_paid_amount|floatformat:0|intcomma:False }} |
| مانده(تومان): |
- {{ invoice.remaining_amount|floatformat:0|intcomma:False }} |
+ {{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} |
diff --git a/invoices/templates/invoices/final_invoice_step.html b/invoices/templates/invoices/final_invoice_step.html
index 9fc706c..1d99072 100644
--- a/invoices/templates/invoices/final_invoice_step.html
+++ b/invoices/templates/invoices/final_invoice_step.html
@@ -74,17 +74,17 @@
پرداختیها
-
{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان
+
{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان
مانده
-
{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان
+
{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان
- {% if invoice.remaining_amount <= 0 %}
+ {% if invoice.get_remaining_amount <= 0 %}
تسویه کامل
{% else %}
باقیمانده دارد
@@ -165,11 +165,11 @@
| پرداختیها |
- {{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان |
+ {{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان |
| مانده |
- {{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان |
+ {{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان |
diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html
index a1514c9..a4767d4 100644
--- a/invoices/templates/invoices/final_settlement_step.html
+++ b/invoices/templates/invoices/final_settlement_step.html
@@ -42,7 +42,7 @@
پرینت
- {% if request.user|is_manager and step_instance.status != 'approved' and step_instance.status != 'completed' and invoice.remaining_amount != 0 %}
+ {% if request.user|is_manager and step_instance.status != 'approved' and step_instance.status != 'completed' and invoice.get_remaining_amount != 0 %}
@@ -128,17 +128,17 @@
پرداختیها
-
{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان
+
{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان
مانده
-
{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان
+
{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان
- {% if invoice.remaining_amount <= 0 %}
+ {% if invoice.get_remaining_amount <= 0 %}
تسویه کامل
{% else %}
باقیمانده دارد
@@ -318,9 +318,9 @@
- {% if invoice.remaining_amount != 0 %}
+ {% if invoice.get_remaining_amount != 0 %}
- مانده فاکتور: {{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان
+ مانده فاکتور: {{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان
امکان تایید تا تسویه کامل فاکتور وجود ندارد.
{% else %}
@@ -329,7 +329,7 @@
diff --git a/invoices/views.py b/invoices/views.py
index 0df8191..778d837 100644
--- a/invoices/views.py
+++ b/invoices/views.py
@@ -942,8 +942,8 @@ def final_settlement_step(request, instance_id, step_id):
# Build approver statuses for template (include reason to display in UI)
reqs = list(step.approver_requirements.select_related('role').all())
- approvals = list(step_instance.approvals.select_related('role').all())
- rejections = list(step_instance.rejections.select_related('role').all())
+ approvals = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
+ rejections = list(step_instance.rejections.select_related('role', 'rejected_by').filter(is_deleted=False))
approvals_by_role = {a.role_id: a for a in approvals}
rejections_by_role = {r.role_id: r for r in rejections}
approver_statuses = []
@@ -978,8 +978,8 @@ def final_settlement_step(request, instance_id, step_id):
# Compute whether current user has already decided (approved/rejected)
current_user_has_decided = False
try:
- user_has_approval = step_instance.approvals.filter(approved_by=request.user).exists()
- user_has_rejection = step_instance.rejections.filter(rejected_by=request.user).exists()
+ user_has_approval = step_instance.approvals.filter(approved_by=request.user, is_deleted=False).exists()
+ user_has_rejection = step_instance.rejections.filter(rejected_by=request.user, is_deleted=False).exists()
current_user_has_decided = bool(user_has_approval or user_has_rejection)
except Exception:
current_user_has_decided = False
@@ -997,8 +997,8 @@ def final_settlement_step(request, instance_id, step_id):
if action == 'approve':
# enforce zero remaining
invoice.calculate_totals()
- if invoice.remaining_amount != 0:
- messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})")
+ if invoice.get_remaining_amount() != 0:
+ messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.get_remaining_amount()})")
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
StepApproval.objects.create(
step_instance=step_instance,
@@ -1203,8 +1203,8 @@ def add_final_payment(request, instance_id, step_id):
'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
'totals': {
'final_amount': str(invoice.final_amount),
- 'paid_amount': str(invoice.paid_amount),
- 'remaining_amount': str(invoice.remaining_amount),
+ 'paid_amount': str(invoice.get_paid_amount()),
+ 'remaining_amount': str(invoice.get_remaining_amount()),
}
})
@@ -1216,14 +1216,17 @@ def delete_final_payment(request, instance_id, step_id, payment_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)
+
+ # Delete payment and recalculate invoice totals
payment.hard_delete()
- invoice.refresh_from_db()
+ invoice.calculate_totals() # This is what was missing!
# On delete, return to awaiting approval
try:
@@ -1231,16 +1234,11 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
si.status = 'in_progress'
si.completed_at = None
si.save()
- try:
- for appr in list(si.approvals.all()):
- appr.delete()
- except Exception:
- pass
- try:
- for rej in list(si.rejections.all()):
- rej.delete()
- except Exception:
- pass
+ # Clear approvals and rejections (like in quote_payment)
+ for appr in list(si.approvals.all()):
+ appr.delete()
+ for rej in list(si.rejections.all()):
+ rej.delete()
except Exception:
pass
@@ -1274,7 +1272,7 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
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),
- 'remaining_amount': str(invoice.remaining_amount),
+ 'paid_amount': str(invoice.get_paid_amount()),
+ 'remaining_amount': str(invoice.get_remaining_amount()),
}})
diff --git a/processes/templates/processes/instance_summary.html b/processes/templates/processes/instance_summary.html
index a8529c6..52b0bf2 100644
--- a/processes/templates/processes/instance_summary.html
+++ b/processes/templates/processes/instance_summary.html
@@ -37,9 +37,9 @@
پرینت فاکتور
{% endif %}
-
+
+
بازگشت
@@ -57,8 +57,8 @@
{% if invoice %}
مبلغ نهایی
{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان
-
پرداختیها
{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان
-
مانده
{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان
+
پرداختیها
{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان
+
مانده
{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان
@@ -256,6 +256,30 @@
+
+
+
{% endblock %}
diff --git a/processes/views.py b/processes/views.py
index dca9607..b5936e4 100644
--- a/processes/views.py
+++ b/processes/views.py
@@ -643,7 +643,7 @@ def export_requests_excel(request):
).select_related('process_instance')
for invoice in invoices:
- if invoice.remaining_amount == 0: # Fully settled
+ if invoice.get_remaining_amount() == 0: # Fully settled
# Find the last payment date for this invoice
last_payment = Payment.objects.filter(
invoice__process_instance=invoice.process_instance,