""" Finance views for the Tenhal Multidisciplinary Healthcare Platform. This module contains views for financial management including: - Invoice CRUD operations - Payment processing - Package management - Payer management - Financial reporting """ from decimal import Decimal from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Q, Sum, Count from django.http import JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.views import View from django.views.generic import ListView, DetailView, CreateView, UpdateView from django.urls import reverse_lazy from django.contrib import messages from core.mixins import ( TenantFilterMixin, RolePermissionMixin, AuditLogMixin, HTMXResponseMixin, SuccessMessageMixin, PaginationMixin, ) from core.models import User, Patient from .models import * from .forms import * class InvoiceListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin, HTMXResponseMixin, ListView): """ Invoice list view with filtering and search. Features: - Filter by status, payer, date range - Search by patient name/MRN, invoice number - Summary statistics - Export to CSV """ model = Invoice template_name = 'finance/invoice_list.html' htmx_template_name = 'finance/partials/invoice_list_partial.html' context_object_name = 'invoices' paginate_by = 25 def get_queryset(self): """Get filtered queryset.""" queryset = super().get_queryset() # Apply search search_query = self.request.GET.get('search', '').strip() if search_query: queryset = queryset.filter( Q(patient__first_name_en__icontains=search_query) | Q(patient__last_name_en__icontains=search_query) | Q(patient__mrn__icontains=search_query) | Q(invoice_number__icontains=search_query) ) # Apply filters status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) payer_id = self.request.GET.get('payer') if payer_id: queryset = queryset.filter(payer_id=payer_id) date_from = self.request.GET.get('date_from') if date_from: queryset = queryset.filter(invoice_date__gte=date_from) date_to = self.request.GET.get('date_to') if date_to: queryset = queryset.filter(issue_date__lte=date_to) # Filter overdue if self.request.GET.get('overdue') == 'true': queryset = queryset.filter( status=Invoice.Status.ISSUED, due_date__lt=timezone.now().date() ) return queryset.select_related('patient', 'payer').order_by('-issue_date') def get_context_data(self, **kwargs): """Add filter options and statistics.""" context = super().get_context_data(**kwargs) # Add filter options context['payers'] = Payer.objects.filter(tenant=self.request.user.tenant) context['status_choices'] = Invoice.Status.choices # Add current filters context['current_filters'] = { 'search': self.request.GET.get('search', ''), 'status': self.request.GET.get('status', ''), 'payer': self.request.GET.get('payer', ''), 'date_from': self.request.GET.get('date_from', ''), 'date_to': self.request.GET.get('date_to', ''), 'overdue': self.request.GET.get('overdue', ''), } # Add summary statistics queryset = self.get_queryset() context['stats'] = { 'total_invoices': queryset.count(), 'total_amount': queryset.aggregate(Sum('total'))['total__sum'] or 0, 'total_paid': sum(inv.amount_paid for inv in queryset), 'pending_amount': sum(inv.amount_due for inv in queryset.filter(status__in=[Invoice.Status.DRAFT, Invoice.Status.ISSUED])), 'overdue_count': queryset.filter( status=Invoice.Status.ISSUED, due_date__lt=timezone.now().date() ).count(), } return context def render_to_response(self, context, **response_kwargs): """Handle CSV export.""" if self.request.GET.get('export') == 'csv': return self._export_to_csv(context['invoices']) return super().render_to_response(context, **response_kwargs) def _export_to_csv(self, invoices): """Export invoices to CSV.""" import csv response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="invoices_{timezone.now().date()}.csv"' writer = csv.writer(response) writer.writerow([ 'Invoice Number', 'Date', 'Patient', 'MRN', 'Payer', 'Total Amount', 'Paid Amount', 'Balance', 'Status', 'Due Date' ]) for invoice in invoices: writer.writerow([ invoice.invoice_number, invoice.issue_date, invoice.patient.full_name_en if hasattr(invoice.patient, 'full_name_en') else str(invoice.patient), invoice.patient.mrn, invoice.payer.name if invoice.payer else 'Self-Pay', invoice.total, invoice.amount_paid, invoice.amount_due, invoice.get_status_display(), invoice.due_date, ]) return response class InvoiceDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView): """ Invoice detail view. Features: - Full invoice details with line items - Payment history - Available actions (send, pay, void) - Print/PDF download """ model = Invoice template_name = 'finance/invoice_detail.html' context_object_name = 'invoice' def get_context_data(self, **kwargs): """Add line items and payments.""" context = super().get_context_data(**kwargs) invoice = self.object # Get line items context['line_items'] = invoice.line_items.all() # Get payments context['payments'] = invoice.payments.all().order_by('-payment_date') # Calculate balance context['balance'] = invoice.amount_due # Check if overdue context['is_overdue'] = ( invoice.status == Invoice.Status.OVERDUE and invoice.due_date < timezone.now().date() ) # Available actions context['can_send'] = invoice.status == Invoice.Status.DRAFT context['can_pay'] = invoice.status in [Invoice.Status.ISSUED, Invoice.Status.PARTIALLY_PAID] context['can_cancel'] = invoice.status not in [Invoice.Status.CANCELLED, Invoice.Status.PAID] return context class InvoiceCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin, SuccessMessageMixin, CreateView): """ Invoice creation view. Features: - Auto-generate invoice number - Add line items (services/packages) - Calculate totals - Link to appointment """ model = Invoice form_class = InvoiceForm template_name = 'finance/invoice_form.html' success_message = "Invoice created successfully! Number: {invoice_number}" allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK, User.Role.FINANCE] def get_success_url(self): """Redirect to invoice detail.""" return reverse_lazy('finance:invoice_detail', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): """Add form title, patient info, and line item formset.""" context = super().get_context_data(**kwargs) context['form_title'] = 'Create New Invoice' context['submit_text'] = 'Create Invoice' # Add line item formset if self.request.POST: context['line_item_formset'] = InvoiceLineItemFormSet( self.request.POST, instance=self.object ) else: context['line_item_formset'] = InvoiceLineItemFormSet( instance=self.object ) # Add services and packages for the item dropdown context['services'] = Service.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name_en') context['packages'] = Package.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name_en') # Get patient if provided in URL or from POST patient_id = self.request.GET.get('patient') or self.request.POST.get('patient') if patient_id: try: patient = Patient.objects.get( pk=patient_id, tenant=self.request.user.tenant ) context['patient'] = patient # Check if patient is Saudi (national_id starts with '1') context['is_saudi_patient'] = ( patient.national_id.startswith('1') if patient.national_id else False ) except Patient.DoesNotExist: context['is_saudi_patient'] = False else: # Default to False (apply VAT) if no patient selected context['is_saudi_patient'] = False # Add all patients with their nationality info for JavaScript import json patients = Patient.objects.filter(tenant=self.request.user.tenant).values('id', 'national_id') patients_nationality = { str(p['id']): p['national_id'].startswith('1') if p['national_id'] else False for p in patients } context['patients_nationality_json'] = json.dumps(patients_nationality) return context def form_valid(self, form): """Set tenant, generate invoice number, and save formset.""" from finance.csid_manager import InvoiceCounterManager from django.db import transaction # Use database transaction with locking to prevent race conditions with transaction.atomic(): # Set tenant form.instance.tenant = self.request.user.tenant # Generate invoice number if not form.instance.invoice_number: form.instance.invoice_number = self._generate_invoice_number() # Set invoice counter (ZATCA requirement) with SELECT FOR UPDATE lock if not form.instance.invoice_counter: # Lock the last invoice to prevent concurrent counter conflicts last_invoice = Invoice.objects.select_for_update().filter( tenant=self.request.user.tenant ).order_by('-invoice_counter').first() if last_invoice: form.instance.invoice_counter = last_invoice.invoice_counter + 1 form.instance.previous_invoice_hash = last_invoice.invoice_hash else: form.instance.invoice_counter = 1 form.instance.previous_invoice_hash = "" # Set initial status if not form.instance.status: form.instance.status = Invoice.Status.DRAFT # Set tax and discount from POST data tax_amount = self.request.POST.get('tax', 0) or 0 discount_amount = self.request.POST.get('discount', 0) or 0 form.instance.tax = Decimal(str(tax_amount)) form.instance.discount = Decimal(str(discount_amount)) # Save invoice first (this will trigger hash and QR code generation) self.object = form.save() # Get and validate formset formset = InvoiceLineItemFormSet( self.request.POST, instance=self.object ) if formset.is_valid(): # Save line items with unit_price from POST line_items = formset.save(commit=False) for i, line_item in enumerate(line_items): unit_price = self.request.POST.get(f'line_items-{i}-unit_price', 0) or 0 line_item.unit_price = Decimal(str(unit_price)) line_item.save() # Handle deleted items for obj in formset.deleted_objects: obj.delete() # Calculate totals manually self.object.subtotal = sum(item.total for item in self.object.line_items.all()) self.object.total = self.object.subtotal + self.object.tax - self.object.discount self.object.save() # Update success message self.success_message = self.success_message.format( invoice_number=self.object.invoice_number ) messages.success(self.request, self.success_message) return redirect(self.get_success_url()) else: # If formset is invalid, delete the invoice and show errors self.object.delete() return self.form_invalid(form) def _generate_invoice_number(self): """Generate unique invoice number.""" import random tenant = self.request.user.tenant year = timezone.now().year for _ in range(10): random_num = random.randint(10000, 99999) number = f"INV-{tenant.code}-{year}-{random_num}" if not Invoice.objects.filter(invoice_number=number).exists(): return number # Fallback timestamp = int(timezone.now().timestamp()) return f"INV-{tenant.code}-{year}-{timestamp}" class InvoiceUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """ Invoice update view. Features: - Update invoice details - Cannot update if paid/void - Recalculate totals """ model = Invoice form_class = InvoiceForm template_name = 'finance/invoice_form.html' success_message = "Invoice updated successfully!" allowed_roles = [User.Role.ADMIN, User.Role.FINANCE] def get_success_url(self): """Redirect to invoice detail.""" return reverse_lazy('finance:invoice_detail', kwargs={'pk': self.object.pk}) def get_form(self, form_class=None): """Disable fields if invoice is paid/cancelled.""" form = super().get_form(form_class) if self.object.status in [Invoice.Status.PAID, Invoice.Status.CANCELLED]: for field in form.fields: form.fields[field].disabled = True return form def get_context_data(self, **kwargs): """Add form title and line item formset.""" context = super().get_context_data(**kwargs) context['form_title'] = f'Update Invoice: {self.object.invoice_number}' context['submit_text'] = 'Update Invoice' # Add line item formset if self.request.POST: context['line_item_formset'] = InvoiceLineItemFormSet( self.request.POST, instance=self.object ) else: context['line_item_formset'] = InvoiceLineItemFormSet( instance=self.object ) # Add services and packages for the item dropdown context['services'] = Service.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name_en') context['packages'] = Package.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name_en') # Check if patient is Saudi (national_id starts with '1') if self.object.patient: context['is_saudi_patient'] = ( self.object.patient.national_id.startswith('1') if self.object.patient.national_id else False ) return context def form_valid(self, form): """Save invoice and formset.""" # Set tax and discount from POST data tax_amount = self.request.POST.get('tax', 0) or 0 discount_amount = self.request.POST.get('discount', 0) or 0 form.instance.tax = Decimal(str(tax_amount)) form.instance.discount = Decimal(str(discount_amount)) # Save invoice self.object = form.save() # Get and validate formset formset = InvoiceLineItemFormSet( self.request.POST, instance=self.object ) if formset.is_valid(): # Save line items with unit_price from POST line_items = formset.save(commit=False) for i, line_item in enumerate(line_items): unit_price = self.request.POST.get(f'line_items-{i}-unit_price', 0) or 0 line_item.unit_price = Decimal(str(unit_price)) line_item.save() # Handle deleted items for obj in formset.deleted_objects: obj.delete() # Calculate totals manually self.object.subtotal = sum(item.total for item in self.object.line_items.all()) self.object.total = self.object.subtotal + self.object.tax - self.object.discount self.object.save() messages.success(self.request, self.success_message) return redirect(self.get_success_url()) else: # If formset is invalid, show errors return self.form_invalid(form) class PaymentCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin, SuccessMessageMixin, CreateView): """ Payment creation view. Features: - Record payment for invoice - Update invoice paid amount - Update invoice status - Generate receipt """ model = Payment form_class = PaymentForm template_name = 'finance/payment_form.html' success_message = "Payment recorded successfully!" allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK, User.Role.FINANCE] def get_form_kwargs(self): """Pass invoice_id to form if provided in URL.""" kwargs = super().get_form_kwargs() invoice_id = self.request.GET.get('invoice') if invoice_id: kwargs['invoice_id'] = invoice_id return kwargs def get_success_url(self): """Redirect to invoice detail.""" return reverse_lazy('finance:invoice_detail', kwargs={'pk': self.object.invoice.pk}) def form_valid(self, form): """Set tenant and processed_by, then save.""" form.instance.tenant = self.request.user.tenant form.instance.processed_by = self.request.user # Set amount from POST data amount_value = self.request.POST.get('amount', 0) or 0 form.instance.amount = Decimal(str(amount_value)) # Set status to COMPLETED by default (payment is being recorded now) form.instance.status = Payment.Status.COMPLETED # Save payment response = super().form_valid(form) # Update invoice status based on payment invoice = self.object.invoice total_paid = sum(p.amount for p in invoice.payments.filter(status=Payment.Status.COMPLETED)) if total_paid >= invoice.total: invoice.status = Invoice.Status.PAID elif total_paid > 0: invoice.status = Invoice.Status.PARTIALLY_PAID invoice.save() # Generate receipt (TODO) self._generate_receipt() return response def _generate_receipt(self): """Generate payment receipt.""" # TODO: Implement receipt generation pass def get_context_data(self, **kwargs): """Add invoice info.""" context = super().get_context_data(**kwargs) # Get invoice from URL invoice_id = self.request.GET.get('invoice') if invoice_id: try: invoice = Invoice.objects.get( pk=invoice_id, tenant=self.request.user.tenant ) context['invoice'] = invoice context['balance'] = invoice.amount_due except Invoice.DoesNotExist: pass context['form_title'] = 'Record Payment' context['submit_text'] = 'Record Payment' return context class PaymentRefundView(LoginRequiredMixin, RolePermissionMixin, View): """ Handle payment refunds. Features: - Mark payment as refunded - Update invoice status - Record refund details """ allowed_roles = [User.Role.ADMIN, User.Role.FINANCE] def post(self, request, pk): """Process refund.""" payment = get_object_or_404( Payment, pk=pk, invoice__tenant=request.user.tenant ) # Check if payment can be refunded if payment.status != Payment.Status.COMPLETED: messages.error(request, "Only completed payments can be refunded.") return redirect('finance:invoice_detail', pk=payment.invoice.pk) # Get refund details from POST refund_reason = request.POST.get('refund_reason', '') # Mark payment as refunded payment.status = Payment.Status.REFUNDED payment.notes = f"REFUNDED: {refund_reason}\n\n{payment.notes}" payment.save() # Update invoice status invoice = payment.invoice total_paid = sum( p.amount for p in invoice.payments.filter(status=Payment.Status.COMPLETED) ) if total_paid <= 0: # No payments left, revert to issued invoice.status = Invoice.Status.ISSUED elif total_paid < invoice.total: invoice.status = Invoice.Status.PARTIALLY_PAID else: invoice.status = Invoice.Status.PAID invoice.save() messages.success(request, f"Payment refunded successfully. Amount: {payment.amount}") return redirect('finance:invoice_detail', pk=invoice.pk) class PaymentListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin, ListView): """ Payment list view. Features: - List all payments - Filter by date, method, invoice - Summary statistics """ model = Payment template_name = 'finance/payment_list.html' context_object_name = 'payments' paginate_by = 25 def get_queryset(self): """Get filtered queryset.""" queryset = Payment.objects.filter( invoice__tenant=self.request.user.tenant ) # Apply filters date_from = self.request.GET.get('date_from') if date_from: queryset = queryset.filter(payment_date__gte=date_from) date_to = self.request.GET.get('date_to') if date_to: queryset = queryset.filter(payment_date__lte=date_to) method = self.request.GET.get('method') if method: queryset = queryset.filter(payment_method=method) return queryset.select_related('invoice', 'invoice__patient').order_by('-payment_date') def get_context_data(self, **kwargs): """Add statistics.""" context = super().get_context_data(**kwargs) queryset = self.get_queryset() context['stats'] = { 'total_payments': queryset.count(), 'total_amount': queryset.aggregate(Sum('amount'))['amount__sum'] or 0, } context['method_choices'] = Payment.PaymentMethod.choices return context class PackageListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin, ListView): """ Package list view. Features: - List all service packages - Filter by specialty - Show pricing and sessions """ model = Package template_name = 'finance/package_list.html' context_object_name = 'packages' paginate_by = 25 def get_queryset(self): """Get filtered queryset.""" queryset = super().get_queryset() # Apply filters specialty = self.request.GET.get('specialty') if specialty: queryset = queryset.filter(services__clinic__specialty=specialty) is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=(is_active == 'true')) return queryset.order_by('services__clinic__specialty', 'name_en') def get_context_data(self, **kwargs): """Add filter options.""" context = super().get_context_data(**kwargs) from core.models import Clinic context['specialty_choices'] = Clinic.Specialty.choices return context class PackageCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin, SuccessMessageMixin, CreateView): """ Package creation view. Features: - Create service package - Set pricing and sessions - Define validity period """ model = Package form_class = PackageForm template_name = 'finance/package_form.html' success_message = "Package created successfully!" success_url = reverse_lazy('finance:package_list') allowed_roles = [User.Role.ADMIN, User.Role.FINANCE] def get_context_data(self, **kwargs): """Add form title and service formset.""" context = super().get_context_data(**kwargs) context['form_title'] = 'Create New Package' context['submit_text'] = 'Create Package' # Add package service formset if self.request.POST: context['service_formset'] = PackageServiceFormSet( self.request.POST, instance=self.object ) else: context['service_formset'] = PackageServiceFormSet( instance=self.object ) return context def form_valid(self, form): """Set tenant and save formset.""" form.instance.tenant = self.request.user.tenant # Save package first self.object = form.save() # Get and validate formset formset = PackageServiceFormSet( self.request.POST, instance=self.object ) if formset.is_valid(): # Save package services formset.save() # Update total sessions self.object.total_sessions = self.object.calculate_total_sessions() self.object.save() messages.success(self.request, self.success_message) return redirect(self.get_success_url()) else: # If formset is invalid, delete the package and show errors self.object.delete() return self.form_invalid(form) class PackageUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """ Package update view. Features: - Update package details - Cannot update if in use """ model = Package form_class = PackageForm template_name = 'finance/package_form.html' success_message = "Package updated successfully!" success_url = reverse_lazy('finance:package_list') allowed_roles = [User.Role.ADMIN, User.Role.FINANCE] def get_context_data(self, **kwargs): """Add form title and service formset.""" context = super().get_context_data(**kwargs) context['form_title'] = f'Update Package: {self.object.name_en}' context['submit_text'] = 'Update Package' # Add package service formset if self.request.POST: context['service_formset'] = PackageServiceFormSet( self.request.POST, instance=self.object ) else: context['service_formset'] = PackageServiceFormSet( instance=self.object ) return context def form_valid(self, form): """Save package and formset.""" # Save package self.object = form.save() # Get and validate formset formset = PackageServiceFormSet( self.request.POST, instance=self.object ) if formset.is_valid(): # Save package services formset.save() # Update total sessions self.object.total_sessions = self.object.calculate_total_sessions() self.object.save() messages.success(self.request, self.success_message) return redirect(self.get_success_url()) else: # If formset is invalid, show errors return self.form_invalid(form) class PayerListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin, ListView): """ Payer list view. Features: - List all payers (insurance companies, etc.) - Filter by type - Show statistics """ model = Payer template_name = 'finance/payer_list.html' context_object_name = 'payers' paginate_by = 25 def get_queryset(self): """Get filtered queryset.""" queryset = super().get_queryset() # Apply filters payer_type = self.request.GET.get('payer_type') if payer_type: queryset = queryset.filter(payer_type=payer_type) is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=(is_active == 'true')) return queryset.order_by('name') def get_context_data(self, **kwargs): """Add statistics.""" context = super().get_context_data(**kwargs) context['payer_type_choices'] = Payer.PayerType.choices # Add statistics queryset = self.get_queryset() context['stats'] = { 'total_payers': queryset.count(), 'active_payers': queryset.filter(is_active=True).count(), } return context class InvoicePDFDownloadView(LoginRequiredMixin, TenantFilterMixin, View): """ Generate and download invoice PDF. Features: - Generate PDF with QR code - Bilingual (Arabic + English) - ZATCA-compliant format """ def get(self, request, pk): """Generate and return PDF.""" from finance.pdf_service import PDFService # Get invoice invoice = get_object_or_404( Invoice, pk=pk, tenant=request.user.tenant ) try: # Generate PDF pdf_content = PDFService.generate_invoice_pdf(invoice, include_xml=True) # Create response response = HttpResponse(pdf_content, content_type='application/pdf') # Check if this is for printing (inline) or downloading (attachment) if request.GET.get('print') == 'true': response['Content-Disposition'] = f'inline; filename="invoice_{invoice.invoice_number}.pdf"' else: response['Content-Disposition'] = f'attachment; filename="invoice_{invoice.invoice_number}.pdf"' return response except Exception as e: messages.error(request, f"Error generating PDF: {str(e)}") return redirect('finance:invoice_detail', pk=pk) class FinancialReportView(LoginRequiredMixin, RolePermissionMixin, ListView): """ Financial reporting view. Features: - Revenue reports - Outstanding invoices - Payment trends - Payer analysis """ model = Invoice template_name = 'finance/financial_report.html' allowed_roles = [User.Role.ADMIN, User.Role.FINANCE] def get_queryset(self): """Get invoices for reporting period.""" queryset = Invoice.objects.filter(tenant=self.request.user.tenant) # Apply date range date_from = self.request.GET.get('date_from') if date_from: queryset = queryset.filter(issue_date__gte=date_from) date_to = self.request.GET.get('date_to') if date_to: queryset = queryset.filter(issue_date__lte=date_to) return queryset def get_context_data(self, **kwargs): """Add comprehensive financial statistics.""" import json from django.db.models.functions import TruncMonth context = super().get_context_data(**kwargs) queryset = self.get_queryset() # Get payments for the period payments = Payment.objects.filter( invoice__tenant=self.request.user.tenant, invoice__in=queryset, status=Payment.Status.COMPLETED ) # Main statistics for top cards total_invoiced = queryset.aggregate(Sum('total'))['total__sum'] or 0 total_collected = payments.aggregate(Sum('amount'))['amount__sum'] or 0 context['stats'] = { 'total_revenue': total_invoiced, 'collected': total_collected, 'outstanding': total_invoiced - total_collected, 'total_invoices': queryset.count(), } # Revenue by service (from invoice line items) from .models import InvoiceLineItem revenue_by_service = InvoiceLineItem.objects.filter( invoice__in=queryset ).values( 'service__name_en' ).annotate( count=Count('id'), revenue=Sum('total') ).order_by('-revenue')[:10] context['revenue_by_service'] = [ { 'service': item['service__name_en'] or 'N/A', 'count': item['count'], 'revenue': item['revenue'] or 0 } for item in revenue_by_service ] # Payment methods breakdown payment_methods_data = [] for method_code, method_name in Payment.PaymentMethod.choices: method_payments = payments.filter(method=method_code) amount = method_payments.aggregate(Sum('amount'))['amount__sum'] or 0 if amount > 0: payment_methods_data.append({ 'name': method_name, 'count': method_payments.count(), 'amount': amount }) context['payment_methods'] = payment_methods_data # Package sales - use package price from invoice line items package_sales_data = InvoiceLineItem.objects.filter( invoice__in=queryset, package__isnull=False # Only line items that reference a package ).values( 'package__name_en' ).annotate( count=Count('id'), revenue=Sum('total') # Use the line item total, not invoice total ).order_by('-revenue')[:10] context['package_sales'] = [ { 'name': item['package__name_en'] or 'N/A', 'count': item['count'], 'revenue': item['revenue'] or 0 } for item in package_sales_data ] # Monthly revenue trend monthly_data = queryset.annotate( month=TruncMonth('issue_date') ).values('month').annotate( revenue=Sum('total') ).order_by('month') monthly_labels = [item['month'].strftime('%b %Y') if item['month'] else 'N/A' for item in monthly_data] monthly_revenue = [float(item['revenue']) if item['revenue'] else 0 for item in monthly_data] context['monthly_labels'] = json.dumps(monthly_labels) context['monthly_revenue'] = json.dumps(monthly_revenue) return context