2026-01-26 10:30:49 -05:00

145 lines
4.7 KiB
Python

"""
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)