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

302 lines
8.3 KiB
Python

"""
Repository for Invoice model operations.
"""
from typing import List, Optional, Dict, Any
from datetime import date, timedelta
from django.db.models import QuerySet, Sum
from django.utils import timezone
from backend.core.models import Invoice, Account, Project
from backend.core.repositories.base import BaseRepository
class InvoiceRepository(BaseRepository[Invoice]):
"""
Repository for Invoice model operations.
"""
model = Invoice
@classmethod
def get_by_customer(cls, customer_id: str) -> QuerySet[Invoice]:
"""
Get invoices by customer.
Args:
customer_id: The customer ID
Returns:
QuerySet of invoices for the customer
"""
return Invoice.objects.filter(customer_id=customer_id)
@classmethod
def get_by_status(cls, status: str) -> QuerySet[Invoice]:
"""
Get invoices by status.
Args:
status: The invoice status
Returns:
QuerySet of invoices with the specified status
"""
return Invoice.objects.filter(status=status)
@classmethod
def get_by_date_range(cls, start_date: date = None, end_date: date = None) -> QuerySet[Invoice]:
"""
Get invoices within a date range.
Args:
start_date: Start date (inclusive)
end_date: End date (inclusive)
Returns:
QuerySet of invoices within the date range
"""
return cls.filter_by_date_range(start_date, end_date)
@classmethod
def get_overdue(cls) -> QuerySet[Invoice]:
"""
Get overdue invoices.
Returns:
QuerySet of overdue invoices
"""
thirty_days_ago = timezone.now().date() - timedelta(days=30)
return Invoice.objects.filter(
status__in=['sent', 'overdue'], # Include both sent and already marked overdue
sent_at__isnull=False, # Must have been sent
).exclude(
status__in=['paid', 'cancelled'] # Not paid or cancelled
).filter(
sent_at__date__lt=thirty_days_ago # Sent more than 30 days ago
)
@classmethod
def get_unpaid(cls) -> QuerySet[Invoice]:
"""
Get unpaid invoices.
Returns:
QuerySet of unpaid invoices (sent but not paid)
"""
return Invoice.objects.filter(status='sent')
@classmethod
def search(cls, search_term: str, search_fields: List[str] = None) -> QuerySet[Invoice]:
"""
Search invoices by customer.
Args:
search_term: The search term
search_fields: Optional list of fields to search (ignored, using predefined fields)
Returns:
QuerySet of matching invoices
"""
return super().search(
search_term,
['customer__name']
)
@classmethod
def filter_invoices(
cls,
customer_id: str = None,
status: str = None,
date_from: date = None,
date_to: date = None,
account_id: str = None,
project_id: str = None
) -> QuerySet[Invoice]:
"""
Filter invoices by multiple criteria.
Args:
customer_id: Filter by customer ID
status: Filter by status
date_from: Filter by start date (inclusive)
date_to: Filter by end date (inclusive)
account_id: Filter by account ID
project_id: Filter by project ID
Returns:
QuerySet of matching invoices
"""
queryset = Invoice.objects.all()
if customer_id:
queryset = queryset.filter(customer_id=customer_id)
if status:
queryset = queryset.filter(status=status)
if date_from:
queryset = queryset.filter(date__gte=date_from)
if date_to:
queryset = queryset.filter(date__lte=date_to)
if account_id:
queryset = queryset.filter(accounts__id=account_id)
if project_id:
queryset = queryset.filter(projects__id=project_id)
return queryset
@classmethod
def create_with_items(
cls,
data: Dict[str, Any],
account_ids: List[str] = None,
project_ids: List[str] = None
) -> Invoice:
"""
Create an invoice with related items.
Args:
data: Invoice data
account_ids: List of account IDs
project_ids: List of project IDs
Returns:
The created invoice
"""
# Create the invoice
invoice = cls.create(data)
# Add accounts
if account_ids:
accounts = Account.objects.filter(id__in=account_ids)
invoice.accounts.set(accounts)
# Add projects
if project_ids:
projects = Project.objects.filter(id__in=project_ids)
invoice.projects.set(projects)
return invoice
@classmethod
def update_status(cls, invoice_id: str, status: str) -> Optional[Invoice]:
"""
Update invoice status.
Args:
invoice_id: The invoice ID
status: The new status
Returns:
The updated invoice or None if not found
"""
invoice = cls.get_by_id(invoice_id)
if not invoice:
return None
invoice.status = status
# Set sent_at if status is 'sent'
if status == 'sent' and not invoice.sent_at:
invoice.sent_at = timezone.now()
# Set date_paid if status is 'paid'
if status == 'paid' and not invoice.date_paid:
invoice.date_paid = timezone.now().date()
invoice.save()
return invoice
@classmethod
def mark_as_paid(cls, invoice_id: str, payment_type: str) -> Optional[Invoice]:
"""
Mark an invoice as paid.
Args:
invoice_id: The invoice ID
payment_type: The payment type
Returns:
The updated invoice or None if not found
"""
invoice = cls.get_by_id(invoice_id)
if not invoice:
return None
invoice.status = 'paid'
invoice.date_paid = timezone.now().date()
invoice.payment_type = payment_type
invoice.save()
return invoice
@classmethod
def get_total_paid(cls, customer_id: str = None, date_from: date = None, date_to: date = None) -> float:
"""
Get total paid invoice amount.
Args:
customer_id: Filter by customer ID
date_from: Filter by start date (inclusive)
date_to: Filter by end date (inclusive)
Returns:
Total paid amount
"""
queryset = Invoice.objects.filter(status='paid')
if customer_id:
queryset = queryset.filter(customer_id=customer_id)
if date_from:
queryset = queryset.filter(date_paid__gte=date_from)
if date_to:
queryset = queryset.filter(date_paid__lte=date_to)
result = queryset.aggregate(total=Sum('total_amount'))
return float(result['total'] or 0)
@classmethod
def get_total_outstanding(cls, customer_id: str = None) -> float:
"""
Get total outstanding invoice amount.
Args:
customer_id: Filter by customer ID
Returns:
Total outstanding amount
"""
queryset = Invoice.objects.filter(status__in=['sent', 'overdue'])
if customer_id:
queryset = queryset.filter(customer_id=customer_id)
result = queryset.aggregate(total=Sum('total_amount'))
return float(result['total'] or 0)
@classmethod
def mark_overdue(cls, invoice_id: str = None) -> int:
"""
Mark invoice(s) as overdue.
Args:
invoice_id: Optional invoice ID. If not provided, all overdue invoices will be marked.
Returns:
Number of invoices marked as overdue
"""
thirty_days_ago = timezone.now().date() - timedelta(days=30)
query = Invoice.objects.filter(
status='sent',
sent_at__isnull=False,
sent_at__date__lt=thirty_days_ago
)
if invoice_id:
query = query.filter(id=invoice_id)
count = query.update(status='overdue')
return count