diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 87da376..c0beb9e 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -63,7 +63,6 @@ class OrganizationAdmin(admin.ModelAdmin): search_fields = ("name", "namespace") autocomplete_fields = ("billing_entity", "origin") inlines = (OrganizationMembershipInline,) - filter_horizontal = ("limit_osb_services",) def get_readonly_fields(self, request, obj=None): readonly_fields = list(super().get_readonly_fields(request, obj) or []) @@ -83,10 +82,9 @@ class BillingEntityAdmin(admin.ModelAdmin): @admin.register(OrganizationOrigin) class OrganizationOriginAdmin(admin.ModelAdmin): - list_display = ("name", "billing_entity", "default_odoo_sale_order_id") + list_display = ("name", "billing_entity") search_fields = ("name",) autocomplete_fields = ("billing_entity",) - filter_horizontal = ("limit_cloudproviders",) @admin.register(OrganizationMembership) diff --git a/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py b/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py new file mode 100644 index 0000000..3ec1032 --- /dev/null +++ b/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2025-10-16 22:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0008_organization_osb_guid_service_osb_service_id_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="organization", + name="limit_cloudproviders", + field=models.ManyToManyField( + blank=True, + related_name="+", + to="core.cloudprovider", + verbose_name="Limit to these Cloud providers", + ), + ), + migrations.AddField( + model_name="organization", + name="limit_osb_services", + field=models.ManyToManyField( + blank=True, + related_name="+", + to="core.service", + verbose_name="Services activated from OSB", + ), + ), + ] diff --git a/src/servala/core/migrations/0010_organizationorigin_billing_entity.py b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py new file mode 100644 index 0000000..d61a75f --- /dev/null +++ b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2025-10-17 00:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0009_organization_limit_cloudproviders_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="organizationorigin", + name="billing_entity", + field=models.ForeignKey( + help_text="If set, this billing entity will be used on new organizations with this origin.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="origins", + to="core.billingentity", + verbose_name="Billing entity", + ), + ), + ] diff --git a/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py b/src/servala/core/migrations/0011_organizationinvitation.py similarity index 53% rename from src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py rename to src/servala/core/migrations/0011_organizationinvitation.py index 811c843..25fb4b1 100644 --- a/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py +++ b/src/servala/core/migrations/0011_organizationinvitation.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-22 09:38 +# Generated by Django 5.2.7 on 2025-10-17 00:58 import django.db.models.deletion import rules.contrib.models @@ -9,88 +9,10 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("core", "0008_organization_osb_guid_service_osb_service_id_and_more"), + ("core", "0010_organizationorigin_billing_entity"), ] operations = [ - migrations.AddField( - model_name="controlplane", - name="wildcard_dns", - field=models.CharField( - blank=True, - help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)", - max_length=255, - null=True, - verbose_name="Wildcard DNS", - ), - ), - migrations.AddField( - model_name="organization", - name="limit_osb_services", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="core.service", - verbose_name="Services activated from OSB", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="billing_entity", - field=models.ForeignKey( - help_text="If set, this billing entity will be used on new organizations with this origin.", - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="origins", - to="core.billingentity", - verbose_name="Billing entity", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="default_odoo_sale_order_id", - field=models.IntegerField( - blank=True, - help_text="If set, this sale order will be used for new organizations with this origin.", - null=True, - verbose_name="Default Odoo Sale Order ID", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="limit_cloudproviders", - field=models.ManyToManyField( - blank=True, - help_text="If set, all organizations with this origin will be limited to these cloud providers.", - related_name="+", - to="core.cloudprovider", - verbose_name="Limit to these Cloud providers", - ), - ), - migrations.AddField( - model_name="servicedefinition", - name="advanced_fields", - field=models.JSONField( - blank=True, - default=list, - help_text=( - "Array of field names that should be hidden behind an 'Advanced' toggle." - "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" - ), - null=True, - verbose_name="Advanced fields", - ), - ), - migrations.AddField( - model_name="serviceoffering", - name="external_links", - field=models.JSONField( - blank=True, - help_text='JSON array of link objects: {"url": "…", "title": "…"}. ', - null=True, - verbose_name="External links", - ), - ), migrations.CreateModel( name="OrganizationInvitation", fields=[ diff --git a/src/servala/core/migrations/0012_serviceoffering_external_links.py b/src/servala/core/migrations/0012_serviceoffering_external_links.py new file mode 100644 index 0000000..d8c2ac7 --- /dev/null +++ b/src/servala/core/migrations/0012_serviceoffering_external_links.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-10-17 02:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0011_organizationinvitation"), + ] + + operations = [ + migrations.AddField( + model_name="serviceoffering", + name="external_links", + field=models.JSONField( + blank=True, + help_text='JSON array of link objects: {"url": "…", "title": "…"}. ', + null=True, + verbose_name="External links", + ), + ), + ] diff --git a/src/servala/core/migrations/0013_controlplane_wildcard_dns.py b/src/servala/core/migrations/0013_controlplane_wildcard_dns.py new file mode 100644 index 0000000..26dbaaf --- /dev/null +++ b/src/servala/core/migrations/0013_controlplane_wildcard_dns.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2025-10-17 02:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0012_serviceoffering_external_links"), + ] + + operations = [ + migrations.AddField( + model_name="controlplane", + name="wildcard_dns", + field=models.CharField( + blank=True, + help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)", + max_length=255, + null=True, + verbose_name="Wildcard DNS", + ), + ), + ] diff --git a/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py b/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py new file mode 100644 index 0000000..20632b9 --- /dev/null +++ b/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2025-10-17 03:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0013_controlplane_wildcard_dns"), + ] + + operations = [ + migrations.AddField( + model_name="servicedefinition", + name="advanced_fields", + field=models.JSONField( + blank=True, + default=list, + help_text=( + "Array of field names that should be hidden behind an 'Advanced' toggle. " + "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" + ), + null=True, + verbose_name="Advanced fields", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 2bc76ff..bbcc16f 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -52,6 +52,12 @@ class Organization(ServalaModelMixin, models.Model): related_name="organizations", verbose_name=_("Members"), ) + limit_cloudproviders = models.ManyToManyField( + to="CloudProvider", + related_name="+", + verbose_name=_("Limit to these Cloud providers"), + blank=True, + ) limit_osb_services = models.ManyToManyField( to="Service", related_name="+", @@ -93,14 +99,6 @@ class Organization(ServalaModelMixin, models.Model): def has_inherited_billing_entity(self): return self.origin and self.billing_entity == self.origin.billing_entity - @property - def limit_cloudproviders(self): - if self.origin: - return self.origin.limit_cloudproviders.all() - from servala.core.models import CloudProvider - - return CloudProvider.objects.none() - def set_owner(self, user): with scopes_disabled(): OrganizationMembership.objects.filter(user=user, organization=self).delete() @@ -129,20 +127,7 @@ class Organization(ServalaModelMixin, models.Model): if owner: instance.set_owner(owner) - if instance.origin and instance.origin.default_odoo_sale_order_id: - sale_order_id = instance.origin.default_odoo_sale_order_id - 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"]) - elif ( + if ( instance.billing_entity.odoo_company_id and instance.billing_entity.odoo_invoice_id ): @@ -176,8 +161,9 @@ class Organization(ServalaModelMixin, models.Model): if self.limit_osb_services.exists(): queryset = self.limit_osb_services.all() if self.limit_cloudproviders.exists(): + allowed_providers = self.limit_cloudproviders.all() queryset = queryset.filter( - offerings__provider__in=self.limit_cloudproviders + offerings__provider__in=allowed_providers ).distinct() return queryset.prefetch_related( "offerings", "offerings__provider" @@ -191,8 +177,9 @@ class Organization(ServalaModelMixin, models.Model): queryset = Service.objects.select_related("category") if self.limit_cloudproviders.exists(): + allowed_providers = self.limit_cloudproviders.all() queryset = queryset.filter( - offerings__provider__in=self.limit_cloudproviders + offerings__provider__in=allowed_providers ).distinct() queryset = queryset.exclude(id__in=self.limit_osb_services.all()) return queryset.prefetch_related("offerings", "offerings__provider") @@ -389,23 +376,6 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): ), null=True, ) - limit_cloudproviders = models.ManyToManyField( - to="CloudProvider", - related_name="+", - verbose_name=_("Limit to these Cloud providers"), - blank=True, - help_text=_( - "If set, all organizations with this origin will be limited to these cloud providers." - ), - ) - default_odoo_sale_order_id = models.IntegerField( - null=True, - blank=True, - verbose_name=_("Default Odoo Sale Order ID"), - help_text=_( - "If set, this sale order will be used for new organizations with this origin." - ), - ) class Meta: verbose_name = _("Organization origin") diff --git a/src/servala/frontend/forms/service.py b/src/servala/frontend/forms/service.py index 23325f3..5dd78a7 100644 --- a/src/servala/frontend/forms/service.py +++ b/src/servala/frontend/forms/service.py @@ -21,15 +21,6 @@ class ServiceFilterForm(forms.Form): ) q = forms.CharField(label=_("Search"), required=False) - def __init__(self, *args, organization=None, **kwargs): - super().__init__(*args, **kwargs) - if organization and organization.limit_cloudproviders.exists(): - allowed_providers = organization.limit_cloudproviders - if allowed_providers.count() <= 1: - self.fields.pop("cloud_provider", None) - else: - self.fields["cloud_provider"].queryset = allowed_providers - def filter_queryset(self, queryset): if category := self.cleaned_data.get("category"): queryset = queryset.filter(category=category) diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html index 3250c52..461d37d 100644 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ b/src/servala/frontend/templates/frontend/organizations/services.html @@ -20,12 +20,10 @@ {% for service in services %}
{% include "includes/service_card.html" %}
{% empty %} -
-
-
-
-

{% translate "No services found." %}

-
+
+
+
+

{% translate "No services found." %}

diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 689f381..ba4f0a4 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -44,9 +44,7 @@ class ServiceListView(OrganizationViewMixin, ListView): @cached_property def filter_form(self): - return ServiceFilterForm( - data=self.request.GET or None, organization=self.request.organization - ) + return ServiceFilterForm(data=self.request.GET or None) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs)