fix until contracts step
This commit is contained in:
		
							parent
							
								
									246a2c0759
								
							
						
					
					
						commit
						af40e169ae
					
				
					 9 changed files with 180 additions and 128 deletions
				
			
		| 
						 | 
				
			
			@ -107,7 +107,7 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'})
 | 
			
		||||
 | 
			
		||||
    # Create or reuse quote
 | 
			
		||||
    quote, _ = Quote.objects.get_or_create(
 | 
			
		||||
    quote, created_q = Quote.objects.get_or_create(
 | 
			
		||||
        process_instance=instance,
 | 
			
		||||
        defaults={
 | 
			
		||||
            'name': f"پیشفاکتور {instance.code}",
 | 
			
		||||
| 
						 | 
				
			
			@ -117,6 +117,15 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Track whether this step was already completed before this edit
 | 
			
		||||
    step_instance_existing = instance.step_instances.filter(step=step).first()
 | 
			
		||||
    was_already_completed = bool(step_instance_existing and step_instance_existing.status == 'completed')
 | 
			
		||||
 | 
			
		||||
    # Snapshot previous items before overwrite for change detection
 | 
			
		||||
    previous_items_map = {}
 | 
			
		||||
    if not created_q:
 | 
			
		||||
        previous_items_map = {qi.item_id: int(qi.quantity) for qi in quote.items.filter(is_deleted=False).all()}
 | 
			
		||||
 | 
			
		||||
    # Replace quote items with submitted ones
 | 
			
		||||
    quote.items.all().delete()
 | 
			
		||||
    for entry in items_payload:
 | 
			
		||||
| 
						 | 
				
			
			@ -139,22 +148,62 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
    quote.calculate_totals()
 | 
			
		||||
 | 
			
		||||
    # Detect changes versus previous state and mark audit fields if editing after completion
 | 
			
		||||
    try:
 | 
			
		||||
        new_items_map = {int(entry.get('id')): int(entry.get('qty') or 1) for entry in items_payload}
 | 
			
		||||
    except Exception:
 | 
			
		||||
        new_items_map = {}
 | 
			
		||||
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
 | 
			
		||||
    if was_already_completed and new_items_map != previous_items_map:
 | 
			
		||||
        # StepInstance-level generic audit (for reuse across steps)
 | 
			
		||||
        if step_instance_existing:
 | 
			
		||||
            step_instance_existing.edited_after_completion = True
 | 
			
		||||
            step_instance_existing.last_edited_at = timezone.now()
 | 
			
		||||
            step_instance_existing.last_edited_by = request.user
 | 
			
		||||
            step_instance_existing.edit_count = (step_instance_existing.edit_count or 0) + 1
 | 
			
		||||
            step_instance_existing.completed_at = timezone.now()
 | 
			
		||||
            step_instance_existing.save(update_fields=['edited_after_completion', 'last_edited_at', 'last_edited_by', 'edit_count', 'completed_at'])
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if quote.status != 'draft':
 | 
			
		||||
            quote.status = 'draft'
 | 
			
		||||
            quote.save(update_fields=['status'])
 | 
			
		||||
 | 
			
		||||
        if next_step:
 | 
			
		||||
            next_step_instance = instance.step_instances.filter(step=next_step).first()
 | 
			
		||||
            if next_step_instance and next_step_instance.status == 'completed':
 | 
			
		||||
                next_step_instance.status = 'in_progress'
 | 
			
		||||
                next_step_instance.completed_at = None
 | 
			
		||||
                next_step_instance.save(update_fields=['status', 'completed_at'])
 | 
			
		||||
                # Clear previous approvals if the step requires re-approval
 | 
			
		||||
                try:
 | 
			
		||||
                    next_step_instance.approvals.all().delete()
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
 | 
			
		||||
            instance.current_step = next_step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    
 | 
			
		||||
    # تکمیل مرحله
 | 
			
		||||
    step_instance, created = StepInstance.objects.get_or_create(
 | 
			
		||||
        process_instance=instance,
 | 
			
		||||
        step=step
 | 
			
		||||
    )
 | 
			
		||||
    step_instance.status = 'completed'
 | 
			
		||||
    step_instance.completed_at = timezone.now()
 | 
			
		||||
    step_instance.save()
 | 
			
		||||
    if not was_already_completed:
 | 
			
		||||
        step_instance.status = 'completed'
 | 
			
		||||
        step_instance.completed_at = timezone.now()
 | 
			
		||||
        step_instance.save(update_fields=['status', 'completed_at'])
 | 
			
		||||
    
 | 
			
		||||
    # انتقال به مرحله بعدی
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
    redirect_url = None
 | 
			
		||||
    if next_step:
 | 
			
		||||
        instance.current_step = next_step
 | 
			
		||||
        instance.save()
 | 
			
		||||
        # Only advance current step if we are currently on this step to avoid regressions
 | 
			
		||||
        if instance.current_step_id == step.id:
 | 
			
		||||
            instance.current_step = next_step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
        # هدایت مستقیم به مرحله پیشنمایش پیشفاکتور
 | 
			
		||||
        redirect_url = reverse('invoices:quote_preview_step', args=[instance.id, next_step.id])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -202,6 +251,7 @@ def quote_preview_step(request, instance_id, step_id):
 | 
			
		|||
        'is_broker': is_broker,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def quote_print(request, instance_id):
 | 
			
		||||
    """صفحه پرینت پیشفاکتور"""
 | 
			
		||||
| 
						 | 
				
			
			@ -213,6 +263,7 @@ def quote_print(request, instance_id):
 | 
			
		|||
        'quote': quote,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required  
 | 
			
		||||
def approve_quote(request, instance_id, step_id):
 | 
			
		||||
| 
						 | 
				
			
			@ -285,6 +336,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    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 []
 | 
			
		||||
| 
						 | 
				
			
			@ -298,6 +350,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
        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}
 | 
			
		||||
| 
						 | 
				
			
			@ -305,20 +358,7 @@ 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
 | 
			
		||||
    # 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']:
 | 
			
		||||
| 
						 | 
				
			
			@ -362,6 +402,13 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
			
		||||
            )
 | 
			
		||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
			
		||||
                # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
            try:
 | 
			
		||||
                if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
                    instance.current_step = step
 | 
			
		||||
                    instance.save(update_fields=['current_step'])
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
            messages.success(request, 'مرحله پرداختها رد شد و برای اصلاح بازگشت.')
 | 
			
		||||
            return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -388,8 +435,6 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        '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,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -412,14 +457,16 @@ def add_quote_payment(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # dynamic permission: users whose roles are among required approvers can add payments
 | 
			
		||||
    # who can add payments
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    is_accountant = False
 | 
			
		||||
    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': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
        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 JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
 | 
			
		||||
    logger = logging.getLogger(__name__)
 | 
			
		||||
| 
						 | 
				
			
			@ -477,48 +524,11 @@ def add_quote_payment(request, instance_id, step_id):
 | 
			
		|||
        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})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def update_quote_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)
 | 
			
		||||
    quote = get_object_or_404(Quote, process_instance=instance)
 | 
			
		||||
    invoice = Invoice.objects.filter(quote=quote).first()
 | 
			
		||||
    if not invoice:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
 | 
			
		||||
    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
			
		||||
 | 
			
		||||
    # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
    try:
 | 
			
		||||
        amount = request.POST.get('amount')
 | 
			
		||||
        payment_date = request.POST.get('payment_date') or payment.payment_date
 | 
			
		||||
        payment_method = request.POST.get('payment_method') or payment.payment_method
 | 
			
		||||
        reference_number = request.POST.get('reference_number') or ''
 | 
			
		||||
        notes = request.POST.get('notes') or ''
 | 
			
		||||
        receipt_image = request.FILES.get('receipt_image')
 | 
			
		||||
        if amount:
 | 
			
		||||
            payment.amount = amount
 | 
			
		||||
        payment.payment_date = payment_date
 | 
			
		||||
        payment.payment_method = payment_method
 | 
			
		||||
        payment.reference_number = reference_number
 | 
			
		||||
        payment.notes = notes
 | 
			
		||||
        # اگر نیاز به ذخیره عکس در Payment دارید، فیلد آن اضافه شده است
 | 
			
		||||
        if receipt_image:
 | 
			
		||||
            payment.receipt_image = receipt_image
 | 
			
		||||
        payment.save()
 | 
			
		||||
    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()
 | 
			
		||||
        if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
            instance.current_step = step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
			
		||||
| 
						 | 
				
			
			@ -535,15 +545,18 @@ 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
 | 
			
		||||
    
 | 
			
		||||
    # who can delete payments
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    is_accountant = False
 | 
			
		||||
    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': 'شما مجوز حذف فیش را ندارید'})
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
        is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
 | 
			
		||||
        is_broker = False
 | 
			
		||||
        is_accountant = False
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        # soft delete using project's BaseModel delete override
 | 
			
		||||
| 
						 | 
				
			
			@ -559,43 +572,17 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
			
		|||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
    try:
 | 
			
		||||
        if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
            instance.current_step = step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
			
		||||
    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def approve_payments(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)
 | 
			
		||||
 | 
			
		||||
    is_fully_paid = quote.get_remaining_amount() <= 0
 | 
			
		||||
 | 
			
		||||
    # تکمیل مرحله
 | 
			
		||||
    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
    step_instance.status = 'completed'
 | 
			
		||||
    step_instance.completed_at = timezone.now()
 | 
			
		||||
    step_instance.save()
 | 
			
		||||
 | 
			
		||||
    # حرکت به مرحله بعد
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
    redirect_url = reverse('processes:request_list')
 | 
			
		||||
    if next_step:
 | 
			
		||||
        instance.current_step = next_step
 | 
			
		||||
        instance.save()
 | 
			
		||||
        redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id])
 | 
			
		||||
 | 
			
		||||
    msg = 'پرداختها تایید شد'
 | 
			
		||||
    if is_fully_paid:
 | 
			
		||||
        msg += ' - مبلغ پیشفاکتور به طور کامل پرداخت شده است.'
 | 
			
		||||
    else:
 | 
			
		||||
        msg += ' - توجه: مبلغ پیشفاکتور به طور کامل پرداخت نشده است.'
 | 
			
		||||
 | 
			
		||||
    return JsonResponse({'success': True, 'message': msg, 'redirect': redirect_url, 'is_fully_paid': is_fully_paid})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def final_invoice_step(request, instance_id, step_id):
 | 
			
		||||
    """تجمیع اقلام پیشفاکتور با تغییرات نصب و صدور فاکتور نهایی"""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue