Implement template/form/view for org create
All checks were successful
Tests / test (push) Successful in 24s
All checks were successful
Tests / test (push) Successful in 24s
This commit is contained in:
parent
e18cafa813
commit
cdd8838b3d
7 changed files with 236 additions and 22 deletions
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
18
src/servala/frontend/templates/frontend/forms/errors.html
Normal file
18
src/servala/frontend/templates/frontend/forms/errors.html
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue