From 169a9bd6247ec9c9878890656f7cc17181a2df0e Mon Sep 17 00:00:00 2001 From: aminhashemi92 Date: Tue, 7 Oct 2025 07:45:09 +0330 Subject: [PATCH] fix force approve and vat show --- .gitignore | 4 +- invoices/models.py | 18 +++++ .../invoices/final_invoice_print.html | 4 + .../invoices/final_invoice_step.html | 4 + .../invoices/final_settlement_step.html | 4 +- .../invoices/quote_preview_step.html | 2 + invoices/templates/invoices/quote_print.html | 4 + invoices/views.py | 79 +++++++++++++++++++ .../templates/processes/request_list.html | 7 +- processes/views.py | 12 +++ 10 files changed, 133 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 77e02cd..d975f31 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,8 @@ *.pyc __pycache__/ local_settings.py -# *.sqlite3 -# db.sqlite3 +*.sqlite3 +db.sqlite3 db.sqlite3-journal media #static diff --git a/invoices/models.py b/invoices/models.py index 4a48c8c..89dcd50 100644 --- a/invoices/models.py +++ b/invoices/models.py @@ -151,6 +151,15 @@ class Quote(NameSlugModel): remaining = Decimal('0') return remaining + def get_vat_amount(self) -> Decimal: + """محاسبه مبلغ مالیات به صورت جداگانه بر اساس VAT_RATE.""" + base_amount = (self.total_amount or Decimal('0')) - (self.discount_amount or Decimal('0')) + try: + vat_rate = Decimal(str(getattr(settings, 'VAT_RATE', 0))) + except Exception: + vat_rate = Decimal('0') + return base_amount * vat_rate + class QuoteItem(BaseModel): """مدل آیتم‌های پیش‌فاکتور""" quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیش‌فاکتور") @@ -291,6 +300,15 @@ class Invoice(NameSlugModel): remaining = self.final_amount - paid return remaining + def get_vat_amount(self) -> Decimal: + """محاسبه مبلغ مالیات به صورت جداگانه بر اساس VAT_RATE.""" + base_amount = (self.total_amount or Decimal('0')) - (self.discount_amount or Decimal('0')) + try: + vat_rate = Decimal(str(getattr(settings, 'VAT_RATE', 0))) + except Exception: + vat_rate = Decimal('0') + return base_amount * vat_rate + def get_status_display_with_color(self): """نمایش وضعیت با رنگ""" diff --git a/invoices/templates/invoices/final_invoice_print.html b/invoices/templates/invoices/final_invoice_print.html index d9c8333..1b28c4e 100644 --- a/invoices/templates/invoices/final_invoice_print.html +++ b/invoices/templates/invoices/final_invoice_print.html @@ -153,6 +153,10 @@ {{ invoice.discount_amount|floatformat:0|intcomma:False }} {% endif %} + + مالیات بر ارزش افزوده(تومان): + {{ invoice.get_vat_amount|floatformat:0|intcomma:False }} + مبلغ نهایی (شامل مالیات)(تومان): {{ invoice.final_amount|floatformat:0|intcomma:False }} diff --git a/invoices/templates/invoices/final_invoice_step.html b/invoices/templates/invoices/final_invoice_step.html index 17e6730..3200a48 100644 --- a/invoices/templates/invoices/final_invoice_step.html +++ b/invoices/templates/invoices/final_invoice_step.html @@ -159,6 +159,10 @@ تخفیف {{ invoice.discount_amount|floatformat:0|intcomma:False }} تومان + + مالیات بر ارزش افزوده + {{ invoice.get_vat_amount|floatformat:0|intcomma:False }} تومان + مبلغ نهایی (با مالیات) {{ invoice.final_amount|floatformat:0|intcomma:False }} تومان diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html index e03a7e2..e32fa41 100644 --- a/invoices/templates/invoices/final_settlement_step.html +++ b/invoices/templates/invoices/final_settlement_step.html @@ -60,7 +60,7 @@
- {% if is_broker %} + {% if is_broker and invoice.get_remaining_amount != 0 %}
ثبت تراکنش تسویه
@@ -193,7 +193,7 @@
- {% if approver_statuses %} + {% if approver_statuses and invoice.get_remaining_amount != 0 and step_instance.status != 'completed' %}
وضعیت تاییدها
diff --git a/invoices/templates/invoices/quote_preview_step.html b/invoices/templates/invoices/quote_preview_step.html index eeead73..08e3bd8 100644 --- a/invoices/templates/invoices/quote_preview_step.html +++ b/invoices/templates/invoices/quote_preview_step.html @@ -213,6 +213,7 @@ {% if quote.discount_amount > 0 %}

تخفیف:

{% endif %} +

مالیات بر ارزش افزوده:

مبلغ نهایی (شامل مالیات):

@@ -220,6 +221,7 @@ {% if quote.discount_amount > 0 %}

{{ quote.discount_amount|floatformat:0|intcomma:False }} تومان

{% endif %} +

{{ quote.get_vat_amount|floatformat:0|intcomma:False }} تومان

