wip for crm

sync
Laurent 11 months ago
parent 81fee53656
commit ff91b3023e
  1. 6
      crm/admin.py
  2. 8
      crm/filters.py
  3. 32
      crm/migrations/0003_remove_prospect_region_prospect_address_and_more.py
  4. 32
      crm/migrations/0004_remove_prospect_name_prospect_entity_name_and_more.py
  5. 18
      crm/migrations/0005_prospect_phone.py
  6. 11
      crm/models.py
  7. 33
      crm/templates/crm/add_prospect.html
  8. 34
      crm/templates/crm/csv_import.html
  9. 15
      crm/templates/crm/events.html
  10. 18
      crm/templates/crm/prospect_list.html
  11. 6
      crm/urls.py
  12. 84
      crm/views.py
  13. BIN
      db.sqlite3.main

@ -11,9 +11,9 @@ from .models import (
@admin.register(Prospect) @admin.register(Prospect)
class ProspectAdmin(admin.ModelAdmin): class ProspectAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'region', 'created_at') list_display = ('entity_name', 'first_name', 'last_name', 'email', 'address', 'zip_code', 'city', 'created_at')
list_filter = ('region', 'created_at') list_filter = ('zip_code', 'created_at')
search_fields = ('name', 'email', 'region') search_fields = ('entity_name', 'first_name', 'last_name', 'email', 'zip_code', 'city')
filter_horizontal = ('users',) filter_horizontal = ('users',)
date_hierarchy = 'created_at' date_hierarchy = 'created_at'

@ -3,16 +3,12 @@ import django_filters
from .models import Event, Status, Prospect from .models import Event, Status, Prospect
class ProspectFilter(django_filters.FilterSet): class ProspectFilter(django_filters.FilterSet):
region = django_filters.CharFilter(lookup_expr='icontains') zip_code = django_filters.CharFilter(lookup_expr='icontains')
events = django_filters.ModelMultipleChoiceFilter( events = django_filters.ModelMultipleChoiceFilter(
queryset=Event.objects.all(), queryset=Event.objects.all(),
field_name='events', field_name='events',
) )
statuses = django_filters.ModelMultipleChoiceFilter(
queryset=Status.objects.all(),
field_name='prospectstatus__status',
)
class Meta: class Meta:
model = Prospect model = Prospect
fields = ['region', 'events', 'statuses'] fields = ['city', 'events']

@ -0,0 +1,32 @@
# 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),
),
]

@ -0,0 +1,32 @@
# 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),
),
]

@ -0,0 +1,18 @@
# 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),
),
]

