111 lines
4.0 KiB
Python
111 lines
4.0 KiB
Python
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()})"
|