fix auto complete final invoice

This commit is contained in:
aminhashemi92 2025-10-09 14:37:59 +03:30
parent 23f50aacd4
commit 6367d34f0c
7 changed files with 160 additions and 39 deletions

View file

@ -54,7 +54,7 @@
<div class="bs-stepper wizard-vertical vertical mt-2">
{% stepper_header instance step %}
<div class="bs-stepper-content">
{% if request.user|is_broker or request.user|is_manager or request.user|is_accountant or request.user|is_water_resource_manager %}
{% if request.user|is_broker or request.user|is_manager or request.user|is_water_resource_manager %}
<div class="card">
<div class="card-body">
<div class="d-flex mb-2">

Binary file not shown.

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-10-09 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoices', '0002_remove_historicalinvoice_paid_amount_and_more'),
]
operations = [
migrations.AddField(
model_name='historicalpayment',
name='payment_stage',
field=models.CharField(choices=[('quote', 'پیش\u200cفاکتور'), ('final_settlement', 'تسویه نهایی')], default='quote', max_length=20, verbose_name='مرحله پرداخت'),
),
migrations.AddField(
model_name='payment',
name='payment_stage',
field=models.CharField(choices=[('quote', 'پیش\u200cفاکتور'), ('final_settlement', 'تسویه نهایی')], default='quote', max_length=20, verbose_name='مرحله پرداخت'),
),
]

View file

@ -350,6 +350,11 @@ class InvoiceItem(BaseModel):
class Payment(BaseModel):
"""مدل پرداخت‌ها"""
PAYMENT_STAGE_CHOICES = [
('quote', 'پیش‌فاکتور'),
('final_settlement', 'تسویه نهایی'),
]
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments', verbose_name="فاکتور")
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="مبلغ پرداخت")
direction = models.CharField(
@ -370,6 +375,12 @@ class Payment(BaseModel):
default='cash',
verbose_name="روش پرداخت"
)
payment_stage = models.CharField(
max_length=20,
choices=PAYMENT_STAGE_CHOICES,
default='quote',
verbose_name="مرحله پرداخت"
)
reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True, unique=True)
payment_date = models.DateField(verbose_name="تاریخ پرداخت")
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)

View file

@ -60,7 +60,7 @@
<div class="bs-stepper-content">
<div class="row g-3">
{% if is_broker and invoice.get_remaining_amount != 0 %}
{% if is_broker and needs_approval %}
<div class="col-12 col-lg-5">
<div class="card border h-100">
<div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div>
@ -193,7 +193,7 @@
</div>
</div>
</div>
{% if approver_statuses and invoice.get_remaining_amount != 0 and step_instance.status != 'completed' %}
{% if approver_statuses and needs_approval and step_instance.status != 'completed' %}
<div class="card border mt-2">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">وضعیت تاییدها</h6>
@ -318,7 +318,11 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% if invoice.get_remaining_amount != 0 %}
{% if not needs_approval %}
<div class="alert alert-info" role="alert">
فاکتور کاملاً تسویه شده است و نیازی به تایید ندارد.
</div>
{% elif invoice.get_remaining_amount != 0 %}
<div class="alert alert-warning" role="alert">
مانده فاکتور: <strong>{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} ریال</strong><br>
امکان تایید تا تسویه کامل فاکتور وجود ندارد.

View file

