Compare commits
No commits in common. "e18cafa813d5d94fdd067b7d510b079ee53a255a" and "d8cc90188e4a6662e5a0bbe3493c57397f4e0da6" have entirely different histories.
e18cafa813
...
d8cc90188e
9 changed files with 2 additions and 298 deletions
|
@ -58,8 +58,3 @@ SERVALA_KEYCLOAK_SERVER_URL=''
|
||||||
# SERVALA_S3_REGION_NAME='eu-central-1'
|
# SERVALA_S3_REGION_NAME='eu-central-1'
|
||||||
# SERVALA_S3_ADDRESSING_STYLE='virtual'
|
# SERVALA_S3_ADDRESSING_STYLE='virtual'
|
||||||
# SERVALA_S3_SIGNATURE_VERSION='s3v4'
|
# SERVALA_S3_SIGNATURE_VERSION='s3v4'
|
||||||
|
|
||||||
SERVALA_ODOO_DB=''
|
|
||||||
SERVALA_ODOO_URL=''
|
|
||||||
SERVALA_ODOO_USERNAME=''
|
|
||||||
SERVALA_ODOO_PASSWORD=''
|
|
||||||
|
|
|
@ -129,7 +129,6 @@ uv run --env-file=.env src/manage.py COMMAND
|
||||||
Useful commands:
|
Useful commands:
|
||||||
|
|
||||||
- ``migrate``: Make sure database migrations are applied.
|
- ``migrate``: Make sure database migrations are applied.
|
||||||
- ``check --deploy``: Runs checks, e.g. for missing or mismatched configuration, including custom servala configuration.
|
|
||||||
- ``showmigrations``: Show current database migrations status. Good for debugging.
|
- ``showmigrations``: Show current database migrations status. Good for debugging.
|
||||||
- ``runserver``: Run development server
|
- ``runserver``: Run development server
|
||||||
- ``clearsessions``: Clear away expired user sessions. Recommended to run regularly, e.g. weekly or monthly (doesn’t
|
- ``clearsessions``: Clear away expired user sessions. Recommended to run regularly, e.g. weekly or monthly (doesn’t
|
||||||
|
|
|
@ -4,6 +4,3 @@ from django.apps import AppConfig
|
||||||
class CoreConfig(AppConfig):
|
class CoreConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "servala.core"
|
name = "servala.core"
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
import servala.core.checks # noqa
|
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core.checks import ERROR, WARNING, CheckMessage, register
|
|
||||||
|
|
||||||
|
|
||||||
@register()
|
|
||||||
def check_servala_settings(app_configs, **kwargs):
|
|
||||||
"""Checks all settings that should be present in all environments."""
|
|
||||||
if app_configs:
|
|
||||||
# Don’t run if we’re meant to only test individual apps
|
|
||||||
return []
|
|
||||||
|
|
||||||
errors = []
|
|
||||||
required_fields = ("URL", "DB", "USERNAME", "PASSWORD")
|
|
||||||
missing_fields = [
|
|
||||||
field for field in required_fields if not settings.ODOO.get(field)
|
|
||||||
]
|
|
||||||
if missing_fields:
|
|
||||||
fields = ", ".join(missing_fields)
|
|
||||||
errors.append(
|
|
||||||
CheckMessage(
|
|
||||||
level=WARNING if settings.DEBUG else ERROR,
|
|
||||||
msg=f"Missing Odoo config: {fields}",
|
|
||||||
hint="Make sure you set the required SERVALA_ODOO_* settings.",
|
|
||||||
id="servala.E001",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
oidc_config = settings.SOCIALACCOUNT_PROVIDERS["openid_connect"]["APPS"][0]
|
|
||||||
missing_fields = [
|
|
||||||
field for field in ("client_id", "secret") if not oidc_config.get(field)
|
|
||||||
]
|
|
||||||
if not oidc_config["settings"]["server_url"]:
|
|
||||||
missing_fields.append("server_url")
|
|
||||||
if missing_fields:
|
|
||||||
fields = ", ".join(
|
|
||||||
[f"SERVALA_KEYCLOAK_{field.upper()}" for field in missing_fields]
|
|
||||||
)
|
|
||||||
errors.append(
|
|
||||||
CheckMessage(
|
|
||||||
level=WARNING if settings.DEBUG else ERROR,
|
|
||||||
msg=f"Missing Keycloak config: {fields}",
|
|
||||||
id="servala.E002",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if settings.SERVALA_ENVIRONMENT not in ("development", "staging", "production"):
|
|
||||||
errors.append(
|
|
||||||
CheckMessage(
|
|
||||||
level=ERROR,
|
|
||||||
msg=f"Invalid environment {settings.SERVALA_ENVIRONMENT}",
|
|
||||||
hint="Must be one of development, staging, production.",
|
|
||||||
id="servala.E003",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
@register(deploy=True)
|
|
||||||
def check_servala_production_settings(app_configs, **kwargs):
|
|
||||||
if app_configs:
|
|
||||||
# Don’t run if we’re meant to only test individual apps
|
|
||||||
return []
|
|
||||||
|
|
||||||
errors = []
|
|
||||||
if settings.SERVALA_ENVIRONMENT == "development":
|
|
||||||
errors.append(
|
|
||||||
CheckMessage(
|
|
||||||
level=ERROR,
|
|
||||||
msg="Environment is set to 'development'.",
|
|
||||||
id="servala.E004",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if "insecure" in settings.SECRET_KEY:
|
|
||||||
errors.append(
|
|
||||||
CheckMessage(
|
|
||||||
level=ERROR, msg="Secret key contains 'insecure'.", id="servala.E005"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if settings.EMAIL_USE_SSL and settings.EMAIL_USE_TLS:
|
|
||||||
errors.append(
|
|
||||||
CheckMessage(
|
|
||||||
level=WARNING,
|
|
||||||
msg="Use either SSL or TLS in email config, not both!",
|
|
||||||
id="servala.W001",
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 5.2 on 2025-05-26 05:08
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("core", "0003_alter_organization_namespace"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="billingentity",
|
|
||||||
name="odoo_company_id",
|
|
||||||
field=models.IntegerField(null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="billingentity",
|
|
||||||
name="odoo_invoice_id",
|
|
||||||
field=models.IntegerField(null=True),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -110,13 +110,6 @@ class BillingEntity(ServalaModelMixin, models.Model):
|
||||||
max_length=100, blank=True, verbose_name=_("ERP reference")
|
max_length=100, blank=True, verbose_name=_("ERP reference")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Odoo IDs are nullable for creation, should never be null in practice
|
|
||||||
# The company ID points at a record of type res.partner with company_type=company
|
|
||||||
# The invoice ID points at a record of type res.partner with company_type=person,
|
|
||||||
# type=invoic, parent_id=company_id (the invoice address).
|
|
||||||
odoo_company_id = models.IntegerField(null=True)
|
|
||||||
odoo_invoice_id = models.IntegerField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Billing entity")
|
verbose_name = _("Billing entity")
|
||||||
verbose_name_plural = _("Billing entities")
|
verbose_name_plural = _("Billing entities")
|
||||||
|
|
|
@ -6,8 +6,7 @@ from django.contrib.auth.models import (
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from servala.core import odoo
|
from .mixins import ServalaModelMixin
|
||||||
from servala.core.models.mixins import ServalaModelMixin
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
|
@ -74,22 +73,3 @@ class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||||
|
|
||||||
def normalize_username(self, username):
|
def normalize_username(self, username):
|
||||||
return super().normalize_username(username).strip().lower()
|
return super().normalize_username(username).strip().lower()
|
||||||
|
|
||||||
def get_odoo_contact(self, organization):
|
|
||||||
if (
|
|
||||||
not organization.billing_entity
|
|
||||||
or not organization.billing_entity.odoo_company_id
|
|
||||||
):
|
|
||||||
return
|
|
||||||
result = odoo.CLIENT.search_read(
|
|
||||||
model="res.partner",
|
|
||||||
domain=[
|
|
||||||
("company_type", "=", "person"),
|
|
||||||
("type", "=", "contact"),
|
|
||||||
("email", "ilike", self.email),
|
|
||||||
("parent_id", "=", organization.billing_entity.odoo_company_id),
|
|
||||||
],
|
|
||||||
fields=odoo.ADDRESS_FIELDS,
|
|
||||||
)
|
|
||||||
if result:
|
|
||||||
return result[0]
|
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
import xmlrpc.client
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
ADDRESS_FIELDS = [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"street",
|
|
||||||
"street2",
|
|
||||||
"city",
|
|
||||||
"zip",
|
|
||||||
"state_id",
|
|
||||||
"country_id",
|
|
||||||
"email",
|
|
||||||
"phone",
|
|
||||||
"vat",
|
|
||||||
"company_type",
|
|
||||||
"type",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class OdooClient:
|
|
||||||
def __init__(self):
|
|
||||||
self.url = settings.ODOO["URL"]
|
|
||||||
self.db = settings.ODOO["DB"]
|
|
||||||
self.username = settings.ODOO["USERNAME"]
|
|
||||||
self.password = settings.ODOO["PASSWORD"]
|
|
||||||
|
|
||||||
self.common_proxy = None
|
|
||||||
self.models_proxy = None
|
|
||||||
self.uid = None
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
"""This method is called on the first client request, not on instantiation,
|
|
||||||
so that we can instantiate the client on startup and reuse it across the entire
|
|
||||||
application."""
|
|
||||||
try:
|
|
||||||
self.common_proxy = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
|
|
||||||
self.uid = self.common_proxy.authenticate(
|
|
||||||
self.db, self.username, self.password, {}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.uid:
|
|
||||||
raise Exception("Authentication failed with Odoo: No UID returned.")
|
|
||||||
|
|
||||||
self.models_proxy = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/object")
|
|
||||||
|
|
||||||
except xmlrpc.client.Fault as e:
|
|
||||||
raise Exception(
|
|
||||||
f"Odoo XML-RPC Fault during connection: {e.faultString}"
|
|
||||||
) from e
|
|
||||||
except ConnectionRefusedError as e:
|
|
||||||
raise Exception(
|
|
||||||
f"Could not connect to Odoo at {self.url}. Connection refused."
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(
|
|
||||||
f"An error occurred while connecting to Odoo: {str(e)}"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
def execute(self, model, method, args_list, **kwargs):
|
|
||||||
if not self.uid or not self.models_proxy:
|
|
||||||
self._connect()
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.models_proxy.execute_kw(
|
|
||||||
self.db, self.uid, self.password, model, method, args_list, kwargs
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
except xmlrpc.client.Fault as e:
|
|
||||||
print(f"Fault! {e}")
|
|
||||||
raise Exception(f"Odoo XML-RPC Fault: {e.faultString}") from e
|
|
||||||
except ConnectionRefusedError as e:
|
|
||||||
raise Exception(
|
|
||||||
f"Connection to Odoo at {self.url} lost or refused during operation."
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise Exception(
|
|
||||||
f"An error occurred while communicating with Odoo: {str(e)}"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
def search_read(self, model, domain, fields, **kwargs):
|
|
||||||
return self.execute(model, "search_read", args_list=[domain, fields], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
CLIENT = OdooClient()
|
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_addresses(user):
|
|
||||||
"""Used during organization creation: retrieves all invoice
|
|
||||||
addresses the user owns or is connected to from the Odoo API."""
|
|
||||||
# We’re building our conditions in order:
|
|
||||||
# - in exceptions, users may be using a billing account’s email
|
|
||||||
# - if the user is associated with an odoo user, return all billing
|
|
||||||
# addresses / organizations created by the user
|
|
||||||
# - if the user is associated with an odoo contact, return all billing
|
|
||||||
# addresses with the same parent_id
|
|
||||||
or_conditions = [("email", "ilike", email)]
|
|
||||||
|
|
||||||
email = user if isinstance(user, str) else user.email
|
|
||||||
odoo_users = CLIENT.search_read(
|
|
||||||
model="res.users",
|
|
||||||
domain=[("login", "=", email)],
|
|
||||||
fields=["id"],
|
|
||||||
limit=1,
|
|
||||||
)
|
|
||||||
if odoo_users and (uid := odoo_users[0].get("id")):
|
|
||||||
or_conditions.append(("create_uid", "=", uid))
|
|
||||||
|
|
||||||
odoo_contacts = CLIENT.search_read(
|
|
||||||
model="res.partner",
|
|
||||||
domain=[
|
|
||||||
("company_type", "=", "person"),
|
|
||||||
("type", "=", "contact"),
|
|
||||||
("email", "ilike", email),
|
|
||||||
],
|
|
||||||
fields=["id", "parent_id"],
|
|
||||||
)
|
|
||||||
if odoo_contacts:
|
|
||||||
for contact in odoo_contacts:
|
|
||||||
or_conditions.append(("parent_id", "=", contact["parent_id"][0]))
|
|
||||||
|
|
||||||
if len(or_conditions) > 1:
|
|
||||||
or_conditions = ["|"] * (len(or_conditions) - 1) + or_conditions
|
|
||||||
|
|
||||||
# The domain requires the partner to be an invoice address, that is:
|
|
||||||
# Of the company_type=person, and type=invoice.
|
|
||||||
# If we were searching for an existing organization, we would also have to
|
|
||||||
# filter for parent_id=odoo_company_id
|
|
||||||
domain = [
|
|
||||||
("company_type", "=", "person"),
|
|
||||||
("type", "=", "invoice"),
|
|
||||||
] + or_conditions
|
|
||||||
|
|
||||||
try:
|
|
||||||
invoice_addresses = CLIENT.search_read(
|
|
||||||
model="res.partner",
|
|
||||||
domain=domain,
|
|
||||||
fields=ADDRESS_FIELDS,
|
|
||||||
)
|
|
||||||
return invoice_addresses or []
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
|
@ -87,6 +87,7 @@ SOCIALACCOUNT_PROVIDERS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
SERVALA_STORAGE_BUCKET_NAME = os.environ.get("SERVALA_STORAGE_BUCKET_NAME")
|
SERVALA_STORAGE_BUCKET_NAME = os.environ.get("SERVALA_STORAGE_BUCKET_NAME")
|
||||||
SERVALA_S3_ENDPOINT_URL = os.environ.get("SERVALA_S3_ENDPOINT_URL")
|
SERVALA_S3_ENDPOINT_URL = os.environ.get("SERVALA_S3_ENDPOINT_URL")
|
||||||
SERVALA_ACCESS_KEY_ID = os.environ.get("SERVALA_ACCESS_KEY_ID")
|
SERVALA_ACCESS_KEY_ID = os.environ.get("SERVALA_ACCESS_KEY_ID")
|
||||||
|
@ -119,13 +120,6 @@ if all(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ODOO = {
|
|
||||||
"URL": os.environ.get("SERVALA_ODOO_URL"),
|
|
||||||
"DB": os.environ.get("SERVALA_ODOO_DB"),
|
|
||||||
"USERNAME": os.environ.get("SERVALA_ODOO_USERNAME"),
|
|
||||||
"PASSWORD": os.environ.get("SERVALA_ODOO_PASSWORD"),
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
# Non-configurable settings below #
|
# Non-configurable settings below #
|
||||||
#######################################
|
#######################################
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue