Invoice addresses: Ask for a state in some countries (#1362)

* Invoice addresses: Ask for a state in some countries

* API, tests, noscript

* Fix shredder tests

* Add test for addresses with long state names
This commit is contained in:
Raphael Michel
2019-08-09 09:55:46 +02:00
committed by GitHub
parent 547f71aac6
commit d919605d79
19 changed files with 367 additions and 27 deletions

View File

@@ -96,7 +96,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
]
@@ -153,10 +153,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -208,7 +209,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
yield headers
@@ -243,10 +244,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
@@ -301,7 +303,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers.append(_('Sales channel'))
@@ -358,10 +360,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row.append(order.sales_channel)
yield row
@@ -503,6 +506,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -552,6 +556,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
@@ -591,6 +596,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -630,6 +636,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,

View File

@@ -5,6 +5,7 @@ from decimal import Decimal
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
@@ -15,7 +16,9 @@ from django.db.models import QuerySet
from django.forms import Select
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import get_language, ugettext_lazy as _
from django.utils.translation import (
get_language, pgettext_lazy, ugettext_lazy as _,
)
from django_countries import countries
from django_countries.fields import Country, CountryField
@@ -25,7 +28,10 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.escapejson import escapejson_attr
@@ -356,8 +362,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta:
model = InvoiceAddress
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference', 'beneficiary')
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
'vat_id', 'internal_reference', 'beneficiary')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
@@ -400,6 +406,29 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
c = [('', pgettext_lazy('address', 'Select state'))]
cc = None
if 'country' in self.data:
cc = str(self.data['country'])
elif 'country' in self.initial:
cc = str(self.initial['country'])
elif self.instance and self.instance.country:
cc = str(self.instance.country)
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
elif 'state' in self.data:
self.data = self.data.copy()
del self.data['state']
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c
)
self.fields['state'].widget.is_required = True
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.1 on 2019-08-08 12:53
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0131_auto_20190729_1422'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='invoice_to_state',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoiceaddress',
name='state',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

@@ -1,6 +1,7 @@
import string
from decimal import Decimal
import pycountry
from django.db import DatabaseError, models, transaction
from django.db.models import Max
from django.db.models.functions import Cast
@@ -11,6 +12,8 @@ from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def invoice_filename(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
@@ -90,6 +93,7 @@ class Invoice(models.Model):
invoice_to_street = models.TextField(null=True)
invoice_to_zipcode = models.CharField(max_length=190, null=True)
invoice_to_city = models.TextField(null=True)
invoice_to_state = models.CharField(max_length=190, null=True)
invoice_to_country = CountryField(null=True)
invoice_to_vat_id = models.TextField(null=True)
invoice_to_beneficiary = models.TextField(null=True)
@@ -140,11 +144,21 @@ class Invoice(models.Model):
def address_invoice_to(self):
if self.invoice_to and not self.invoice_to_company and not self.invoice_to_name:
return self.invoice_to
state_name = ""
if self.invoice_to_state:
state_name = self.invoice_to_state
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
)
parts = [
self.invoice_to_company,
self.invoice_to_name,
self.invoice_to_street,
(self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or ""),
((self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or "") + " " + (state_name or "")).strip(),
self.invoice_to_country.name if self.invoice_to_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])

View File

@@ -9,6 +9,7 @@ from decimal import Decimal
from typing import Any, Dict, List, Union
import dateutil
import pycountry
import pytz
from django.conf import settings
from django.db import models, transaction
@@ -2019,6 +2020,7 @@ class InvoiceAddress(models.Model):
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
vat_id_validated = models.BooleanField(default=False)
@@ -2045,6 +2047,22 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
@property
def name(self):
if not self.name_parts:

View File

@@ -73,12 +73,15 @@ def build_invoice(invoice: Invoice) -> Invoice:
addr_template = pgettext("invoice", """{i.company}
{i.name}
{i.street}
{i.zipcode} {i.city}
{i.zipcode} {i.city} {state}
{country}""")
invoice.invoice_to = addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old
).strip()
invoice.invoice_to = "\n".join(
a.strip() for a in addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old,
state=ia.state_for_address
).split("\n") if a.strip()
)
invoice.internal_reference = ia.internal_reference
invoice.invoice_to_company = ia.company
invoice.invoice_to_name = ia.name
@@ -86,6 +89,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.invoice_to_zipcode = ia.zipcode
invoice.invoice_to_city = ia.city
invoice.invoice_to_country = ia.country
invoice.invoice_to_state = ia.state
invoice.invoice_to_beneficiary = ia.beneficiary
if ia.vat_id:

View File

@@ -880,6 +880,19 @@ PERSON_NAME_SCHEMES = OrderedDict([
},
}),
])
COUNTRIES_WITH_STATE_IN_ADDRESS = {
# Source: http://www.bitboost.com/ref/international-address-formats.html
# This is not a list of countries that *have* states, this is a list of countries where states
# are actually *used* in postal addresses. This is obviously not complete and opinionated.
# Country: [(List of subdivision types as defined by pycountry), (short or long form to be used)]
'AU': (['State', 'Territory'], 'short'),
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'MY': (['State'], 'long'),
'MX': (['State', 'Federal District'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
}
settings_hierarkey = Hierarkey(attribute_name='settings')

View File

@@ -0,0 +1,16 @@
import pycountry
from django.http import JsonResponse
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def states(request):
cc = request.GET.get("country", "DE")
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return JsonResponse({'data': []})
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return JsonResponse({'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
]})