Invoice issuer address: Add state field (#5603)

* Invoice issuer address: Add state field

* Update src/pretix/base/settings.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/base/models/invoices.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2025-11-14 09:56:46 +01:00
committed by GitHub
parent 5583298322
commit eb740204d4
13 changed files with 126 additions and 9 deletions

View File

@@ -209,6 +209,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + pgettext('address', 'State'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
@@ -291,6 +292,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_state,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.24 on 2025-11-10 16:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0295_user_is_verified"),
]
operations = [
migrations.AddField(
model_name="invoice",
name="invoice_from_state",
field=models.CharField(max_length=190, null=True),
),
]

View File

@@ -142,6 +142,7 @@ class Invoice(models.Model):
invoice_from_name = models.CharField(max_length=190, null=True)
invoice_from_zipcode = models.CharField(max_length=190, null=True)
invoice_from_city = models.CharField(max_length=190, null=True)
invoice_from_state = models.CharField(max_length=190, null=True)
invoice_from_country = FastCountryField(null=True)
invoice_from_tax_id = models.CharField(max_length=190, null=True)
invoice_from_vat_id = models.CharField(max_length=190, null=True)
@@ -218,10 +219,23 @@ class Invoice(models.Model):
taxidrow = "ABN: %s" % self.invoice_from_tax_id
else:
taxidrow = pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id
state_name = ""
if self.invoice_from_state:
state_name = self.invoice_from_state
if str(self.invoice_from_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_from_country)][1] == 'long':
try:
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_from_country, self.invoice_from_state)
).name
except:
pass
parts = [
self.invoice_from_name,
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
((self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or "") + " " + (state_name or "")).strip(),
self.invoice_from_country.name if self.invoice_from_country else "",
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
taxidrow,
@@ -230,10 +244,22 @@ class Invoice(models.Model):
@property
def address_invoice_from(self):
state_name = ""
if self.invoice_from_state:
state_name = self.invoice_from_state
if str(self.invoice_from_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_from_country)][1] == 'long':
try:
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_from_country, self.invoice_from_state)
).name
except:
pass
parts = [
self.invoice_from_name,
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
" ".join(s for s in [self.invoice_from_zipcode, self.invoice_from_city, state_name] if s)
self.invoice_from_country.name if self.invoice_from_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])

View File

@@ -93,6 +93,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
invoice.invoice_from_state = invoice.event.settings.get('invoice_address_from_state')
invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
@@ -459,6 +460,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
cancellation.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
cancellation.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
cancellation.invoice_from_state = invoice.event.settings.get('invoice_address_from_state')
cancellation.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
cancellation.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
cancellation.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
@@ -562,6 +564,7 @@ def build_preview_invoice_pdf(event):
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
invoice.invoice_from_state = invoice.event.settings.get('invoice_address_from_state')
invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')

View File

@@ -40,6 +40,7 @@ from datetime import datetime
from decimal import Decimal
from typing import Any
import pycountry
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -1229,13 +1230,32 @@ DEFAULTS = {
label=_("City"),
)
},
'invoice_address_from_state': {
'default': '',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': {
'choices': [('', '')],
},
'form_kwargs': {
"label": pgettext_lazy('address', 'State'),
'choices': [('', '')],
},
},
'invoice_address_from_country': {
'default': '',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**country_choice_kwargs()),
'form_kwargs': lambda: dict(label=_('Country'), **country_choice_kwargs()),
'form_kwargs': lambda: dict(
label=_('Country'),
widget=forms.Select(attrs={
'data-trigger-address-info': 'on',
}),
**country_choice_kwargs()
),
},
'invoice_address_from_tax_id': {
'default': '',
@@ -3971,6 +3991,16 @@ def validate_event_settings(event, settings_dict):
raise ValidationError({
'invoice_address_company_required': _('You have to require invoice addresses to require for company names.')
})
if settings_dict.get('invoice_address_from_state') and settings_dict.get('invoice_address_from_country'):
cc = str(settings_dict.get('invoice_address_from_country'))
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'invoice_address_from_state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + settings_dict.get('invoice_address_from_state')):
raise ValidationError(
{'invoice_address_from_state': ['"{}" is not a known subdivision of the country "{}".'.format(settings_dict.get('invoice_address_from_state'), cc)]}
)
payment_term_last = settings_dict.get('payment_term_last')
if payment_term_last and event.presale_end: