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 %}
- {% if approver_statuses %}
+ {% if approver_statuses and invoice.get_remaining_amount != 0 and step_instance.status != 'completed' %}
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
|