progress
This commit is contained in:
@@ -17,7 +17,17 @@ from .pensions import (
|
||||
SeparationAgreement, LifeTable, NumberTable, PensionResult
|
||||
)
|
||||
from .templates import DocumentTemplate, DocumentTemplateVersion, TemplateKeyword
|
||||
from .billing import BillingBatch, BillingBatchFile
|
||||
from .billing import (
|
||||
BillingBatch, BillingBatchFile, StatementTemplate, BillingStatement,
|
||||
BillingStatementItem, StatementPayment, StatementStatus
|
||||
)
|
||||
from .timers import (
|
||||
Timer, TimeEntry, TimerSession, TimerTemplate, TimerStatus, TimerType
|
||||
)
|
||||
from .file_management import (
|
||||
FileStatusHistory, FileTransferHistory, FileArchiveInfo,
|
||||
FileClosureChecklist, FileAlert
|
||||
)
|
||||
from .lookups import (
|
||||
Employee, FileType, FileStatus, TransactionType, TransactionCode,
|
||||
State, GroupLookup, Footer, PlanInfo, FormIndex, FormList,
|
||||
@@ -34,5 +44,9 @@ __all__ = [
|
||||
"Employee", "FileType", "FileStatus", "TransactionType", "TransactionCode",
|
||||
"State", "GroupLookup", "Footer", "PlanInfo", "FormIndex", "FormList",
|
||||
"PrinterSetup", "SystemSetup", "FormKeyword", "TemplateKeyword",
|
||||
"BillingBatch", "BillingBatchFile"
|
||||
"BillingBatch", "BillingBatchFile", "StatementTemplate", "BillingStatement",
|
||||
"BillingStatementItem", "StatementPayment", "StatementStatus",
|
||||
"Timer", "TimeEntry", "TimerSession", "TimerTemplate", "TimerStatus", "TimerType",
|
||||
"FileStatusHistory", "FileTransferHistory", "FileArchiveInfo",
|
||||
"FileClosureChecklist", "FileAlert"
|
||||
]
|
||||
@@ -1,5 +1,8 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Float, Text, Index
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Date, Float, Boolean, Text, Index, ForeignKey, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.models.base import BaseModel
|
||||
import enum
|
||||
|
||||
|
||||
class BillingBatch(BaseModel):
|
||||
@@ -45,3 +48,174 @@ class BillingBatchFile(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class StatementStatus(str, enum.Enum):
|
||||
"""Statement status enumeration"""
|
||||
DRAFT = "draft"
|
||||
PENDING_APPROVAL = "pending_approval"
|
||||
APPROVED = "approved"
|
||||
SENT = "sent"
|
||||
PAID = "paid"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class StatementTemplate(BaseModel):
|
||||
"""
|
||||
Templates for billing statement generation
|
||||
Allows customization of statement format, footer text, etc.
|
||||
"""
|
||||
__tablename__ = "statement_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(100), nullable=False, unique=True)
|
||||
description = Column(Text)
|
||||
|
||||
# Template content
|
||||
header_template = Column(Text) # HTML/Jinja2 template for header
|
||||
footer_template = Column(Text) # HTML/Jinja2 template for footer
|
||||
css_styles = Column(Text) # Custom CSS for statement styling
|
||||
|
||||
# Template settings
|
||||
is_default = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
created_by = Column(String(50))
|
||||
|
||||
# Relationships
|
||||
statements = relationship("BillingStatement", back_populates="template")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StatementTemplate(name='{self.name}', default={self.is_default})>"
|
||||
|
||||
|
||||
class BillingStatement(BaseModel):
|
||||
"""
|
||||
Generated billing statements for files/clients
|
||||
Tracks statement metadata, status, and generation details
|
||||
"""
|
||||
__tablename__ = "billing_statements"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
statement_number = Column(String(50), unique=True, nullable=False) # Unique statement identifier
|
||||
|
||||
# File/Client reference
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False)
|
||||
customer_id = Column(String(45), ForeignKey("rolodex.id")) # Optional direct customer reference
|
||||
|
||||
# Statement period
|
||||
period_start = Column(Date, nullable=False)
|
||||
period_end = Column(Date, nullable=False)
|
||||
statement_date = Column(Date, nullable=False, default=func.current_date())
|
||||
due_date = Column(Date)
|
||||
|
||||
# Financial totals
|
||||
previous_balance = Column(Float, default=0.0)
|
||||
current_charges = Column(Float, default=0.0)
|
||||
payments_credits = Column(Float, default=0.0)
|
||||
total_due = Column(Float, nullable=False)
|
||||
|
||||
# Trust account information
|
||||
trust_balance = Column(Float, default=0.0)
|
||||
trust_applied = Column(Float, default=0.0)
|
||||
|
||||
# Statement details
|
||||
status = Column(Enum(StatementStatus), default=StatementStatus.DRAFT)
|
||||
template_id = Column(Integer, ForeignKey("statement_templates.id"))
|
||||
|
||||
# Generated content
|
||||
html_content = Column(Text) # Generated HTML content
|
||||
pdf_path = Column(String(500)) # Path to generated PDF file
|
||||
|
||||
# Billing metadata
|
||||
billed_transaction_count = Column(Integer, default=0)
|
||||
approved_by = Column(String(50)) # User who approved the statement
|
||||
approved_at = Column(DateTime)
|
||||
sent_by = Column(String(50)) # User who sent the statement
|
||||
sent_at = Column(DateTime)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
created_by = Column(String(50))
|
||||
|
||||
# Notes and customization
|
||||
custom_footer = Column(Text) # Override template footer for this statement
|
||||
internal_notes = Column(Text) # Internal notes not shown to client
|
||||
|
||||
# Relationships
|
||||
file = relationship("File", back_populates="billing_statements")
|
||||
customer = relationship("Rolodex", back_populates="billing_statements")
|
||||
template = relationship("StatementTemplate", back_populates="statements")
|
||||
statement_items = relationship("BillingStatementItem", back_populates="statement", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BillingStatement(number='{self.statement_number}', file_no='{self.file_no}', total={self.total_due})>"
|
||||
|
||||
|
||||
class BillingStatementItem(BaseModel):
|
||||
"""
|
||||
Individual line items on a billing statement
|
||||
Links to ledger entries that were included in the statement
|
||||
"""
|
||||
__tablename__ = "billing_statement_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
statement_id = Column(Integer, ForeignKey("billing_statements.id"), nullable=False)
|
||||
ledger_id = Column(Integer, ForeignKey("ledger.id"), nullable=False)
|
||||
|
||||
# Item display details (cached from ledger for statement consistency)
|
||||
date = Column(Date, nullable=False)
|
||||
description = Column(Text)
|
||||
quantity = Column(Float, default=0.0)
|
||||
rate = Column(Float, default=0.0)
|
||||
amount = Column(Float, nullable=False)
|
||||
|
||||
# Item categorization
|
||||
item_category = Column(String(50)) # fees, costs, payments, adjustments
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
|
||||
# Relationships
|
||||
statement = relationship("BillingStatement", back_populates="statement_items")
|
||||
ledger_entry = relationship("Ledger")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BillingStatementItem(statement_id={self.statement_id}, amount={self.amount})>"
|
||||
|
||||
|
||||
class StatementPayment(BaseModel):
|
||||
"""
|
||||
Payments applied to billing statements
|
||||
Tracks payment history and application
|
||||
"""
|
||||
__tablename__ = "statement_payments"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
statement_id = Column(Integer, ForeignKey("billing_statements.id"), nullable=False)
|
||||
|
||||
# Payment details
|
||||
payment_date = Column(Date, nullable=False)
|
||||
payment_amount = Column(Float, nullable=False)
|
||||
payment_method = Column(String(50)) # check, credit_card, trust_transfer, etc.
|
||||
reference_number = Column(String(100)) # check number, transaction ID, etc.
|
||||
|
||||
# Application details
|
||||
applied_to_fees = Column(Float, default=0.0)
|
||||
applied_to_costs = Column(Float, default=0.0)
|
||||
applied_to_trust = Column(Float, default=0.0)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
created_by = Column(String(50))
|
||||
notes = Column(Text)
|
||||
|
||||
# Relationships
|
||||
statement = relationship("BillingStatement")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StatementPayment(statement_id={self.statement_id}, amount={self.payment_amount})>"
|
||||
|
||||
|
||||
|
||||
191
app/models/file_management.py
Normal file
191
app/models/file_management.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
File management models for enhanced file operations
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Date, Float, Text, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class FileStatusHistory(BaseModel):
|
||||
"""
|
||||
Track file status changes over time
|
||||
Provides audit trail for file status transitions
|
||||
"""
|
||||
__tablename__ = "file_status_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
|
||||
# Status change details
|
||||
old_status = Column(String(45), nullable=False)
|
||||
new_status = Column(String(45), nullable=False)
|
||||
change_date = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||
|
||||
# Who made the change
|
||||
changed_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
changed_by_name = Column(String(100)) # Cached name for reporting
|
||||
|
||||
# Additional context
|
||||
notes = Column(Text) # Reason for status change
|
||||
system_generated = Column(Boolean, default=False) # True if automated change
|
||||
|
||||
# Relationships
|
||||
file = relationship("File")
|
||||
changed_by = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<FileStatusHistory(file_no='{self.file_no}', {self.old_status} -> {self.new_status})>"
|
||||
|
||||
|
||||
class FileTransferHistory(BaseModel):
|
||||
"""
|
||||
Track file transfers between attorneys
|
||||
Maintains chain of custody for files
|
||||
"""
|
||||
__tablename__ = "file_transfer_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
|
||||
# Transfer details
|
||||
old_attorney_id = Column(String(10), ForeignKey("employees.empl_num"), nullable=False)
|
||||
new_attorney_id = Column(String(10), ForeignKey("employees.empl_num"), nullable=False)
|
||||
transfer_date = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||
|
||||
# Who authorized the transfer
|
||||
authorized_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
authorized_by_name = Column(String(100))
|
||||
|
||||
# Transfer context
|
||||
reason = Column(Text) # Reason for transfer
|
||||
effective_date = Column(Date) # When transfer becomes effective (may be future)
|
||||
|
||||
# Rate changes
|
||||
old_hourly_rate = Column(Float, nullable=True)
|
||||
new_hourly_rate = Column(Float, nullable=True)
|
||||
|
||||
# Relationships
|
||||
file = relationship("File")
|
||||
old_attorney = relationship("Employee", foreign_keys=[old_attorney_id])
|
||||
new_attorney = relationship("Employee", foreign_keys=[new_attorney_id])
|
||||
authorized_by = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<FileTransferHistory(file_no='{self.file_no}', {self.old_attorney_id} -> {self.new_attorney_id})>"
|
||||
|
||||
|
||||
class FileArchiveInfo(BaseModel):
|
||||
"""
|
||||
Archive information for files
|
||||
Tracks where files are stored and retrieval information
|
||||
"""
|
||||
__tablename__ = "file_archive_info"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, unique=True, index=True)
|
||||
|
||||
# Archive details
|
||||
archive_date = Column(Date, nullable=False, default=func.current_date())
|
||||
archive_location = Column(String(200)) # Physical or digital location
|
||||
box_number = Column(String(50)) # Physical box identifier
|
||||
shelf_location = Column(String(100)) # Physical shelf/room location
|
||||
|
||||
# Digital archive info
|
||||
digital_path = Column(String(500)) # Path to digital archive
|
||||
backup_location = Column(String(500)) # Backup storage location
|
||||
|
||||
# Archive metadata
|
||||
archived_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
archived_by_name = Column(String(100))
|
||||
retrieval_instructions = Column(Text) # How to retrieve the file
|
||||
|
||||
# Retention information
|
||||
retention_date = Column(Date) # When file can be destroyed
|
||||
destruction_date = Column(Date) # When file was actually destroyed
|
||||
destruction_authorized_by = Column(String(100))
|
||||
|
||||
# Archive status
|
||||
is_retrievable = Column(Boolean, default=True)
|
||||
last_verified = Column(Date) # Last time archive was verified to exist
|
||||
|
||||
# Relationships
|
||||
file = relationship("File")
|
||||
archived_by = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<FileArchiveInfo(file_no='{self.file_no}', location='{self.archive_location}')>"
|
||||
|
||||
|
||||
class FileClosureChecklist(BaseModel):
|
||||
"""
|
||||
Checklist items for file closure process
|
||||
Ensures all necessary steps are completed before closing
|
||||
"""
|
||||
__tablename__ = "file_closure_checklist"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
|
||||
# Checklist item details
|
||||
item_name = Column(String(200), nullable=False)
|
||||
item_description = Column(Text)
|
||||
is_required = Column(Boolean, default=True) # Must be completed to close file
|
||||
|
||||
# Completion tracking
|
||||
is_completed = Column(Boolean, default=False)
|
||||
completed_date = Column(DateTime(timezone=True))
|
||||
completed_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
completed_by_name = Column(String(100))
|
||||
|
||||
# Additional info
|
||||
notes = Column(Text) # Notes about completion
|
||||
sort_order = Column(Integer, default=0) # Display order
|
||||
|
||||
# Relationships
|
||||
file = relationship("File")
|
||||
completed_by = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
status = "✓" if self.is_completed else "○"
|
||||
return f"<FileClosureChecklist({status} {self.item_name} - {self.file_no})>"
|
||||
|
||||
|
||||
|
||||
class FileAlert(BaseModel):
|
||||
"""
|
||||
Alerts and reminders for files
|
||||
Automated notifications for important dates and events
|
||||
"""
|
||||
__tablename__ = "file_alerts"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
|
||||
# Alert details
|
||||
alert_type = Column(String(50), nullable=False) # deadline, follow_up, billing, etc.
|
||||
title = Column(String(200), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Timing
|
||||
alert_date = Column(Date, nullable=False) # When alert should trigger
|
||||
created_date = Column(Date, default=func.current_date())
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_acknowledged = Column(Boolean, default=False)
|
||||
acknowledged_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
acknowledged_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Notification settings
|
||||
notify_attorney = Column(Boolean, default=True)
|
||||
notify_admin = Column(Boolean, default=False)
|
||||
notification_days_advance = Column(Integer, default=7) # Days before alert_date
|
||||
|
||||
# Relationships
|
||||
file = relationship("File")
|
||||
acknowledged_by = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
status = "🔔" if self.is_active and not self.is_acknowledged else "✓"
|
||||
return f"<FileAlert({status} {self.alert_type} - {self.file_no} on {self.alert_date})>"
|
||||
@@ -65,4 +65,7 @@ class File(BaseModel):
|
||||
separation_agreements = relationship("SeparationAgreement", back_populates="file", cascade="all, delete-orphan")
|
||||
payments = relationship("Payment", back_populates="file", cascade="all, delete-orphan")
|
||||
notes = relationship("FileNote", back_populates="file", cascade="all, delete-orphan")
|
||||
documents = relationship("Document", back_populates="file", cascade="all, delete-orphan")
|
||||
documents = relationship("Document", back_populates="file", cascade="all, delete-orphan")
|
||||
billing_statements = relationship("BillingStatement", back_populates="file", cascade="all, delete-orphan")
|
||||
timers = relationship("Timer", back_populates="file", cascade="all, delete-orphan")
|
||||
time_entries = relationship("TimeEntry", back_populates="file", cascade="all, delete-orphan")
|
||||
@@ -45,6 +45,9 @@ class Rolodex(BaseModel):
|
||||
phone_numbers = relationship("Phone", back_populates="rolodex_entry", cascade="all, delete-orphan")
|
||||
files = relationship("File", back_populates="owner")
|
||||
payments = relationship("Payment", back_populates="client")
|
||||
billing_statements = relationship("BillingStatement", back_populates="customer")
|
||||
timers = relationship("Timer", back_populates="customer")
|
||||
time_entries = relationship("TimeEntry", back_populates="customer")
|
||||
|
||||
|
||||
class Phone(BaseModel):
|
||||
|
||||
227
app/models/timers.py
Normal file
227
app/models/timers.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Timer and time tracking models for integrated time tracking system
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.models.base import BaseModel
|
||||
import enum
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
class TimerStatus(str, enum.Enum):
|
||||
"""Timer status enumeration"""
|
||||
STOPPED = "stopped"
|
||||
RUNNING = "running"
|
||||
PAUSED = "paused"
|
||||
|
||||
|
||||
class TimerType(str, enum.Enum):
|
||||
"""Timer type enumeration"""
|
||||
BILLABLE = "billable"
|
||||
NON_BILLABLE = "non_billable"
|
||||
ADMINISTRATIVE = "administrative"
|
||||
|
||||
|
||||
class Timer(BaseModel):
|
||||
"""
|
||||
Active timer sessions for time tracking
|
||||
Represents a running, paused, or stopped timer
|
||||
"""
|
||||
__tablename__ = "timers"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# User and assignment
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no")) # Optional file assignment
|
||||
customer_id = Column(String(45), ForeignKey("rolodex.id")) # Optional customer assignment
|
||||
|
||||
# Timer details
|
||||
title = Column(String(200), nullable=False) # Brief description of task
|
||||
description = Column(Text) # Detailed description of work
|
||||
timer_type = Column(Enum(TimerType), default=TimerType.BILLABLE)
|
||||
|
||||
# Time tracking
|
||||
status = Column(Enum(TimerStatus), default=TimerStatus.STOPPED)
|
||||
total_seconds = Column(Integer, default=0) # Total accumulated time in seconds
|
||||
|
||||
# Session timing
|
||||
started_at = Column(DateTime(timezone=True)) # When timer was first started
|
||||
last_started_at = Column(DateTime(timezone=True)) # When current session started
|
||||
last_paused_at = Column(DateTime(timezone=True)) # When timer was last paused
|
||||
stopped_at = Column(DateTime(timezone=True)) # When timer was stopped
|
||||
|
||||
# Billing information
|
||||
hourly_rate = Column(Float) # Override rate for this timer
|
||||
is_billable = Column(Boolean, default=True)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
|
||||
# Task categorization
|
||||
task_category = Column(String(50)) # research, drafting, client_call, court_appearance, etc.
|
||||
notes = Column(Text) # Additional notes
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="timers")
|
||||
file = relationship("File", back_populates="timers")
|
||||
customer = relationship("Rolodex", back_populates="timers")
|
||||
time_entries = relationship("TimeEntry", back_populates="timer", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Timer(id={self.id}, title='{self.title}', status='{self.status}', total_seconds={self.total_seconds})>"
|
||||
|
||||
@property
|
||||
def total_hours(self) -> float:
|
||||
"""Get total time in hours"""
|
||||
return self.total_seconds / 3600.0 if self.total_seconds else 0.0
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""Check if timer is currently running or paused"""
|
||||
return self.status in [TimerStatus.RUNNING, TimerStatus.PAUSED]
|
||||
|
||||
def get_current_session_seconds(self) -> int:
|
||||
"""Get seconds for current running session"""
|
||||
if self.status == TimerStatus.RUNNING and self.last_started_at:
|
||||
now = datetime.now(timezone.utc)
|
||||
session_duration = (now - self.last_started_at).total_seconds()
|
||||
return int(session_duration)
|
||||
return 0
|
||||
|
||||
def get_total_current_seconds(self) -> int:
|
||||
"""Get total seconds including current running session"""
|
||||
return self.total_seconds + self.get_current_session_seconds()
|
||||
|
||||
|
||||
class TimeEntry(BaseModel):
|
||||
"""
|
||||
Completed time entries that can be converted to billing transactions
|
||||
Represents finalized time that has been logged
|
||||
"""
|
||||
__tablename__ = "time_entries"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# Source timer (optional - entries can be created manually)
|
||||
timer_id = Column(Integer, ForeignKey("timers.id"))
|
||||
|
||||
# User and assignment
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no")) # Optional file assignment
|
||||
customer_id = Column(String(45), ForeignKey("rolodex.id")) # Optional customer assignment
|
||||
|
||||
# Time entry details
|
||||
title = Column(String(200), nullable=False)
|
||||
description = Column(Text)
|
||||
entry_type = Column(Enum(TimerType), default=TimerType.BILLABLE)
|
||||
|
||||
# Time information
|
||||
hours = Column(Float, nullable=False) # Time in decimal hours (e.g., 1.5 = 1 hour 30 minutes)
|
||||
entry_date = Column(DateTime(timezone=True), nullable=False) # Date/time when work was performed
|
||||
|
||||
# Billing information
|
||||
hourly_rate = Column(Float) # Rate for this entry
|
||||
is_billable = Column(Boolean, default=True)
|
||||
billed = Column(Boolean, default=False) # Whether this has been converted to a ledger entry
|
||||
|
||||
# Related ledger entry (after billing)
|
||||
ledger_id = Column(Integer, ForeignKey("ledger.id")) # Link to created billing transaction
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
created_by = Column(String(50)) # Who created this entry
|
||||
|
||||
# Task categorization
|
||||
task_category = Column(String(50)) # research, drafting, client_call, court_appearance, etc.
|
||||
notes = Column(Text) # Additional notes
|
||||
|
||||
# Approval workflow
|
||||
approved = Column(Boolean, default=False)
|
||||
approved_by = Column(String(50))
|
||||
approved_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Relationships
|
||||
timer = relationship("Timer", back_populates="time_entries")
|
||||
user = relationship("User", back_populates="time_entries")
|
||||
file = relationship("File", back_populates="time_entries")
|
||||
customer = relationship("Rolodex", back_populates="time_entries")
|
||||
ledger_entry = relationship("Ledger")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TimeEntry(id={self.id}, title='{self.title}', hours={self.hours}, billed={self.billed})>"
|
||||
|
||||
@property
|
||||
def calculated_amount(self) -> float:
|
||||
"""Calculate billable amount based on hours and rate"""
|
||||
if not self.is_billable or not self.hourly_rate:
|
||||
return 0.0
|
||||
return self.hours * self.hourly_rate
|
||||
|
||||
|
||||
class TimerSession(BaseModel):
|
||||
"""
|
||||
Individual timer sessions for detailed tracking
|
||||
Each start/stop cycle creates a session record
|
||||
"""
|
||||
__tablename__ = "timer_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
timer_id = Column(Integer, ForeignKey("timers.id"), nullable=False)
|
||||
|
||||
# Session timing
|
||||
started_at = Column(DateTime(timezone=True), nullable=False)
|
||||
ended_at = Column(DateTime(timezone=True))
|
||||
duration_seconds = Column(Integer, default=0)
|
||||
|
||||
# Session notes
|
||||
notes = Column(Text) # Notes for this specific session
|
||||
|
||||
# Pause tracking
|
||||
pause_count = Column(Integer, default=0) # Number of times paused during session
|
||||
total_pause_seconds = Column(Integer, default=0) # Total time spent paused
|
||||
|
||||
# Relationships
|
||||
timer = relationship("Timer")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TimerSession(id={self.id}, timer_id={self.timer_id}, duration_seconds={self.duration_seconds})>"
|
||||
|
||||
@property
|
||||
def duration_hours(self) -> float:
|
||||
"""Get session duration in hours"""
|
||||
return self.duration_seconds / 3600.0 if self.duration_seconds else 0.0
|
||||
|
||||
|
||||
class TimerTemplate(BaseModel):
|
||||
"""
|
||||
Predefined timer templates for common tasks
|
||||
Allows quick creation of timers with pre-filled information
|
||||
"""
|
||||
__tablename__ = "timer_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# Template details
|
||||
name = Column(String(100), nullable=False)
|
||||
title_template = Column(String(200), nullable=False) # Template for timer title
|
||||
description_template = Column(Text) # Template for timer description
|
||||
|
||||
# Default settings
|
||||
timer_type = Column(Enum(TimerType), default=TimerType.BILLABLE)
|
||||
task_category = Column(String(50))
|
||||
default_rate = Column(Float)
|
||||
is_billable = Column(Boolean, default=True)
|
||||
|
||||
# Template metadata
|
||||
created_by = Column(String(50))
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
is_active = Column(Boolean, default=True)
|
||||
usage_count = Column(Integer, default=0) # Track how often template is used
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TimerTemplate(id={self.id}, name='{self.name}', usage_count={self.usage_count})>"
|
||||
@@ -37,6 +37,8 @@ class User(BaseModel):
|
||||
# Relationships
|
||||
audit_logs = relationship("AuditLog", back_populates="user")
|
||||
submitted_tickets = relationship("SupportTicket", foreign_keys="SupportTicket.user_id", back_populates="submitter")
|
||||
timers = relationship("Timer", back_populates="user", cascade="all, delete-orphan")
|
||||
time_entries = relationship("TimeEntry", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(username='{self.username}', email='{self.email}')>"
|
||||
Reference in New Issue
Block a user