shafafiyat/invoices/views.py
2025-08-21 09:18:51 +03:30

415 lines
17 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
from .models import Item, Quote, QuoteItem, Payment, Invoice
@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.all().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()
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,
})
@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)
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).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, _ = 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,
}
)
# 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()
# تکمیل مرحله
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('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'),
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()
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,
})
@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)
# تایید پیش‌فاکتور
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 = instance.step_instances.filter(step=step).first()
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,
})
@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,
}
)
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)})
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)
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': 'خطا در ویرایش فیش'})
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)
try:
# soft delete using project's BaseModel delete override
payment.delete()
except Exception:
return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
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})