fix force approve and vat show

This commit is contained in:
aminhashemi92 2025-10-07 07:45:09 +03:30
parent 65cc48769d
commit 169a9bd624
10 changed files with 133 additions and 5 deletions

4
.gitignore vendored
View file

@ -9,8 +9,8 @@
*.pyc *.pyc
__pycache__/ __pycache__/
local_settings.py local_settings.py
# *.sqlite3 *.sqlite3
# db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
media media
#static #static

View file

@ -151,6 +151,15 @@ class Quote(NameSlugModel):
remaining = Decimal('0') remaining = Decimal('0')
return remaining 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): class QuoteItem(BaseModel):
"""مدل آیتم‌های پیش‌فاکتور""" """مدل آیتم‌های پیش‌فاکتور"""
quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیش‌فاکتور") 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 remaining = self.final_amount - paid
return remaining 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): def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ""" """نمایش وضعیت با رنگ"""

View file

@ -153,6 +153,10 @@
<td><strong>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</strong></td> <td><strong>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</strong></td>
</tr> </tr>
{% endif %} {% endif %}
<tr class="total-section">
<td colspan="5" class="text-end"><strong>مالیات بر ارزش افزوده(تومان):</strong></td>
<td><strong>{{ invoice.get_vat_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section border-top border-2"> <tr class="total-section border-top border-2">
<td colspan="5" class="text-end"><strong>مبلغ نهایی (شامل مالیات)(تومان):</strong></td> <td colspan="5" class="text-end"><strong>مبلغ نهایی (شامل مالیات)(تومان):</strong></td>
<td><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td> <td><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td>

View file

@ -159,6 +159,10 @@
<th colspan="6" class="text-end">تخفیف</th> <th colspan="6" class="text-end">تخفیف</th>
<th class="text-end">{{ invoice.discount_amount|floatformat:0|intcomma:False }} تومان</th> <th class="text-end">{{ invoice.discount_amount|floatformat:0|intcomma:False }} تومان</th>
</tr> </tr>
<tr>
<th colspan="6" class="text-end">مالیات بر ارزش افزوده</th>
<th class="text-end">{{ invoice.get_vat_amount|floatformat:0|intcomma:False }} تومان</th>
</tr>
<tr> <tr>
<th colspan="6" class="text-end">مبلغ نهایی (با مالیات)</th> <th colspan="6" class="text-end">مبلغ نهایی (با مالیات)</th>
<th class="text-end">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</th> <th class="text-end">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</th>

View file

@ -60,7 +60,7 @@
<div class="bs-stepper-content"> <div class="bs-stepper-content">
<div class="row g-3"> <div class="row g-3">
{% if is_broker %} {% if is_broker and invoice.get_remaining_amount != 0 %}
<div class="col-12 col-lg-5"> <div class="col-12 col-lg-5">
<div class="card border h-100"> <div class="card border h-100">
<div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div> <div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div>
@ -193,7 +193,7 @@
</div> </div>
</div> </div>
</div> </div>
{% if approver_statuses %} {% if approver_statuses and invoice.get_remaining_amount != 0 and step_instance.status != 'completed' %}
<div class="card border mt-2"> <div class="card border mt-2">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">وضعیت تاییدها</h6> <h6 class="mb-0">وضعیت تاییدها</h6>

View file

@ -213,6 +213,7 @@
{% if quote.discount_amount > 0 %} {% if quote.discount_amount > 0 %}
<p class="mb-2">تخفیف:</p> <p class="mb-2">تخفیف:</p>
{% endif %} {% endif %}
<p class="mb-2">مالیات بر ارزش افزوده:</p>
<p class="mb-0 fw-bold">مبلغ نهایی (شامل مالیات):</p> <p class="mb-0 fw-bold">مبلغ نهایی (شامل مالیات):</p>
</td> </td>
<td class="px-4 py-5"> <td class="px-4 py-5">
@ -220,6 +221,7 @@
{% if quote.discount_amount > 0 %} {% if quote.discount_amount > 0 %}
<p class="fw-medium mb-2">{{ quote.discount_amount|floatformat:0|intcomma:False }} تومان</p> <p class="fw-medium mb-2">{{ quote.discount_amount|floatformat:0|intcomma:False }} تومان</p>
{% endif %} {% endif %}
<p class="fw-medium mb-2">{{ quote.get_vat_amount|floatformat:0|intcomma:False }} تومان</p>
<p class="fw-bold mb-0">{{ quote.final_amount|floatformat:0|intcomma:False }} تومان</p> <p class="fw-bold mb-0">{{ quote.final_amount|floatformat:0|intcomma:False }} تومان</p>
</td> </td>
</tr> </tr>

View file

@ -212,6 +212,10 @@
<td><strong>{{ quote.discount_amount|floatformat:0|intcomma:False }}</strong></td> <td><strong>{{ quote.discount_amount|floatformat:0|intcomma:False }}</strong></td>
</tr> </tr>
{% endif %} {% endif %}
<tr class="total-section">
<td colspan="5" class="text-end"><strong>مالیات بر ارزش افزوده(تومان):</strong></td>
<td><strong>{{ quote.get_vat_amount|floatformat:0|intcomma:False }}</strong></td>
</tr>
<tr class="total-section border-top border-2"> <tr class="total-section border-top border-2">
<td colspan="5" class="text-end"><strong>مبلغ نهایی (با مالیات)(تومان):</strong></td> <td colspan="5" class="text-end"><strong>مبلغ نهایی (با مالیات)(تومان):</strong></td>
<td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td> <td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>

View file

@ -898,6 +898,29 @@ def add_special_charge(request, instance_id, step_id):
unit_price=amount_dec, unit_price=amount_dec,
) )
invoice.calculate_totals() 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])}) 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': 'امکان حذف این مورد وجود ندارد'}) return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'})
inv_item.hard_delete() inv_item.hard_delete()
invoice.calculate_totals() 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])}) 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 # Ensure step instance exists
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'}) 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) # Build approver statuses for template (include reason to display in UI)
reqs = list(step.approver_requirements.select_related('role').all()) reqs = list(step.approver_requirements.select_related('role').all())
@ -1048,6 +1111,14 @@ def final_settlement_step(request, instance_id, step_id):
except Exception: except Exception:
messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.') messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) 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 # Mark step completed regardless of remaining amount/approvals
step_instance.status = 'approved' step_instance.status = 'approved'
step_instance.save() step_instance.save()
@ -1094,6 +1165,14 @@ def add_final_payment(request, instance_id, step_id):
except Exception: except Exception:
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) 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() amount = (request.POST.get('amount') or '').strip()
payment_date = (request.POST.get('payment_date') or '').strip() payment_date = (request.POST.get('payment_date') or '').strip()
payment_method = (request.POST.get('payment_method') or '').strip() payment_method = (request.POST.get('payment_method') or '').strip()

View file

@ -245,7 +245,12 @@
<small class="text-muted">{{ item.progress_percentage }}%</small> <small class="text-muted">{{ item.progress_percentage }}%</small>
</div> </div>
</td> </td>
<td>{{ item.instance.get_status_display_with_color|safe }}</td> <td>
{{ item.instance.get_status_display_with_color|safe }}
{% if item.emergency_approved %}
<span class="badge bg-warning text-dark ms-1" title="تایید اضطراری">تایید اضطراری</span>
{% endif %}
</td>
<td> <td>
{% if item.installation_scheduled_date %} {% if item.installation_scheduled_date %}
<div> <div>

View file

@ -123,6 +123,17 @@ def request_list(request):
reference_date = None reference_date = None
installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date 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({ instances_with_progress.append({
'instance': instance, 'instance': instance,
'progress_percentage': round(progress_percentage), 'progress_percentage': round(progress_percentage),
@ -130,6 +141,7 @@ def request_list(request):
'total_steps': total_steps, 'total_steps': total_steps,
'installation_scheduled_date': installation_scheduled_date, 'installation_scheduled_date': installation_scheduled_date,
'installation_overdue_days': overdue_days, 'installation_overdue_days': overdue_days,
'emergency_approved': emergency_approved,
}) })
# Summary stats for header cards # Summary stats for header cards