""" Billing and invoice services. """ from typing import List, Optional from datetime import date, timedelta from django.utils import timezone from django.db import transaction from backend.core.models import Account, Project, Invoice from backend.core.repositories.customers.customers import CustomerRepository from backend.core.repositories.accounts.accounts import AccountRepository from backend.core.repositories.projects.projects import ProjectRepository from backend.core.repositories.invoices.invoices import InvoiceRepository from backend.core.repositories.revenues.revenues import RevenueRepository class BillingService: """ Service for billing and invoice operations. """ @staticmethod @transaction.atomic def generate_invoice_for_customer( customer_id: str, invoice_date: date = None, include_accounts: bool = True, include_projects: bool = True, account_ids: List[str] = None, project_ids: List[str] = None ) -> Optional[Invoice]: """ Generate an invoice for a customer. Args: customer_id: Customer ID invoice_date: Invoice date (defaults to today) include_accounts: Whether to include all accounts include_projects: Whether to include all completed projects account_ids: Specific account IDs to include (if not including all) project_ids: Specific project IDs to include (if not including all) Returns: Generated invoice or None if no items to invoice """ # Get the customer customer = CustomerRepository.get_by_id(customer_id) if not customer: return None # Use today's date if not specified if not invoice_date: invoice_date = timezone.now().date() # Get accounts to invoice if include_accounts: # Get all active accounts accounts_to_invoice = AccountRepository.get_active_by_customer(customer_id) elif account_ids: # Get specific accounts accounts_to_invoice = Account.objects.filter(id__in=account_ids) else: accounts_to_invoice = Account.objects.none() # Get projects to invoice if include_projects: # Get all completed projects without invoices projects_to_invoice = ProjectRepository.get_without_invoice() elif project_ids: # Get specific projects projects_to_invoice = Project.objects.filter(id__in=project_ids) else: projects_to_invoice = Project.objects.none() # Calculate total amount total_amount = 0 # Add account revenue for account in accounts_to_invoice: active_revenue = RevenueRepository.get_active_by_account(account.id) if active_revenue: # For monthly billing, divide annual amount by 12 or use the full amount total_amount += active_revenue.amount # Add project amounts for project in projects_to_invoice: total_amount += project.amount # Don't create an invoice if there's nothing to invoice if total_amount <= 0: return None # Create the invoice invoice_data = { 'customer': customer_id, 'date': invoice_date, 'status': 'draft', 'total_amount': total_amount } # Convert querysets to list of IDs account_ids_list = [str(account.id) for account in accounts_to_invoice] project_ids_list = [str(project.id) for project in projects_to_invoice] # Create the invoice with items invoice = InvoiceRepository.create_with_items( invoice_data, account_ids_list, project_ids_list ) return invoice @staticmethod def mark_overdue_invoices() -> int: """ Identify and mark overdue invoices. Returns: Number of invoices marked as overdue """ thirty_days_ago = timezone.now().date() - timedelta(days=30) # Get sent invoices that are more than 30 days old overdue_invoices = Invoice.objects.filter( status='sent', date__lt=thirty_days_ago ) # Update them to overdue status count = overdue_invoices.update(status='overdue') return count @staticmethod def get_outstanding_balance(customer_id: str) -> float: """ Get outstanding balance for a customer. Args: customer_id: Customer ID Returns: Total outstanding amount """ return InvoiceRepository.get_total_outstanding(customer_id)