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 @@
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,