Implement template/form/view for org create
All checks were successful
Tests / test (push) Successful in 24s

This commit is contained in:
Tobias Kunze 2025-05-27 14:17:20 +02:00
parent e18cafa813
commit cdd8838b3d
7 changed files with 236 additions and 22 deletions

View file

@ -124,6 +124,19 @@ class BillingEntity(ServalaModelMixin, models.Model):
def __str__(self):
return self.name
@classmethod
def create_from_data(cls, odoo_data):
instance = BillingEntity.objects.create(name=odoo_data.get("name"))
# TODO implement odoo creation from data
return instance
@classmethod
def create_from_id(cls, odoo_id):
# TODO implement odoo creation from ID
# instance = BillingEntity.objects.create(name=odoo_data.get("name"))
# return instance
pass
class OrganizationOrigin(ServalaModelMixin, models.Model):
"""

View file

@ -97,6 +97,7 @@ def get_invoice_addresses(user):
# addresses / organizations created by the user
# - if the user is associated with an odoo contact, return all billing
# addresses with the same parent_id
email = user.email
or_conditions = [("email", "ilike", email)]
email = user if isinstance(user, str) else user.email

View file

@ -1,6 +1,9 @@
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
from servala.frontend.forms.mixins import HtmxMixin
@ -8,3 +11,102 @@ 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 'ba_'
ba_name = forms.CharField(
label=_("Contact Person / Company Name"), required=False, max_length=100
)
ba_street = forms.CharField(label=_("Street"), required=False, max_length=100)
ba_street2 = forms.CharField(
label=_("Street 2 (Optional)"), required=False, max_length=100
)
ba_city = forms.CharField(label=_("City"), required=False, max_length=100)
ba_zip = forms.CharField(label=_("ZIP Code"), required=False, max_length=20)
# For state & country, Odoo uses structured data. For now, text input.
# These will need mapping logic when actual Odoo creation is implemented.
ba_state_name = forms.CharField(
label=_("State / Province"), required=False, max_length=100
)
ba_country_name = forms.CharField(
label=_("Country"), required=False, max_length=100
)
ba_email = forms.EmailField(label=_("Billing Email"), required=False)
ba_phone = forms.CharField(label=_("Billing Phone"), required=False, max_length=30)
ba_vat = forms.CharField(label=_("VAT ID"), required=False, max_length=50)
class Meta(OrganizationForm.Meta):
pass
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.odoo_addresses = get_invoice_addresses(self.user)
if self.odoo_addresses:
address_choices = [("", _("---------"))]
for addr in self.odoo_addresses:
display_parts = [
addr.get("name"),
addr.get("street"),
addr.get("city"),
addr.get("zip"),
]
display_name = ", ".join(filter(None, display_parts))
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:
# No existing Odoo addresses. Force 'new' choice.
self.fields["billing_processing_choice"].choices = [
("new", _("Create a new billing address")),
]
self.fields["billing_processing_choice"].initial = "new"
self.fields["billing_processing_choice"].widget = forms.HiddenInput()
self.fields["existing_odoo_address_id"].widget = forms.HiddenInput()
def clean(self):
cleaned_data = super().clean()
choice = cleaned_data.get("billing_processing_choice")
if choice == "new":
required_fields = [
"ba_name",
"ba_street",
"ba_city",
"ba_zip",
"ba_state_name",
"ba_country_name",
"ba_email",
]
for field_name in required_fields:
if not cleaned_data.get(field_name):
self.add_error(
field_name,
_(
"This field is required when creating a new billing address."
),
)
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 existing address.")
)
return cleaned_data

View file

@ -0,0 +1,18 @@
{% load i18n %}
{% if form.non_field_errors or form.errors %}
<div class="alert alert-danger form-errors" role="alert">
<div>
{% if form.non_field_errors %}
{% if form.non_field_errors|length > 1 %}
<ul>
{% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% else %}
{{ form.non_field_errors.0 }}
{% endif %}
{% else %}
{% translate "We could not save your changes." %}
{% endif %}
</div>
</div>
{% endif %}

View file

@ -1,21 +1,4 @@
{% load i18n %}
{% if form.non_field_errors or form.errors %}
<div class="alert alert-danger form-errors" role="alert">
<div>
{% if form.non_field_errors %}
{% if form.non_field_errors|length > 1 %}
<ul>
{% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% else %}
{{ form.non_field_errors.0 }}
{% endif %}
{% else %}
{% translate "We could not save your changes." %}
{% endif %}
</div>
</div>
{% endif %}
{% include "frontend/forms/errors.html" %}
<div class="form-body">
<div class="row">
{% for field, errors in fields %}{{ field.as_field_group }}{% endfor %}

View file

@ -6,5 +6,67 @@
{% endblock page_title %}
{% endblock html_title %}
{% block card_content %}
{% include "includes/form.html" %}
<form method="post" class="form form-vertical">
{% include "frontend/forms/errors.html" %}
{% csrf_token %}
<div class="form-body">
<div class="row">
{{ form.name.as_field_group }}
<hr class="my-4">
<h4>{% translate "Billing Information" %}</h4>
{{ form.billing_processing_choice.as_field_group }}
<div id="existing_billing_address_section" class="mt-3">{{ form.existing_odoo_address_id.as_field_group }}</div>
<div id="new_billing_address_section" class="mt-3">
{{ form.ba_name.as_field_group }}
{{ form.ba_street.as_field_group }}
{{ form.ba_street2.as_field_group }}
{{ form.ba_city.as_field_group }}
{{ form.ba_zip.as_field_group }}
{{ form.ba_state_name.as_field_group }}
{{ form.ba_country_name.as_field_group }}
{{ form.ba_email.as_field_group }}
{{ form.ba_phone.as_field_group }}
{{ form.ba_vat.as_field_group }}
</div>
<div class="col-sm-12 d-flex justify-content-end">
<button class="btn btn-primary me-1 mb-1" type="submit">{% translate "Create Organization" %}</button>
</div>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const choiceRadios = document.querySelectorAll('[name="billing_processing_choice"]')
const existingSection = document.getElementById('existing_billing_address_section')
const newSection = document.getElementById('new_billing_address_section')
const existingAddressField = document.querySelector('[name="existing_odoo_address_id"]')
const toggleSections = () => {
let selectedValue = 'new'
const checkedRadio = document.querySelector('[name="billing_processing_choice"]:checked')
if (checkedRadio) {
selectedValue = checkedRadio.value
}
if (selectedValue === 'existing') {
if (existingSection) existingSection.style.display = '';
if (newSection) newSection.style.display = 'none';
} else {
if (existingSection) existingSection.style.display = 'none';
if (newSection) newSection.style.display = '';
}
}
if (choiceRadios.length > 0 && existingSection && newSection) {
toggleSections()
choiceRadios.forEach((radio) => {
radio.addEventListener('change', toggleSections)
})
} else {
// No existing addresses found, a new address has to be entered.
if (existingSection) existingSection.style.display = 'none'
newSection.style.display = ''
}
});
</script>
{% endblock card_content %}

View file

@ -1,18 +1,53 @@
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView
from rules.contrib.views import AutoPermissionRequiredMixin
from servala.core.models import Organization
from servala.frontend.forms import OrganizationForm
from servala.core.models import BillingEntity, Organization
from servala.frontend.forms.organization import OrganizationCreateForm, OrganizationForm
from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin
class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView):
form_class = OrganizationForm
form_class = OrganizationCreateForm
model = Organization
template_name = "frontend/organizations/create.html"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
billing_choice = form.cleaned_data.get("billing_processing_choice")
billing_entity = None
if billing_choice == "new":
billing_entity = BillingEntity.create_from_data(
{
key[3:]: value
for key, value in form.cleaned_data.items()
if key.startswith("ba_")
}
)
elif odoo_id := form.cleaned_data.get("existing_odoo_address_id"):
billing_entity = BillingEntity.objects.filter(
odoo_invoice_id=odoo_id
).first()
if not billing_entity:
billing_entity = BillingEntity.create_from_id(odoo_id)
if not billing_entity:
form.add_error(
None,
_(
"Could not determine or create the billing entity. Please check your input."
),
)
return self.form_invalid(form)
form.instance.billing_entity = billing_entity
instance = form.instance.create_organization(
form.instance, owner=self.request.user
)