This commit is contained in:
aminhashemi92 2025-09-29 17:38:11 +03:30
parent 810c87e2e0
commit b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions

View file

@ -5,6 +5,7 @@ from django.contrib import messages
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.utils import timezone
from django.conf import settings
from django.urls import reverse
from decimal import Decimal, InvalidOperation
import json
@ -356,16 +357,16 @@ def quote_payment_step(request, instance_id, step_id):
reqs = list(step.approver_requirements.select_related('role').all())
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
approvals_list = list(step_instance.approvals.select_related('role').all())
approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
approvals_by_role = {a.role_id: a for a in approvals_list}
approver_statuses = [
{
approver_statuses = []
for r in reqs:
appr = approvals_by_role.get(r.role_id)
approver_statuses.append({
'role': r.role,
'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
}
for r in reqs
]
'status': (appr.decision if appr else None),
'reason': (appr.reason if appr else ''),
})
# dynamic permission: who can approve/reject this step (based on requirements)
try:
@ -374,6 +375,15 @@ def quote_payment_step(request, instance_id, step_id):
can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
except Exception:
can_approve_reject = False
# Compute whether current user has already decided (approved/rejected)
current_user_has_decided = False
try:
user_has_approval = step_instance.approvals.filter(approved_by=request.user, is_deleted=False).exists()
user_has_rejection = step_instance.rejections.filter(rejected_by=request.user, is_deleted=False).exists()
current_user_has_decided = bool(user_has_approval or user_has_rejection)
except Exception:
current_user_has_decided = False
# Accountant/Admin approval and rejection via POST (multi-role)
@ -452,6 +462,7 @@ def quote_payment_step(request, instance_id, step_id):
'is_broker': is_broker,
'is_accountant': is_accountant,
'can_approve_reject': can_approve_reject,
'current_user_has_decided': current_user_has_decided,
})
@ -537,7 +548,17 @@ def add_quote_payment(request, instance_id, step_id):
si.status = 'in_progress'
si.completed_at = None
si.save()
si.approvals.all().delete()
except Exception:
pass
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
@ -554,7 +575,8 @@ def add_quote_payment(request, instance_id, step_id):
)
# Clear previous approvals if the step requires re-approval
try:
subsequent_step_instance.approvals.all().delete()
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
except Exception:
@ -596,7 +618,7 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
try:
# soft delete using project's BaseModel delete override
payment.delete()
payment.hard_delete()
except Exception:
return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
# On delete, return to awaiting approval
@ -605,7 +627,10 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
si.status = 'in_progress'
si.completed_at = None
si.save()
si.approvals.all().delete()
for appr in list(si.approvals.all()):
appr.delete()
for rej in list(si.rejections.all()):
rej.delete()
except Exception:
pass
@ -622,7 +647,8 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
)
# Clear previous approvals if the step requires re-approval
try:
subsequent_step_instance.approvals.all().delete()
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
except Exception:
@ -707,16 +733,15 @@ def final_invoice_step(request, instance_id, step_id):
if ch.unit_price:
row['base_price'] = _to_decimal(ch.unit_price)
# Compute final invoice lines
# Compute final invoice lines (include fully removed items for display)
rows = []
total_amount = Decimal('0')
for _, r in item_id_to_row.items():
final_qty = max(0, (r['base_qty'] + r['added_qty'] - r['removed_qty']))
if final_qty == 0:
continue
unit_price_dec = _to_decimal(r['base_price'])
line_total = Decimal(final_qty) * unit_price_dec
total_amount += line_total
line_total = Decimal(final_qty) * unit_price_dec if final_qty > 0 else Decimal('0')
if final_qty > 0:
total_amount += line_total
rows.append({
'item': r['item'],
'quantity': final_qty,
@ -725,6 +750,7 @@ def final_invoice_step(request, instance_id, step_id):
'base_qty': r['base_qty'],
'added_qty': r['added_qty'],
'removed_qty': r['removed_qty'],
'is_removed': True if final_qty == 0 else False,
})
# Create or reuse final invoice
@ -745,6 +771,8 @@ def final_invoice_step(request, instance_id, step_id):
except Exception:
qs.delete()
for r in rows:
if r['quantity'] <= 0:
continue
from .models import InvoiceItem
InvoiceItem.objects.create(
invoice=invoice,
@ -918,12 +946,21 @@ def final_settlement_step(request, instance_id, step_id):
except Exception:
can_approve_reject = False
# Compute whether current user has already decided (approved/rejected)
current_user_has_decided = False
try:
user_has_approval = step_instance.approvals.filter(approved_by=request.user).exists()
user_has_rejection = step_instance.rejections.filter(rejected_by=request.user).exists()
current_user_has_decided = bool(user_has_approval or user_has_rejection)
except Exception:
current_user_has_decided = False
# Accountant/Admin approval and rejection (multi-role)
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject', 'force_approve']:
req_roles = [req.role for req in step.approver_requirements.select_related('role').all()]
user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all())
matching_role = next((r for r in user_roles if r in req_roles), None)
if matching_role is None:
if matching_role is None and request.POST.get('action') != 'force_approve':
messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
@ -972,6 +1009,24 @@ def final_settlement_step(request, instance_id, step_id):
messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
if action == 'force_approve':
# Only MANAGER can force approve
try:
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
except Exception:
messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.')
return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
# Mark step completed regardless of remaining amount/approvals
step_instance.status = 'approved'
step_instance.save()
if next_step:
instance.current_step = next_step
instance.save()
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
return redirect('processes:request_list')
# broker flag for payment management permission
profile = getattr(request.user, 'profile', None)
is_broker = False
@ -991,6 +1046,8 @@ def final_settlement_step(request, instance_id, step_id):
'approver_statuses': approver_statuses,
'can_approve_reject': can_approve_reject,
'is_broker': is_broker,
'current_user_has_decided': current_user_has_decided,
'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,
})
@ -1065,10 +1122,20 @@ def add_final_payment(request, instance_id, step_id):
# On delete, return to awaiting approval
try:
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
si.status = 'in_progress'
if si.status != 'approved':
si.status = 'in_progress'
si.completed_at = None
si.save()
si.approvals.all().delete()
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
@ -1085,7 +1152,8 @@ def add_final_payment(request, instance_id, step_id):
)
# Clear previous approvals if the step requires re-approval
try:
subsequent_step_instance.approvals.all().delete()
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
except Exception:
@ -1124,7 +1192,7 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
except Exception:
return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
payment.delete()
payment.hard_delete()
invoice.refresh_from_db()
# On delete, return to awaiting approval
@ -1133,7 +1201,16 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
si.status = 'in_progress'
si.completed_at = None
si.save()
si.approvals.all().delete()
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
@ -1150,7 +1227,8 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
)
# Clear previous approvals if the step requires re-approval
try:
subsequent_step_instance.approvals.all().delete()
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
except Exception: