from django.core.exceptions import ValidationError from django.conf import settings from django.db import models from django.db.models import Q from apps.logs.services import build_workspace_log_metadata from apps.logs.services.constants import SECTION_TIME_ENTRIES from core.models.base import BaseModel from apps.workspaces.models import Workspace from apps.projects.models import Project from apps.tags.models import Tag User = settings.AUTH_USER_MODEL class TimeEntry(BaseModel): workspace = models.ForeignKey( Workspace, on_delete=models.CASCADE, related_name="time_entries", ) user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="time_entries", ) project = models.ForeignKey( Project, on_delete=models.SET_NULL, null=True, blank=True, related_name="time_entries", ) description = models.TextField(blank=True) start_time = models.DateTimeField() end_time = models.DateTimeField(null=True, blank=True) duration = models.DurationField(null=True, blank=True) tags = models.ManyToManyField( Tag, blank=True, related_name="time_entries", ) is_billable = models.BooleanField(default=False) hourly_rate = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True, ) currency = models.CharField( max_length=3, default="USD", ) class Meta: db_table = "time_entry" ordering = ("-updated_at", "-created_at") indexes = [ models.Index(fields=["workspace"], name="time_entry_workspace_idx"), models.Index(fields=["user"], name="time_entry_user_idx"), models.Index(fields=["project"], name="time_entry_project_idx"), models.Index(fields=["start_time"], name="time_entry_start_idx"), models.Index(fields=["workspace", "start_time"], name="time_entry_workspace_start_idx"), ] constraints = [ models.UniqueConstraint( fields=["workspace", "user"], condition=Q(end_time__isnull=True, is_deleted=False), name="unique_running_timer_per_user", ) ] def __str__(self): return f"{self.user} - {self.start_time}" def get_additional_data(self): target_label = self.description.strip() if self.description else f"Time entry {self.start_time.isoformat()}" return build_workspace_log_metadata( section=SECTION_TIME_ENTRIES, workspace_id=self.workspace_id, target_id=self.id, target_label=target_label, extra={"entry_user_id": str(self.user_id)}, ) def clean(self): if self.project and self.project.workspace_id != self.workspace_id: raise ValidationError("Project must belong to the same workspace.") for tag in self.tags.all(): if tag.workspace_id != self.workspace_id: raise ValidationError("Tags must belong to the same workspace.")