97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
from django.db import models
|
|
from core.models.profile import TeamProfile
|
|
from core.models.account import Account, AccountAddress
|
|
from core.models.project import Project
|
|
from core.models.base import BaseModel
|
|
|
|
|
|
class ProjectScope(BaseModel):
|
|
"""Scope of work definition for a project"""
|
|
name = models.CharField(max_length=255)
|
|
account = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.PROTECT,
|
|
related_name='project_scopes',
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
project = models.ForeignKey(Project, on_delete=models.PROTECT, related_name='project_scopes')
|
|
account_address = models.ForeignKey(
|
|
AccountAddress,
|
|
on_delete=models.PROTECT,
|
|
related_name='project_scopes',
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
description = models.TextField(blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ['name']
|
|
verbose_name = "Project Scope"
|
|
verbose_name_plural = "Project Scopes"
|
|
indexes = [
|
|
models.Index(fields=['project', 'is_active']),
|
|
models.Index(fields=['account_address', 'is_active']),
|
|
]
|
|
constraints = [
|
|
# Ensure only one active scope per project/address combination (when address present)
|
|
models.UniqueConstraint(
|
|
fields=['project', 'account_address'],
|
|
condition=models.Q(is_active=True, account_address__isnull=False),
|
|
name='unique_active_project_scope_per_address',
|
|
),
|
|
]
|
|
|
|
def __str__(self):
|
|
project_label = str(self.project_id) if self.project_id else "Unassigned Project"
|
|
return f"{self.name} - {project_label}"
|
|
|
|
|
|
class ProjectScopeCategory(BaseModel):
|
|
"""Category of work definition for a project"""
|
|
name = models.CharField(max_length=255)
|
|
scope = models.ForeignKey(ProjectScope, on_delete=models.CASCADE, related_name='project_areas')
|
|
order = models.PositiveIntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['scope', 'order', 'name']
|
|
verbose_name = "Project Scope Category"
|
|
verbose_name_plural = "Project Scope Categories"
|
|
indexes = [
|
|
models.Index(fields=['scope', 'order']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.scope.name} - {self.name}"
|
|
|
|
|
|
class ProjectScopeTask(BaseModel):
|
|
"""Specific task definition for a project"""
|
|
category = models.ForeignKey(ProjectScopeCategory, on_delete=models.CASCADE, related_name='project_tasks')
|
|
description = models.TextField()
|
|
checklist_description = models.TextField()
|
|
order = models.PositiveIntegerField(default=0)
|
|
estimated_minutes = models.PositiveIntegerField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['category', 'order']
|
|
verbose_name = "Project Scope Task"
|
|
verbose_name_plural = "Project Scope Tasks"
|
|
indexes = [
|
|
models.Index(fields=['category', 'order']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.category.name}: {self.description[:50]}..."
|
|
|
|
|
|
class ProjectScopeTaskCompletion(BaseModel):
|
|
"""Record of a task template being completed during a project"""
|
|
task = models.ForeignKey(ProjectScopeTask, on_delete=models.PROTECT, related_name='completions')
|
|
project = models.ForeignKey(Project, on_delete=models.PROTECT, related_name='task_completions')
|
|
account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name='task_completions', null=True, blank=True)
|
|
account_address = models.ForeignKey(AccountAddress, on_delete=models.PROTECT, null=True, blank=True, related_name='project_task_completions')
|
|
completed_by = models.ForeignKey(TeamProfile, on_delete=models.PROTECT)
|
|
completed_at = models.DateTimeField()
|
|
notes = models.TextField(blank=True) |