diff --git a/.env.example b/.env.example index 1bbba50..13c1e64 100644 --- a/.env.example +++ b/.env.example @@ -58,8 +58,3 @@ SERVALA_KEYCLOAK_SERVER_URL='' # SERVALA_S3_REGION_NAME='eu-central-1' # SERVALA_S3_ADDRESSING_STYLE='virtual' # SERVALA_S3_SIGNATURE_VERSION='s3v4' - -SERVALA_ODOO_DB='' -SERVALA_ODOO_URL='' -SERVALA_ODOO_USERNAME='' -SERVALA_ODOO_PASSWORD='' diff --git a/README.md b/README.md index 7241854..df8d74c 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,6 @@ uv run --env-file=.env src/manage.py COMMAND Useful commands: - ``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. - ``runserver``: Run development server - ``clearsessions``: Clear away expired user sessions. Recommended to run regularly, e.g. weekly or monthly (doesn’t diff --git a/src/servala/core/apps.py b/src/servala/core/apps.py index f7cbde2..ad73de8 100644 --- a/src/servala/core/apps.py +++ b/src/servala/core/apps.py @@ -4,6 +4,3 @@ from django.apps import AppConfig class CoreConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "servala.core" - - def ready(self): - import servala.core.checks # noqa diff --git a/src/servala/core/checks.py b/src/servala/core/checks.py deleted file mode 100644 index d1bb708..0000000 --- a/src/servala/core/checks.py +++ /dev/null @@ -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", - ) - ) diff --git a/src/servala/core/migrations/0004_billingentity_odoo_fields.py b/src/servala/core/migrations/0004_billingentity_odoo_fields.py deleted file mode 100644 index bcfee2d..0000000 --- a/src/servala/core/migrations/0004_billingentity_odoo_fields.py +++ /dev/null @@ -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), - ), - ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 201a8b9..7ebda11 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -110,13 +110,6 @@ class BillingEntity(ServalaModelMixin, models.Model): 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: verbose_name = _("Billing entity") verbose_name_plural = _("Billing entities") diff --git a/src/servala/core/models/user.py b/src/servala/core/models/user.py index 5d513f4..0084593 100644 --- a/src/servala/core/models/user.py +++ b/src/servala/core/models/user.py @@ -6,8 +6,7 @@ from django.contrib.auth.models import ( from django.db import models from django.utils.translation import gettext_lazy as _ -from servala.core import odoo -from servala.core.models.mixins import ServalaModelMixin +from .mixins import ServalaModelMixin class UserManager(BaseUserManager): @@ -74,22 +73,3 @@ class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser): def normalize_username(self, username): 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] diff --git a/src/servala/core/odoo.py b/src/servala/core/odoo.py deleted file mode 100644 index 81a24bc..0000000 --- a/src/servala/core/odoo.py +++ /dev/null @@ -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 [] diff --git a/src/servala/settings.py b/src/servala/settings.py index 9db4b4f..c5905c2 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -87,6 +87,7 @@ SOCIALACCOUNT_PROVIDERS = { } } + SERVALA_STORAGE_BUCKET_NAME = os.environ.get("SERVALA_STORAGE_BUCKET_NAME") SERVALA_S3_ENDPOINT_URL = os.environ.get("SERVALA_S3_ENDPOINT_URL") 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 # #######################################