Coverage for modules\crm\models.py: 92%

83 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-05 09:19 +0800

1from decimal import Decimal 

2from django.db import models 

3from core.models import TenantScopedModel 

4from modules.authentication.models import User 

5 

6class Company(TenantScopedModel): 

7 """Company / Account in CRM.""" 

8 name = models.CharField(max_length=255, db_index=True) 

9 domain = models.CharField(max_length=255, blank=True, null=True) 

10 industry = models.CharField(max_length=128, blank=True, null=True) 

11 phone = models.CharField(max_length=32, blank=True, null=True) 

12 email = models.EmailField(blank=True, null=True) 

13 address = models.TextField(blank=True, null=True) 

14 city = models.CharField(max_length=128, blank=True, null=True) 

15 country = models.CharField(max_length=64, blank=True, null=True) 

16 annual_revenue = models.DecimalField(max_digits=18, decimal_places=2, default=Decimal('0.00')) 

17 employees_count = models.IntegerField(default=0) 

18 

19 class Meta: 

20 ordering = ['name'] 

21 

22 def __str__(self): 

23 return self.name 

24 

25class Contact(TenantScopedModel): 

26 """Individual contact in CRM.""" 

27 first_name = models.CharField(max_length=128) 

28 last_name = models.CharField(max_length=128, blank=True) 

29 email = models.EmailField(blank=True, null=True, db_index=True) 

30 phone = models.CharField(max_length=32, blank=True, null=True) 

31 job_title = models.CharField(max_length=128, blank=True, null=True) 

32 company = models.ForeignKey( 

33 Company, 

34 on_delete=models.SET_NULL, 

35 null=True, 

36 blank=True, 

37 related_name='contacts' 

38 ) 

39 is_primary = models.BooleanField(default=False) 

40 avatar_url = models.URLField(blank=True, null=True) 

41 

42 class Meta: 

43 ordering = ['first_name', 'last_name'] 

44 

45 @property 

46 def full_name(self) -> str: 

47 return f"{self.first_name} {self.last_name}".strip() 

48 

49 def __str__(self): 

50 return self.full_name 

51 

52class Pipeline(TenantScopedModel): 

53 """Sales Pipeline configuration.""" 

54 name = models.CharField(max_length=128) 

55 is_default = models.BooleanField(default=False) 

56 

57 def __str__(self): 

58 return self.name 

59 

60class DealStage(TenantScopedModel): 

61 """Stage in a sales pipeline.""" 

62 pipeline = models.ForeignKey(Pipeline, on_delete=models.CASCADE, related_name='stages') 

63 name = models.CharField(max_length=128) 

64 order = models.IntegerField(default=0) 

65 probability = models.IntegerField(default=10) # Percentage 0-100 

66 color = models.CharField(max_length=32, default='#3B82F6') 

67 

68 class Meta: 

69 ordering = ['order'] 

70 

71 def __str__(self): 

72 return f"{self.pipeline.name} - {self.name}" 

73 

74class Deal(TenantScopedModel): 

75 """Sales Deal / Opportunity.""" 

76 STATUS_CHOICES = [ 

77 ('OPEN', 'Open'), 

78 ('WON', 'Won'), 

79 ('LOST', 'Lost'), 

80 ] 

81 

82 title = models.CharField(max_length=255) 

83 value = models.DecimalField(max_digits=18, decimal_places=2, default=Decimal('0.00')) 

84 currency = models.CharField(max_length=3, default='PHP') 

85 status = models.CharField(max_length=16, choices=STATUS_CHOICES, default='OPEN') 

86 pipeline = models.ForeignKey(Pipeline, on_delete=models.CASCADE, related_name='deals') 

87 stage = models.ForeignKey(DealStage, on_delete=models.CASCADE, related_name='deals') 

88 contact = models.ForeignKey(Contact, on_delete=models.SET_NULL, null=True, blank=True, related_name='deals') 

89 company = models.ForeignKey(Company, on_delete=models.SET_NULL, null=True, blank=True, related_name='deals') 

90 expected_close_date = models.DateField(null=True, blank=True) 

91 closed_at = models.DateTimeField(null=True, blank=True) 

92 assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_deals') 

93 

94 class Meta: 

95 ordering = ['-created_at'] 

96 

97 def __str__(self): 

98 return f"{self.title} (${self.value})" 

99 

100class Activity(TenantScopedModel): 

101 """Polymorphic activity log (calls, emails, meetings, tasks, notes).""" 

102 TARGET_TYPE_CHOICES = [ 

103 ('CONTACT', 'Contact'), 

104 ('COMPANY', 'Company'), 

105 ('DEAL', 'Deal'), 

106 ] 

107 ACTIVITY_TYPE_CHOICES = [ 

108 ('CALL', 'Call'), 

109 ('EMAIL', 'Email'), 

110 ('MEETING', 'Meeting'), 

111 ('TASK', 'Task'), 

112 ('NOTE', 'Note'), 

113 ] 

114 

115 target_type = models.CharField(max_length=32, choices=TARGET_TYPE_CHOICES) 

116 target_id = models.UUIDField(db_index=True) 

117 activity_type = models.CharField(max_length=32, choices=ACTIVITY_TYPE_CHOICES) 

118 subject = models.CharField(max_length=255) 

119 body = models.TextField(blank=True, null=True) 

120 due_date = models.DateTimeField(null=True, blank=True) 

121 completed_at = models.DateTimeField(null=True, blank=True) 

122 assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_crm_activities') 

123 created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_crm_activities') 

124 

125 class Meta: 

126 ordering = ['-created_at'] 

127 

128 def __str__(self): 

129 return f"{self.activity_type}: {self.subject}"