odoo lead creation

This commit is contained in:
Tobias Brunner 2025-01-27 16:51:23 +01:00
parent b98a507f65
commit 483f076d1a
No known key found for this signature in database
15 changed files with 404 additions and 27 deletions

View file

@ -130,3 +130,35 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
ODOO_CONFIG = {
"url": env.str("ODOO_URL"),
"db": env.str("ODOO_DB"),
"username": env.str("ODOO_USERNAME"),
"password": env.str("ODOO_PASSWORD"),
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
}
},
"loggers": {
"odoo_api": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
},
}

14
hub/services/forms.py Normal file
View file

@ -0,0 +1,14 @@
from django import forms
from .models import Lead
class LeadForm(forms.ModelForm):
class Meta:
model = Lead
fields = ["name", "company", "email", "phone"]
widgets = {
"name": forms.TextInput(attrs={"class": "form-control"}),
"company": forms.TextInput(attrs={"class": "form-control"}),
"email": forms.EmailInput(attrs={"class": "form-control"}),
"phone": forms.TextInput(attrs={"class": "form-control"}),
}

View file

@ -0,0 +1,41 @@
# Generated by Django 5.1.5 on 2025-01-27 15:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("services", "0004_cloudprovider_slug"),
]
operations = [
migrations.CreateModel(
name="Lead",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200)),
("company", models.CharField(max_length=200)),
("email", models.EmailField(max_length=254)),
("phone", models.CharField(max_length=50)),
("created_at", models.DateTimeField(auto_now_add=True)),
("odoo_lead_id", models.IntegerField(blank=True, null=True)),
(
"service",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="services.service",
),
),
],
),
]

View file

