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