Add confirmation and summary
This commit is contained in:
		
							parent
							
								
									9b3973805e
								
							
						
					
					
						commit
						35799b7754
					
				
					 25 changed files with 1419 additions and 265 deletions
				
			
		| 
						 | 
				
			
			@ -9,7 +9,9 @@ from django.urls import reverse
 | 
			
		|||
from decimal import Decimal, InvalidOperation
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from processes.models import ProcessInstance, ProcessStep, StepInstance
 | 
			
		||||
from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval
 | 
			
		||||
from accounts.models import Role
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
from .models import Item, Quote, QuoteItem, Payment, Invoice
 | 
			
		||||
from installations.models import InstallationReport, InstallationItemChange
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +30,7 @@ def quote_step(request, instance_id, step_id):
 | 
			
		|||
        return redirect('processes:request_list')
 | 
			
		||||
    
 | 
			
		||||
    # دریافت آیتمها
 | 
			
		||||
    items = Item.objects.all().order_by('name')
 | 
			
		||||
    items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name')
 | 
			
		||||
    existing_quote = Quote.objects.filter(process_instance=instance).first()
 | 
			
		||||
    existing_quote_items = {}
 | 
			
		||||
    if existing_quote:
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +42,14 @@ def quote_step(request, instance_id, step_id):
 | 
			
		|||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
    
 | 
			
		||||
    # determine if current user is broker
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    try:
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        is_broker = False
 | 
			
		||||
 | 
			
		||||
    return render(request, 'invoices/quote_step.html', {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
        'step': step,
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +59,7 @@ def quote_step(request, instance_id, step_id):
 | 
			
		|||
        'existing_quote': existing_quote,
 | 
			
		||||
        'previous_step': previous_step,
 | 
			
		||||
        'next_step': next_step,
 | 
			
		||||
        'is_broker': is_broker,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +68,13 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
    """ساخت/بروزرسانی پیشفاکتور از اقلام انتخابی"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    # enforce permission: only BROKER can create/update quote
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    try:
 | 
			
		||||
        if not (profile and profile.has_role(UserRoles.BROKER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز ثبت/ویرایش پیشفاکتور را ندارید'})
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز ثبت/ویرایش پیشفاکتور را ندارید'})
 | 
			
		||||
    
 | 
			
		||||
    try:
 | 
			
		||||
        items_payload = json.loads(request.POST.get('items') or '[]')
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +90,7 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        except Exception:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
    default_item_ids = set(Item.objects.filter(is_default_in_quotes=True).values_list('id', flat=True))
 | 
			
		||||
    default_item_ids = set(Item.objects.filter(is_default_in_quotes=True, is_deleted=False).values_list('id', flat=True))
 | 
			
		||||
    if default_item_ids:
 | 
			
		||||
        for default_id in default_item_ids:
 | 
			
		||||
            if default_id not in payload_by_id:
 | 
			
		||||
| 
						 | 
				
			
			@ -163,6 +181,14 @@ def quote_preview_step(request, instance_id, step_id):
 | 
			
		|||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
    
 | 
			
		||||
    # determine if current user is broker for UI controls
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    try:
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        is_broker = False
 | 
			
		||||
 | 
			
		||||
    return render(request, 'invoices/quote_preview_step.html', {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
        'step': step,
 | 
			
		||||
| 
						 | 
				
			
			@ -170,6 +196,7 @@ def quote_preview_step(request, instance_id, step_id):
 | 
			
		|||
        'quote': quote,
 | 
			
		||||
        'previous_step': previous_step,
 | 
			
		||||
        'next_step': next_step,
 | 
			
		||||
        'is_broker': is_broker,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +217,13 @@ def approve_quote(request, instance_id, step_id):
 | 
			
		|||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    quote = get_object_or_404(Quote, process_instance=instance)
 | 
			
		||||
    # enforce permission: only BROKER can approve
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    try:
 | 
			
		||||
        if not (profile and profile.has_role(UserRoles.BROKER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز تایید پیشفاکتور را ندارید'})
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز تایید پیشفاکتور را ندارید'})
 | 
			
		||||
    
 | 
			
		||||
    # تایید پیشفاکتور
 | 
			
		||||
    quote.status = 'sent'
 | 
			
		||||
| 
						 | 
				
			
			@ -247,7 +281,97 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        'is_fully_paid': quote.get_remaining_amount() <= 0,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    step_instance = instance.step_instances.filter(step=step).first()
 | 
			
		||||
    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
 | 
			
		||||
    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_by_role = {a.role_id: a for a in approvals_list}
 | 
			
		||||
    approver_statuses = [
 | 
			
		||||
        {
 | 
			
		||||
            '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
 | 
			
		||||
    ]
 | 
			
		||||
    # dynamic permission: who can approve/reject this step (based on requirements)
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = {r.role_id for r in reqs}
 | 
			
		||||
        user_role_ids = {ur.id for ur in user_roles}
 | 
			
		||||
        can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
 | 
			
		||||
    except Exception:
 | 
			
		||||
        can_approve_reject = False
 | 
			
		||||
    # approver status map for template
 | 
			
		||||
    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_by_role = {a.role_id: a for a in approvals_list}
 | 
			
		||||
    approver_statuses = [
 | 
			
		||||
        {
 | 
			
		||||
            '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
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # Accountant/Admin approval and rejection via POST (multi-role)
 | 
			
		||||
    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
 | 
			
		||||
        # match user's role against step required approver roles
 | 
			
		||||
        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:
 | 
			
		||||
            messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
 | 
			
		||||
            return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
        action = request.POST.get('action')
 | 
			
		||||
        if action == 'approve':
 | 
			
		||||
            StepApproval.objects.update_or_create(
 | 
			
		||||
                step_instance=step_instance,
 | 
			
		||||
                role=matching_role,
 | 
			
		||||
                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
			
		||||
            )
 | 
			
		||||
            if step_instance.is_fully_approved():
 | 
			
		||||
                step_instance.status = 'completed'
 | 
			
		||||
                step_instance.completed_at = timezone.now()
 | 
			
		||||
                step_instance.save()
 | 
			
		||||
                # move to next step
 | 
			
		||||
                redirect_url = 'processes:request_list'
 | 
			
		||||
                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(redirect_url)
 | 
			
		||||
            messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقشها.')
 | 
			
		||||
            return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
        if action == 'reject':
 | 
			
		||||
            reason = (request.POST.get('reject_reason') or '').strip()
 | 
			
		||||
            if not reason:
 | 
			
		||||
                messages.error(request, 'علت رد شدن را وارد کنید')
 | 
			
		||||
                return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
            StepApproval.objects.update_or_create(
 | 
			
		||||
                step_instance=step_instance,
 | 
			
		||||
                role=matching_role,
 | 
			
		||||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
			
		||||
            )
 | 
			
		||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
			
		||||
            messages.success(request, 'مرحله پرداختها رد شد و برای اصلاح بازگشت.')
 | 
			
		||||
            return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
    # role flags for permissions (legacy flags kept for compatibility)
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    is_accountant = False
 | 
			
		||||
    try:
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
        is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        is_broker = False
 | 
			
		||||
        is_accountant = False
 | 
			
		||||
 | 
			
		||||
    return render(request, 'invoices/quote_payment_step.html', {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
| 
						 | 
				
			
			@ -258,6 +382,12 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        'totals': totals,
 | 
			
		||||
        'previous_step': previous_step,
 | 
			
		||||
        'next_step': next_step,
 | 
			
		||||
        'approver_statuses': approver_statuses,
 | 
			
		||||
        'is_broker': is_broker,
 | 
			
		||||
        'is_accountant': is_accountant,
 | 
			
		||||
        # dynamic permissions: any role required to approve can also manage payments
 | 
			
		||||
        'can_manage_payments': can_approve_reject,
 | 
			
		||||
        'can_approve_reject': can_approve_reject,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -279,6 +409,16 @@ def add_quote_payment(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # dynamic permission: users whose roles are among required approvers can add payments
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
 | 
			
		||||
        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
			
		||||
        user_role_ids = set(user_roles_qs.values_list('id', flat=True))
 | 
			
		||||
        if len(req_role_ids.intersection(user_role_ids)) == 0:
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
 | 
			
		||||
    logger = logging.getLogger(__name__)
 | 
			
		||||
    try:
 | 
			
		||||
        amount = (request.POST.get('amount') or '').strip()
 | 
			
		||||
| 
						 | 
				
			
			@ -325,6 +465,15 @@ def add_quote_payment(request, instance_id, step_id):
 | 
			
		|||
        logger.exception('Error adding quote payment (instance=%s, step=%s)', instance_id, step_id)
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'خطا در ثبت فیش', 'error': str(e)})
 | 
			
		||||
 | 
			
		||||
    # After modifying payments, set step back to in_progress (awaiting approval)
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
			
		||||
    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -360,6 +509,15 @@ def update_quote_payment(request, instance_id, step_id, payment_id):
 | 
			
		|||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'})
 | 
			
		||||
 | 
			
		||||
    # On update, return to awaiting approval
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
			
		||||
    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -374,11 +532,30 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
			
		|||
    if not invoice:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
 | 
			
		||||
    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
			
		||||
    # dynamic permission: users whose roles are among required approvers can delete payments
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
 | 
			
		||||
        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
			
		||||
        user_role_ids = set(user_roles_qs.values_list('id', flat=True))
 | 
			
		||||
        if len(req_role_ids.intersection(user_role_ids)) == 0:
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        # soft delete using project's BaseModel delete override
 | 
			
		||||
        payment.delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
 | 
			
		||||
    # On delete, return to awaiting approval
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
			
		||||
    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -534,6 +711,14 @@ def final_invoice_step(request, instance_id, step_id):
 | 
			
		|||
    # Choices for special items from DB
 | 
			
		||||
    special_choices = list(Item.objects.filter(is_special=True).values('id', 'name'))
 | 
			
		||||
 | 
			
		||||
    # role flag for manager-only actions
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_manager = False
 | 
			
		||||
    try:
 | 
			
		||||
        is_manager = bool(profile and profile.has_role(UserRoles.MANAGER))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        is_manager = False
 | 
			
		||||
 | 
			
		||||
    return render(request, 'invoices/final_invoice_step.html', {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
        'step': step,
 | 
			
		||||
| 
						 | 
				
			
			@ -543,6 +728,7 @@ def final_invoice_step(request, instance_id, step_id):
 | 
			
		|||
        'invoice_specials': invoice.items.select_related('item').filter(item__is_special=True, is_deleted=False).all(),
 | 
			
		||||
        'previous_step': previous_step,
 | 
			
		||||
        'next_step': next_step,
 | 
			
		||||
        'is_manager': is_manager,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -564,6 +750,12 @@ def approve_final_invoice(request, instance_id, step_id):
 | 
			
		|||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # only MANAGER can approve
 | 
			
		||||
    try:
 | 
			
		||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
			
		||||
    # Block approval when there is any remaining (positive or negative)
 | 
			
		||||
    invoice.calculate_totals()
 | 
			
		||||
    # if invoice.remaining_amount != 0:
 | 
			
		||||
| 
						 | 
				
			
			@ -592,6 +784,12 @@ def add_special_charge(request, instance_id, step_id):
 | 
			
		|||
    """افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی بهصورت آیتم جداگانه"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # only MANAGER can add special charges
 | 
			
		||||
    try:
 | 
			
		||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    # charge_type was removed from UI; we no longer require it
 | 
			
		||||
    item_id = request.POST.get('item_id')
 | 
			
		||||
    amount = (request.POST.get('amount') or '').strip()
 | 
			
		||||
| 
						 | 
				
			
			@ -623,6 +821,12 @@ def add_special_charge(request, instance_id, step_id):
 | 
			
		|||
def delete_special_charge(request, instance_id, step_id, item_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # only MANAGER can delete special charges
 | 
			
		||||
    try:
 | 
			
		||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    from .models import InvoiceItem
 | 
			
		||||
    inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice)
 | 
			
		||||
    # allow deletion only for special items
 | 
			
		||||
| 
						 | 
				
			
			@ -648,13 +852,87 @@ def final_settlement_step(request, instance_id, step_id):
 | 
			
		|||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
 | 
			
		||||
    # Ensure step instance exists
 | 
			
		||||
    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
 | 
			
		||||
    # Build approver statuses for template
 | 
			
		||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
			
		||||
    approvals_map = {a.role_id: a.decision for a in step_instance.approvals.select_related('role').all()}
 | 
			
		||||
    approver_statuses = [{'role': r.role, 'status': approvals_map.get(r.role_id)} for r in reqs]
 | 
			
		||||
    # dynamic permission to control approve/reject UI
 | 
			
		||||
    try:
 | 
			
		||||
        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
			
		||||
        user_role_ids = set(user_roles_qs.values_list('id', flat=True))
 | 
			
		||||
        req_role_ids = {r.role_id for r in reqs}
 | 
			
		||||
        can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
 | 
			
		||||
    except Exception:
 | 
			
		||||
        can_approve_reject = False
 | 
			
		||||
 | 
			
		||||
    # Accountant/Admin approval and rejection (multi-role)
 | 
			
		||||
    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
 | 
			
		||||
        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:
 | 
			
		||||
            messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
 | 
			
		||||
            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
        action = request.POST.get('action')
 | 
			
		||||
        if action == 'approve':
 | 
			
		||||
            # enforce zero remaining
 | 
			
		||||
            invoice.calculate_totals()
 | 
			
		||||
            if invoice.remaining_amount != 0:
 | 
			
		||||
                messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})")
 | 
			
		||||
                return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
            StepApproval.objects.update_or_create(
 | 
			
		||||
                step_instance=step_instance,
 | 
			
		||||
                role=matching_role,
 | 
			
		||||
                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
			
		||||
            )
 | 
			
		||||
            if step_instance.is_fully_approved():
 | 
			
		||||
                step_instance.status = 'completed'
 | 
			
		||||
                step_instance.completed_at = timezone.now()
 | 
			
		||||
                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')
 | 
			
		||||
            messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقشها.')
 | 
			
		||||
            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
        if action == 'reject':
 | 
			
		||||
            reason = (request.POST.get('reject_reason') or '').strip()
 | 
			
		||||
            if not reason:
 | 
			
		||||
                messages.error(request, 'علت رد شدن را وارد کنید')
 | 
			
		||||
                return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
            StepApproval.objects.update_or_create(
 | 
			
		||||
                step_instance=step_instance,
 | 
			
		||||
                role=matching_role,
 | 
			
		||||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
			
		||||
            )
 | 
			
		||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
			
		||||
            messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
 | 
			
		||||
            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
    # broker flag for payment management permission
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    try:
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        is_broker = False
 | 
			
		||||
 | 
			
		||||
    return render(request, 'invoices/final_settlement_step.html', {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
        'step': step,
 | 
			
		||||
        'invoice': invoice,
 | 
			
		||||
        'payments': invoice.payments.filter(is_deleted=False).all(),
 | 
			
		||||
        'step_instance': step_instance,
 | 
			
		||||
        'previous_step': previous_step,
 | 
			
		||||
        'next_step': next_step,
 | 
			
		||||
        'approver_statuses': approver_statuses,
 | 
			
		||||
        'can_approve_reject': can_approve_reject,
 | 
			
		||||
        'is_broker': is_broker,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -662,7 +940,14 @@ def final_settlement_step(request, instance_id, step_id):
 | 
			
		|||
@login_required
 | 
			
		||||
def add_final_payment(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # Only BROKER can add final settlement payments
 | 
			
		||||
    try:
 | 
			
		||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
    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()
 | 
			
		||||
| 
						 | 
				
			
			@ -717,6 +1002,14 @@ def add_final_payment(request, instance_id, step_id):
 | 
			
		|||
    )
 | 
			
		||||
    # After creation, totals auto-updated by model save. Respond with redirect and new totals for UX.
 | 
			
		||||
    invoice.refresh_from_db()
 | 
			
		||||
    # After payment change, set step back to in_progress
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    return JsonResponse({
 | 
			
		||||
        'success': True,
 | 
			
		||||
        'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
 | 
			
		||||
| 
						 | 
				
			
			@ -732,10 +1025,25 @@ def add_final_payment(request, instance_id, step_id):
 | 
			
		|||
@login_required
 | 
			
		||||
def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
			
		||||
    # Only BROKER can delete final settlement payments
 | 
			
		||||
    try:
 | 
			
		||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
    payment.delete()
 | 
			
		||||
    invoice.refresh_from_db()
 | 
			
		||||
    # After payment change, set step back to in_progress
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': {
 | 
			
		||||
        'final_amount': str(invoice.final_amount),
 | 
			
		||||
        'paid_amount': str(invoice.paid_amount),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue