diff --git a/.env.example b/.env.example index aa287ce..58eb7b3 100644 --- a/.env.example +++ b/.env.example @@ -60,9 +60,4 @@ SERVALA_KEYCLOAK_SERVER_URL='' # SERVALA_S3_SIGNATURE_VERSION='s3v4' # Configuration for Sentry error reporting -SERVALA_SENTRY_DSN='' - -SERVALA_ODOO_DB='' -SERVALA_ODOO_URL='' -SERVALA_ODOO_USERNAME='' -SERVALA_ODOO_PASSWORD='' +SERVALA_SENTRY_DSN='' \ No newline at end of file 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/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index cff0b83..447852e 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -4,7 +4,6 @@ * xref:web-portal.adoc[] ** xref:web-portal-admin.adoc[Admin] ** xref:web-portal-controlplanes.adoc[Control-Planes] -** xref:web-portal-billingentity.adoc[Billing Entities] * xref:web-portal-planning.adoc[] ** xref:user-stories.adoc[] diff --git a/docs/modules/ROOT/pages/web-portal-billingentity.adoc b/docs/modules/ROOT/pages/web-portal-billingentity.adoc deleted file mode 100644 index cd7b662..0000000 --- a/docs/modules/ROOT/pages/web-portal-billingentity.adoc +++ /dev/null @@ -1,26 +0,0 @@ -= Web Portal Billing Entities - -Billing entities are used to connect an invoice address in Odoo to an organization in Servala. - -When creating a new organization, the billing information is required to be added. - -== Existing Billing Address - -With the email address of the currently logged-in user, Odoo is searched for existing `res.partner` records and presented in the dropdown. - -Search is done this way: - -* `res.partner` records created by a matching Odoo user. -* User email matches an invoice address or contact address - -== New Billing Address - -When choosing to add a new billing address, two new records are created in the Odoo `res.partner` model: - -* A record with the field `company_type = company` -* A record with the following field configuration: -** `company_type = person` -** `type = invoice` -** `parent_id = company_id` - -The resulting database IDs are stored in the Servala portal database for referencing the records in Odoo. 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/migrations/0005_organization_sale_order_fields.py b/src/servala/core/migrations/0005_organization_sale_order_fields.py deleted file mode 100644 index d5f6b4d..0000000 --- a/src/servala/core/migrations/0005_organization_sale_order_fields.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.1 on 2025-06-15 16:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0004_billingentity_odoo_fields"), - ] - - operations = [ - migrations.AddField( - model_name="organization", - name="odoo_sale_order_id", - field=models.IntegerField( - blank=True, null=True, verbose_name="Odoo Sale Order ID" - ), - ), - migrations.AddField( - model_name="organization", - name="odoo_sale_order_name", - field=models.CharField( - blank=True, - max_length=100, - null=True, - verbose_name="Odoo Sale Order Name", - ), - ), - ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 1662dfa..7ebda11 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -1,7 +1,7 @@ import rules import urlman from django.conf import settings -from django.db import models, transaction +from django.db import models from django.utils.functional import cached_property from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -9,7 +9,6 @@ from django_scopes import ScopedManager, scopes_disabled from servala.core import rules as perms from servala.core.models.mixins import ServalaModelMixin -from servala.core.odoo import CLIENT class Organization(ServalaModelMixin, models.Model): @@ -46,13 +45,6 @@ class Organization(ServalaModelMixin, models.Model): verbose_name=_("Members"), ) - odoo_sale_order_id = models.IntegerField( - null=True, blank=True, verbose_name=_("Odoo Sale Order ID") - ) - odoo_sale_order_name = models.CharField( - max_length=100, null=True, blank=True, verbose_name=_("Odoo Sale Order Name") - ) - class urls(urlman.Urls): base = "/org/{self.slug}/" details = "{base}details/" @@ -74,7 +66,6 @@ class Organization(ServalaModelMixin, models.Model): ) @classmethod - @transaction.atomic def create_organization(cls, instance, owner): try: instance.origin @@ -84,32 +75,6 @@ class Organization(ServalaModelMixin, models.Model): ) instance.save() instance.set_owner(owner) - - if ( - instance.billing_entity.odoo_company_id - and instance.billing_entity.odoo_invoice_id - ): - payload = { - "partner_id": instance.billing_entity.odoo_company_id, - "partner_invoice_id": instance.billing_entity.odoo_invoice_id, - "state": "sale", - "client_order_ref": f"Servala (Organization: {instance.name})", - "internal_note": "auto-generated by Servala Portal", - } - sale_order_id = CLIENT.execute("sale.order", "create", [payload]) - - sale_order_data = CLIENT.search_read( - model="sale.order", - domain=[["id", "=", sale_order_id]], - fields=["name"], - limit=1, - ) - - instance.odoo_sale_order_id = sale_order_id - if sale_order_data: - instance.odoo_sale_order_name = sale_order_data[0]["name"] - instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"]) - return instance class Meta: @@ -145,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=invoice, 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") @@ -159,131 +117,6 @@ class BillingEntity(ServalaModelMixin, models.Model): def __str__(self): return self.name - @classmethod - @transaction.atomic - def create_from_data(cls, name, odoo_data): - """ - Creates a BillingEntity and corresponding Odoo records. - - This method creates a `res.partner` record in Odoo with `company_type='company'` - for the main company, and another `res.partner` record with `company_type='person'` - and `type='invoice'` (linked via `parent_id` to the first record) for the - invoice address. The IDs of these Odoo records are stored in the BillingEntity. - - Args: - odoo_data (dict): A dictionary containing the data for creating - the BillingEntity and Odoo records. - - Expected keys in `odoo_data`: - - `invoice_street` (str): Street for the invoice address. - - `invoice_street2` (str): Second line of street address for the invoice address. - - `invoice_city` (str): City for the invoice address. - - `invoice_zip` (str): ZIP/Postal code for the invoice address. - - `invoice_country` (int): Odoo `res.country` ID for the invoice address country. - - `invoice_email` (str): Email address for the invoice contact. - - `invoice_phone` (str): Phone number for the invoice contact. - """ - instance = cls.objects.create(name=name) - company_payload = { - "name": odoo_data.get("company_name", name), - "company_type": "company", - } - company_id = CLIENT.execute("res.partner", "create", [company_payload]) - instance.odoo_company_id = company_id - - invoice_address_payload = { - "name": name, - "company_type": "person", - "type": "invoice", - "parent_id": company_id, - } - invoice_optional_fields = { - "street": odoo_data.get("invoice_street"), - "street2": odoo_data.get("invoice_street2"), - "city": odoo_data.get("invoice_city"), - "zip": odoo_data.get("invoice_zip"), - "country_id": odoo_data.get("invoice_country"), - "email": odoo_data.get("invoice_email"), - } - invoice_address_payload.update( - {k: v for k, v in invoice_optional_fields.items() if v is not None} - ) - - invoice_address_id = CLIENT.execute( - "res.partner", "create", [invoice_address_payload] - ) - instance.odoo_invoice_id = invoice_address_id - - instance.save(update_fields=["odoo_company_id", "odoo_invoice_id"]) - return instance - - @classmethod - @transaction.atomic - def create_from_id(cls, name, odoo_id): - parent_data = CLIENT.search_read( - model="res.partner", - domain=[["id", "=", odoo_id]], - fields=["parent_id"], - limit=1, - ) - # Data validation: If the data is not as expected, we just return None, - # rather than raising an exception, for now. - if not parent_data: - return - if not (parent_info := parent_data[0].get("parent_id")): - return - if not isinstance(parent_info, (list, tuple)) or not len(parent_info) > 0: - # parent_info is a tuple of the parent’s ID and name - return - - instance = cls.objects.create( - name=name, odoo_invoice_id=odoo_id, odoo_company_id=parent_info[0] - ) - return instance - - @cached_property - def odoo_data(self): - data = { - "company": None, - "invoice_address": None, - } - - company_fields = ["name", "company_type"] - invoice_address_fields = [ - "name", - "company_type", - "type", - "parent_id", - "street", - "street2", - "city", - "zip", - "country_id", - "email", - ] - - if self.odoo_company_id: - company_records = CLIENT.search_read( - model="res.partner", - domain=[["id", "=", self.odoo_company_id]], - fields=company_fields, - limit=1, - ) - if company_records: - data["company"] = company_records[0] - - if self.odoo_invoice_id: - invoice_address_records = CLIENT.search_read( - model="res.partner", - domain=[["id", "=", self.odoo_invoice_id]], - fields=invoice_address_fields, - limit=1, - ) - if invoice_address_records: - data["invoice_address"] = invoice_address_records[0] - - return data - class OrganizationOrigin(ServalaModelMixin, models.Model): """ diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 6eb912e..ba57c9c 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -210,35 +210,15 @@ class ControlPlane(ServalaModelMixin, models.Model): except Exception as e: return False, _("Connection error: {}").format(str(e)) - def get_or_create_namespace(self, organization): + def get_or_create_namespace(self, name): api_instance = kubernetes.client.CoreV1Api(self.get_kubernetes_client()) - name = organization.namespace try: api_instance.read_namespace(name=name) except kubernetes.client.ApiException as e: if e.status == 404: - labels = { - "servala.com/organization_id": str(organization.id), - } - annotations = { - "servala.com/organization": organization.name, - "servala.com/origin": organization.origin.name, - "servala.com/billing": organization.billing_entity.name, - } - - for field in ("company_id", "invoice_id"): - if value := getattr(organization.billing_entity, f"odoo_{field}"): - labels[f"servala.com/erp_{field}"] = str(value) - - if organization.odoo_sale_order_id: - labels["servala.com/erp_sale_order_id"] = str( - organization.odoo_sale_order_id - ) - + # Namespace does not exist, create it body = kubernetes.client.V1Namespace( - metadata=kubernetes.client.V1ObjectMeta( - name=name, labels=labels, annotations=annotations - ) + metadata=kubernetes.client.V1ObjectMeta(name=name) ) api_instance.create_namespace(body=body) else: @@ -571,7 +551,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): @classmethod def create_instance(cls, name, organization, context, created_by, spec_data): # Ensure the namespace exists - context.control_plane.get_or_create_namespace(organization) + context.control_plane.get_or_create_namespace(organization.namespace) try: instance = cls.objects.create( name=name, 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 3d98e18..0000000 --- a/src/servala/core/odoo.py +++ /dev/null @@ -1,209 +0,0 @@ -import xmlrpc.client - -from django.conf import settings -from django_scopes import scopes_disabled - -ADDRESS_FIELDS = [ - "id", - "name", - "street", - "street2", - "city", - "zip", - "state_id", - "country_id", - "email", - "phone", - "vat", - "company_type", - "type", - "parent_id", -] - - -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() - -# Odoo countries do not change, so they are fetched once per process -COUNTRIES = [] - - -def get_odoo_countries(): - global COUNTRIES - if COUNTRIES: - return COUNTRIES - - try: - odoo_countries_data = CLIENT.search_read( - model="res.country", domain=[], fields=["id", "name"] - ) - # Format as Django choices: [(value, label), ...] - COUNTRIES = [ - (country["id"], country["name"]) for country in odoo_countries_data - ] - # Sort by country name for better UX in dropdowns - COUNTRIES.sort(key=lambda x: x[1]) - except Exception as e: - # Log the error or handle it as appropriate for your application - # For now, return an empty list or a default if Odoo is unavailable - print(f"Error fetching Odoo countries: {e}") - return [("", "Error fetching countries")] # Or just [] - - return COUNTRIES - - -def get_odoo_access_conditions(user): - # We’re building our conditions in order: - # - in exceptions, users may be using a billing account’s email - # - if the user is an admin or owner of a Servala organization - # - 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 - from servala.core.models.organization import ( - OrganizationMembership, - OrganizationRole, - ) - - email = user.email - or_conditions = [("email", "ilike", email)] - - odoo_users = CLIENT.search_read( - model="res.users", - domain=[("login", "=", email), ("active", "=", True)], - fields=["id", "share"], - limit=1, - ) - - if odoo_users: - odoo_user = odoo_users[0] - if odoo_user.get("share") is False: - # An Odoo internal user (share=False) should see all invoice addresses, - # so we short-circuit the entire search logic here - return [] - elif uid := odoo_user.get("id"): - # For portal users or users not in Odoo, apply standard filters. - 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])) - - with scopes_disabled(): - servala_invoice_ids = list( - OrganizationMembership.objects.filter( - user=user, role__in=[OrganizationRole.ADMIN, OrganizationRole.OWNER] - ) - .values_list("organization__billing_entity__odoo_invoice_id", flat=True) - .distinct() - ) - servala_invoice_ids = [pk for pk in servala_invoice_ids if pk] - if servala_invoice_ids: - or_conditions.append(("id", "in", servala_invoice_ids)) - - if len(or_conditions) > 1: - or_conditions = ["|"] * (len(or_conditions) - 1) + or_conditions - return or_conditions - - -def get_invoice_addresses(user): - """Used during organization creation: retrieves all invoice - addresses the user owns or is connected to from the Odoo API.""" - - or_conditions = get_odoo_access_conditions(user) - domain = [ - ("company_type", "=", "person"), - ("type", "=", "invoice"), - ] + or_conditions - - try: - invoice_addresses = CLIENT.search_read( - model="res.partner", - domain=domain, - fields=ADDRESS_FIELDS, - ) - if invoice_addresses: - invoice_addresses.sort( - key=lambda addr: ( - addr["parent_id"][1] if addr.get("parent_id") else "", - addr["name"], - addr["id"], - ) - ) - return invoice_addresses or [] - except Exception: - return [] diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 915ad7b..41cc26c 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -1,9 +1,6 @@ -from django import forms from django.forms import ModelForm -from django.utils.translation import gettext_lazy as _ from servala.core.models import Organization -from servala.core.odoo import get_invoice_addresses, get_odoo_countries from servala.frontend.forms.mixins import HtmxMixin @@ -11,100 +8,3 @@ class OrganizationForm(HtmxMixin, ModelForm): class Meta: model = Organization fields = ("name",) - - -class OrganizationCreateForm(OrganizationForm): - billing_processing_choice = forms.ChoiceField( - choices=[ - ("existing", _("Use an existing billing address")), - ("new", _("Create a new billing address")), - ], - widget=forms.RadioSelect, - label=_("Billing Address"), - initial="new", # Will change to 'existing' if options are found - ) - existing_odoo_address_id = forms.ChoiceField( - label=_("Existing Billing Address"), - required=False, - ) - - # Fields for creating a new billing address in Odoo, prefixed with 'invoice_' - invoice_street = forms.CharField(label=_("Line 1"), required=False, max_length=100) - invoice_street2 = forms.CharField(label=_("Line 2"), required=False, max_length=100) - invoice_city = forms.CharField(label=_("City"), required=False, max_length=100) - invoice_zip = forms.CharField(label=_("Postal Code"), required=False, max_length=20) - invoice_country = forms.ChoiceField( - label=_("Country"), - required=False, - choices=get_odoo_countries(), - ) - invoice_email = forms.EmailField(label=_("Billing Email"), required=False) - invoice_phone = forms.CharField(label=_("Phone"), required=False, max_length=30) - - class Meta(OrganizationForm.Meta): - pass - - def __init__(self, *args, user=None, **kwargs): - super().__init__(*args, **kwargs) - - if not self.initial.get("invoice_country"): - default_country_name = "Switzerland" - country_choices = self.fields["invoice_country"].choices - for country_id, country_name_label in country_choices: - if country_name_label == default_country_name: - self.initial["invoice_country"] = country_id - break - - self.user = user - self.odoo_addresses = get_invoice_addresses(self.user) - - if self.odoo_addresses: - address_choices = [("", _("---------"))] - for addr in self.odoo_addresses: - parent_name = None - parent_info = addr.get("parent_id") - if ( - parent_info - and isinstance(parent_info, (list, tuple)) - and len(parent_info) > 1 - ): - parent_name = parent_info[1] - - display_parts = [ - parent_name, - addr.get("name"), - addr.get("street"), - addr.get("city"), - addr.get("zip"), - ] - display_name = ", ".join([part for part in display_parts if part]) - address_choices.append((str(addr["id"]), display_name)) - - self.fields["existing_odoo_address_id"].choices = address_choices - if not self.is_bound and "billing_processing_choice" not in self.initial: - self.fields["billing_processing_choice"].initial = "existing" - else: - self.fields.pop("billing_processing_choice") - self.fields["existing_odoo_address_id"].widget = forms.HiddenInput() - - def clean(self): - cleaned_data = super().clean() - choice = cleaned_data.get("billing_processing_choice") - if not choice or choice == "new": - required_fields = [ - "invoice_street", - "invoice_city", - "invoice_zip", - "invoice_country", - "invoice_email", - ] - for field_name in required_fields: - if not cleaned_data.get(field_name): - self.add_error(field_name, _("This field is required.")) - else: - existing_id_str = cleaned_data.get("existing_odoo_address_id") - if not existing_id_str: - self.add_error( - "existing_odoo_address_id", _("Please select an invoice address.") - ) - return cleaned_data diff --git a/src/servala/frontend/forms/renderers.py b/src/servala/frontend/forms/renderers.py index eb50961..b6a9995 100644 --- a/src/servala/frontend/forms/renderers.py +++ b/src/servala/frontend/forms/renderers.py @@ -20,8 +20,6 @@ class VerticalFormRenderer(TemplatesSetting): def get_class_names(self, field): input_type = self.get_field_input_type(field) errors = "is-invalid " if field.errors else "" - if input_type == "radio": - return f"{errors}form-check-input" if input_type == "checkbox": return f"{errors}form-check-input" return f"{errors}form-control" diff --git a/src/servala/frontend/templates/django/forms/widgets/input_option.html b/src/servala/frontend/templates/django/forms/widgets/input_option.html deleted file mode 100644 index 9a7654d..0000000 --- a/src/servala/frontend/templates/django/forms/widgets/input_option.html +++ /dev/null @@ -1,6 +0,0 @@ -{% include "django/forms/widgets/input.html" %} -{% if widget.wrap_label %} - -{% endif %} diff --git a/src/servala/frontend/templates/django/forms/widgets/radio.html b/src/servala/frontend/templates/django/forms/widgets/radio.html deleted file mode 100644 index 1f363b9..0000000 --- a/src/servala/frontend/templates/django/forms/widgets/radio.html +++ /dev/null @@ -1,15 +0,0 @@ -{# Change compared to Django: only render widget.attrs.class in actual option widget, not in wrapper #} -{% with id=widget.attrs.id %} -
- {% translate "Name" %} - | - {% partial org-name %} -|
---|---|
- {% translate "Namespace" %} - | -
- {{ form.instance.namespace }}
- {% translate "System-generated namespace for Kubernetes resources." %}
- |
-
- {% translate "Invoice Contact Name" %} - | -{{ odoo_data.invoice_address.name|default:"" }} | -
---|---|
- {% translate "Street" %} - | -{{ odoo_data.invoice_address.street|default:"" }} | -
- {% translate "Street 2" %} - | -{{ odoo_data.invoice_address.street2 }} | -
- {% translate "City" %} - | -{{ odoo_data.invoice_address.city|default:"" }} | -
- {% translate "ZIP Code" %} - | -{{ odoo_data.invoice_address.zip|default:"" }} | -
- {% translate "Country" %} - | -{{ odoo_data.invoice_address.country_id.1|default:"" }} | -- {% translate "Invoice Email" %} - | -{{ odoo_data.invoice_address.email|default:"" }} | - - {% endif %} - -
+ {% translate "Name" %} + | + {% partial org-name %} +|
---|---|
+ {% translate "Namespace" %} + | +
+ {{ form.instance.namespace }}
+ {% translate "System-generated namespace for Kubernetes resources." %}
+ |
+