""" Billable time model for tracking time entries for billing. Tracks individual billable time entries with references to work items, sessions, and clients, including rates, amounts, and billing details. """ from datetime import datetime from typing import TYPE_CHECKING, Optional from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, Integer, Numeric, String, Text, TIMESTAMP from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base, TimestampMixin, UUIDMixin if TYPE_CHECKING: from .client import Client from .session import Session from .work_item import WorkItem class BillableTime(Base, UUIDMixin, TimestampMixin): """ Billable time model representing individual billable time entries. Tracks time entries for billing purposes with detailed information about the work performed, rates applied, and amounts calculated. Links to work items, sessions, and clients for comprehensive billing tracking. Attributes: work_item_id: Foreign key to work_items table session_id: Foreign key to sessions table client_id: Foreign key to clients table start_time: When the billable time started end_time: When the billable time ended duration_minutes: Duration in minutes (auto-calculated or manual) hourly_rate: Hourly rate applied to this time entry total_amount: Total billable amount (calculated) is_billable: Whether this time entry is actually billable description: Description of the work performed category: Category of work (consulting, development, support, etc.) notes: Additional notes about this time entry invoiced_at: When this time entry was invoiced invoice_id: Reference to invoice if applicable """ __tablename__ = "billable_time" # Foreign keys work_item_id: Mapped[Optional[str]] = mapped_column( CHAR(36), ForeignKey("work_items.id", ondelete="SET NULL"), doc="Foreign key to work_items table" ) session_id: Mapped[Optional[str]] = mapped_column( CHAR(36), ForeignKey("sessions.id", ondelete="CASCADE"), doc="Foreign key to sessions table" ) client_id: Mapped[Optional[str]] = mapped_column( CHAR(36), ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, doc="Foreign key to clients table" ) # Time tracking start_time: Mapped[datetime] = mapped_column( TIMESTAMP, nullable=False, doc="When the billable time started" ) end_time: Mapped[Optional[datetime]] = mapped_column( TIMESTAMP, doc="When the billable time ended" ) duration_minutes: Mapped[int] = mapped_column( Integer, nullable=False, doc="Duration in minutes (auto-calculated or manual)" ) # Billing information hourly_rate: Mapped[float] = mapped_column( Numeric(10, 2), nullable=False, doc="Hourly rate applied to this time entry" ) total_amount: Mapped[float] = mapped_column( Numeric(10, 2), nullable=False, doc="Total billable amount (calculated: duration * rate)" ) is_billable: Mapped[bool] = mapped_column( Boolean, default=True, server_default="1", nullable=False, doc="Whether this time entry is actually billable" ) # Work details description: Mapped[str] = mapped_column( Text, nullable=False, doc="Description of the work performed" ) category: Mapped[str] = mapped_column( String(50), nullable=False, doc="Category: consulting, development, support, maintenance, troubleshooting, project_work, training, documentation" ) notes: Mapped[Optional[str]] = mapped_column( Text, doc="Additional notes about this time entry" ) # Invoice tracking invoiced_at: Mapped[Optional[datetime]] = mapped_column( TIMESTAMP, doc="When this time entry was invoiced" ) invoice_id: Mapped[Optional[str]] = mapped_column( String(100), doc="Reference to invoice if applicable" ) # Relationships work_item: Mapped[Optional["WorkItem"]] = relationship( "WorkItem", doc="Relationship to WorkItem model" ) session: Mapped[Optional["Session"]] = relationship( "Session", doc="Relationship to Session model" ) client: Mapped["Client"] = relationship( "Client", doc="Relationship to Client model" ) # Constraints and indexes __table_args__ = ( CheckConstraint( "category IN ('consulting', 'development', 'support', 'maintenance', 'troubleshooting', 'project_work', 'training', 'documentation')", name="ck_billable_time_category" ), CheckConstraint( "duration_minutes > 0", name="ck_billable_time_duration_positive" ), CheckConstraint( "hourly_rate >= 0", name="ck_billable_time_rate_non_negative" ), CheckConstraint( "total_amount >= 0", name="ck_billable_time_amount_non_negative" ), CheckConstraint( "end_time IS NULL OR end_time >= start_time", name="ck_billable_time_end_after_start" ), Index("idx_billable_time_work_item", "work_item_id"), Index("idx_billable_time_session", "session_id"), Index("idx_billable_time_client", "client_id"), Index("idx_billable_time_start", "start_time"), Index("idx_billable_time_billable", "is_billable"), Index("idx_billable_time_category", "category"), Index("idx_billable_time_invoiced", "invoiced_at"), ) def __repr__(self) -> str: """String representation of the billable time entry.""" return f""