145 lines
4.7 KiB
Python
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) |