{{ quote.final_amount|floatformat:0|intcomma:False }} تومان

diff --git a/invoices/templates/invoices/quote_print.html b/invoices/templates/invoices/quote_print.html index fc445a6..fb67046 100644 --- a/invoices/templates/invoices/quote_print.html +++ b/invoices/templates/invoices/quote_print.html @@ -212,6 +212,10 @@ {{ quote.discount_amount|floatformat:0|intcomma:False }} {% endif %} + + مالیات بر ارزش افزوده(تومان): + {{ quote.get_vat_amount|floatformat:0|intcomma:False }} + مبلغ نهایی (با مالیات)(تومان): {{ quote.final_amount|floatformat:0|intcomma:False }} diff --git a/invoices/views.py b/invoices/views.py index 778d837..04ea85c 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -898,6 +898,29 @@ def add_special_charge(request, instance_id, step_id): unit_price=amount_dec, ) invoice.calculate_totals() + # If the next step was completed, reopen it (set to in_progress) due to invoice change + try: + step = get_object_or_404(instance.process.steps, id=step_id) + next_step = instance.process.steps.filter(order__gt=step.order).first() + if next_step: + si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=next_step) + if si.status in ['completed', 'approved']: + si.status = 'in_progress' + si.completed_at = None + si.save(update_fields=['status', 'completed_at']) + # Clear prior approvals/rejections as the underlying totals changed + 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 + except Exception: + pass return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])}) @@ -921,6 +944,29 @@ def delete_special_charge(request, instance_id, step_id, item_id): return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'}) inv_item.hard_delete() invoice.calculate_totals() + # If the next step was completed, reopen it (set to in_progress) due to invoice change + try: + step = get_object_or_404(instance.process.steps, id=step_id) + next_step = instance.process.steps.filter(order__gt=step.order).first() + if next_step: + si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=next_step) + if si.status in ['completed', 'approved']: + si.status = 'in_progress' + si.completed_at = None + si.save(update_fields=['status', 'completed_at']) + # Clear prior approvals/rejections as the underlying totals changed + 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 + except Exception: + pass return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])}) @@ -939,6 +985,23 @@ 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'}) + + # Auto-complete step when invoice is fully settled (no approvals needed) + try: + invoice.calculate_totals() + if invoice.get_remaining_amount() == 0: + if step_instance.status != 'completed': + step_instance.status = 'completed' + step_instance.completed_at = timezone.now() + step_instance.save() + # if next_step: + # instance.current_step = next_step + # instance.save(update_fields=['current_step']) + # return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) + # return redirect('processes:request_list') + except Exception: + # If totals calculation fails, continue with normal flow + pass # Build approver statuses for template (include reason to display in UI) reqs = list(step.approver_requirements.select_related('role').all()) @@ -1048,6 +1111,14 @@ def final_settlement_step(request, instance_id, step_id): except Exception: messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.') return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) + # Allow emergency approval only when invoice has a remaining (non-zero) + try: + invoice.calculate_totals() + if invoice.get_remaining_amount() == 0: + messages.error(request, 'فاکتور تسویه شده است؛ تایید اضطراری لازم نیست.') + return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) + except Exception: + pass # Mark step completed regardless of remaining amount/approvals step_instance.status = 'approved' step_instance.save() @@ -1094,6 +1165,14 @@ def add_final_payment(request, instance_id, step_id): except Exception: return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) + # Prevent adding payments if invoice already settled + try: + invoice.calculate_totals() + if invoice.get_remaining_amount() == 0: + return JsonResponse({'success': False, 'message': 'فاکتور تسویه شده است؛ افزودن تراکنش مجاز نیست'}) + except Exception: + pass + 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() diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html index 9ec7e92..eee1727 100644 --- a/processes/templates/processes/request_list.html +++ b/processes/templates/processes/request_list.html @@ -245,7 +245,12 @@ {{ item.progress_percentage }}%
- {{ item.instance.get_status_display_with_color|safe }} + + {{ item.instance.get_status_display_with_color|safe }} + {% if item.emergency_approved %} + تایید اضطراری + {% endif %} + {% if item.installation_scheduled_date %}
diff --git a/processes/views.py b/processes/views.py index e959b2e..372c8e7 100644 --- a/processes/views.py +++ b/processes/views.py @@ -123,6 +123,17 @@ def request_list(request): reference_date = None installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date + + # Emergency approved flag (final settlement step forced approval) + try: + final_settlement_step = instance.process.steps.filter(order=8).first() + emergency_approved = False + if final_settlement_step: + si = instance.step_instances.filter(step=final_settlement_step).first() + emergency_approved = bool(si and si.status == 'approved') + except Exception: + emergency_approved = False + instances_with_progress.append({ 'instance': instance, 'progress_percentage': round(progress_percentage), @@ -130,6 +141,7 @@ def request_list(request): 'total_steps': total_steps, 'installation_scheduled_date': installation_scheduled_date, 'installation_overdue_days': overdue_days, + 'emergency_approved': emergency_approved, }) # Summary stats for header cards