@ -107,3 +107,16 @@ class Service(models.Model):
def __str__(self):
return self.name
class Lead(models.Model):
service = models.ForeignKey(Service, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
company = models.CharField(max_length=200)
email = models.EmailField()
phone = models.CharField(max_length=50)
created_at = models.DateTimeField(auto_now_add=True)
odoo_lead_id = models.IntegerField(null=True, blank=True)
def __str__(self):
return f"{self.name} - {self.company} ({self.service})"

105
hub/services/odoo.py Normal file
View file

@ -0,0 +1,105 @@
import odoorpc
import logging
from django.conf import settings
from urllib.parse import urlparse
# Set up logging
logger = logging.getLogger(__name__)
class OdooAPI:
def __init__(self):
self.odoo = None
self._connect()
def _connect(self):
"""Establish connection to Odoo with detailed error logging"""
try:
# Parse URL to get host
url = settings.ODOO_CONFIG["url"]
parsed_url = urlparse(url)
host = parsed_url.netloc or parsed_url.path # If no netloc, use path
# Log connection attempt
logger.info(f"Attempting to connect to Odoo at {host}")
logger.debug(
f"Connection parameters: HOST={host}, DB={settings.ODOO_CONFIG['db']}, "
f"USER={settings.ODOO_CONFIG['username']}"
)
# Try to establish connection
self.odoo = odoorpc.ODOO(host, port=443, protocol="jsonrpc+ssl")
# Try to login
logger.info("Connection established, attempting login...")
self.odoo.login(
settings.ODOO_CONFIG["db"],
settings.ODOO_CONFIG["username"],
settings.ODOO_CONFIG["password"],
)
logger.info("Successfully logged into Odoo")
# Test the connection by making a simple API call
version_info = self.odoo.version
logger.info(f"Connected to Odoo version: {version_info}")
except odoorpc.error.RPCError as e:
logger.error(f"RPC Error connecting to Odoo: {str(e)}")
logger.debug("Full RPC error details:", exc_info=True)
raise
except odoorpc.error.InternalError as e:
logger.error(f"Internal Odoo error: {str(e)}")
logger.debug("Full internal error details:", exc_info=True)
raise
except Exception as e:
logger.error(f"Unexpected error connecting to Odoo: {str(e)}")
logger.debug("Full error details:", exc_info=True)
raise
def create_lead(self, lead):
"""Create a lead in Odoo with detailed logging"""
try:
logger.info(
f"Attempting to create lead for {lead.name} from {lead.company}"
)
# Prepare lead data
lead_data = {
"name": f"Interest in {lead.service.name}",
"contact_name": lead.name,
"partner_name": lead.company,
"email_from": lead.email,
"phone": lead.phone,
"description": f"""
Service: {lead.service.name}
Provider: {lead.service.cloud_provider.name}
Categories: {', '.join(cat.name for cat in lead.service.categories.all())}
""",
"type": "lead",
}
logger.debug(f"Prepared lead data: {lead_data}")
# Ensure we have a valid connection
if not self.odoo:
logger.warning("No active Odoo connection, attempting to reconnect")
self._connect()
# Get the CRM lead model
Lead = self.odoo.env["crm.lead"]
logger.debug("Successfully got CRM lead model")
# Create the lead
odoo_lead_id = Lead.create(lead_data)
logger.info(f"Successfully created lead in Odoo with ID: {odoo_lead_id}")
return odoo_lead_id
except odoorpc.error.RPCError as e:
logger.error(f"RPC Error creating lead: {str(e)}")
logger.debug("Full RPC error details:", exc_info=True)
raise
except Exception as e:
logger.error(f"Unexpected error creating lead: {str(e)}")
logger.debug("Full error details:", exc_info=True)
raise

View file

@ -1,13 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Servala - The Cloud Native Services Hub</title>
<title>Services Marketplace</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.rich-text-content {
overflow-wrap: break-word;
word-wrap: break-word;
}
.rich-text-content img {
max-width: 100%;
height: auto;
}
.description-preview img {
max-width: 100%;
height: auto;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
@ -18,8 +31,8 @@
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.view_name == 'services:service_list' %}active{% endif %}"
href="{% url 'services:service_list' %}">Services</a>
<a class="nav-link {% if request.resolver_match.view_name == 'services:service_list' %}active{% endif %}"
href="{% url 'services:service_list' %}">Services</a>
</li>
</ul>
</div>
@ -32,25 +45,5 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_css %}
<style>
.rich-text-content {
overflow-wrap: break-word;
word-wrap: break-word;
}
.rich-text-content img {
max-width: 100%;
height: auto;
}
.description-preview img {
max-width: 100%;
height: auto;
}
</style>
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,77 @@
{% extends 'services/base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<h2 class="card-title mb-4">Show Interest in {{ service.name }}</h2>
<div class="mb-4">
<h5>Service Details</h5>
<p><strong>Provider:</strong> {{ service.cloud_provider.name }}</p>
<p><strong>Service Level:</strong> {{ service.service_level.name }}</p>
<p><strong>Price:</strong> ${{ service.price }}</p>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">Name</label>
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{{ form.name.errors }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.company.id_for_label }}" class="form-label">Company</label>
{{ form.company }}
{% if form.company.errors %}
<div class="invalid-feedback d-block">
{{ form.company.errors }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">Email</label>
{{ form.email }}
{% if form.email.errors %}
<div class="invalid-feedback d-block">
{{ form.email.errors }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">Phone</label>
{{ form.phone }}
{% if form.phone.errors %}
<div class="invalid-feedback d-block">
{{ form.phone.errors }}
</div>
{% endif %}
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url 'services:service_detail' service.id %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -59,6 +59,7 @@
</div>
</div>
<a href="{% url 'services:create_lead' service.id %}" class="btn btn-success">Show Interest</a>
<a href="{% url 'services:service_list' %}" class="btn btn-secondary">Back to Services</a>
</div>
</div>

View file

@ -113,6 +113,7 @@
</small>
</p>
<a href="{% url 'services:service_detail' service.pk %}" class="btn btn-primary">View Details</a>
<a href="{% url 'services:create_lead' service.id %}" class="btn btn-success">Show Interest</a>
</div>
</div>
</div>
@ -126,4 +127,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends 'services/base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-body text-center">
<h2 class="card-title mb-4">Thank You!</h2>
<div class="mb-4">
<p class="lead">Thank you for your interest in {{ service.name }}!</p>
<p>We have received your inquiry and our team will contact you shortly.</p>
</div>
<div class="mb-4">
<h5>Service Details</h5>
<p><strong>Provider:</strong> {{ service.cloud_provider.name }}</p>
<p><strong>Service Level:</strong> {{ service.service_level.name }}</p>
</div>
<div class="mb-4">
<p class="text-muted">A confirmation email will be sent to your provided email address.</p>
</div>
<div class="mt-4">
<a href="{% url 'services:service_detail' service.id %}" class="btn btn-primary me-2">
Back to Service Details
</a>
<a href="{% url 'services:service_list' %}" class="btn btn-secondary">
Browse More Services
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -6,5 +6,7 @@ app_name = "services"
urlpatterns = [
path("", views.service_list, name="service_list"),
path("service/<int:pk>/", views.service_detail, name="service_detail"),
path("service/<int:service_id>/interest/", views.create_lead, name="create_lead"),
path("service/<int:service_id>/thank-you/", views.thank_you, name="thank_you"),
path("provider/<slug:slug>/", views.provider_detail, name="provider_detail"),
]

View file

@ -1,7 +1,16 @@
from django.shortcuts import render, get_object_or_404
import logging
from django.conf import settings
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.db.models import Q
from .models import Service, CloudProvider, Country, ServiceLevel, Category
from .forms import LeadForm
from .odoo import OdooAPI
logger = logging.getLogger(__name__)
def service_list(request):
services = Service.objects.all()
@ -59,3 +68,43 @@ def provider_detail(request, slug):
"services": services,
}
return render(request, "services/provider_detail.html", context)
def thank_you(request, service_id):
service = get_object_or_404(Service, id=service_id)
return render(request, "services/thank_you.html", {"service": service})
def create_lead(request, service_id):
service = get_object_or_404(Service, id=service_id)
if request.method == "POST":
form = LeadForm(request.POST)
if form.is_valid():
lead = form.save(commit=False)
lead.service = service
try:
logger.info(f"Attempting to create lead for service: {service.name}")
odoo = OdooAPI()
odoo_lead_id = odoo.create_lead(lead)
lead.odoo_lead_id = odoo_lead_id
lead.save()
logger.info(f"Successfully created lead with Odoo ID: {odoo_lead_id}")
return redirect("services:thank_you", service_id=service.id)
except Exception as e:
logger.error(f"Failed to create lead: {str(e)}", exc_info=True)
error_message = "Sorry, there was an error processing your request. Please try again later."
if settings.DEBUG:
error_message += f" Error: {str(e)}"
messages.error(request, error_message)
else:
form = LeadForm()
return render(
request, "services/lead_form.html", {"form": form, "service": service}
)

0
odoo_api.log Normal file
View file

View file

@ -8,6 +8,7 @@ dependencies = [
"django>=5.1.5",
"django-prose-editor[sanitize]>=0.10.3",
"environs[django]~=14.0",
"odoorpc>=0.10.1",
"pillow>=11.1.0",
]

11
uv.lock generated
View file

@ -161,6 +161,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/31/d65594efd3b42b1de2335d576eb77525691fc320dbf8617948ee05c008e5/nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b", size = 541249 },
]
[[package]]
name = "odoorpc"
version = "0.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/0a/9f907fbfefd2486bb4a3faab03f094f03b300b413004f348a3583ecc1898/OdooRPC-0.10.1.tar.gz", hash = "sha256:d0bc524c5b960781165575bad9c13d032d6f968c3c09276271045ddbbb483aa5", size = 58086 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/60/8c5ea2a63151d6c1215e127909eeee3c16e792bbae92ab596dd921d6669d/OdooRPC-0.10.1-py2.py3-none-any.whl", hash = "sha256:a0900bdd5c989c414b1ef40dafccd9363f179312d9166d9486cf70c7c2f0dd44", size = 38482 },
]
[[package]]
name = "packaging"
version = "24.2"
@ -214,6 +223,7 @@ dependencies = [
{ name = "django" },
{ name = "django-prose-editor", extra = ["sanitize"] },
{ name = "environs", extra = ["django"] },
{ name = "odoorpc" },
{ name = "pillow" },
]
@ -228,6 +238,7 @@ requires-dist = [
{ name = "django-browser-reload", marker = "extra == 'dev'", specifier = "~=1.13" },
{ name = "django-prose-editor", extras = ["sanitize"], specifier = ">=0.10.3" },
{ name = "environs", extras = ["django"], specifier = "~=14.0" },
{ name = "odoorpc", specifier = ">=0.10.1" },
{ name = "pillow", specifier = ">=11.1.0" },
]