diff --git a/bizdev/admin.py b/bizdev/admin.py
index 916b63f..147ced8 100644
--- a/bizdev/admin.py
+++ b/bizdev/admin.py
@@ -12,9 +12,9 @@ import io
import time
import logging
-from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate
+from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate, DeclinationReason
from .forms import FileImportForm, EmailTemplateSelectionForm
-from .filters import StaffUserFilter, ProspectProfileFilter
+from .filters import ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter
from tournaments.models import CustomUser
from tournaments.models.enums import UserOrigin
@@ -92,14 +92,14 @@ create_activity_for_prospect.short_description = "Create event"
@admin.register(Prospect)
class ProspectAdmin(SyncedObjectAdmin):
- readonly_fields = ['related_events', 'entity_names', 'current_status']
+ readonly_fields = ['related_activities', 'entity_names', 'current_status', 'id']
fieldsets = [
(None, {
- 'fields': ['related_events', 'entity_names', 'first_name', 'last_name', 'email', 'phone', 'name_unsure', 'official_user', 'entities']
+ 'fields': ['related_activities', 'id', 'entity_names', 'related_user', 'first_name', 'last_name', 'email', 'phone', 'official_user', 'name_unsure', 'entities']
}),
]
list_display = ('entity_names', 'first_name', 'last_name', 'email', 'last_update', 'current_status')
- list_filter = ('creation_date', StaffUserFilter, 'source', ProspectProfileFilter)
+ list_filter = (ProspectStatusFilter, ProspectDeclineReasonFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter)
search_fields = ('first_name', 'last_name', 'email')
date_hierarchy = 'creation_date'
change_list_template = "admin/bizdev/prospect/change_list.html"
@@ -107,27 +107,16 @@ class ProspectAdmin(SyncedObjectAdmin):
filter_horizontal = ['entities']
actions = ['send_email', create_activity_for_prospect, contacted_by_sms, mark_as_should_test, mark_as_testing, mark_as_customer, declined_too_expensive, declined_use_something_else]
- def related_events(self, obj):
- events = obj.events.all()
- if events:
- event_links = []
- for event in events:
- url = f"/kingdom/bizdev/event/{event.id}/change/"
- event_links.append(f'{event.html_desc()}')
- return format_html('
'.join(event_links))
+ def related_activities(self, obj):
+ activities = obj.activities.all()
+ if activities:
+ activity_links = []
+ for activity in activities:
+ url = f"/kingdom/bizdev/activity/{activity.id}/change/"
+ activity_links.append(f'{activity.html_desc()}')
+ return format_html('
'.join(activity_links))
return "No events"
-
- related_events.short_description = "Related Events"
-
- def changelist_view(self, request, extra_context=None):
- # Add the URL with a filter for the current user
- user_filter_url = "{}?related_user__id__exact={}".format(
- reverse('admin:bizdev_prospect_changelist'),
- request.user.id
- )
- extra_context = extra_context or {}
- extra_context['user_filter_url'] = user_filter_url
- return super().changelist_view(request, extra_context=extra_context)
+ related_activities.short_description = "Related Activities"
def get_urls(self):
urls = super().get_urls()
@@ -292,7 +281,8 @@ class ProspectAdmin(SyncedObjectAdmin):
# Create Event if attachment_text or status is provided
if attachment_text or status_text:
# Map status text to Status enum
- status_value = Status.NONE
+ status_value = None
+ declination_reason = None
if status_text:
if 'CONTACTED' in status_text:
status_value = Status.CONTACTED
@@ -302,19 +292,30 @@ class ProspectAdmin(SyncedObjectAdmin):
status_value = Status.SHOULD_TEST
elif 'CUSTOMER' in status_text:
status_value = Status.CUSTOMER
+ elif 'TESTING' in status_text:
+ status_value = Status.TESTING
elif 'LOST' in status_text:
status_value = Status.LOST
elif 'DECLINED_TOO_EXPENSIVE' in status_text:
- status_value = Status.DECLINED_TOO_EXPENSIVE
- elif 'DECLINED_USE_SOMETHING_ELSE' in status_text:
- status_value = Status.DECLINED_USE_SOMETHING_ELSE
- elif 'DECLINED_UNRELATED' in status_text or 'NOK' in status_text:
+ status_value = Status.DECLINED
+ declination_reason = DeclinationReason.TOO_EXPENSIVE
+ elif 'USE_OTHER_PRODUCT' in status_text:
+ status_value = Status.DECLINED
+ declination_reason = DeclinationReason.USE_OTHER_PRODUCT
+ elif 'USE_ANDROID' in status_text:
+ status_value = Status.DECLINED
+ declination_reason = DeclinationReason.USE_ANDROID
+ elif 'NOK' in status_text:
+ status_value = Status.DECLINED
+ declination_reason = DeclinationReason.UNKNOWN
+ elif 'DECLINED_UNRELATED' in status_text:
status_value = Status.DECLINED_UNRELATED
activity = Activity.objects.create(
type=ActivityType.SMS,
attachment_text=attachment_text,
status=status_value,
+ declination_reason=declination_reason,
description=f"Imported from CSV - Status: {status_text}" if status_text else "Imported from CSV"
)
activity.prospects.add(prospect)
@@ -378,7 +379,7 @@ class ProspectAdmin(SyncedObjectAdmin):
@admin.register(Activity)
class ActivityAdmin(SyncedObjectAdmin):
- list_display = ('creation_date', 'type', 'description', 'attachment_text')
+ list_display = ('creation_date', 'status', 'type', 'description', 'attachment_text')
list_filter = ('status', 'type')
search_fields = ('description',)
filter_horizontal = ('prospects',)
diff --git a/bizdev/filters.py b/bizdev/filters.py
index 782481f..8edb770 100644
--- a/bizdev/filters.py
+++ b/bizdev/filters.py
@@ -1,12 +1,12 @@
import django_filters
-from django.db.models import Q
+from django.db.models import Max, F, Q
from django.contrib.auth import get_user_model
from django.contrib import admin
from django.utils import timezone
from dateutil.relativedelta import relativedelta
-from .models import Activity, Prospect
+from .models import Activity, Prospect, Status, DeclinationReason
User = get_user_model()
@@ -68,3 +68,46 @@ class ProspectProfileFilter(admin.SimpleListFilter):
official_user__isnull=False,
official_user__events__isnull=True
)
+
+class ProspectStatusFilter(admin.SimpleListFilter):
+ title = 'Status'
+ parameter_name = 'status'
+
+ def lookups(self, request, model_admin):
+ return [(tag.name, tag.value) for tag in Status]
+
+ def queryset(self, request, queryset):
+ if self.value():
+ # Get prospects whose most recent activity has the selected status
+ return queryset.filter(
+ activities__status=self.value()
+ ).annotate(
+ latest_activity_date=Max('activities__creation_date')
+ ).filter(
+ activities__creation_date=F('latest_activity_date'),
+ activities__status=self.value()
+ ).distinct()
+ else:
+ return queryset
+
+class ProspectDeclineReasonFilter(admin.SimpleListFilter):
+ title = 'Decline reason'
+ parameter_name = 'reason'
+
+ def lookups(self, request, model_admin):
+ return [(tag.name, tag.value) for tag in DeclinationReason]
+
+
+ def queryset(self, request, queryset):
+ if self.value():
+ # Get prospects whose most recent activity has the selected status
+ return queryset.filter(
+ activities__declination_reason=self.value()
+ ).annotate(
+ latest_activity_date=Max('activities__creation_date')
+ ).filter(
+ activities__creation_date=F('latest_activity_date'),
+ activities__declination_reason=self.value()
+ ).distinct()
+ else:
+ return queryset
diff --git a/bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py b/bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py
new file mode 100644
index 0000000..9508faf
--- /dev/null
+++ b/bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1 on 2025-07-10 13:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bizdev', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='activity',
+ name='declination_reason',
+ field=models.CharField(blank=True, choices=[('TOO_EXPENSIVE', 'Too expensive'), ('USE_OTHER_PRODUCT', 'Use other product'), ('USE_ANDROID', 'Use Android'), ('UNKNOWN', 'Unknown')], max_length=50, null=True),
+ ),
+ migrations.AlterField(
+ model_name='activity',
+ name='status',
+ field=models.CharField(blank=True, choices=[('CONTACTED', 'Contacted'), ('RESPONDED', 'Responded'), ('SHOULD_TEST', 'Should test'), ('TESTING', 'Testing'), ('CUSTOMER', 'Customer'), ('LOST', 'Lost customer'), ('DECLINED', 'Declined'), ('DECLINED_UNRELATED', 'Declined without significance')], max_length=50, null=True),
+ ),
+ ]
diff --git a/bizdev/models.py b/bizdev/models.py
index 86b5031..785910b 100644
--- a/bizdev/models.py
+++ b/bizdev/models.py
@@ -11,18 +11,21 @@ from sync.models import BaseModel
User = get_user_model()
class Status(models.TextChoices):
- NONE = 'NONE', 'None'
CONTACTED = 'CONTACTED', 'Contacted'
RESPONDED = 'RESPONDED', 'Responded'
SHOULD_TEST = 'SHOULD_TEST', 'Should test'
TESTING = 'TESTING', 'Testing'
CUSTOMER = 'CUSTOMER', 'Customer'
LOST = 'LOST', 'Lost customer'
- DECLINED_TOO_EXPENSIVE = 'DECLINED_TOO_EXPENSIVE', 'Too expensive'
- DECLINED_USE_SOMETHING_ELSE = 'DECLINED_USE_SOMETHING_ELSE', 'Use something else'
- DECLINED_OTHER = 'DECLINED_OTHER', 'Declined other reason'
+ DECLINED = 'DECLINED', 'Declined'
DECLINED_UNRELATED = 'DECLINED_UNRELATED', 'Declined without significance'
+class DeclinationReason(models.TextChoices):
+ TOO_EXPENSIVE = 'TOO_EXPENSIVE', 'Too expensive'
+ USE_OTHER_PRODUCT = 'USE_OTHER_PRODUCT', 'Use other product'
+ USE_ANDROID = 'USE_ANDROID', 'Use Android'
+ UNKNOWN = 'UNKNOWN', 'Unknown'
+
class ActivityType(models.TextChoices):
MAIL = 'MAIL', 'Mailing List'
SMS = 'SMS', 'SMS Campaign'
@@ -60,11 +63,11 @@ class Prospect(BaseModel):
def delete_dependencies(self):
pass
- class Meta:
- permissions = [
- ("manage_prospects", "Can manage prospects"),
- ("view_prospects", "Can view prospects"),
- ]
+ # class Meta:
+ # permissions = [
+ # ("manage_prospects", "Can manage prospects"),
+ # ("view_prospects", "Can view prospects"),
+ # ]
def current_status(self):
last_activity = self.activities.exclude(status=None).order_by('-creation_date').first()
@@ -72,6 +75,12 @@ class Prospect(BaseModel):
return last_activity.status
return None
+ def current_declination_reason(self):
+ last_activity = self.activities.exclude(status=None).order_by('-creation_date').first()
+ if last_activity:
+ return last_activity.declination_reason
+ return None
+
def entity_names(self):
entity_names = [entity.name for entity in self.entities.all()]
return " - ".join(entity_names)
@@ -83,7 +92,8 @@ class Prospect(BaseModel):
return self.full_name()
class Activity(BaseModel):
- status = models.CharField(max_length=50, default=Status.NONE, choices=Status.choices, null=True, blank=True)
+ status = models.CharField(max_length=50, choices=Status.choices, null=True, blank=True)
+ declination_reason = models.CharField(max_length=50, choices=DeclinationReason.choices, null=True, blank=True)
type = models.CharField(max_length=20, choices=ActivityType.choices, null=True, blank=True)
description = models.TextField(null=True, blank=True)
attachment_text = models.TextField(null=True, blank=True)
@@ -103,16 +113,16 @@ class Activity(BaseModel):
class Meta:
verbose_name_plural = "Activities"
ordering = ['-creation_date']
- permissions = [
- ("manage_events", "Can manage events"),
- ("view_events", "Can view events"),
- ]
+ # permissions = [
+ # ("manage_events", "Can manage events"),
+ # ("view_events", "Can view events"),
+ # ]
- def __str__(self):
- return f"{self.get_type_display()} - {self.creation_date.date()}"
+ # def __str__(self):
+ # return f"{self.get_type_display()} - {self.creation_date.date()}"
def html_desc(self):
- fields = [field for field in [self.creation_date.strftime("%d/%m/%Y %H:%M"), self.type, self.status, self.attachment_text, self.description] if field is not None]
+ fields = [field for field in [self.creation_date.strftime("%d/%m/%Y %H:%M"), self.status, self.attachment_text, self.description, self.type] if field is not None]
html = '
| {field} | ' diff --git a/bizdev/templates/admin/bizdev/app_index.html b/bizdev/templates/admin/bizdev/app_index.html deleted file mode 100644 index 9c83f1a..0000000 --- a/bizdev/templates/admin/bizdev/app_index.html +++ /dev/null @@ -1,25 +0,0 @@ - -{% extends "admin/app_index.html" %} -{% load i18n %} - -{% block content %} -