from django.contrib.auth.models import AbstractUser, BaseUserManager from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone class UserManager(BaseUserManager): """Define a model manager for User model with no username field.""" use_in_migrations = True def _create_user(self, email, password, **extra_fields): """Create and save a User with the given email and password.""" if not email: raise ValueError('The given email must be set') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_user(self, email, password=None, **extra_fields): """Create and save a regular User with the given email and password.""" extra_fields.setdefault('is_staff', False) extra_fields.setdefault('is_superuser', False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): """Create and save a SuperUser with the given email and password.""" extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) if extra_fields.get('is_staff') is not True: raise ValueError('Superuser must have is_staff=True.') if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') return self._create_user(email, password, **extra_fields) class Tenant(models.Model): name = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name class User(AbstractUser): username = None email = models.EmailField(unique=True) tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, null=True, blank=True) role = models.CharField(max_length=100, blank=True) USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] objects = UserManager() class BaseModel(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_created_by') updated_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_updated_by') deleted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_deleted_by') restored_at = models.DateTimeField(null=True, blank=True) restored_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_restored_by') class Meta: abstract = True def delete(self, user=None, *args, **kwargs): self.is_deleted = True self.deleted_at = timezone.now() if user: self.deleted_by = user self.save() ActivityLog.objects.create( actor=user, action=f'deleted a {self.__class__.__name__}', content_object=self, details={'id': self.id, 'name': str(self)} ) def restore(self, user=None, *args, **kwargs): self.is_deleted = False self.deleted_at = None self.deleted_by = None self.restored_at = timezone.now() if user: self.restored_by = user self.save() ActivityLog.objects.create( actor=user, action=f'restored a {self.__class__.__name__}', content_object=self, details={'id': self.id, 'name': str(self)} ) class ActivityLog(models.Model): actor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) action = models.CharField(max_length=255) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') timestamp = models.DateTimeField(auto_now_add=True, db_index=True) details = models.JSONField(null=True, blank=True) class Meta: ordering = ['-timestamp'] def get_action_badge(self): if 'deleted' in self.action: return ('bg-danger', 'deleted') elif 'restored' in self.action: return ('bg-success', 'restored') elif 'created' in self.action: return ('bg-primary', 'created') elif 'updated' in self.action: return ('bg-info', 'updated') else: action_verb = self.action.split(' ')[0] return ('bg-secondary', action_verb) class Customer(BaseModel): STATUS_CHOICES = ( ('active', 'Active'), ('inactive', 'Inactive'), ('lead', 'Lead'), ) tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) name = models.CharField(max_length=255) email = models.EmailField() phone = models.CharField(max_length=50, blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', db_index=True) owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='customers') class Meta: unique_together = ('tenant', 'email') indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['created_at']), ] def __str__(self): return self.name class Lead(BaseModel): STATUS_CHOICES = ( ('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ) customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='leads') source = models.CharField(max_length=255, blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='new', db_index=True) assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='leads') def __str__(self): return f"Lead for {self.customer.name}" class Opportunity(BaseModel): STAGE_CHOICES = ( ('prospecting', 'Prospecting'), ('qualification', 'Qualification'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost'), ) name = models.CharField(max_length=255, blank=True, null=True) lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='opportunities') value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) stage = models.CharField(max_length=20, choices=STAGE_CHOICES, default='prospecting', db_index=True) probability = models.FloatField(null=True, blank=True) close_date = models.DateField(null=True, blank=True) def __str__(self): return self.name class ContactHistory(BaseModel): INTERACTION_CHOICES = ( ('email', 'Email'), ('phone', 'Phone Call'), ('meeting', 'Meeting'), ('note', 'Note'), ) customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='contact_history') user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) note = models.TextField() interaction_type = models.CharField(max_length=20, choices=INTERACTION_CHOICES, default='note') def __str__(self): return f"Contact with {self.customer.name} on {self.created_at.date()}" class Note(BaseModel): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') content = models.TextField() def __str__(self): return f"Note for {self.content_object}"