@ -564,6 +564,7 @@ def add_quote_payment(request, instance_id, step_id):
amount=amount_dec,
payment_date=payment_date,
payment_method=payment_method,
payment_stage='quote',
reference_number=reference_number,
receipt_image=receipt_image,
notes=notes,
@ -1049,10 +1050,56 @@ 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'})
# Check if there are changes that require approval
# (used for both auto-complete and UI display)
has_special_charges = False
has_installation_changes = False
has_final_settlement_payments = False
try:
has_special_charges = invoice.items.filter(item__is_special=True, is_deleted=False).exists()
except Exception:
pass
try:
from installations.models import InstallationAssignment
assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
if assignment:
reports = assignment.reports.all()
for report in reports:
if report.item_changes.filter(is_deleted=False).exists():
has_installation_changes = True
break
except Exception:
pass
# Check if there are payments added during final settlement step
# using the payment_stage field
try:
final_settlement_payments = invoice.payments.filter(
is_deleted=False,
payment_stage='final_settlement'
)
if final_settlement_payments.exists():
has_final_settlement_payments = True
except Exception:
pass
# Auto-complete step when invoice is fully settled (no approvals needed)
# BUT only if no special charges were added in final_invoice step
# AND no installation item changes were made
# AND no payments were added in this final settlement step
# (meaning the remaining amount is from the original quote_payment step)
try:
invoice.calculate_totals()
if invoice.get_remaining_amount() == 0:
remaining = invoice.get_remaining_amount()
# Only auto-complete if:
# 1. Remaining amount is zero
# 2. No special charges were added (meaning this is settling the original quote)
# 3. No installation item changes (meaning no items added/removed in installation step)
# 4. No payments added in final settlement step (meaning no new receipts need approval)
if remaining == 0 and not has_special_charges and not has_installation_changes and not has_final_settlement_payments:
if step_instance.status != 'completed':
step_instance.status = 'completed'
step_instance.completed_at = timezone.now()
@ -1199,6 +1246,21 @@ def final_settlement_step(request, instance_id, step_id):
except Exception:
is_broker = False
# Determine if approval is needed
# Approval is needed if:
# 1. Remaining amount is not zero, OR
# 2. Special charges were added (meaning new balance was created in final_invoice step), OR
# 3. Installation item changes were made (meaning items were added/removed in installation step), OR
# 4. Payments were added in final settlement step (new receipts need approval)
needs_approval = True
try:
remaining = invoice.get_remaining_amount()
# No approval needed only if: remaining is zero AND no special charges AND no installation changes AND no final settlement payments
if remaining == 0 and not has_special_charges and not has_installation_changes and not has_final_settlement_payments:
needs_approval = False
except Exception:
needs_approval = True
return render(request, 'invoices/final_settlement_step.html', {
'instance': instance,
'step': step,
@ -1211,6 +1273,7 @@ def final_settlement_step(request, instance_id, step_id):
'can_approve_reject': can_approve_reject,
'is_broker': is_broker,
'current_user_has_decided': current_user_has_decided,
'needs_approval': needs_approval,
'is_manager': bool(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).filter(slug=UserRoles.MANAGER.value).exists()) if getattr(request.user, 'profile', None) else False,
})
@ -1283,6 +1346,7 @@ def add_final_payment(request, instance_id, step_id):
amount=amount_dec,
payment_date=payment_date,
payment_method=payment_method,
payment_stage='final_settlement',
reference_number=reference_number,
direction='in' if direction != 'out' else 'out',
receipt_image=receipt_image,

View file

@ -94,6 +94,17 @@ def request_list(request):
reports_map[pid] = row['visited_date']
except Exception:
reports_map = {}
# Build a map to check if installation reports exist (for approval status logic)
has_installation_report_map = {}
if instance_ids:
try:
report_exists_qs = InstallationReport.objects.filter(
assignment__process_instance_id__in=instance_ids
).values_list('assignment__process_instance_id', flat=True).distinct()
has_installation_report_map = {pid: True for pid in report_exists_qs}
except Exception:
has_installation_report_map = {}
# Calculate progress for each instance and attach install schedule info
instances_with_progress = []
@ -140,43 +151,51 @@ def request_list(request):
try:
current_step_instance = instance.step_instances.filter(step=instance.current_step).first()
if current_step_instance:
# Check if this step requires approvals
required_roles = current_step_instance.required_roles()
if required_roles:
# Get approvals by role
approvals_by_role = current_step_instance.approvals_by_role()
# Check for rejections
latest_rejection = current_step_instance.get_latest_rejection()
if latest_rejection and current_step_instance.status == 'rejected':
role_name = latest_rejection.role.name if latest_rejection.role else 'نامشخص'
current_step_approval_status = {
'status': 'rejected',
'role': role_name,
'display': f'رد شده توسط {role_name}'
}
else:
# Check approval status
pending_roles = []
approved_roles = []
for role in required_roles:
if approvals_by_role.get(role.id) == 'approved':
approved_roles.append(role.name)
else:
pending_roles.append(role.name)
# Special check: For installation report step (order=6), only show approval status if report exists
should_show_approval_status = True
if instance.current_step.order == 6:
# Check if installation report exists
if not has_installation_report_map.get(instance.id, False):
should_show_approval_status = False
if should_show_approval_status:
# Check if this step requires approvals
required_roles = current_step_instance.required_roles()
if required_roles:
# Get approvals by role
approvals_by_role = current_step_instance.approvals_by_role()
if pending_roles:
# Check for rejections
latest_rejection = current_step_instance.get_latest_rejection()
if latest_rejection and current_step_instance.status == 'rejected':
role_name = latest_rejection.role.name if latest_rejection.role else 'نامشخص'
current_step_approval_status = {
'status': 'pending',
'roles': pending_roles,
'display': f'در انتظار تایید {" و ".join(pending_roles)}'
}
elif approved_roles and not pending_roles:
current_step_approval_status = {
'status': 'approved',
'roles': approved_roles,
'display': f'تایید شده توسط {" و ".join(approved_roles)}'
'status': 'rejected',
'role': role_name,
'display': f'رد شده توسط {role_name}'
}
else:
# Check approval status
pending_roles = []
approved_roles = []
for role in required_roles:
if approvals_by_role.get(role.id) == 'approved':
approved_roles.append(role.name)
else:
pending_roles.append(role.name)
if pending_roles:
current_step_approval_status = {
'status': 'pending',
'roles': pending_roles,
'display': f'در انتظار تایید {" و ".join(pending_roles)}'
}
elif approved_roles and not pending_roles:
current_step_approval_status = {
'status': 'approved',
'roles': approved_roles,
'display': f'تایید شده توسط {" و ".join(approved_roles)}'
}
except Exception:
current_step_approval_status = None