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
« 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
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)
19 class Meta:
20 ordering = ['name']
22 def __str__(self):
23 return self.name
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)
42 class Meta:
43 ordering = ['first_name', 'last_name']
45 @property
46 def full_name(self) -> str:
47 return f"{self.first_name} {self.last_name}".strip()
49 def __str__(self):
50 return self.full_name
52class Pipeline(TenantScopedModel):
53 """Sales Pipeline configuration."""
54 name = models.CharField(max_length=128)
55 is_default = models.BooleanField(default=False)
57 def __str__(self):
58 return self.name
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')
68 class Meta:
69 ordering = ['order']
71 def __str__(self):
72 return f"{self.pipeline.name} - {self.name}"
74class Deal(TenantScopedModel):
75 """Sales Deal / Opportunity."""
76 STATUS_CHOICES = [
77 ('OPEN', 'Open'),
78 ('WON', 'Won'),
79 ('LOST', 'Lost'),
80 ]
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')
94 class Meta:
95 ordering = ['-created_at']
97 def __str__(self):
98 return f"{self.title} (${self.value})"
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 ]
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')
125 class Meta:
126 ordering = ['-created_at']
128 def __str__(self):
129 return f"{self.activity_type}: {self.subject}"