from decimal import Decimal from django.db import models from django.utils import timezone class SalesWorkspace(models.Model): class Stage(models.TextChoices): NEW = "new", "New lead" QUALIFIED = "qualified", "Qualified" PROPOSAL = "proposal", "Proposal / Quote" NEGOTIATION = "negotiation", "Negotiation" WON = "won", "Won" LOST = "lost", "Lost" class LayoutTemplate(models.TextChoices): CUSTOMER_360 = "customer360", "Customer 360" QUOTATION_FOCUS = "quotation_focus", "Quotation focus" PLANNING = "planning", "Planning board" customer_name = models.CharField(max_length=160) project_name = models.CharField(max_length=160, blank=True) project_number = models.CharField(max_length=60, blank=True) zipcode = models.CharField(max_length=20, blank=True) address = models.CharField(max_length=255, blank=True) city = models.CharField(max_length=120, blank=True) contact_name = models.CharField(max_length=160, blank=True) contact_email = models.EmailField(blank=True) contact_phone = models.CharField(max_length=40, blank=True) opportunity_title = models.CharField(max_length=180) stage = models.CharField(max_length=20, choices=Stage.choices, default=Stage.NEW) estimated_value = models.DecimalField(max_digits=12, decimal_places=2, default=0) layout_template = models.CharField( max_length=24, choices=LayoutTemplate.choices, default=LayoutTemplate.CUSTOMER_360 ) summary = models.TextField(blank=True) next_step = models.CharField(max_length=180, blank=True) next_meeting_at = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-updated_at", "-created_at"] def __str__(self): project = f" · {self.project_number}" if self.project_number else "" return f"{self.customer_name} — {self.opportunity_title}{project}" @property def quote_total(self): total = sum((line.subtotal for line in self.quote_lines.all()), Decimal("0.00")) return total.quantize(Decimal("0.01")) if total else Decimal("0.00") @property def open_activities_count(self): return self.activities.filter(is_done=False).count() @property def pipeline_label(self): return self.get_stage_display() @property def next_meeting_is_upcoming(self): return bool(self.next_meeting_at and self.next_meeting_at >= timezone.now()) class QuoteLine(models.Model): workspace = models.ForeignKey( SalesWorkspace, related_name="quote_lines", on_delete=models.CASCADE ) product_name = models.CharField(max_length=140) description = models.CharField(max_length=255, blank=True) quantity = models.PositiveIntegerField(default=1) unit_price = models.DecimalField(max_digits=10, decimal_places=2) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at", "id"] def __str__(self): return f"{self.product_name} × {self.quantity}" @property def subtotal(self): return (self.unit_price or Decimal("0.00")) * self.quantity class ActivityItem(models.Model): class ActivityType(models.TextChoices): CALL = "call", "Call" MEETING = "meeting", "Meeting" EMAIL = "email", "Email" TASK = "task", "Task" workspace = models.ForeignKey( SalesWorkspace, related_name="activities", on_delete=models.CASCADE ) title = models.CharField(max_length=180) activity_type = models.CharField(max_length=20, choices=ActivityType.choices) due_at = models.DateTimeField() owner = models.CharField(max_length=120, blank=True) notes = models.TextField(blank=True) is_done = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["due_at", "id"] def __str__(self): return f"{self.title} ({self.get_activity_type_display()})"