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

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 

7 

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 ] 

14 

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) 

20 

21 class Meta: 

22 ordering = ['-check_in'] 

23 

24 def __str__(self): 

25 return f"{self.user.email} - {self.status}" 

26 

27 

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 ) 

47 

48 class Meta: 

49 unique_together = ('tenant_id', 'code') 

50 ordering = ['name'] 

51 

52 def __str__(self): 

53 return f"{self.name} ({self.code})" 

54 

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 ] 

70 

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) 

83 

84 class Meta: 

85 unique_together = ('tenant_id', 'user') 

86 ordering = ['-joined_at'] 

87 

88 def __str__(self): 

89 return f"{self.user.full_name} - {self.role} ({self.job_title or 'No Title'})" 

90 

91class Invitation(TenantScopedModel): 

92 """Pending user invitations to join an organization.""" 

93 STATUS_CHOICES = [ 

94 ('PENDING', 'Pending'), 

95 ('ACCEPTED', 'Accepted'), 

96 ('EXPIRED', 'Expired'), 

97 ] 

98 

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() 

111 

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) 

116 

117 def __str__(self): 

118 return f"Invite for {self.email} ({self.role})"