kamau1's picture
fix: correct TaskStatus enum usage, update Postgres ENUM definition, and add validator for empty task_scheduled_date
0a3426b
"""
TASK Models - For any project type
"""
from sqlalchemy import Column, String, Boolean, Integer, Text, Date, DateTime, Numeric, ForeignKey, Double, CheckConstraint
from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY, ENUM
from sqlalchemy.orm import relationship
from datetime import datetime, date
from typing import Optional
from app.models.base import BaseModel
from app.models.enums import TaskStatus, TicketPriority
class Task(BaseModel):
"""
Tasks (Project Work Items)
Tasks represent discrete work items for ANY project type that require:
- Field agent assignment
- Location-based execution
- Expense tracking and reimbursement
- Status tracking and completion verification
Common Use Cases:
1. **Infrastructure Projects:**
- Install fiber cable from pole A to pole B
- Maintenance of network equipment
- Site surveys for network expansion
- Equipment testing and quality checks
2. **Customer Service Projects (FTTH, Fixed Wireless, etc.):**
- Deliver ONT devices to warehouse
- Pick up faulty equipment from customer sites
- Conduct pre-installation site surveys
- Customer training and orientation visits
- Equipment distribution to field agents
3. **General Operations:**
- Any work requiring compensation tracking
- Logistics and transportation tasks
- Multi-location work assignments
Key Features:
- Flexible task_type field (no enum constraint, stored as TEXT)
- Optional location with GPS coordinates
- Links to project regions for team organization
- Status tracking: pending β†’ assigned β†’ in_progress β†’ completed
- Priority levels for scheduling
- Timeline tracking (scheduled, started, completed)
Workflow:
1. Manager creates Task for work that needs to be done
2. Task is converted to Ticket (source='task') for field assignment
3. Ticket assigned to field agent(s)
4. Agent executes work and logs expenses via TicketExpense
5. Manager reviews and approves expenses
6. Agent receives reimbursement
Business Rules:
- Task must belong to a project (any type)
- Optional region assignment for geographic organization
- Timeline validation (completed_at >= started_at)
- Can generate multiple tickets (if work needs to be re-done)
Expense Tracking:
- Tasks β†’ Tickets β†’ TicketAssignments β†’ TicketExpenses
- All expenses linked to ticket assignments for accountability
- Expenses require approval before payment
- Supports transport, materials, accommodation, meals, etc.
"""
__tablename__ = "tasks"
# Project Link (any project type)
project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
# Task Details
task_title = Column(Text, nullable=False)
task_description = Column(Text, nullable=True)
task_type = Column(Text, nullable=True) # 'installation', 'maintenance', 'survey', 'testing'
# Location (where work needs to be done)
location_name = Column(Text, nullable=True) # e.g., "Pole 45, Ngong Road"
project_region_id = Column(UUID(as_uuid=True), ForeignKey("project_regions.id", ondelete="SET NULL"), nullable=True, index=True)
task_address_line1 = Column(Text, nullable=True)
task_address_line2 = Column(Text, nullable=True)
task_maps_link = Column(Text, nullable=True) # Google Maps link for navigation
task_latitude = Column(Double, nullable=True)
task_longitude = Column(Double, nullable=True)
# Status (Assignment handled via tickets)
status = Column(ENUM(TaskStatus, name="taskstatus", create_type=False, values_callable=lambda x: [e.value for e in x]), nullable=False, default=TaskStatus.PENDING.value)
priority = Column(ENUM(TicketPriority, name="ticket_priority", create_type=False, values_callable=lambda x: [e.value for e in x]), nullable=False, default=TicketPriority.NORMAL.value)
# Timeline
scheduled_date = Column(Date, nullable=True)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
# Metadata
notes = Column(Text, nullable=True)
additional_metadata = Column(JSONB, default={}, nullable=False)
# Created by
created_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
# Relationships
project = relationship("Project", back_populates="tasks")
project_region = relationship("ProjectRegion", foreign_keys=[project_region_id])
created_by = relationship("User", foreign_keys=[created_by_user_id])
# tickets = relationship("Ticket", back_populates="task", lazy="dynamic") # Tickets generated from this task
# Constraints
__table_args__ = (
CheckConstraint(
'completed_at IS NULL OR started_at IS NULL OR completed_at >= started_at',
name='chk_task_dates'
),
)
def __repr__(self):
return f"<Task {self.task_title} ({self.status})>"
@property
def is_completed(self) -> bool:
"""Check if task is completed"""
return self.status == TaskStatus.COMPLETED
@property
def is_overdue(self) -> bool:
"""Check if task is overdue (past scheduled date and not completed)"""
if not self.scheduled_date or self.is_completed:
return False
return date.today() > self.scheduled_date
@property
def has_location(self) -> bool:
"""Check if task has location coordinates"""
return self.task_latitude is not None and self.task_longitude is not None
@property
def duration_days(self) -> Optional[int]:
"""Calculate duration in days if both start and completion dates exist"""
if self.started_at and self.completed_at:
delta = self.completed_at - self.started_at
return delta.days
return None
def can_start(self) -> bool:
"""Check if task can be started"""
return self.status in [TaskStatus.PENDING, TaskStatus.ASSIGNED]
def can_complete(self) -> bool:
"""Check if task can be completed"""
return self.status in [TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS]
def can_cancel(self) -> bool:
"""Check if task can be cancelled"""
return self.status not in [TaskStatus.COMPLETED, TaskStatus.CANCELLED]