@ -12,8 +12,13 @@ class EventType(models.TextChoices):
class Prospect(models.Model): class Prospect(models.Model):
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
name = models.CharField(max_length=200) entity_name = models.CharField(max_length=200, null=True, blank=True)
region = models.CharField(max_length=100) first_name = models.CharField(max_length=200, null=True, blank=True)
last_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)
phone = models.CharField(max_length=25, null=True, blank=True)
users = models.ManyToManyField(get_user_model(), blank=True) users = models.ManyToManyField(get_user_model(), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey( created_by = models.ForeignKey(
@ -37,7 +42,7 @@ class Prospect(models.Model):
] ]
def __str__(self): def __str__(self):
return f"{self.name} ({self.email})" return ' - '.join(filter(None, [self.entity_name, self.first_name, self.last_name, f"({self.email})"]))
class Status(models.Model): class Status(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)

@ -7,8 +7,18 @@
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="name">Name:</label> <label for="entity_name">Entité (nom de club...):</label>
<input type="text" name="name" id="name" required /> <input type="text" name="entity_name" id="entity_name" />
</div>
<div class="form-group">
<label for="first_name">Prénom:</label>
<input type="text" name="first_name" id="first_name" />
</div>
<div class="form-group">
<label for="last_name">Nom:</label>
<input type="text" name="last_name" id="last_name" />
</div> </div>
<div class="form-group"> <div class="form-group">
@ -17,8 +27,23 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="region">Region:</label> <label for="phone">Téléphone:</label>
<input type="text" name="region" id="region" required /> <input type="text" name="phone" id="phone" />
</div>
<div class="form-group">
<label for="address">Adresse:</label>
<input type="text" name="address" id="address" />
</div>
<div class="form-group">
<label for="zip_code">Code postal:</label>
<input type="text" name="zip_code" id="zip_code" />
</div>
<div class="form-group">
<label for="city">Ville:</label>
<input type="text" name="city" id="city" />
</div> </div>
<button type="submit" class="small-button margin-v20"> <button type="submit" class="small-button margin-v20">

@ -0,0 +1,34 @@
{% extends "crm/base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Import Prospects from CSV</h2>
<div class="card">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
{{ form.as_p }}
</div>
<div class="alert alert-info">
<h5>CSV Format Requirements:</h5>
<p>The CSV file should contain the following columns in order:</p>
<ul>
<li>Column 1: Club Code</li>
<li>Column 2: Last Name</li>
<li>Column 3: First Name</li>
<li>Column 4: Email</li>
<li>Column 9: ZIP Code</li>
<li>Column 10: City</li>
</ul>
</div>
<button type="submit" class="btn btn-primary">Import CSV</button>
</form>
</div>
</div>
</div>
{% endblock %}

@ -7,11 +7,18 @@
{% if request.user|is_crm_manager %} {% if request.user|is_crm_manager %}
<div class="d-flex"> <div class="d-flex">
<a href="{% url 'crm:add_event' %}" class="small-button margin-v20">
Add Event <a href="{% url 'crm:prospect-list' %}" class="small-button margin-v20">
Prospects
</a>
<a href="{% url 'crm:add-event' %}" class="small-button margin-v20">
Ajouter un évènement
</a>
<a href="{% url 'crm:add-prospect' %}" class="small-button margin-v20 left-margin">
Ajouter un prospect
</a> </a>
<a href="{% url 'crm:add_prospect' %}" class="small-button margin-v20 left-margin"> <a href="{% url 'crm:csv-import' %}" class="small-button margin-v20 left-margin">
Add Prospect Import
</a> </a>
</div> </div>

@ -17,7 +17,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<a href="{% url 'crm:prospect-import' %}" class="btn btn-success">Import CSV</a> <a href="{% url 'crm:csv-import' %}" class="btn btn-success">Import CSV</a>
<a href="{% url 'crm:send-bulk-email' %}" class="btn btn-primary">Send Email</a> <a href="{% url 'crm:send-bulk-email' %}" class="btn btn-primary">Send Email</a>
</div> </div>
@ -26,10 +26,12 @@
<thead> <thead>
<tr> <tr>
<th><input type="checkbox" id="select-all"></th> <th><input type="checkbox" id="select-all"></th>
<th>Name</th> <th>Entité</th>
<th>Prénom</th>
<th>Nom</th>
<th>Email</th> <th>Email</th>
<th>Region</th> <th>Téléphone</th>
<th>Status</th> <th>Statut</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@ -37,9 +39,11 @@
{% for prospect in prospects %} {% for prospect in prospects %}
<tr> <tr>
<td><input type="checkbox" name="selected_prospects" value="{{ prospect.id }}"></td> <td><input type="checkbox" name="selected_prospects" value="{{ prospect.id }}"></td>
<td>{{ prospect.name }}</td> <td>{{ prospect.entity_name }}</td>
<td>{{ prospect.email }}</td> <td>{{ prospect.first_name }}</td>
<td>{{ prospect.region }}</td> <td>{{ prospect.last_name }}</td>
<td><a href="mailto:{{ prospect.email }}">{{ prospect.email }}</a></td>
<td>{{ prospect.phone }}</td>
<td> <td>
{% for status in prospect.prospectstatus_set.all %} {% for status in prospect.prospectstatus_set.all %}
<span class="badge bg-primary">{{ status.status.name }}</span> <span class="badge bg-primary">{{ status.status.name }}</span>

@ -5,11 +5,11 @@ app_name = 'crm'
urlpatterns = [ urlpatterns = [
path('', views.EventListView.as_view(), name='planned_events'),path('', views.EventListView.as_view(), name='planned-events'), path('', views.EventListView.as_view(), name='planned_events'),path('', views.EventListView.as_view(), name='planned-events'),
path('events/add/', views.EventCreateView.as_view(), name='add_event'), path('events/add/', views.EventCreateView.as_view(), name='add-event'),
path('events/<int:pk>/edit/', views.EditEventView.as_view(), name='edit_event'), path('events/<int:pk>/edit/', views.EditEventView.as_view(), name='edit_event'),
path('events/<int:pk>/start/', views.StartEventView.as_view(), name='start_event'), path('events/<int:pk>/start/', views.StartEventView.as_view(), name='start_event'),
path('prospects/', views.ProspectListView.as_view(), name='prospect-list'), path('prospects/', views.ProspectListView.as_view(), name='prospect-list'),
path('add-prospect/', views.add_prospect, name='add_prospect'), path('add-prospect/', views.add_prospect, name='add-prospect'),
path('prospects/import/', views.CSVImportView.as_view(), name='prospect-import'), path('prospects/import/', views.CSVImportView.as_view(), name='csv-import'),
path('email/send/', views.SendBulkEmailView.as_view(), name='send-bulk-email'), path('email/send/', views.SendBulkEmailView.as_view(), name='send-bulk-email'),
] ]

@ -13,6 +13,7 @@ from django.contrib.sites.shortcuts import get_current_site
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.mail import send_mail from django.core.mail import send_mail
from django.conf import settings from django.conf import settings
from django.db import IntegrityError
from .models import Event, Prospect, EmailTracker, EmailCampaign, EventType from .models import Event, Prospect, EmailTracker, EmailCampaign, EventType
from .filters import ProspectFilter from .filters import ProspectFilter
@ -27,15 +28,27 @@ from datetime import datetime
@permission_required('crm.view_crm', raise_exception=True) @permission_required('crm.view_crm', raise_exception=True)
def add_prospect(request): def add_prospect(request):
if request.method == 'POST': if request.method == 'POST':
name = request.POST.get('name') entity_name = request.POST.get('entity_name')
first_name = request.POST.get('first_name')
last_name = request.POST.get('last_name')
email = request.POST.get('email') email = request.POST.get('email')
region = request.POST.get('region') phone = request.POST.get('phone')
address = request.POST.get('address')
zip_code = request.POST.get('zip_code')
city = request.POST.get('city')
# region = request.POST.get('region')
try: try:
prospect = Prospect.objects.create( prospect = Prospect.objects.create(
name=name, entity_name=entity_name,
first_name=first_name,
last_name=last_name,
email=email, email=email,
region=region, phone=phone,
address=address,
zip_code=zip_code,
city=city,
# region=region,
created_by=request.user, created_by=request.user,
modified_by=request.user modified_by=request.user
) )
@ -126,6 +139,7 @@ class ProspectListView(CRMAccessMixin, ListView):
) )
return context return context
class CSVImportView(CRMAccessMixin, FormView): class CSVImportView(CRMAccessMixin, FormView):
template_name = 'crm/csv_import.html' template_name = 'crm/csv_import.html'
form_class = CSVImportForm form_class = CSVImportForm
@ -134,17 +148,63 @@ class CSVImportView(CRMAccessMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
csv_file = TextIOWrapper( csv_file = TextIOWrapper(
form.cleaned_data['csv_file'].file, form.cleaned_data['csv_file'].file,
encoding='utf-8' encoding='utf-8-sig' # Handle potential BOM in CSV
) )
reader = csv.DictReader(csv_file) reader = csv.reader(csv_file, delimiter=';') # Using semicolon delimiter
# Skip header if exists
next(reader, None)
created_count = 0
updated_count = 0
error_count = 0
for row in reader: for row in reader:
Prospect.objects.create( try:
email=row['email'], if len(row) < 10: # Ensure we have enough columns
name=row.get('name', ''), continue
region=row.get('region', ''),
created_by=self.request.user, # Extract data from correct columns
modified_by=self.request.user entity_name = row[0].strip()
last_name = row[1].strip()
first_name = row[2].strip()
email = row[3].strip()
phone = row[4].strip()
zip_code = row[8].strip()
city = row[9].strip()
# Try to update existing prospect or create new one
prospect, created = Prospect.objects.update_or_create(
email=email, # Use email as unique identifier
defaults={
'entity_name': entity_name,
'first_name': first_name,
'last_name': last_name,
'phone': phone,
'zip_code': zip_code,
'city': city,
'modified_by': self.request.user,
}
)
if created:
prospect.created_by = self.request.user
prospect.save()
created_count += 1
else:
updated_count += 1
except Exception as e:
error_count += 1
messages.error(
self.request,
f"Error processing row with email {email}: {str(e)}"
)
# Add success message
messages.success(
self.request,
f"Import completed: {created_count} created, {updated_count} updated, {error_count} errors"
) )
return super().form_valid(form) return super().form_valid(form)

Binary file not shown.
Loading…
Cancel
Save