Coverage for modules\team\models.py: 88%
57 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 09:19 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 09:19 +0800
1import secrets
2from datetime import timedelta
3from django.db import models
4from django.utils import timezone
5from core.models import TenantScopedModel
6from modules.authentication.models import User
8class AttendanceLog(TenantScopedModel):
9 """Employee attendance clock in/out record for a tenant."""
10 STATUS_CHOICES = [
11 ('CLOCKED_IN', 'Clocked In'),
12 ('CLOCKED_OUT', 'Clocked Out'),
13 ]
15 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='attendance_logs')
16 check_in = models.DateTimeField(default=timezone.now)
17 check_out = models.DateTimeField(null=True, blank=True)
18 status = models.CharField(max_length=32, choices=STATUS_CHOICES, default='CLOCKED_IN')
19 notes = models.TextField(blank=True, null=True)
21 class Meta:
22 ordering = ['-check_in']
24 def __str__(self):
25 return f"{self.user.email} - {self.status}"
28class Department(TenantScopedModel):
29 """Department within an organization."""
30 name = models.CharField(max_length=128)
31 code = models.CharField(max_length=32)
32 description = models.TextField(blank=True, null=True)
33 parent = models.ForeignKey(
34 'self',
35 on_delete=models.SET_NULL,
36 null=True,
37 blank=True,
38 related_name='sub_departments'
39 )
40 manager = models.ForeignKey(
41 User,
42 on_delete=models.SET_NULL,
43 null=True,
44 blank=True,
45 related_name='managed_departments'
46 )
48 class Meta:
49 unique_together = ('tenant_id', 'code')
50 ordering = ['name']
52 def __str__(self):
53 return f"{self.name} ({self.code})"
55class TeamMember(TenantScopedModel):
56 """Team member profile and role within a tenant organization."""
57 ROLE_CHOICES = [
58 ('OWNER', 'Owner'),
59 ('ADMIN', 'Administrator'),
60 ('MANAGER', 'Manager'),
61 ('HR', 'Human Resources'),
62 ('MEMBER', 'Member'),
63 ('GUEST', 'Guest'),
64 ]
65 STATUS_CHOICES = [
66 ('ACTIVE', 'Active'),
67 ('INVITED', 'Invited'),
68 ('SUSPENDED', 'Suspended'),
69 ]
71 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='team_profiles')
72 department = models.ForeignKey(
73 Department,
74 on_delete=models.SET_NULL,
75 null=True,
76 blank=True,
77 related_name='members'
78 )
79 job_title = models.CharField(max_length=128, blank=True, null=True)
80 role = models.CharField(max_length=32, choices=ROLE_CHOICES, default='MEMBER')
81 status = models.CharField(max_length=32, choices=STATUS_CHOICES, default='ACTIVE')
82 joined_at = models.DateTimeField(default=timezone.now)
84 class Meta:
85 unique_together = ('tenant_id', 'user')
86 ordering = ['-joined_at']
88 def __str__(self):
89 return f"{self.user.full_name} - {self.role} ({self.job_title or 'No Title'})"
91class Invitation(TenantScopedModel):
92 """Pending user invitations to join an organization."""
93 STATUS_CHOICES = [
94 ('PENDING', 'Pending'),
95 ('ACCEPTED', 'Accepted'),
96 ('EXPIRED', 'Expired'),
97 ]
99 email = models.EmailField(max_length=255)
100 role = models.CharField(max_length=32, default='MEMBER')
101 department = models.ForeignKey(
102 Department,
103 on_delete=models.SET_NULL,
104 null=True,
105 blank=True
106 )
107 invited_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_invitations')
108 token = models.CharField(max_length=64, unique=True, default=secrets.token_urlsafe)
109 status = models.CharField(max_length=32, choices=STATUS_CHOICES, default='PENDING')
110 expires_at = models.DateTimeField()
112 def save(self, *args, **kwargs):
113 if not self.expires_at:
114 self.expires_at = timezone.now() + timedelta(days=7)
115 super().save(*args, **kwargs)
117 def __str__(self):
118 return f"Invite for {self.email} ({self.role})"