diff --git a/api/serializers.py b/api/serializers.py
index e530a5a..b5bac71 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -1,10 +1,5 @@
from rest_framework import serializers
-from tournaments.models.court import Court
-from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer, Image
from django.db.utils import IntegrityError
-from django.conf import settings
-
-# email
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
@@ -12,9 +7,9 @@ from django.core.mail import EmailMessage
from django.contrib.sites.shortcuts import get_current_site
from api.tokens import account_activation_token
-
from shared.cryptography import encryption_util
-from tournaments.models.draw_log import DrawLog
+
+from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer, Image, DrawLog, Court
from tournaments.models.enums import UserOrigin, RegistrationPaymentMode
class EncryptedUserField(serializers.Field):
diff --git a/api/urls.py b/api/urls.py
index 13faa2e..24ded13 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -29,6 +29,10 @@ router.register(r'device-token', views.DeviceTokenViewSet)
router.register(r'data-access', DataAccessViewSet)
router.register(r'unregistered-teams', views.UnregisteredTeamViewSet)
router.register(r'unregistered-players', views.UnregisteredPlayerViewSet)
+### CRM
+router.register(r'crm-prospects', views.CRMProspectViewSet)
+router.register(r'crm-entities', views.CRMEntityViewSet)
+router.register(r'crm-activities', views.CRMActivityViewSet)
urlpatterns = [
path('', include(router.urls)),
diff --git a/api/views.py b/api/views.py
index 820f642..181e6e6 100644
--- a/api/views.py
+++ b/api/views.py
@@ -1,6 +1,9 @@
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer, ImageSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image
+from crm.serializers import CRMActivitySerializer, CRMProspectSerializer, CRMEntitySerializer
+from crm.models import Activity, Prospect, Entity
+
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.decorators import api_view
@@ -568,3 +571,17 @@ def validate_stripe_account(request):
'error': f'Unexpected error: {str(e)}',
'needs_onboarding': True,
}, status=200)
+
+
+### CRM
+class CRMActivityViewSet(SoftDeleteViewSet):
+ queryset = Activity.objects.all()
+ serializer_class = CRMActivitySerializer
+
+class CRMProspectViewSet(SoftDeleteViewSet):
+ queryset = Prospect.objects.all()
+ serializer_class = CRMProspectSerializer
+
+class CRMEntityViewSet(SoftDeleteViewSet):
+ queryset = Entity.objects.all()
+ serializer_class = CRMEntitySerializer
diff --git a/crm/admin.py b/crm/admin.py
index 819de9a..5b72094 100644
--- a/crm/admin.py
+++ b/crm/admin.py
@@ -1,96 +1,455 @@
+from django.http import HttpResponseRedirect
from django.contrib import admin
+from django.urls import path, reverse
+from django.contrib import messages
+from django.shortcuts import render, redirect
+from django.contrib.auth import get_user_model
from django.utils.html import format_html
-from .models import (
- Prospect,
- Status,
- ProspectStatus,
- Event,
- EmailCampaign,
- EmailTracker
-)
+
+import csv
+import io
+import time
+
+from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate
+from .forms import FileImportForm, EmailTemplateSelectionForm
+from .filters import StaffUserFilter, ProspectProfileFilter
+
+from tournaments.models import CustomUser
+from tournaments.models.enums import UserOrigin
+from sync.admin import SyncedObjectAdmin
+
+User = get_user_model()
+
+@admin.register(Entity)
+class EntityAdmin(SyncedObjectAdmin):
+ list_display = ('name', 'address', 'zip_code', 'city')
+ search_fields = ('name', 'address', 'zip_code', 'city')
+
+@admin.register(EmailTemplate)
+class EmailTemplateAdmin(SyncedObjectAdmin):
+ list_display = ('name', 'subject', 'body')
+ search_fields = ('name', 'subject')
+
+def contacted_by_sms(modeladmin, request, queryset):
+ create_activity_for_prospect(modeladmin, request, queryset, None, Status.CONTACTED)
+contacted_by_sms.short_description = "Contacted by SMS"
+
+def mark_as_customer(modeladmin, request, queryset):
+ create_activity_for_prospect(modeladmin, request, queryset, None, Status.CUSTOMER)
+mark_as_customer.short_description = "Mark as customer"
+
+def mark_as_should_test(modeladmin, request, queryset):
+ create_activity_for_prospect(modeladmin, request, queryset, None, Status.TESTING)
+mark_as_should_test.short_description = "Mark as should test"
+
+def mark_as_testing(modeladmin, request, queryset):
+ create_activity_for_prospect(modeladmin, request, queryset, None, Status.CUSTOMER)
+mark_as_testing.short_description = "Mark as testing"
+
+def declined_too_expensive(modeladmin, request, queryset):
+ create_activity_for_prospect(modeladmin, request, queryset, None, Status.DECLINED_TOO_EXPENSIVE)
+declined_too_expensive.short_description = "Declined too expensive"
+
+def declined_use_something_else(modeladmin, request, queryset):
+ create_activity_for_prospect(modeladmin, request, queryset, None, Status.DECLINED_USE_SOMETHING_ELSE)
+declined_use_something_else.short_description = "Declined use something else"
+
+def create_activity_for_prospect(modeladmin, request, queryset, type, status):
+ for prospect in queryset:
+ activity = Activity.objects.create(
+ type=type,
+ status=status,
+ related_user = request.user
+ )
+ activity.prospects.add(prospect)
+
+ modeladmin.message_user(
+ request,
+ f'{queryset.count()} prospects were marked as {status}.'
+ )
+
+def create_activity_for_prospect(modeladmin, request, queryset):
+ # Only allow single selection
+ if queryset.count() != 1:
+ messages.error(request, "Please select exactly one prospect.")
+ return
+
+ prospect = queryset.first()
+
+ # Build the URL with pre-populated fields
+ url = reverse('admin:crm_activity_add')
+ url += f'?prospect={prospect.id}'
+
+ # You can add more fields as URL parameters
+ # url += f'&title=Event for {prospect.name}&status=pending'
+
+ return redirect(url)
+create_activity_for_prospect.short_description = "Create event"
@admin.register(Prospect)
-class ProspectAdmin(admin.ModelAdmin):
- list_display = ('entity_name', 'first_name', 'last_name', 'email', 'address', 'zip_code', 'city', 'created_at')
- list_filter = ('zip_code', 'created_at')
- search_fields = ('entity_name', 'first_name', 'last_name', 'email', 'zip_code', 'city')
- filter_horizontal = ('users',)
- date_hierarchy = 'created_at'
-
-@admin.register(Status)
-class StatusAdmin(admin.ModelAdmin):
- list_display = ('name', 'created_at')
- search_fields = ('name',)
-
-@admin.register(ProspectStatus)
-class ProspectStatusAdmin(admin.ModelAdmin):
- list_display = ('prospect', 'status', 'created_at')
- list_filter = ('status', 'created_at')
- search_fields = ('prospect__name', 'prospect__email')
- date_hierarchy = 'created_at'
-
-@admin.register(Event)
-class EventAdmin(admin.ModelAdmin):
- list_display = ('get_event_display', 'type', 'date', 'status', 'created_at')
- list_filter = ('type', 'status', 'date')
+class ProspectAdmin(SyncedObjectAdmin):
+ readonly_fields = ['related_events', 'entity_names', 'current_status']
+ fieldsets = [
+ (None, {
+ 'fields': ['related_events', 'entity_names', 'first_name', 'last_name', 'email', 'phone', 'name_unsure', 'official_user', 'entities']
+ }),
+ ]
+ list_display = ('entity_names', 'first_name', 'last_name', 'email', 'last_update', 'current_status')
+ list_filter = ('creation_date', StaffUserFilter, 'source', ProspectProfileFilter)
+ search_fields = ('first_name', 'last_name', 'email')
+ date_hierarchy = 'creation_date'
+ change_list_template = "admin/crm/prospect/change_list.html"
+ ordering = ['-last_update']
+ 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/crm/event/{event.id}/change/"
+ event_links.append(f'{event.html_desc()}')
+ return format_html('
'.join(event_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:crm_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)
+
+ def get_urls(self):
+ urls = super().get_urls()
+ custom_urls = [
+ path('import_file/', self.admin_site.admin_view(self.import_file), name='import_file'),
+ path('import_app_users/', self.admin_site.admin_view(self.import_app_users), name='import_app_users'),
+ path('cleanup/', self.admin_site.admin_view(self.cleanup), name='cleanup'),
+ ]
+ return custom_urls + urls
+
+ def cleanup(self, request):
+ Entity.objects.all().delete()
+ Prospect.objects.all().delete()
+ Activity.objects.all().delete()
+
+ messages.success(request, 'cleanup CRM objects')
+ return redirect('admin:crm_prospect_changelist')
+
+ def import_app_users(self, request):
+ users = CustomUser.objects.filter(origin=UserOrigin.APP)
+
+ created_count = 0
+ for user in users:
+ is_customer = user.purchases.count() > 0
+ entity_name = user.latest_event_club_name()
+
+ prospect, prospect_created = Prospect.objects.get_or_create(
+ email=user.email,
+ defaults={
+ 'first_name': user.first_name,
+ 'last_name': user.last_name,
+ 'phone': user.phone,
+ 'name_unsure': False,
+ 'official_user': user,
+ 'source': 'App',
+ }
+ )
+ if entity_name:
+ entity, entity_created = Entity.objects.get_or_create(
+ name=entity_name,
+ defaults={'name': entity_name}
+ )
+ prospect.entities.add(entity)
+
+ if is_customer:
+ activity = Activity.objects.create(
+ status=Status.CUSTOMER,
+ )
+ activity.prospects.add(prospect)
+ if prospect_created:
+ created_count += 1
+
+ messages.success(request, f'Imported {created_count} app users into prospects')
+ return redirect('admin:crm_prospect_changelist')
+
+ def import_file(self, request):
+ """
+ Handle file import - displays form and processes file upload
+ """
+ if request.method == 'POST':
+ form = FileImportForm(request.POST, request.FILES)
+ if form.is_valid():
+ # Call the import_csv method with the uploaded file
+ try:
+ result = self.import_csv(form.cleaned_data['file'], form.cleaned_data['source'])
+ messages.success(request, f'File imported successfully: {result}')
+ return redirect('admin:crm_prospect_changelist')
+ except Exception as e:
+ messages.error(request, f'Error importing file: {str(e)}')
+ else:
+ messages.error(request, 'Please correct the errors below.')
+ else:
+ form = FileImportForm()
+
+ context = {
+ 'form': form,
+ 'title': 'Import File',
+ 'app_label': self.model._meta.app_label,
+ 'opts': self.model._meta,
+ 'has_change_permission': self.has_change_permission(request),
+ }
+ return render(request, 'admin/crm/prospect/import_file.html', context)
+
+ def import_csv(self, file, source):
+ """
+ Process the uploaded CSV file
+ CSV format: entity_name,last_name,first_name,email,phone,attachment_text,status,related_user
+ """
+ try:
+ # Read the file content
+ file_content = file.read().decode('utf-8')
+ csv_reader = csv.reader(io.StringIO(file_content))
+
+ created_prospects = 0
+ updated_prospects = 0
+ created_entities = 0
+ created_events = 0
+
+ for row in csv_reader:
+ if len(row) < 8:
+ continue # Skip rows that don't have enough columns
+
+ entity_name = row[0].strip()
+ last_name = row[1].strip()
+ first_name = row[2].strip()
+ email = row[3].strip()
+ phone = row[4].strip() if row[4].strip() else None
+ if phone and not phone.startswith('0'):
+ phone = '0' + phone
+ attachment_text = row[5].strip() if row[5].strip() else None
+ status_text = row[6].strip() if row[6].strip() else None
+ related_user_name = row[7].strip() if row[7].strip() else None
+
+ # Create or get Entity
+ entity = None
+ if entity_name:
+ entity, entity_created = Entity.objects.get_or_create(
+ name=entity_name,
+ defaults={'name': entity_name}
+ )
+ if entity_created:
+ created_entities += 1
+
+ # Get related user if provided
+ related_user = None
+ if related_user_name:
+ try:
+ related_user = User.objects.get(username=related_user_name)
+ except User.DoesNotExist:
+ # Try to find by first name if username doesn't exist
+ related_user = User.objects.filter(first_name__icontains=related_user_name).first()
+
+ # Create or update Prospect
+ prospect, prospect_created = Prospect.objects.get_or_create(
+ email=email,
+ defaults={
+ 'first_name': first_name,
+ 'last_name': last_name,
+ 'phone': phone,
+ 'name_unsure': False,
+ 'related_user': related_user,
+ 'source': source,
+ }
+ )
+
+ if prospect_created:
+ created_prospects += 1
+ else:
+ # Check if names are different and mark as name_unsure
+ if (prospect.first_name != first_name or prospect.last_name != last_name):
+ prospect.name_unsure = True
+ # Update related_user if provided
+ if related_user:
+ prospect.related_user = related_user
+ prospect.save()
+ updated_prospects += 1
+
+ # Associate entity with prospect
+ if entity:
+ prospect.entities.add(entity)
+
+ # 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
+ if status_text:
+ if 'CONTACTED' in status_text:
+ status_value = Status.CONTACTED
+ elif 'RESPONDED' in status_text:
+ status_value = Status.RESPONDED
+ elif 'SHOULD_TEST' in status_text:
+ status_value = Status.SHOULD_TEST
+ elif 'CUSTOMER' in status_text:
+ status_value = Status.CUSTOMER
+ 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_UNRELATED
+
+ activity = Activity.objects.create(
+ type=ActivityType.SMS,
+ attachment_text=attachment_text,
+ status=status_value,
+ description=f"Imported from CSV - Status: {status_text}" if status_text else "Imported from CSV"
+ )
+ activity.prospects.add(prospect)
+ created_events += 1
+
+ result = f"Successfully imported: {created_prospects} new prospects, {updated_prospects} updated prospects, {created_entities} new entities, {created_events} new events"
+ return result
+
+ except Exception as e:
+ raise Exception(f"Error processing CSV file: {str(e)}")
+
+ def send_email(self, request, queryset):
+
+ if 'apply' in request.POST:
+ form = EmailTemplateSelectionForm(request.POST)
+ if form.is_valid():
+ email_template = form.cleaned_data['email_template']
+
+ self.process_selected_items_with_template(request, queryset, email_template)
+
+ self.message_user(request, f"Email sent to {queryset.count()} prospects using the '{email_template.name}' template.", messages.SUCCESS)
+ return HttpResponseRedirect(request.get_full_path())
+ else:
+ form = EmailTemplateSelectionForm()
+
+ return render(request, 'admin/crm/select_email_template.html', {
+ 'prospects': queryset,
+ 'form': form,
+ 'title': 'Send Email to Prospects'
+ })
+ send_email.short_description = "Send email"
+
+ def process_selected_items_with_template(self, request, queryset, email_template):
+
+ sent_count = 0
+ error_emails = []
+ all_emails = []
+
+ for prospect in queryset:
+ mail_body = email_template.body.replace('{{name}}', prospect.first_name)
+ all_emails.append(prospect.email)
+
+ try:
+ send_mail(
+ email_template.subject,
+ mail_body,
+ request.user.email,
+ [prospect.email],
+ fail_silently=False,
+ )
+ sent_count += 1
+ except Exception as e:
+ error_emails.append(prospect.email)
+
+ time.sleep(1)
+
+
+@admin.register(Activity)
+class ActivityAdmin(SyncedObjectAdmin):
+ list_display = ('creation_date', 'type', 'description', 'attachment_text')
+ list_filter = ('status', 'type')
search_fields = ('description',)
filter_horizontal = ('prospects',)
- date_hierarchy = 'date'
+ date_hierarchy = 'creation_date'
+
+ def get_form(self, request, obj=None, **kwargs):
+ form = super().get_form(request, obj, **kwargs)
+
+ # Pre-populate fields from URL parameters
+ if 'prospect' in request.GET:
+ try:
+ prospect_id = request.GET['prospect']
+ prospect = Prospect.objects.get(id=prospect_id)
+ form.base_fields['prospects'].initial = prospect
+ form.base_fields['related_user'].initial = request.user
+
+ # You can set other fields based on the prospect
+ # form.base_fields['title'].initial = f"Event for {prospect.}"
+ # form.base_fields['status'].initial = 'pending'
+
+ except (Prospect.DoesNotExist, ValueError):
+ pass
+
+ return form
def get_event_display(self, obj):
return str(obj)
- get_event_display.short_description = 'Event'
-
-@admin.register(EmailCampaign)
-class EmailCampaignAdmin(admin.ModelAdmin):
- list_display = ('subject', 'event', 'sent_at')
- list_filter = ('sent_at',)
- search_fields = ('subject', 'content')
- date_hierarchy = 'sent_at'
- readonly_fields = ('sent_at',)
-
-@admin.register(EmailTracker)
-class EmailTrackerAdmin(admin.ModelAdmin):
- list_display = (
- 'campaign',
- 'prospect',
- 'tracking_id',
- 'sent_status',
- 'opened_status',
- 'clicked_status'
- )
- list_filter = ('sent', 'opened', 'clicked')
- search_fields = (
- 'prospect__name',
- 'prospect__email',
- 'campaign__subject'
- )
- readonly_fields = (
- 'tracking_id', 'sent', 'sent_at',
- 'opened', 'opened_at',
- 'clicked', 'clicked_at'
- )
- date_hierarchy = 'sent_at'
-
- def sent_status(self, obj):
- return self._get_status_html(obj.sent, obj.sent_at)
- sent_status.short_description = 'Sent'
- sent_status.allow_tags = True
-
- def opened_status(self, obj):
- return self._get_status_html(obj.opened, obj.opened_at)
- opened_status.short_description = 'Opened'
- opened_status.allow_tags = True
-
- def clicked_status(self, obj):
- return self._get_status_html(obj.clicked, obj.clicked_at)
- clicked_status.short_description = 'Clicked'
- clicked_status.allow_tags = True
-
- def _get_status_html(self, status, date):
- if status:
- return format_html(
- '✓ {}',
- date.strftime('%Y-%m-%d %H:%M') if date else ''
- )
- return format_html('✗')
+ get_event_display.short_description = 'Activity'
+
+# @admin.register(EmailCampaign)
+# class EmailCampaignAdmin(admin.ModelAdmin):
+# list_display = ('subject', 'event', 'sent_at')
+# list_filter = ('sent_at',)
+# search_fields = ('subject', 'content')
+# date_hierarchy = 'sent_at'
+# readonly_fields = ('sent_at',)
+
+# @admin.register(EmailTracker)
+# class EmailTrackerAdmin(admin.ModelAdmin):
+# list_display = (
+# 'campaign',
+# 'prospect',
+# 'tracking_id',
+# 'sent_status',
+# 'opened_status',
+# 'clicked_status'
+# )
+# list_filter = ('sent', 'opened', 'clicked')
+# search_fields = (
+# 'prospect__name',
+# 'prospect__email',
+# 'campaign__subject'
+# )
+# readonly_fields = (
+# 'tracking_id', 'sent', 'sent_at',
+# 'opened', 'opened_at',
+# 'clicked', 'clicked_at'
+# )
+# date_hierarchy = 'sent_at'
+
+# def sent_status(self, obj):
+# return self._get_status_html(obj.sent, obj.sent_at)
+# sent_status.short_description = 'Sent'
+# sent_status.allow_tags = True
+
+# def opened_status(self, obj):
+# return self._get_status_html(obj.opened, obj.opened_at)
+# opened_status.short_description = 'Opened'
+# opened_status.allow_tags = True
+
+# def clicked_status(self, obj):
+# return self._get_status_html(obj.clicked, obj.clicked_at)
+# clicked_status.short_description = 'Clicked'
+# clicked_status.allow_tags = True
+
+# def _get_status_html(self, status, date):
+# if status:
+# return format_html(
+# '✓ {}',
+# date.strftime('%Y-%m-%d %H:%M') if date else ''
+# )
+# return format_html('✗')
diff --git a/crm/admin_urls.py b/crm/admin_urls.py
new file mode 100644
index 0000000..5d94c6e
--- /dev/null
+++ b/crm/admin_urls.py
@@ -0,0 +1,70 @@
+from django.urls import path
+from django.http import HttpResponse
+from tournaments.models import CustomUser
+from tournaments.models.enums import UserOrigin
+from django.core.mail import send_mail
+
+import time
+
+def users_list(with_tournaments):
+ return CustomUser.objects.filter(origin=UserOrigin.APP).exclude(purchase__isnull=False).filter(events__isnull=with_tournaments)
+
+def email_users_with_tournaments_count(request):
+ users = users_list(False)
+ emails = [user.email for user in users]
+ return HttpResponse(f'users = {len(users)}, \n\nemails = {emails}')
+
+def email_users_count(request):
+ users = users_list(True)
+ emails = [user.email for user in users]
+ return HttpResponse(f'users = {len(users)}, \n\nemails = {emails}')
+
+def email_users_view(request):
+ return email_users(request, users_list(True), 0)
+
+def email_users_with_tournaments(request):
+ return email_users(request, users_list(False), 1)
+
+def email_users(request, users, template_index):
+
+ users = users_list(True)
+
+ subject = 'check Padel Club'
+ from_email = 'laurent@padelclub.app'
+
+ sent_count = 0
+ error_emails = []
+ all_emails = []
+
+ for user in users:
+ mail_body = template(user, template_index) # f'Bonjour {user.first_name}, cool la vie ?'
+ all_emails.append(user.email)
+
+ try:
+ send_mail(
+ subject,
+ mail_body,
+ from_email,
+ [user.email],
+ fail_silently=False,
+ )
+ sent_count += 1
+ except Exception as e:
+ error_emails.append(user.email)
+
+ time.sleep(1)
+
+ return HttpResponse(f'users = {len(users)}, sent = {sent_count}, errors = {len(error_emails)}, \n\nemails = {all_emails}, \n\nerror emails = {error_emails}')
+
+def template(user, index):
+ if index == 0:
+ return f'Bonjour {user.first_name}, \n\n'
+ else:
+ return f'Bonjour {user.first_name}, \n\nJe te remercie d\'avoir téléchargé Padel Club. J\'ai pu voir que tu avais créé quelques tournois mais sans aller plus loin, est-ce que tu pourrais me dire ce qui t\'as freiné ?\n\nLaurent Morvillier'
+
+urlpatterns = [
+ path('email_users/', email_users_view, name='crm_email_users'),
+ path('email_users_count/', email_users_count, name='crm_email_count'),
+ path('email_users_with_tournaments_count/', email_users_with_tournaments_count, name='crm_email_with_tournaments_count'),
+ path('email_users_with_tournaments/', email_users_with_tournaments, name='email_users_with_tournaments'),
+]
diff --git a/crm/filters.py b/crm/filters.py
index 97e433b..782481f 100644
--- a/crm/filters.py
+++ b/crm/filters.py
@@ -1,14 +1,20 @@
import django_filters
from django.db.models import Q
+from django.contrib.auth import get_user_model
+from django.contrib import admin
+from django.utils import timezone
-from .models import Event, Status, Prospect
+from dateutil.relativedelta import relativedelta
+from .models import Activity, Prospect
+
+User = get_user_model()
class ProspectFilter(django_filters.FilterSet):
zip_code = django_filters.CharFilter(lookup_expr='istartswith', label='Code postal')
- events = django_filters.ModelMultipleChoiceFilter(
- queryset=Event.objects.all(),
- field_name='events',
+ activities = django_filters.ModelMultipleChoiceFilter(
+ queryset=Activity.objects.all(),
+ field_name='activities',
)
city = django_filters.CharFilter(lookup_expr='icontains', label='Ville')
name = django_filters.CharFilter(method='filter_name', label='Nom')
@@ -20,4 +26,45 @@ class ProspectFilter(django_filters.FilterSet):
class Meta:
model = Prospect
- fields = ['name', 'city', 'events', 'zip_code']
+ fields = ['name', 'city', 'activities', 'zip_code']
+
+class StaffUserFilter(admin.SimpleListFilter):
+ title = 'staff user'
+ parameter_name = 'user'
+
+ def lookups(self, request, model_admin):
+ staff_users = User.objects.filter(is_staff=True)
+ return [(user.id, user.username) for user in staff_users]
+
+ def queryset(self, request, queryset):
+ # Filter the queryset based on the selected user ID
+ if self.value():
+ return queryset.filter(related_user__id=self.value())
+ return queryset
+
+class ProspectProfileFilter(admin.SimpleListFilter):
+ title = 'Prospect profiles' # displayed in the admin UI
+ parameter_name = 'profile' # URL parameter
+
+ def lookups(self, request, model_admin):
+ return (
+ ('tournament_at_least_1_month_old', 'tournaments > 1 month old'),
+ ('no_tournaments', 'No tournaments'),
+ )
+
+ def queryset(self, request, queryset):
+ if not self.value():
+ return queryset
+
+ two_months_ago = timezone.now().date() - relativedelta(months=2)
+
+ if self.value() == 'tournament_at_least_2_month_old':
+ return queryset.filter(
+ official_user__isnull=False,
+ official_user__events__creation_date__lte=two_months_ago
+ )
+ elif self.value() == 'no_tournaments':
+ return queryset.filter(
+ official_user__isnull=False,
+ official_user__events__isnull=True
+ )
diff --git a/crm/forms.py b/crm/forms.py
index 10edf6c..e076e80 100644
--- a/crm/forms.py
+++ b/crm/forms.py
@@ -1,46 +1,61 @@
from django import forms
-from .models import Prospect, Event
-import datetime
-
-class SmallTextArea(forms.Textarea):
- def __init__(self, *args, **kwargs):
- kwargs.setdefault('attrs', {})
- kwargs['attrs'].update({
- 'rows': 2,
- 'cols': 100,
- 'style': 'height: 80px; width: 800px;'
- })
- super().__init__(*args, **kwargs)
-
-class ProspectForm(forms.ModelForm):
- class Meta:
- model = Prospect
- fields = ['entity_name', 'first_name', 'last_name', 'email',
- 'phone', 'address', 'zip_code', 'city']
-
-class BulkEmailForm(forms.Form):
- prospects = forms.ModelMultipleChoiceField(
- queryset=Prospect.objects.all(),
- widget=forms.CheckboxSelectMultiple
- )
- subject = forms.CharField(max_length=200)
- content = forms.CharField(widget=forms.Textarea)
-
-class EventForm(forms.ModelForm):
- prospects = forms.ModelMultipleChoiceField(
- queryset=Prospect.objects.all(),
- widget=forms.SelectMultiple(attrs={'class': 'select2'}),
- required=False
- )
- description = forms.CharField(widget=SmallTextArea)
- attachment_text = forms.CharField(widget=SmallTextArea)
- class Meta:
- model = Event
- fields = ['date', 'type', 'description', 'attachment_text', 'prospects', 'status']
- widgets = {
- 'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
- }
+from .models import EmailTemplate
+
+# class SmallTextArea(forms.Textarea):
+# def __init__(self, *args, **kwargs):
+# kwargs.setdefault('attrs', {})
+# kwargs['attrs'].update({
+# 'rows': 2,
+# 'cols': 100,
+# 'style': 'height: 80px; width: 800px;'
+# })
+# super().__init__(*args, **kwargs)
+
+# class ProspectForm(forms.ModelForm):
+# class Meta:
+# model = Prospect
+# fields = ['entity_name', 'first_name', 'last_name', 'email',
+# 'phone', 'address', 'zip_code', 'city']
+
+# class BulkEmailForm(forms.Form):
+# prospects = forms.ModelMultipleChoiceField(
+# queryset=Prospect.objects.all(),
+# widget=forms.CheckboxSelectMultiple
+# )
+# subject = forms.CharField(max_length=200)
+# content = forms.CharField(widget=forms.Textarea)
+
+# class EventForm(forms.ModelForm):
+# prospects = forms.ModelMultipleChoiceField(
+# queryset=Prospect.objects.all(),
+# widget=forms.SelectMultiple(attrs={'class': 'select2'}),
+# required=False
+# )
+# description = forms.CharField(widget=SmallTextArea)
+# attachment_text = forms.CharField(widget=SmallTextArea)
+
+# class Meta:
+# model = Event
+# fields = ['date', 'type', 'description', 'attachment_text', 'prospects', 'status']
+# widgets = {
+# 'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
+# }
+
+class FileImportForm(forms.Form):
+ source = forms.CharField(max_length=200)
+ file = forms.FileField(
+ label='Select file to import',
+ help_text='Choose a file to upload and process',
+ widget=forms.FileInput(attrs={'accept': '.csv,.xlsx,.xls,.txt'})
+ )
class CSVImportForm(forms.Form):
csv_file = forms.FileField()
+
+class EmailTemplateSelectionForm(forms.Form):
+ email_template = forms.ModelChoiceField(
+ queryset=EmailTemplate.objects.all(),
+ empty_label="Select an email template...",
+ widget=forms.Select(attrs={'class': 'form-control'})
+ )
diff --git a/crm/migrations/0001_initial.py b/crm/migrations/0001_initial.py
index 66531fe..53b1496 100644
--- a/crm/migrations/0001_initial.py
+++ b/crm/migrations/0001_initial.py
@@ -1,9 +1,9 @@
-# Generated by Django 4.2.11 on 2024-12-08 15:10
+# Generated by Django 5.1 on 2025-07-09 13:33
+import django.db.models.deletion
+import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
-import django.db.models.deletion
-import uuid
class Migration(migrations.Migration):
@@ -16,79 +16,87 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='Prospect',
+ name='Activity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('email', models.EmailField(max_length=254, unique=True)),
- ('name', models.CharField(max_length=200)),
- ('region', models.CharField(max_length=100)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)),
- ],
- ),
- migrations.CreateModel(
- name='Status',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=100, unique=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ],
- ),
- migrations.CreateModel(
- name='ProspectStatus',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('prospect', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crm.prospect')),
- ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='crm.status')),
+ ('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
+ ('last_update', models.DateTimeField(default=django.utils.timezone.now)),
+ ('data_access_ids', models.JSONField(default=list)),
+ ('status', models.CharField(blank=True, choices=[('NONE', 'None'), ('CONTACTED', 'Contacted'), ('RESPONDED', 'Responded'), ('SHOULD_TEST', 'Should test'), ('TESTING', 'Testing'), ('CUSTOMER', 'Customer'), ('LOST', 'Lost customer'), ('DECLINED_TOO_EXPENSIVE', 'Too expensive'), ('DECLINED_USE_SOMETHING_ELSE', 'Use something else'), ('DECLINED_OTHER', 'Declined other reason'), ('DECLINED_UNRELATED', 'Declined without significance')], default='NONE', max_length=50, null=True)),
+ ('type', models.CharField(blank=True, choices=[('MAIL', 'Mailing List'), ('SMS', 'SMS Campaign'), ('CALL', 'Call'), ('PRESS', 'Press Release')], max_length=20, null=True)),
+ ('description', models.TextField(blank=True, null=True)),
+ ('attachment_text', models.TextField(blank=True, null=True)),
+ ('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
- 'ordering': ['-created_at'],
+ 'verbose_name_plural': 'Activities',
+ 'ordering': ['-creation_date'],
+ 'permissions': [('manage_events', 'Can manage events'), ('view_events', 'Can view events')],
},
),
migrations.CreateModel(
- name='Event',
+ name='EmailTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('date', models.DateTimeField()),
- ('type', models.CharField(choices=[('MAIL', 'Mailing List'), ('SMS', 'SMS Campaign'), ('PRESS', 'Press Release')], max_length=10)),
- ('description', models.TextField()),
- ('attachment_text', models.TextField(blank=True)),
- ('status', models.CharField(choices=[('PLANNED', 'Planned'), ('ACTIVE', 'Active'), ('COMPLETED', 'Completed')], max_length=20)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('prospects', models.ManyToManyField(related_name='events', to='crm.prospect')),
+ ('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
+ ('last_update', models.DateTimeField(default=django.utils.timezone.now)),
+ ('data_access_ids', models.JSONField(default=list)),
+ ('name', models.CharField(max_length=100)),
+ ('subject', models.CharField(max_length=200)),
+ ('body', models.TextField(blank=True, null=True)),
+ ('activities', models.ManyToManyField(blank=True, related_name='email_templates', to='crm.activity')),
+ ('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
- 'ordering': ['-created_at'],
+ 'abstract': False,
},
),
migrations.CreateModel(
- name='EmailCampaign',
+ name='Entity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('subject', models.CharField(max_length=200)),
- ('content', models.TextField()),
- ('sent_at', models.DateTimeField(blank=True, null=True)),
- ('event', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='crm.event')),
+ ('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
+ ('last_update', models.DateTimeField(default=django.utils.timezone.now)),
+ ('data_access_ids', models.JSONField(default=list)),
+ ('name', models.CharField(blank=True, max_length=200, null=True)),
+ ('address', models.CharField(blank=True, max_length=200, null=True)),
+ ('zip_code', models.CharField(blank=True, max_length=20, null=True)),
+ ('city', models.CharField(blank=True, max_length=500, null=True)),
+ ('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('official_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
+ options={
+ 'verbose_name_plural': 'Entities',
+ },
),
migrations.CreateModel(
- name='EmailTracker',
+ name='Prospect',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('tracking_id', models.UUIDField(default=uuid.uuid4, editable=False)),
- ('sent', models.BooleanField(default=False)),
- ('sent_at', models.DateTimeField(blank=True, null=True)),
- ('opened', models.BooleanField(default=False)),
- ('opened_at', models.DateTimeField(blank=True, null=True)),
- ('clicked', models.BooleanField(default=False)),
- ('clicked_at', models.DateTimeField(blank=True, null=True)),
- ('error_message', models.TextField(blank=True)),
- ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crm.emailcampaign')),
- ('prospect', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crm.prospect')),
+ ('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
+ ('last_update', models.DateTimeField(default=django.utils.timezone.now)),
+ ('data_access_ids', models.JSONField(default=list)),
+ ('first_name', models.CharField(blank=True, max_length=200, null=True)),
+ ('last_name', models.CharField(blank=True, max_length=200, null=True)),
+ ('email', models.EmailField(max_length=254, unique=True)),
+ ('phone', models.CharField(blank=True, max_length=25, null=True)),
+ ('name_unsure', models.BooleanField(default=False)),
+ ('source', models.CharField(blank=True, max_length=100, null=True)),
+ ('entities', models.ManyToManyField(blank=True, related_name='prospects', to='crm.entity')),
+ ('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('official_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
- 'unique_together': {('campaign', 'prospect')},
+ 'permissions': [('manage_prospects', 'Can manage prospects'), ('view_prospects', 'Can view prospects')],
},
),
+ migrations.AddField(
+ model_name='activity',
+ name='prospects',
+ field=models.ManyToManyField(related_name='activities', to='crm.prospect'),
+ ),
]
diff --git a/crm/migrations/0002_alter_event_options_alter_prospect_options_and_more.py b/crm/migrations/0002_alter_event_options_alter_prospect_options_and_more.py
deleted file mode 100644
index 92b7621..0000000
--- a/crm/migrations/0002_alter_event_options_alter_prospect_options_and_more.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Generated by Django 4.2.11 on 2024-12-08 20:58
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('crm', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='event',
- options={'ordering': ['-created_at'], 'permissions': [('manage_events', 'Can manage events'), ('view_events', 'Can view events')]},
- ),
- migrations.AlterModelOptions(
- name='prospect',
- options={'permissions': [('manage_prospects', 'Can manage prospects'), ('view_prospects', 'Can view prospects')]},
- ),
- migrations.AddField(
- model_name='event',
- name='created_by',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL),
- ),
- migrations.AddField(
- model_name='event',
- name='modified_at',
- field=models.DateTimeField(auto_now=True),
- ),
- migrations.AddField(
- model_name='event',
- name='modified_by',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_events', to=settings.AUTH_USER_MODEL),
- ),
- migrations.AddField(
- model_name='prospect',
- name='created_by',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_prospects', to=settings.AUTH_USER_MODEL),
- ),
- migrations.AddField(
- model_name='prospect',
- name='modified_at',
- field=models.DateTimeField(auto_now=True),
- ),
- migrations.AddField(
- model_name='prospect',
- name='modified_by',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_prospects', to=settings.AUTH_USER_MODEL),
- ),
- migrations.AlterField(
- model_name='event',
- name='date',
- field=models.DateTimeField(default=django.utils.timezone.now),
- ),
- ]
diff --git a/crm/migrations/0003_remove_prospect_region_prospect_address_and_more.py b/crm/migrations/0003_remove_prospect_region_prospect_address_and_more.py
deleted file mode 100644
index 7ed0884..0000000
--- a/crm/migrations/0003_remove_prospect_region_prospect_address_and_more.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Generated by Django 5.1 on 2024-12-16 15:43
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('crm', '0002_alter_event_options_alter_prospect_options_and_more'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='prospect',
- name='region',
- ),
- migrations.AddField(
- model_name='prospect',
- name='address',
- field=models.CharField(blank=True, max_length=200, null=True),
- ),
- migrations.AddField(
- model_name='prospect',
- name='city',
- field=models.CharField(blank=True, max_length=500, null=True),
- ),
- migrations.AddField(
- model_name='prospect',
- name='zip_code',
- field=models.CharField(blank=True, max_length=20, null=True),
- ),
- ]
diff --git a/crm/migrations/0004_remove_prospect_name_prospect_entity_name_and_more.py b/crm/migrations/0004_remove_prospect_name_prospect_entity_name_and_more.py
deleted file mode 100644
index 47776b9..0000000
--- a/crm/migrations/0004_remove_prospect_name_prospect_entity_name_and_more.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Generated by Django 5.1 on 2024-12-16 16:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('crm', '0003_remove_prospect_region_prospect_address_and_more'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='prospect',
- name='name',
- ),
- migrations.AddField(
- model_name='prospect',
- name='entity_name',
- field=models.CharField(blank=True, max_length=200, null=True),
- ),
- migrations.AddField(
- model_name='prospect',
- name='first_name',
- field=models.CharField(blank=True, max_length=200, null=True),
- ),
- migrations.AddField(
- model_name='prospect',
- name='last_name',
- field=models.CharField(blank=True, max_length=200, null=True),
- ),
- ]
diff --git a/crm/migrations/0005_prospect_phone.py b/crm/migrations/0005_prospect_phone.py
deleted file mode 100644
index 1b5a25f..0000000
--- a/crm/migrations/0005_prospect_phone.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.1 on 2024-12-16 16:49
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('crm', '0004_remove_prospect_name_prospect_entity_name_and_more'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='prospect',
- name='phone',
- field=models.CharField(blank=True, max_length=25, null=True),
- ),
- ]
diff --git a/crm/models.py b/crm/models.py
index 5028da6..86b5031 100644
--- a/crm/models.py
+++ b/crm/models.py
@@ -1,39 +1,64 @@
+from typing import Self
from django.db import models
from django.contrib.auth import get_user_model
+
+from django.db.models.signals import m2m_changed
+from django.dispatch import receiver
from django.utils import timezone
-import uuid
+
+from sync.models import BaseModel
User = get_user_model()
-class EventType(models.TextChoices):
- MAILING = 'MAIL', 'Mailing List'
+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_UNRELATED = 'DECLINED_UNRELATED', 'Declined without significance'
+
+class ActivityType(models.TextChoices):
+ MAIL = 'MAIL', 'Mailing List'
SMS = 'SMS', 'SMS Campaign'
+ CALL = 'CALL', 'Call'
PRESS = 'PRESS', 'Press Release'
-class Prospect(models.Model):
- email = models.EmailField(unique=True)
- entity_name = models.CharField(max_length=200, null=True, blank=True)
- first_name = models.CharField(max_length=200, null=True, blank=True)
- last_name = models.CharField(max_length=200, null=True, blank=True)
+class Entity(BaseModel):
+ name = models.CharField(max_length=200, null=True, blank=True)
address = models.CharField(max_length=200, null=True, blank=True)
zip_code = models.CharField(max_length=20, null=True, blank=True)
city = models.CharField(max_length=500, null=True, blank=True)
+ official_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
+ # status = models.IntegerField(default=Status.NONE, choices=Status.choices)
+
+ def delete_dependencies(self):
+ pass
+
+ class Meta:
+ verbose_name_plural = "Entities"
+
+ def __str__(self):
+ return self.name
+
+class Prospect(BaseModel):
+ first_name = models.CharField(max_length=200, null=True, blank=True)
+ last_name = models.CharField(max_length=200, null=True, blank=True)
+ email = models.EmailField(unique=True)
phone = models.CharField(max_length=25, null=True, blank=True)
- users = models.ManyToManyField(get_user_model(), blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
- created_by = models.ForeignKey(
- User,
- on_delete=models.SET_NULL,
- null=True,
- related_name='created_prospects'
- )
- modified_by = models.ForeignKey(
- User,
- on_delete=models.SET_NULL,
- null=True,
- related_name='modified_prospects'
- )
- modified_at = models.DateTimeField(auto_now=True)
+ name_unsure = models.BooleanField(default=False)
+ official_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
+
+ entities = models.ManyToManyField(Entity, blank=True, related_name='prospects')
+ source = models.CharField(max_length=100, null=True, blank=True)
+
+ def delete_dependencies(self):
+ pass
class Meta:
permissions = [
@@ -41,80 +66,89 @@ class Prospect(models.Model):
("view_prospects", "Can view prospects"),
]
+ def current_status(self):
+ last_activity = self.activities.exclude(status=None).order_by('-creation_date').first()
+ if last_activity:
+ return last_activity.status
+ return None
+
+ def entity_names(self):
+ entity_names = [entity.name for entity in self.entities.all()]
+ return " - ".join(entity_names)
+
def full_name(self):
return f'{self.first_name} {self.last_name}'
def __str__(self):
- return ' - '.join(filter(None, [self.entity_name, self.first_name, self.last_name, f"({self.email})"]))
+ return self.full_name()
-class Status(models.Model):
- name = models.CharField(max_length=100, unique=True)
- created_at = models.DateTimeField(auto_now_add=True)
+class Activity(BaseModel):
+ status = models.CharField(max_length=50, default=Status.NONE, choices=Status.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)
+ prospects = models.ManyToManyField(Prospect, related_name='activities')
def __str__(self):
- return self.name
+ if self.status:
+ return self.status
+ elif self.type:
+ return self.type
+ else:
+ return f'desc = {self.description}, attachment_text = {self.attachment_text}'
-class ProspectStatus(models.Model):
- prospect = models.ForeignKey(Prospect, on_delete=models.CASCADE)
- status = models.ForeignKey(Status, on_delete=models.PROTECT)
- created_at = models.DateTimeField(auto_now_add=True)
+ def delete_dependencies(self):
+ pass
class Meta:
- ordering = ['-created_at']
-
-class Event(models.Model):
- date = models.DateTimeField(default=timezone.now)
- type = models.CharField(max_length=10, choices=EventType.choices)
- description = models.TextField()
- attachment_text = models.TextField(blank=True)
- prospects = models.ManyToManyField(Prospect, related_name='events')
- status = models.CharField(max_length=20, choices=[
- ('PLANNED', 'Planned'),
- ('ACTIVE', 'Active'),
- ('COMPLETED', 'Completed'),
- ])
- created_at = models.DateTimeField(auto_now_add=True)
- created_by = models.ForeignKey(
- User,
- on_delete=models.SET_NULL,
- null=True,
- related_name='created_events'
- )
- modified_by = models.ForeignKey(
- User,
- on_delete=models.SET_NULL,
- null=True,
- related_name='modified_events'
- )
- modified_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- ordering = ['-created_at']
+ verbose_name_plural = "Activities"
+ ordering = ['-creation_date']
permissions = [
("manage_events", "Can manage events"),
("view_events", "Can view events"),
]
def __str__(self):
- return f"{self.get_type_display()} - {self.date.date()}"
-
-class EmailCampaign(models.Model):
- event = models.OneToOneField(Event, on_delete=models.CASCADE)
+ 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]
+ html = '
| {field} | ' + html += '
| Origin | +Has Purchase | +Date Joined | +|
|---|---|---|---|
| {{ user.email }} | +{{ user.get_origin_display }} | +{{ user.has_purchase|yesno:"Yes,No" }} | +{{ user.date_joined|date:"M d, Y" }} | +
| No users found matching criteria. | +|||
{% trans 'Select a file to import and click "Import File" to process it.' %}
+{% trans 'Supported file formats: CSV, Excel (XLSX, XLS), and Text files.' %}
+