odoo lead creation
This commit is contained in:
parent
b98a507f65
commit
483f076d1a
15 changed files with 404 additions and 27 deletions
|
@ -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
14
hub/services/forms.py
Normal 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"}),
|
||||
}
|
41
hub/services/migrations/0005_lead.py
Normal file
41
hub/services/migrations/0005_lead.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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
105
hub/services/odoo.py
Normal 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
|
|
@ -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">
|
||||
|
@ -19,7 +32,7 @@
|
|||
<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>
|
||||
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>
|
77
hub/services/templates/services/lead_form.html
Normal file
77
hub/services/templates/services/lead_form.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
37
hub/services/templates/services/thank_you.html
Normal file
37
hub/services/templates/services/thank_you.html
Normal 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 %}
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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
0
odoo_api.log
Normal 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
11
uv.lock
generated
|
@ -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" },
|
||||
]
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue