Spaces:
Sleeping
Sleeping
| """ | |
| 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})>" | |
| def is_completed(self) -> bool: | |
| """Check if task is completed""" | |
| return self.status == TaskStatus.COMPLETED | |
| 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 | |
| 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 | |
| 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] | |