1069 lines
48 KiB
Python
1069 lines
48 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
|
import logging
|
|
from django.contrib.auth.decorators import login_required
|
|
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.urls import reverse
|
|
from decimal import Decimal, InvalidOperation
|
|
import json
|
|
|
|
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
|
|
|
|
|
|
@login_required
|
|
def quote_step(request, instance_id, step_id):
|
|
"""مرحله انتخاب اقلام و ساخت پیشفاکتور"""
|
|
instance = get_object_or_404(
|
|
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
|
|
id=instance_id
|
|
)
|
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
|
|
|
# بررسی دسترسی به مرحله
|
|
if not instance.can_access_step(step):
|
|
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
|
return redirect('processes:request_list')
|
|
|
|
# دریافت آیتمها
|
|
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:
|
|
existing_quote_items = {qi.item_id: qi.quantity for qi in existing_quote.items.all()}
|
|
|
|
step_instance = instance.step_instances.filter(step=step).first()
|
|
|
|
# Navigation logic
|
|
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,
|
|
'step_instance': step_instance,
|
|
'items': items,
|
|
'existing_quote_items': existing_quote_items,
|
|
'existing_quote': existing_quote,
|
|
'previous_step': previous_step,
|
|
'next_step': next_step,
|
|
'is_broker': is_broker,
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
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 '[]')
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({'success': False, 'message': 'دادههای اقلام نامعتبر است'})
|
|
|
|
# اطمینان از حضور اقلام پیشفرض حتی اگر کلاینت ارسال نکرده باشد
|
|
payload_by_id = {}
|
|
for entry in items_payload:
|
|
try:
|
|
iid = int(entry.get('id'))
|
|
payload_by_id[iid] = int(entry.get('qty') or 1)
|
|
except Exception:
|
|
continue
|
|
|
|
default_item_ids = set(Item.objects.filter(is_default_in_quotes=True, is_deleted=False, is_special=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:
|
|
# مقدار پیش فرض را قرار بده
|
|
default_qty = Item.objects.filter(id=default_id).values_list('default_quantity', flat=True).first() or 1
|
|
payload_by_id[default_id] = int(default_qty)
|
|
|
|
# بازسازی payload نهایی معتبر
|
|
items_payload = [{'id': iid, 'qty': qty} for iid, qty in payload_by_id.items() if qty and qty > 0]
|
|
|
|
if not items_payload:
|
|
return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'})
|
|
|
|
# Create or reuse quote
|
|
quote, created_q = Quote.objects.get_or_create(
|
|
process_instance=instance,
|
|
defaults={
|
|
'name': f"پیشفاکتور {instance.code}",
|
|
'customer': instance.representative or request.user,
|
|
'valid_until': timezone.now().date(),
|
|
'created_by': request.user,
|
|
}
|
|
)
|
|
|
|
# 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:
|
|
try:
|
|
item_id = int(entry.get('id'))
|
|
qty = int(entry.get('qty') or 1)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if qty <= 0:
|
|
continue
|
|
item = Item.objects.filter(id=item_id).first()
|
|
if not item:
|
|
continue
|
|
QuoteItem.objects.create(
|
|
quote=quote,
|
|
item=item,
|
|
quantity=qty,
|
|
unit_price=item.unit_price,
|
|
total_price=item.unit_price * qty,
|
|
)
|
|
|
|
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
|
|
)
|
|
if not was_already_completed:
|
|
step_instance.status = 'completed'
|
|
step_instance.completed_at = timezone.now()
|
|
step_instance.save(update_fields=['status', 'completed_at'])
|
|
|
|
# انتقال به مرحله بعدی
|
|
redirect_url = None
|
|
if next_step:
|
|
# 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])
|
|
|
|
return JsonResponse({'success': True, 'quote_id': quote.id, 'redirect': redirect_url})
|
|
|
|
|
|
@login_required
|
|
def quote_preview_step(request, instance_id, step_id):
|
|
"""مرحله صدور پیشفاکتور - نمایش و تایید فاکتور"""
|
|
instance = get_object_or_404(
|
|
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile', 'broker', 'broker__company', 'broker__affairs', 'broker__affairs__county', 'broker__affairs__county__city'),
|
|
id=instance_id
|
|
)
|
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
|
|
|
# بررسی دسترسی به مرحله
|
|
if not instance.can_access_step(step):
|
|
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
|
return redirect('processes:request_list')
|
|
|
|
# دریافت پیشفاکتور
|
|
quote = get_object_or_404(Quote, process_instance=instance)
|
|
|
|
step_instance = instance.step_instances.filter(step=step).first()
|
|
|
|
# Navigation logic
|
|
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,
|
|
'step_instance': step_instance,
|
|
'quote': quote,
|
|
'previous_step': previous_step,
|
|
'next_step': next_step,
|
|
'is_broker': is_broker,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def quote_print(request, instance_id):
|
|
"""صفحه پرینت پیشفاکتور"""
|
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
|
quote = get_object_or_404(Quote, process_instance=instance)
|
|
|
|
return render(request, 'invoices/quote_print.html', {
|
|
'instance': instance,
|
|
'quote': quote,
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
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'
|
|
quote.save()
|
|
|
|
# تکمیل مرحله
|
|
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()
|
|
|
|
# انتقال به مرحله بعدی
|
|
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
|
redirect_url = None
|
|
if next_step:
|
|
instance.current_step = next_step
|
|
instance.save()
|
|
redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id])
|
|
else:
|
|
# در صورت نبود مرحله بعدی، بازگشت به لیست درخواستها
|
|
redirect_url = reverse('processes:request_list')
|
|
|
|
messages.success(request, 'پیشفاکتور با موفقیت تایید شد.')
|
|
return JsonResponse({'success': True, 'message': 'پیشفاکتور با موفقیت تایید شد.', 'redirect': redirect_url})
|
|
|
|
|
|
@login_required
|
|
def quote_payment_step(request, instance_id, step_id):
|
|
"""مرحله سوم: ثبت فیشهای واریزی پیشفاکتور"""
|
|
instance = get_object_or_404(
|
|
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
|
|
id=instance_id
|
|
)
|
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
|
|
|
# بررسی دسترسی
|
|
if not instance.can_access_step(step):
|
|
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
|
return redirect('processes:request_list')
|
|
|
|
quote = get_object_or_404(Quote, process_instance=instance)
|
|
invoice = Invoice.objects.filter(quote=quote).first()
|
|
payments = invoice.payments.select_related('created_by').filter(is_deleted=False).all() if invoice else []
|
|
|
|
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
|
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
|
|
|
totals = {
|
|
'final_amount': quote.final_amount,
|
|
'paid_amount': quote.get_paid_amount(),
|
|
'remaining_amount': quote.get_remaining_amount(),
|
|
'is_fully_paid': quote.get_remaining_amount() <= 0,
|
|
}
|
|
|
|
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
|
|
|
|
|
|
# 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)
|
|
# 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)
|
|
|
|
# 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,
|
|
'step': step,
|
|
'step_instance': step_instance,
|
|
'quote': quote,
|
|
'payments': payments,
|
|
'totals': totals,
|
|
'previous_step': previous_step,
|
|
'next_step': next_step,
|
|
'approver_statuses': approver_statuses,
|
|
'is_broker': is_broker,
|
|
'is_accountant': is_accountant,
|
|
'can_approve_reject': can_approve_reject,
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def add_quote_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)
|
|
quote = get_object_or_404(Quote, process_instance=instance)
|
|
invoice, _ = Invoice.objects.get_or_create(
|
|
process_instance=instance,
|
|
quote=quote,
|
|
defaults={
|
|
'name': f"Invoice {quote.name}",
|
|
'customer': quote.customer,
|
|
'due_date': timezone.now().date(),
|
|
'created_by': request.user,
|
|
}
|
|
)
|
|
|
|
# who can add payments
|
|
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 JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
|
|
|
|
logger = logging.getLogger(__name__)
|
|
try:
|
|
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()
|
|
reference_number = (request.POST.get('reference_number') or '').strip()
|
|
notes = (request.POST.get('notes') or '').strip()
|
|
receipt_image = request.FILES.get('receipt_image')
|
|
# Server-side validation for required fields
|
|
if not amount:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ را وارد کنید'})
|
|
if not payment_date:
|
|
return JsonResponse({'success': False, 'message': 'تاریخ پرداخت را وارد کنید'})
|
|
if not payment_method:
|
|
return JsonResponse({'success': False, 'message': 'روش پرداخت را انتخاب کنید'})
|
|
if not reference_number:
|
|
return JsonResponse({'success': False, 'message': 'شماره مرجع را وارد کنید'})
|
|
if not receipt_image:
|
|
return JsonResponse({'success': False, 'message': 'تصویر فیش را بارگذاری کنید'})
|
|
# Normalize date to YYYY-MM-DD (accept YYYY/MM/DD from Persian datepicker)
|
|
if '/' in payment_date:
|
|
payment_date = payment_date.replace('/', '-')
|
|
|
|
# Prevent overpayment
|
|
try:
|
|
amount_dec = Decimal(amount)
|
|
except InvalidOperation:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'})
|
|
remaining = quote.get_remaining_amount()
|
|
if amount_dec > remaining:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده پیشفاکتور است'})
|
|
|
|
Payment.objects.create(
|
|
invoice=invoice,
|
|
amount=amount_dec,
|
|
payment_date=payment_date,
|
|
payment_method=payment_method,
|
|
reference_number=reference_number,
|
|
receipt_image=receipt_image,
|
|
notes=notes,
|
|
created_by=request.user,
|
|
)
|
|
except Exception as e:
|
|
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
|
|
# 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 delete_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)
|
|
|
|
# who can delete payments
|
|
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 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
|
|
# 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})
|
|
|
|
|
|
@login_required
|
|
def final_invoice_step(request, instance_id, step_id):
|
|
"""تجمیع اقلام پیشفاکتور با تغییرات نصب و صدور فاکتور نهایی"""
|
|
instance = get_object_or_404(
|
|
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
|
|
id=instance_id
|
|
)
|
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
|
|
|
if not instance.can_access_step(step):
|
|
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
|
return redirect('processes:request_list')
|
|
|
|
quote = get_object_or_404(Quote, process_instance=instance)
|
|
|
|
# Helper to make safe Decimal from various inputs (handles commas/persian digits)
|
|
def _to_decimal(value):
|
|
if isinstance(value, Decimal):
|
|
return value
|
|
try:
|
|
if isinstance(value, (int, float)):
|
|
return Decimal(str(value))
|
|
s = str(value or '').strip()
|
|
if not s:
|
|
return Decimal('0')
|
|
# normalize commas and Persian digits
|
|
persian = '۰۱۲۳۴۵۶۷۸۹'
|
|
latin = '0123456789'
|
|
tbl = str.maketrans({persian[i]: latin[i] for i in range(10)})
|
|
s = s.translate(tbl).replace(',', '')
|
|
return Decimal(s)
|
|
except Exception:
|
|
return Decimal('0')
|
|
|
|
# Build initial map from quote
|
|
item_id_to_row = {}
|
|
for qi in quote.items.all():
|
|
item_id_to_row[qi.item_id] = {
|
|
'item': qi.item,
|
|
'base_qty': qi.quantity,
|
|
'base_price': _to_decimal(qi.unit_price),
|
|
'added_qty': 0,
|
|
'removed_qty': 0,
|
|
}
|
|
|
|
# Read installation changes from latest report (if any)
|
|
latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
|
|
if latest_report:
|
|
for ch in latest_report.item_changes.all():
|
|
row = item_id_to_row.setdefault(ch.item_id, {
|
|
'item': ch.item,
|
|
'base_qty': 0,
|
|
'base_price': _to_decimal(ch.unit_price or ch.item.unit_price),
|
|
'added_qty': 0,
|
|
'removed_qty': 0,
|
|
})
|
|
if ch.change_type == 'add':
|
|
row['added_qty'] += ch.quantity
|
|
if ch.unit_price:
|
|
row['base_price'] = _to_decimal(ch.unit_price)
|
|
else:
|
|
row['removed_qty'] += ch.quantity
|
|
if ch.unit_price:
|
|
row['base_price'] = _to_decimal(ch.unit_price)
|
|
|
|
# Compute final invoice lines
|
|
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
|
|
rows.append({
|
|
'item': r['item'],
|
|
'quantity': final_qty,
|
|
'unit_price': unit_price_dec,
|
|
'total_price': line_total,
|
|
'base_qty': r['base_qty'],
|
|
'added_qty': r['added_qty'],
|
|
'removed_qty': r['removed_qty'],
|
|
})
|
|
|
|
# Create or reuse final invoice
|
|
invoice, _ = Invoice.objects.get_or_create(
|
|
process_instance=instance,
|
|
customer=quote.customer,
|
|
quote=quote,
|
|
defaults={
|
|
'name': f"فاکتور نهایی {instance.code}",
|
|
'due_date': timezone.now().date(),
|
|
'created_by': request.user,
|
|
}
|
|
)
|
|
# Replace only non-special items (preserve special charges added by user)
|
|
qs = invoice.items.select_related('item').filter(item__is_special=False)
|
|
try:
|
|
qs._raw_delete(qs.db)
|
|
except Exception:
|
|
qs.delete()
|
|
for r in rows:
|
|
from .models import InvoiceItem
|
|
InvoiceItem.objects.create(
|
|
invoice=invoice,
|
|
item=r['item'],
|
|
quantity=r['quantity'],
|
|
unit_price=r['unit_price'],
|
|
)
|
|
invoice.calculate_totals()
|
|
|
|
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
|
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
|
|
|
# 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,
|
|
'invoice': invoice,
|
|
'rows': rows,
|
|
'special_choices': special_choices,
|
|
'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,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def final_invoice_print(request, instance_id):
|
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
|
invoice = get_object_or_404(Invoice, process_instance=instance)
|
|
items = invoice.items.select_related('item').filter(is_deleted=False).all()
|
|
return render(request, 'invoices/final_invoice_print.html', {
|
|
'instance': instance,
|
|
'invoice': invoice,
|
|
'items': items,
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
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:
|
|
# return JsonResponse({
|
|
# 'success': False,
|
|
# 'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})"
|
|
# })
|
|
# mark step completed
|
|
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()
|
|
# move to next
|
|
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])
|
|
return JsonResponse({'success': True, 'message': 'فاکتور نهایی تایید شد', 'redirect': redirect_url})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
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()
|
|
if not item_id:
|
|
return JsonResponse({'success': False, 'message': 'آیتم را انتخاب کنید'})
|
|
try:
|
|
amount_dec = Decimal(amount)
|
|
except (InvalidOperation, TypeError):
|
|
return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'})
|
|
if amount_dec <= 0:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ باید مثبت باشد'})
|
|
|
|
# Fetch existing special item from DB
|
|
special_item = get_object_or_404(Item, id=item_id, is_special=True)
|
|
|
|
from .models import InvoiceItem
|
|
InvoiceItem.objects.create(
|
|
invoice=invoice,
|
|
item=special_item,
|
|
quantity=1,
|
|
unit_price=amount_dec,
|
|
)
|
|
invoice.calculate_totals()
|
|
return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
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
|
|
try:
|
|
if not getattr(inv_item.item, 'is_special', False):
|
|
return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'})
|
|
except Exception:
|
|
return JsonResponse({'success': False, 'message': 'امکان حذف این مورد وجود ندارد'})
|
|
inv_item.hard_delete()
|
|
invoice.calculate_totals()
|
|
return JsonResponse({'success': True, 'redirect': reverse('invoices:final_invoice_step', args=[instance.id, step_id])})
|
|
|
|
|
|
@login_required
|
|
def final_settlement_step(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)
|
|
if not instance.can_access_step(step):
|
|
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
|
return redirect('processes:request_list')
|
|
invoice = get_object_or_404(Invoice, process_instance=instance)
|
|
|
|
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,
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@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()
|
|
reference_number = (request.POST.get('reference_number') or '').strip()
|
|
direction = (request.POST.get('direction') or 'in').strip()
|
|
receipt_image = request.FILES.get('receipt_image')
|
|
if not amount:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ را وارد کنید'})
|
|
if not payment_date:
|
|
return JsonResponse({'success': False, 'message': 'تاریخ پرداخت را وارد کنید'})
|
|
if not payment_method:
|
|
return JsonResponse({'success': False, 'message': 'روش پرداخت را انتخاب کنید'})
|
|
if not reference_number:
|
|
return JsonResponse({'success': False, 'message': 'شماره مرجع را وارد کنید'})
|
|
if not receipt_image:
|
|
return JsonResponse({'success': False, 'message': 'تصویر فیش الزامی است'})
|
|
if '/' in payment_date:
|
|
payment_date = payment_date.replace('/', '-')
|
|
try:
|
|
amount_dec = Decimal(amount)
|
|
except InvalidOperation:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'})
|
|
# Only allow outgoing (پرداخت به مشتری) when current net due is negative
|
|
# Compute net due explicitly from current items/payments
|
|
try:
|
|
current_paid = sum((p.amount if p.direction == 'in' else -p.amount) for p in invoice.payments.filter(is_deleted=False).all())
|
|
except Exception:
|
|
current_paid = Decimal('0')
|
|
# Ensure invoice totals are up-to-date for final_amount
|
|
invoice.calculate_totals()
|
|
net_due = invoice.final_amount - current_paid
|
|
if direction == 'out' and net_due >= 0:
|
|
return JsonResponse({'success': False, 'message': 'در حال حاضر مانده به نفع مشتری نیست'})
|
|
|
|
# Amount constraints by sign of net due
|
|
if net_due > 0 and direction == 'in' and amount_dec > net_due:
|
|
return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده فاکتور است'})
|
|
if net_due < 0 and direction == 'out' and amount_dec > abs(net_due):
|
|
return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده بدهی شرکت به مشتری است'})
|
|
if net_due < 0 and direction == 'in':
|
|
return JsonResponse({'success': False, 'message': 'در حال حاضر مانده به نفع مشتری است؛ دریافت از مشتری مجاز نیست'})
|
|
|
|
Payment.objects.create(
|
|
invoice=invoice,
|
|
amount=amount_dec,
|
|
payment_date=payment_date,
|
|
payment_method=payment_method,
|
|
reference_number=reference_number,
|
|
direction='in' if direction != 'out' else 'out',
|
|
receipt_image=receipt_image,
|
|
created_by=request.user,
|
|
)
|
|
# 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]),
|
|
'totals': {
|
|
'final_amount': str(invoice.final_amount),
|
|
'paid_amount': str(invoice.paid_amount),
|
|
'remaining_amount': str(invoice.remaining_amount),
|
|
}
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@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),
|
|
'remaining_amount': str(invoice.remaining_amount),
|
|
}})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def approve_final_settlement(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)
|
|
# Block approval if any remaining exists (positive or negative)
|
|
invoice.calculate_totals()
|
|
if invoice.remaining_amount != 0:
|
|
return JsonResponse({
|
|
'success': False,
|
|
'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})"
|
|
})
|
|
# complete step
|
|
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()
|
|
# move next
|
|
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])
|
|
return JsonResponse({'success': True, 'message': 'تسویه حساب نهایی ثبت شد', 'redirect': redirect_url})
|