from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django_choices_field.fields import TextChoicesField from core.models.base import BaseModel, Address, Contact from core.models.enums import AddressChoices, StatusChoices class Customer(BaseModel): """Customer model with contact information""" name = models.CharField(max_length=200, unique=True) status = TextChoicesField(choices_enum=StatusChoices, default=StatusChoices.ACTIVE, help_text="Current status of the customer") start_date = models.DateField(default=timezone.now) end_date = models.DateField(blank=True, null=True) billing_terms = models.TextField() billing_email = models.EmailField(blank=True) wave_customer_id = models.CharField(max_length=255, blank=True, null=True, help_text="Wave customer ID") class Meta: ordering = ['name'] verbose_name = "Customer" verbose_name_plural = "Customers" indexes = [ models.Index(fields=['status', 'start_date']) ] def __str__(self): return self.name @property def is_active(self): """Check if the customer is currently active based on dates and status""" today = timezone.now().date() return self.status == 'ACTIVE' and self.start_date <= today and ( self.end_date is None or self.end_date >= today) class CustomerAddress(Address): """Address information for a customer""" customer = models.ForeignKey('Customer', on_delete=models.CASCADE, related_name='addresses') address_type = TextChoicesField(choices_enum=AddressChoices, default=AddressChoices.BILLING, help_text="Type of address") is_active = models.BooleanField(default=True) is_primary = models.BooleanField(default=False) class Meta: verbose_name = "Customer Address" verbose_name_plural = "Customer Addresses" indexes = [models.Index(fields=['customer', 'address_type', 'is_active'])] constraints = [models.UniqueConstraint(fields=['customer'], condition=models.Q(is_primary=True, is_active=True), name='unique_primary_address_per_customer')] def save(self, *args, **kwargs): if not CustomerAddress.objects.filter(customer=self.customer, is_active=True).exists(): self.is_primary = True super().save(*args, **kwargs) def get_address_type_display(self) -> str: try: # address_type may be the raw value; coerce to the enum, then read its label return AddressChoices(self.address_type).label except (ValueError, TypeError): # Fallback to string value if something unexpected is stored return str(self.address_type) def __str__(self): primary_indicator = " (Primary)" if self.is_primary else "" return f"{self.customer.name} - {self.get_address_type_display()}{primary_indicator}" class CustomerContact(Contact): """Contact information for a customer""" customer = models.ForeignKey('Customer', on_delete=models.CASCADE, related_name='contacts') email = models.EmailField(blank=True) is_primary = models.BooleanField(default=False) is_active = models.BooleanField(default=True) notes = models.TextField(blank=True) class Meta: verbose_name = "Customer Contact" verbose_name_plural = "Customer Contacts" indexes = [ models.Index(fields=['customer', 'is_active']), ] constraints = [ # Only one primary contact per customer models.UniqueConstraint( fields=['customer'], condition=models.Q(is_primary=True, is_active=True), name='unique_primary_contact_per_customer' ), # Prevent duplicate phone numbers for the same customer (when phone provided) models.UniqueConstraint( fields=['customer', 'phone'], condition=models.Q(is_active=True, phone__isnull=False) & ~models.Q(phone=''), name='unique_phone_per_customer' ), # Prevent duplicate emails for same customer (when email provided) models.UniqueConstraint( fields=['customer', 'email'], condition=models.Q(is_active=True, email__isnull=False) & ~models.Q(email=''), name='unique_email_per_customer' ) ] def save(self, *args, **kwargs): # Auto-set first active contact as primary if self.is_active and not CustomerContact.objects.filter( customer=self.customer, is_active=True ).exclude(pk=self.pk).exists(): self.is_primary = True super().save(*args, **kwargs) def clean(self): """Validate contact data""" if self.is_primary and not self.is_active: raise ValidationError("Primary contact must be active") if not self.phone and not self.email: raise ValidationError("Contact must have either phone number or email address") def __str__(self): primary_indicator = " (Primary)" if self.is_primary else "" return f"{self.full_name} - {self.customer.name}{primary_indicator}"