fix force approve and vat show
This commit is contained in:
parent
65cc48769d
commit
169a9bd624
10 changed files with 133 additions and 5 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""نمایش وضعیت با رنگ"""
|
"""نمایش وضعیت با رنگ"""
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue