Compare commits

...

7 Commits

Author SHA1 Message Date
dependabot[bot]
74a960e239 Update celery requirement from ==5.5.* to ==5.6.* (#5676) 2025-12-03 17:00:53 +01:00
Raphael Michel
5a1bcae085 Invoice address: Improve VAT ID input (#5647)
* Remove unmaintained depdendency vat_moss

* VAT ID normalization: Auto-add country codes

* VAT ID: County-specific labels

* Invoice address: Allow to set VAT ID as required per country

* Fix failing tests

* Update src/pretix/base/settings.py

Co-authored-by: luelista <weller@rami.io>

* Review fixes

---------

Co-authored-by: luelista <weller@rami.io>
2025-12-03 16:48:19 +01:00
SJang1
051eb78312 Translations: Update Korean
Currently translated at 50.8% (3140 of 6172 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ko/

powered by weblate
2025-12-03 16:29:20 +01:00
Ana Rute Pacheco Vivas
15808e55fd Translations: Update Portuguese (Portugal)
Currently translated at 83.1% (5134 of 6172 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_PT/

powered by weblate
2025-12-03 16:29:20 +01:00
David Ibáñez Cerdeira
c886c0b415 Translations: Update Galician
Currently translated at 9.2% (569 of 6172 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2025-12-03 16:29:20 +01:00
David Ibáñez Cerdeira
47472447eb Translations: Update Galician
Currently translated at 9.1% (567 of 6172 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2025-12-03 16:29:20 +01:00
Richard Schreiber
1a40215e91 Fix N+1 queries in API (#5684)
* Fix N+1 query in API quotas list

* fix membership N+1

* fix vouchers N+1 budget_used

* rename and reuse Voucher.annotate_budget_used_orders to budget_used

* fix flake8
2025-12-03 15:37:40 +01:00
22 changed files with 512 additions and 243 deletions

View File

@@ -30,7 +30,7 @@ dependencies = [
"babel",
"BeautifulSoup4==4.14.*",
"bleach==6.2.*",
"celery==5.5.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.18.*",
@@ -62,7 +62,7 @@ dependencies = [
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.5.*",
"kombu==5.6.*",
"libsass==0.23.*",
"lxml",
"markdown==3.9", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
@@ -99,7 +99,6 @@ dependencies = [
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==1.0.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.7.*",
"zeep==4.3.*"

View File

@@ -795,6 +795,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_vatid_required_countries',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
@@ -943,6 +944,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_vatid_required_countries',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',

View File

@@ -567,7 +567,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.quotas.all()
return self.request.event.quotas.select_related('subevent').prefetch_related('items', 'variations').all()
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()).distinct()

View File

@@ -721,7 +721,7 @@ class MembershipViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Membership.objects.filter(
customer__organizer=self.request.organizer
)
).select_related('customer')
def get_serializer_context(self):
ctx = super().get_serializer_context()

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
@@ -64,8 +65,13 @@ class VoucherViewSet(viewsets.ModelViewSet):
permission = 'can_view_vouchers'
write_permission = 'can_change_vouchers'
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
return self.request.event.vouchers.select_related('seat').all()
return Voucher.annotate_budget_used(
self.request.event.vouchers
).select_related(
'item', 'quota', 'seat', 'variation'
)
@transaction.atomic()
def create(self, request, *args, **kwargs):

View File

@@ -83,7 +83,7 @@ from pretix.base.invoicing.transmission import (
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
VATIDFinalError, VATIDTemporaryError, normalize_vat_id, validate_vat_id,
)
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
@@ -1165,13 +1165,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but depending on the country you reside in we might need to charge you '
'additional taxes if you do not enter it.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
else:
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but it might be required for you to claim tax benefits on your invoice '
'depending on your and the sellers country of residence.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
transmission_type_choices = [
@@ -1358,13 +1356,24 @@ class BaseInvoiceAddressForm(forms.ModelForm):
"transmission method.")}
)
vat_id_applicable = (
'vat_id' in self.fields and
data.get('is_business') and
ask_for_vat_id(data.get('country'))
)
vat_id_required = vat_id_applicable and str(data.get('country')) in self.event.settings.invoice_address_vatid_required_countries
if vat_id_required and not data.get('vat_id'):
raise ValidationError({
"vat_id": _("This field is required.")
})
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and ask_for_vat_id(data.get('country')) and data.get('vat_id'):
pass # Skip re-validation if it is validated
elif self.validate_vat_id and vat_id_applicable:
try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
if self.all_optional:
self.instance.vat_id_validated = False
@@ -1372,6 +1381,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
else:
raise ValidationError({"vat_id": e.message})
except VATIDTemporaryError as e:
# We couldn't check it online, but we can still normalize it
normalized_id = normalize_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id = data['vat_id'] = normalized_id
self.instance.vat_id_validated = False
if self.request and self.vat_warning:
messages.warning(self.request, e.message)

View File

@@ -32,7 +32,6 @@ from itertools import groupby
from typing import Tuple
import bleach
import vat_moss.exchange_rates
from bidi import get_display
from django.contrib.staticfiles import finders
from django.db.models import Sum
@@ -1059,7 +1058,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def fmt(val):
try:
return vat_moss.exchange_rates.format(val, self.invoice.foreign_currency_display)
return money_filter(val, self.invoice.foreign_currency_display)
except ValueError:
return localize(val) + ' ' + self.invoice.foreign_currency_display

View File

@@ -623,7 +623,7 @@ class Voucher(LoggedModel):
return max(1, self.min_usages - self.redeemed)
@classmethod
def annotate_budget_used_orders(cls, qs):
def annotate_budget_used(cls, qs):
opq = OrderPosition.objects.filter(
voucher_id=OuterRef('pk'),
voucher_budget_use__isnull=False,
@@ -632,7 +632,7 @@ class Voucher(LoggedModel):
Order.STATUS_PENDING
]
).order_by().values('voucher_id').annotate(s=Sum('voucher_budget_use')).values('s')
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=13, decimal_places=2)), Decimal('0.00')))
return qs.annotate(budget_used=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=13, decimal_places=2)), Decimal('0.00')))
def budget_used(self):
ops = OrderPosition.objects.filter(

View File

@@ -27,7 +27,6 @@ from decimal import Decimal
from xml.etree import ElementTree
import requests
import vat_moss.id
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from zeep import Client, Transport
@@ -42,14 +41,142 @@ logger = logging.getLogger(__name__)
error_messages = {
'unavailable': _(
'Your VAT ID could not be checked, as the VAT checking service of '
'your country is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'
'your country is currently not available. We will therefore need to '
'charge you the same tax rate as if you did not enter a VAT ID.'
),
'invalid': _('This VAT ID is not valid. Please re-check your input.'),
'country_mismatch': _('Your VAT ID does not match the selected country.'),
}
VAT_ID_PATTERNS = {
# Patterns generated by consulting the following URLs:
#
# - http://en.wikipedia.org/wiki/VAT_identification_number
# - http://ec.europa.eu/taxation_customs/vies/faq.html
# - https://euipo.europa.eu/tunnel-web/secure/webdav/guest/document_library/Documents/COSME/VAT%20numbers%20EU.pdf
# - http://www.skatteetaten.no/en/International-pages/Felles-innhold-benyttes-i-flere-malgrupper/Brochure/Guide-to-value-added-tax-in-Norway/?chapter=7159
'AT': { # Austria
'regex': '^U\\d{8}$',
'country_code': 'AT'
},
'BE': { # Belgium
'regex': '^(1|0?)\\d{9}$',
'country_code': 'BE'
},
'BG': { # Bulgaria
'regex': '^\\d{9,10}$',
'country_code': 'BG'
},
'CH': { # Switzerland
'regex': '^\\dE{9}$',
'country_code': 'CH'
},
'CY': { # Cyprus
'regex': '^\\d{8}[A-Z]$',
'country_code': 'CY'
},
'CZ': { # Czech Republic
'regex': '^\\d{8,10}$',
'country_code': 'CZ'
},
'DE': { # Germany
'regex': '^\\d{9}$',
'country_code': 'DE'
},
'DK': { # Denmark
'regex': '^\\d{8}$',
'country_code': 'DK'
},
'EE': { # Estonia
'regex': '^\\d{9}$',
'country_code': 'EE'
},
'EL': { # Greece
'regex': '^\\d{9}$',
'country_code': 'GR'
},
'ES': { # Spain
'regex': '^[A-Z0-9]\\d{7}[A-Z0-9]$',
'country_code': 'ES'
},
'FI': { # Finland
'regex': '^\\d{8}$',
'country_code': 'FI'
},
'FR': { # France
'regex': '^[A-Z0-9]{2}\\d{9}$',
'country_code': 'FR'
},
'GB': { # United Kingdom
'regex': '^(GD\\d{3}|HA\\d{3}|\\d{9}|\\d{12})$',
'country_code': 'GB'
},
'HR': { # Croatia
'regex': '^\\d{11}$',
'country_code': 'HR'
},
'HU': { # Hungary
'regex': '^\\d{8}$',
'country_code': 'HU'
},
'IE': { # Ireland
'regex': '^(\\d{7}[A-Z]{1,2}|\\d[A-Z+*]\\d{5}[A-Z])$',
'country_code': 'IE'
},
'IT': { # Italy
'regex': '^\\d{11}$',
'country_code': 'IT'
},
'LT': { # Lithuania
'regex': '^(\\d{9}|\\d{12})$',
'country_code': 'LT'
},
'LU': { # Luxembourg
'regex': '^\\d{8}$',
'country_code': 'LU'
},
'LV': { # Latvia
'regex': '^\\d{11}$',
'country_code': 'LV'
},
'MT': { # Malta
'regex': '^\\d{8}$',
'country_code': 'MT'
},
'NL': { # Netherlands
'regex': '^\\d{9}B\\d{2}$',
'country_code': 'NL'
},
'NO': { # Norway
'regex': '^\\d{9}MVA$',
'country_code': 'NO'
},
'PL': { # Poland
'regex': '^\\d{10}$',
'country_code': 'PL'
},
'PT': { # Portugal
'regex': '^\\d{9}$',
'country_code': 'PT'
},
'RO': { # Romania
'regex': '^\\d{2,10}$',
'country_code': 'RO'
},
'SE': { # Sweden
'regex': '^\\d{12}$',
'country_code': 'SE'
},
'SI': { # Slovenia
'regex': '^\\d{8}$',
'country_code': 'SI'
},
'SK': { # Slovakia
'regex': '^\\d{10}$',
'country_code': 'SK'
},
}
class VATIDError(Exception):
def __init__(self, message):
@@ -64,13 +191,57 @@ class VATIDTemporaryError(VATIDError):
pass
def normalize_vat_id(vat_id, country_code):
"""
Accepts a VAT ID and normaizes it, getting rid of spaces, periods, dashes
etc and converting it to upper case.
Original function from https://github.com/wbond/vat_moss-python
Copyright (c) 2015 Will Bond <will@wbond.net>
MIT License
"""
if not vat_id:
return None
if not isinstance(vat_id, str):
raise TypeError('VAT ID is not a string')
if len(vat_id) < 3:
raise ValueError('VAT ID must be at least three character long')
# Normalize the ID for simpler regexes
vat_id = re.sub('\\s+', '', vat_id)
vat_id = vat_id.replace('-', '')
vat_id = vat_id.replace('.', '')
vat_id = vat_id.upper()
# Clean the different shapes a number can take in Switzerland depending on purpse
if country_code == "CH":
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
# Fix people using GR prefix for Greece
if vat_id[0:2] == "GR" and country_code == "GR":
vat_id = "EL" + vat_id[2:]
# Check if we already have a valid country prefix. If not, we try to figure out if we can
# add one, since in some countries (e.g. Italy) it's very custom to enter it without the prefix
if vat_id[:2] in VAT_ID_PATTERNS and re.match(VAT_ID_PATTERNS[vat_id[0:2]]['regex'], vat_id[2:]):
# Prefix set and prefix matches pattern, nothing to do
pass
elif re.match(VAT_ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], vat_id):
# Prefix not set but adding it fixes pattern
vat_id = cc_to_vat_prefix(country_code) + vat_id
else:
# We have no idea what this is
pass
return vat_id
def _validate_vat_id_NO(vat_id, country_code):
# Inspired by vat_moss library
if not vat_id.startswith("NO"):
# prefix is not usually used in Norway, but expected by vat_moss library
vat_id = "NO" + vat_id
try:
vat_id = vat_moss.id.normalize(vat_id)
vat_id = normalize_vat_id(vat_id, country_code)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
@@ -104,7 +275,7 @@ def _validate_vat_id_NO(vat_id, country_code):
def _validate_vat_id_EU(vat_id, country_code):
# Inspired by vat_moss library
try:
vat_id = vat_moss.id.normalize(vat_id)
vat_id = normalize_vat_id(vat_id, country_code)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
@@ -112,11 +283,10 @@ def _validate_vat_id_EU(vat_id, country_code):
raise VATIDFinalError(error_messages['invalid'])
number = vat_id[2:]
if vat_id[:2] != cc_to_vat_prefix(country_code):
raise VATIDFinalError(error_messages['country_mismatch'])
if not re.match(vat_moss.id.ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
if not re.match(VAT_ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
raise VATIDFinalError(error_messages['invalid'])
# We are relying on the country code of the normalized VAT-ID and not the user/InvoiceAddress-provided
@@ -175,9 +345,12 @@ def _validate_vat_id_EU(vat_id, country_code):
def _validate_vat_id_CH(vat_id, country_code):
if vat_id[:3] != 'CHE':
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
raise VATIDFinalError(error_messages['country_mismatch'])
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
try:
vat_id = normalize_vat_id(vat_id, country_code)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
try:
transport = Transport(
cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")),

View File

@@ -629,13 +629,40 @@ DEFAULTS = {
'form_kwargs': dict(
label=_("Ask for VAT ID"),
help_text=format_lazy(
_("Only works if an invoice address is asked for. VAT ID is never required and only requested from "
"business customers in the following countries: {countries}"),
_("Only works if an invoice address is asked for. VAT ID is only requested from business customers "
"in the following countries: {countries}."),
countries=lazy(lambda *args: ', '.join(sorted(gettext(Country(cc).name) for cc in VAT_ID_COUNTRIES)), str)()
),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
)
},
'invoice_address_vatid_required_countries': {
'default': ['IT', 'GR'],
'type': list,
'form_class': forms.MultipleChoiceField,
'serializer_class': serializers.MultipleChoiceField,
'serializer_kwargs': dict(
choices=lazy(
lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]),
list
)(),
),
'form_kwargs': dict(
label=_("Require VAT ID in"),
choices=lazy(
lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]),
list
)(),
help_text=format_lazy(
_("VAT ID is optional by default, because not all businesses are assigned a VAT ID in all countries. "
"VAT ID will be required for all business addresses in the selected countries."),
),
widget=forms.CheckboxSelectMultiple(attrs={
"class": "scrolling-multiple-choice",
'data-display-dependency': '#id_invoice_address_vatid'
}),
)
},
'invoice_address_explanation_text': {
'default': '',
'type': LazyI18nString,

View File

@@ -22,7 +22,7 @@
import pycountry
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import pgettext
from django.utils.translation import gettext, pgettext, pgettext_lazy
from django_countries.fields import Country
from django_scopes import scope
@@ -36,6 +36,22 @@ from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
)
VAT_ID_LABELS = {
# VAT ID is a EU concept and Switzerland has a distinct, but differently-named concept
"CH": pgettext_lazy("tax_id_swiss", "UID"), # Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
# Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
# separately to each company and only used in cross-country transactions. Therefore, it makes sense
# to call it just "VAT ID" on the form, and people will either know their VAT ID or they don't.
# In contrast, in Italy the EU-compatible VAT ID is not separately assigned, but is just "IT" + the national tax
# number (Partita IVA) and also used on domestic transactions. So someone who never purchased something international
# for their company, might still know the value, if we call it the right way and not just "VAT ID".
"IT": pgettext_lazy("tax_id_italy", "VAT ID / P.IVA"), # Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
"GR": pgettext_lazy("tax_id_greece", "VAT ID / TIN"), # Translators: Translate to only "ΑΦΜ" in Greek
"ES": pgettext_lazy("tax_id_spain", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Spanish
"PT": pgettext_lazy("tax_id_portugal", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Portuguese
}
def _info(cc):
info = {
@@ -47,7 +63,12 @@ def _info(cc):
'required': 'if_any' if cc in COUNTRIES_WITH_STATE_IN_ADDRESS else False,
'label': COUNTRY_STATE_LABEL.get(cc, pgettext('address', 'State')),
},
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
'vat_id': {
'visible': cc in VAT_ID_COUNTRIES,
'required': False,
'label': VAT_ID_LABELS.get(cc, gettext("VAT ID")),
'helptext_visible': True,
},
}
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return {'data': [], **info}
@@ -124,4 +145,10 @@ def address_form(request):
"required": transmission_type.identifier == selected_transmission_type and k in required
}
if is_business and country in event.settings.invoice_address_vatid_required_countries and info["vat_id"]["visible"]:
info["vat_id"]["required"] = True
if info["vat_id"]["required"]:
# The help text explains that it is optional, so we want to hide that if it is required
info["vat_id"]["helptext_visible"] = False
return JsonResponse(info)

View File

@@ -927,6 +927,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_vatid_required_countries',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',

View File

@@ -43,6 +43,7 @@
{% bootstrap_field form.invoice_name_required layout="control" %}
{% bootstrap_field form.invoice_address_company_required layout="control" %}
{% bootstrap_field form.invoice_address_vatid layout="control" %}
{% bootstrap_field form.invoice_address_vatid_required_countries layout="control" %}
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
{% bootstrap_field form.invoice_address_custom_field layout="control" %}

View File

@@ -165,7 +165,7 @@
{% if v.budget|default_if_none:"NONE" != "NONE" %}
<br>
<small class="text-muted">
{{ v.budget_used_orders|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
{{ v.budget_used|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
</small>
{% endif %}
</td>

View File

@@ -87,7 +87,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.exclude(
qs = Voucher.annotate_budget_used(self.request.event.vouchers.exclude(
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
).select_related(
'item', 'variation', 'seat'

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: anonymous <noreply@weblate.org>\n"
"PO-Revision-Date: 2025-12-02 12:14+0000\n"
"Last-Translator: David Ibáñez Cerdeira <dibanez@gmail.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix/"
"gl/>\n"
"Language: gl\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10\n"
"X-Generator: Weblate 5.14.3\n"
#: pretix/_base_settings.py:87
#, fuzzy
@@ -90,7 +90,7 @@ msgstr "Danés"
#: pretix/_base_settings.py:102
msgid "Galician"
msgstr ""
msgstr "Galego"
#: pretix/_base_settings.py:103
#, fuzzy
@@ -347,7 +347,7 @@ msgstr ""
#: pretix/api/serializers/item.py:345
msgid "Only admission products can currently be personalized."
msgstr ""
msgstr "Produtos de admisión única actualmente poden ser personalizados."
#: pretix/api/serializers/item.py:356
msgid ""
@@ -464,7 +464,7 @@ msgstr ""
#: pretix/api/views/checkin.py:627 pretix/api/views/checkin.py:634
msgid "Medium connected to other event"
msgstr ""
msgstr "O medio conectou a outro acontecemento"
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:764
#, python-brace-format
@@ -637,6 +637,8 @@ msgid ""
"This includes product added or deleted and changes to nested objects like "
"variations or bundles."
msgstr ""
"Isto inclúe produtos engadidos ou eliminados e cambios en obxectos aniñados "
"como variacións ou paquetes."
#: pretix/api/webhooks.py:380
#, fuzzy
@@ -698,6 +700,8 @@ msgid ""
"Only includes explicit changes to the voucher, not e.g. an increase of the "
"number of redemptions."
msgstr ""
"Só inclúe cambios explícitos no cupón, non, por exemplo, un aumento do "
"número de canxeos."
#: pretix/api/webhooks.py:421
#, fuzzy
@@ -736,11 +740,11 @@ msgstr "Este campo es obligatorio."
#: pretix/base/addressvalidation.py:213
msgid "Enter a postal code in the format XXX."
msgstr ""
msgstr "Introduza un código postal co formato XXX."
#: pretix/base/addressvalidation.py:222 pretix/base/addressvalidation.py:224
msgid "Enter a postal code in the format XXXX."
msgstr ""
msgstr "Introduza un código postal co formato XXXX."
#: pretix/base/auth.py:146
#, fuzzy, python-brace-format
@@ -782,16 +786,19 @@ msgstr "Contrasinal"
#: pretix/base/auth.py:176 pretix/base/auth.py:183
msgid "Your password must contain both numeric and alphabetic characters."
msgstr ""
msgstr "O teu contrasinal debe conter caracteres numéricos e alfabéticos."
#: pretix/base/auth.py:202 pretix/base/auth.py:212
#, python-format
#, fuzzy, python-format
msgid "Your password may not be the same as your previous password."
msgid_plural ""
"Your password may not be the same as one of your %(history_length)s previous "
"passwords."
msgstr[0] ""
"Pode que o teu contrasinal non sexa o mesmo que o teu contrasinal anterior."
msgstr[1] ""
"Pode que o teu contrasinal non sexa o mesmo que un dos teus contrasinais "
"anteriores de %(history_length)."
#: pretix/base/channels.py:168
msgid "Online shop"
@@ -799,18 +806,22 @@ msgstr "Tenda en liña"
#: pretix/base/channels.py:174
msgid "API"
msgstr ""
msgstr "API"
#: pretix/base/channels.py:175
msgid ""
"API sales channels come with no built-in functionality, but may be used for "
"custom integrations."
msgstr ""
"As canles de vendas da API non inclúen funcionalidades integradas, pero "
"pódense usar para integracións personalizadas."
#: pretix/base/context.py:38
#, python-brace-format
msgid "<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>"
msgstr ""
"<a {a_name_attr}>con tecnoloxía de {name}</a> <a {a_attr}>baseado en "
"pretix</a>"
#: pretix/base/context.py:48
#, fuzzy, python-brace-format
@@ -829,7 +840,7 @@ msgstr "Código fonte"
#: pretix/base/customersso/oidc.py:61
#, python-brace-format
msgid "Configuration option \"{name}\" is missing."
msgstr ""
msgstr "Falta a opción de configuración \"{name}\"."
#: pretix/base/customersso/oidc.py:69 pretix/base/customersso/oidc.py:74
#, python-brace-format
@@ -845,9 +856,9 @@ msgid "Incompatible SSO provider: \"{error}\"."
msgstr ""
#: pretix/base/customersso/oidc.py:111
#, fuzzy, python-brace-format
#, python-brace-format
msgid "You are not requesting \"{scope}\"."
msgstr "Su pedido: {code}"
msgstr "Non estás solicitando \"{scope}\"."
#: pretix/base/customersso/oidc.py:206 pretix/base/customersso/oidc.py:214
#: pretix/base/customersso/oidc.py:237 pretix/base/customersso/oidc.py:254
@@ -863,6 +874,8 @@ msgid ""
"The email address on this account is not yet verified. Please first confirm "
"the email address in your customer account."
msgstr ""
"O enderezo de correo electrónico desta conta aínda non está verificado. "
"Primeiro, confirma o enderezo de correo electrónico na túa conta de cliente."
#: pretix/base/datasync/datasync.py:263
#, python-brace-format
@@ -3425,7 +3438,7 @@ msgstr "Manterme rexistrado"
#: pretix/base/forms/auth.py:65 pretix/base/forms/auth.py:285
msgid "This combination of credentials is not known to our system."
msgstr "Esta combinación de credenciais é descoñecida para o noso sistema"
msgstr "Esta combinación de credenciais é descoñecida para o noso sistema."
#: pretix/base/forms/auth.py:66 pretix/base/forms/user.py:94
#: pretix/presale/forms/customer.py:385 pretix/presale/forms/customer.py:457

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-11-18 17:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"PO-Revision-Date: 2025-12-03 13:00+0000\n"
"Last-Translator: SJang1 <git@sjang.dev>\n"
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/ko/"
">\n"
"Language: ko\n"
@@ -290,33 +290,27 @@ msgstr "묶음 상품은 그 자체로 또 다른 묶음을 포함 할 수 없
#: pretix/api/serializers/item.py:235
msgid "The program start must not be empty."
msgstr ""
msgstr "프로그램 시작일정은 비어 있어서는 안 됩니다."
#: pretix/api/serializers/item.py:239
msgid "The program end must not be empty."
msgstr ""
msgstr "프로그램 종료일정은 비어 있어서는 안 됩니다."
#: pretix/api/serializers/item.py:242 pretix/base/models/items.py:2321
#, fuzzy
#| msgid "The maximum date must not be before the minimum value."
msgid "The program end must not be before the program start."
msgstr "종료일(최대 날짜)은 시작일(최소값)보다 앞서면 안됩니다."
msgstr "종료일은 시작일보다 앞서면 안됩니다."
#: pretix/api/serializers/item.py:247 pretix/base/models/items.py:2315
#, fuzzy
#| msgid "You can not select a subevent if your event is not an event series."
msgid "You cannot use program times on an event series."
msgstr ""
"당신의이벤트가 이벤트 시리즈가 아닌 경우 하위 이벤트를 선택할 수 없습니다."
msgstr "이벤트 시리즈에 있는 시간은 사용하실 수 없습니다."
#: pretix/api/serializers/item.py:337
#, fuzzy
msgid ""
"Updating add-ons, bundles, program times or variations via PATCH/PUT is not "
"supported. Please use the dedicated nested endpoint."
msgstr ""
"추가 기능, 묶음 상품들, 또는 변형은 PATCH/PUT를 통해 업데이트 할 수 없습니"
"다. 전용 중첩은 마지막 지점에서 사용하세요"
"추가 기능, 묶음 상품들, 또는 변형은 PATCH/PUT를 통해 업데이트 할 수 없습니다."
" 전용 중첩은 마지막 지점에서 사용하세요."
#: pretix/api/serializers/item.py:345
msgid "Only admission products can currently be personalized."
@@ -573,22 +567,15 @@ msgid "Event series date deleted"
msgstr "이벤트 시리즈 날짜 삭제"
#: pretix/api/webhooks.py:374
#, fuzzy
#| msgid "Product name"
msgid "Product changed"
msgstr "제품"
msgstr "제품 변경됨"
#: pretix/api/webhooks.py:375
#, fuzzy
#| msgid ""
#| "Product changed (including product added or deleted and including changes "
#| "to nested objects like variations or bundles)"
msgid ""
"This includes product added or deleted and changes to nested objects like "
"variations or bundles."
msgstr ""
"제품 변경(제품 추가 또는 삭제, 변형 또는 번들과 같은 중첩된 객체에 대한 변경 "
"포함)"
msgstr "여기에는 추가되거나 삭제되거나 변경된 중첩오브젝트나 번들과 같은 사항이 포함"
"됩니다."
#: pretix/api/webhooks.py:380
msgid "Shop taken live"
@@ -623,16 +610,12 @@ msgid "Waiting list entry received voucher"
msgstr "대기자 명단 항목이 바우처를 받았습니다"
#: pretix/api/webhooks.py:412
#, fuzzy
#| msgid "Voucher code"
msgid "Voucher added"
msgstr "바우처 코드"
msgstr "바우처 추가됨"
#: pretix/api/webhooks.py:416
#, fuzzy
#| msgid "Voucher assigned"
msgid "Voucher changed"
msgstr "바우처 할당"
msgstr "바우처 변경됨"
#: pretix/api/webhooks.py:417
msgid ""
@@ -643,10 +626,8 @@ msgstr ""
"하지 않습니다."
#: pretix/api/webhooks.py:421
#, fuzzy
#| msgid "Voucher redeemed"
msgid "Voucher deleted"
msgstr "바우처 상환"
msgstr "바우처 제거됨"
#: pretix/api/webhooks.py:425
msgid "Customer account created"
@@ -671,7 +652,7 @@ msgstr "고객 계정 익명화되었습니다"
#: pretix/plugins/banktransfer/payment.py:513
#: pretix/presale/forms/customer.py:152
msgid "This field is required."
msgstr "이 필드는 필수입니다"
msgstr "이 필드는 필수입니다."
#: pretix/base/addressvalidation.py:213
msgid "Enter a postal code in the format XXX."
@@ -721,7 +702,7 @@ msgstr "비밀번호"
#: pretix/base/auth.py:176 pretix/base/auth.py:183
msgid "Your password must contain both numeric and alphabetic characters."
msgstr "비밀번호는 숫자와 알파벳 문자가 모두 포함되어야 합니다"
msgstr "비밀번호는 숫자와 알파벳 문자가 모두 포함되어야 합니다."
#: pretix/base/auth.py:202 pretix/base/auth.py:212
#, python-format
@@ -815,28 +796,21 @@ msgstr ""
"소를 확인해 주십시요."
#: pretix/base/datasync/datasync.py:263
#, fuzzy, python-brace-format
#| msgid ""
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
#| "your {provider_name} settings."
#, python-brace-format
msgid ""
"Field \"{field_name}\" does not exist. Please check your {provider_name} "
"settings."
msgstr ""
"필드 \"{field_name}\"은 {available_inputs}에 유효하지 않습니다. "
"{provider_name} 설정을 확인해 주세요."
msgstr "필드 \"{field_name}\"은 존재하지 않습니다. 당신의 {provider_name} 설정을 확인"
"해 주세요."
#: pretix/base/datasync/datasync.py:270
#, fuzzy, python-brace-format
#| msgid ""
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
#| "your {provider_name} settings."
#, python-brace-format
msgid ""
"Field \"{field_name}\" requires {required_input}, but only got "
"{available_inputs}. Please check your {provider_name} settings."
msgstr ""
"필드 \"{field_name}\" {available_inputs}에 유효하지 않습니다. "
"{provider_name} 설정을 확인해 주세요."
"필드 \"{field_name}\"는 {required_input}을 필요로 하지만, {available_inputs}"
"만 받았습니다. 당신의 {provider_name} 설정을 확인해 주세요."
#: pretix/base/datasync/datasync.py:281
#, python-brace-format
@@ -848,16 +822,12 @@ msgstr ""
"지 않았습니다"
#: pretix/base/datasync/sourcefields.py:128
#, fuzzy
#| msgid "Order positions"
msgid "Order position details"
msgstr "주문 위치"
msgstr "주문 위치 세부 정보"
#: pretix/base/datasync/sourcefields.py:129
#, fuzzy
#| msgid "Attendee email"
msgid "Attendee details"
msgstr "참석자 이메일"
msgstr "참석자 정보들"
#: pretix/base/datasync/sourcefields.py:130 pretix/base/exporters/answers.py:66
#: pretix/base/models/items.py:1766 pretix/control/navigation.py:172
@@ -867,10 +837,8 @@ msgid "Questions"
msgstr "질문들"
#: pretix/base/datasync/sourcefields.py:131
#, fuzzy
#| msgid "Product data"
msgid "Product details"
msgstr "상품 데이터"
msgstr "상품 정보"
#: pretix/base/datasync/sourcefields.py:132
#: pretix/control/templates/pretixcontrol/event/settings.html:279
@@ -895,17 +863,13 @@ msgid "Invoice address"
msgstr "송장 주소"
#: pretix/base/datasync/sourcefields.py:134
#, fuzzy
#| msgid "Meta information"
msgid "Event information"
msgstr "메타 정보(데이타에 대한 정보)"
msgstr "이벤트 정보"
#: pretix/base/datasync/sourcefields.py:135
#, fuzzy
#| msgid "Send recovery information"
msgctxt "subevent"
msgid "Event or date information"
msgstr "복구 정보를 전송하다"
msgstr "이벤트 또는 날짜 정보"
#: pretix/base/datasync/sourcefields.py:175
#: pretix/base/exporters/orderlist.py:605
@@ -930,10 +894,8 @@ msgstr "참석자 이름"
#: pretix/base/datasync/sourcefields.py:187
#: pretix/base/datasync/sourcefields.py:604
#: pretix/base/datasync/sourcefields.py:628
#, fuzzy
#| msgid "Attendee name"
msgid "Attendee"
msgstr "참석자 이름"
msgstr "참석자"
#: pretix/base/datasync/sourcefields.py:207
#: pretix/base/exporters/orderlist.py:612 pretix/base/forms/questions.py:687
@@ -947,10 +909,8 @@ msgid "Attendee email"
msgstr "참석자 이메일"
#: pretix/base/datasync/sourcefields.py:219
#, fuzzy
#| msgid "Attendee email"
msgid "Attendee or order email"
msgstr "참석자 이메일"
msgstr "참석자 또는 구매 이메일"
#: pretix/base/datasync/sourcefields.py:232
#: pretix/base/exporters/orderlist.py:613 pretix/base/pdf.py:189
@@ -963,28 +923,20 @@ msgid "Attendee company"
msgstr "참석자 회사"
#: pretix/base/datasync/sourcefields.py:241
#, fuzzy
#| msgid "Attendee address"
msgid "Attendee address street"
msgstr "참석자 주소"
msgstr "참석자 주소 도로명"
#: pretix/base/datasync/sourcefields.py:250
#, fuzzy
#| msgid "Attendee ZIP code"
msgid "Attendee address ZIP code"
msgstr "참석자 우편번호"
msgstr "참석자 주소 우편번호"
#: pretix/base/datasync/sourcefields.py:259
#, fuzzy
#| msgid "Attendee address"
msgid "Attendee address city"
msgstr "참석자 주소"
msgstr "참석자 주소 도시"
#: pretix/base/datasync/sourcefields.py:268
#, fuzzy
#| msgid "Attendee address"
msgid "Attendee address country"
msgstr "참석자 주소"
msgstr "참석자 주소 국가"
#: pretix/base/datasync/sourcefields.py:279
#: pretix/base/exporters/orderlist.py:653 pretix/base/pdf.py:347
@@ -1020,16 +972,12 @@ msgid "Invoice address country"
msgstr "송장 주소 국가"
#: pretix/base/datasync/sourcefields.py:353
#, fuzzy
#| msgid "Order total"
msgid "Order email"
msgstr "주문 합계"
msgstr "주문 이메일"
#: pretix/base/datasync/sourcefields.py:362
#, fuzzy
#| msgid "Order time"
msgid "Order email domain"
msgstr "주문 시간"
msgstr "주문 이메일 도메인"
#: pretix/base/datasync/sourcefields.py:371
#: pretix/base/exporters/invoices.py:203 pretix/base/exporters/invoices.py:332
@@ -1061,10 +1009,8 @@ msgid "Order code"
msgstr "주문 코드"
#: pretix/base/datasync/sourcefields.py:380
#, fuzzy
#| msgid "Event end date and time"
msgid "Event and order code"
msgstr "이벤트 종료 날짜 및 시간"
msgstr "이벤트와 주문 번호"
#: pretix/base/datasync/sourcefields.py:389
#: pretix/base/exporters/orderlist.py:263 pretix/base/notifications.py:201
@@ -1076,10 +1022,8 @@ msgid "Order total"
msgstr "주문 합계"
#: pretix/base/datasync/sourcefields.py:398
#, fuzzy
#| msgid "Product name and variation"
msgid "Product and variation name"
msgstr "제품명 및 변형"
msgstr "제품명 및 변형 이름"
#: pretix/base/datasync/sourcefields.py:410 pretix/base/exporters/items.py:57
#: pretix/base/exporters/orderlist.py:598
@@ -1089,16 +1033,12 @@ msgid "Product ID"
msgstr "상품 식별 아이디"
#: pretix/base/datasync/sourcefields.py:419
#, fuzzy
#| msgid "Count add-on products"
msgid "Product is admission product"
msgstr "추가된 제품을 포함합니다"
msgstr "상품은 입장 상품입니다"
#: pretix/base/datasync/sourcefields.py:428
#, fuzzy
#| msgid "Short form"
msgid "Event short form"
msgstr "짧은 형식"
msgstr "이벤트 짧은 형식"
#: pretix/base/datasync/sourcefields.py:437 pretix/base/exporters/events.py:57
#: pretix/base/exporters/orderlist.py:263
@@ -1141,10 +1081,8 @@ msgid "Order code and position number"
msgstr "주문 코드 및 위치 번호"
#: pretix/base/datasync/sourcefields.py:482
#, fuzzy
#| msgid "Ticket code"
msgid "Ticket price"
msgstr "티켓 코드"
msgstr "티켓 가격"
#: pretix/base/datasync/sourcefields.py:491 pretix/base/notifications.py:204
#: pretix/control/forms/filter.py:216 pretix/control/forms/modelimport.py:90
@@ -1152,22 +1090,16 @@ msgid "Order status"
msgstr "주문 상태"
#: pretix/base/datasync/sourcefields.py:500
#, fuzzy
#| msgid "Device status"
msgid "Ticket status"
msgstr "기기 상태"
msgstr "티켓 상태"
#: pretix/base/datasync/sourcefields.py:509
#, fuzzy
#| msgid "Purchase date and time"
msgid "Order date and time"
msgstr "구매 날짜 및 시간"
#: pretix/base/datasync/sourcefields.py:518
#, fuzzy
#| msgid "Printing date and time"
msgid "Payment date and time"
msgstr "인쇄 날짜 및 시간"
msgstr "결제 날짜 및 시간"
#: pretix/base/datasync/sourcefields.py:527
#: pretix/base/exporters/orderlist.py:272
@@ -1178,35 +1110,27 @@ msgid "Order locale"
msgstr "주문 지역 설정"
#: pretix/base/datasync/sourcefields.py:536
#, fuzzy
#| msgid "Order position"
msgid "Order position ID"
msgstr "주문 위치"
msgstr "주문 위치 ID"
#: pretix/base/datasync/sourcefields.py:545
#: pretix/base/exporters/orderlist.py:292
#, fuzzy
#| msgid "Order time"
msgid "Order link"
msgstr "주문 시간"
msgstr "주문 링크"
#: pretix/base/datasync/sourcefields.py:560
#, fuzzy
#| msgid "Ticket design"
msgid "Ticket link"
msgstr "티켓 디자인"
msgstr "티켓 링크"
#: pretix/base/datasync/sourcefields.py:578
#, fuzzy, python-brace-format
#| msgid "Check-in list {val}"
#, python-brace-format
msgid "Check-in datetime on list {}"
msgstr "체크인 목록 {val}"
msgstr "{} 리스트에 있는 체크인 날짜와 시간"
#: pretix/base/datasync/sourcefields.py:590
#, fuzzy, python-brace-format
#| msgid "Question {val}"
#, python-brace-format
msgid "Question: {name}"
msgstr "질문 {val}"
msgstr "질문: {name}"
#: pretix/base/datasync/sourcefields.py:604
#: pretix/base/datasync/sourcefields.py:614 pretix/base/settings.py:3642
@@ -2422,9 +2346,9 @@ msgid "Fees"
msgstr "수수료"
#: pretix/base/exporters/orderlist.py:277
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Gross at {rate} % tax"
msgstr "세율{%}의 세금으로 총합"
msgstr "세율 {rate}%의 세금으로 총합"
#: pretix/base/exporters/orderlist.py:278
#, python-brace-format
@@ -2472,9 +2396,9 @@ msgid "External customer ID"
msgstr "외부고객 아이디"
#: pretix/base/exporters/orderlist.py:296
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Paid by {method}"
msgstr "{방법}에 의해 결제됨"
msgstr "{method}에 의해 결제됨"
#: pretix/base/exporters/orderlist.py:458
#: pretix/base/exporters/orderlist.py:914
@@ -2677,10 +2601,8 @@ msgid "Check-in lists"
msgstr "체크인 목록"
#: pretix/base/exporters/orderlist.py:672
#, fuzzy
#| msgid "Additional footer link"
msgid "Position order link"
msgstr "추가 하단 링크"
msgstr "주문 링크 위치"
#: pretix/base/exporters/orderlist.py:841
msgid "Order transaction data"
@@ -3320,15 +3242,13 @@ msgid "Repeat password"
msgstr "반복 비밀번호"
#: pretix/base/forms/auth.py:220 pretix/base/forms/user.py:99
#, fuzzy
#| msgid "Email address"
msgid "Your email address"
msgstr "이메일 주소"
msgstr "당신의 이메일 주소"
#: pretix/base/forms/auth.py:327 pretix/control/forms/orders.py:1041
#: pretix/control/templates/pretixcontrol/shredder/download.html:53
msgid "Confirmation code"
msgstr ""
msgstr "확인 코드"
#: pretix/base/forms/questions.py:137 pretix/base/forms/questions.py:264
msgctxt "name_salutation"
@@ -3411,14 +3331,12 @@ msgstr ""
"야 할 수도 있습니다."
#: pretix/base/forms/questions.py:1181
#, fuzzy
#| msgid "Cancellation requested"
msgid "No invoice requested"
msgstr "취소 요청"
msgstr "청구서 요청되지 않음"
#: pretix/base/forms/questions.py:1183
msgid "Invoice transmission method"
msgstr ""
msgstr "청구서 전송 방식"
#: pretix/base/forms/questions.py:1329
msgid "You need to provide a company name."
@@ -3432,21 +3350,20 @@ msgstr "이름을 입력해야 합니다."
msgid ""
"If you enter an invoice address, you also need to select an invoice "
"transmission method."
msgstr ""
msgstr "당신이 청구서 주소를 입력하신다면, 청구서 수신 방법도 선택하셔야 합니다."
#: pretix/base/forms/questions.py:1385
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid ""
"The selected transmission type is not available in your country or for your "
"type of address."
msgstr "선택한 미디어 유형이 정리함 설정에서 활성화되지 않았습니다."
msgstr "선택한 전송 유형은 당신의 국가 또는 지역에서 이용하실 수 없습니다."
#: pretix/base/forms/questions.py:1394
msgid ""
"The selected type of invoice transmission requires a field that is currently "
"not available, please reach out to the organizer."
msgstr ""
msgstr "선택하신 청구서 전송 유형은 현재 사용할 수 없는 필드의 입력을 필요로 하니, 주"
"최자에게 문의 해 주세요."
#: pretix/base/forms/questions.py:1398
msgid "This field is required for the selected type of invoice transmission."
@@ -3466,10 +3383,8 @@ msgstr ""
"대가 대신 사용됩니다."
#: pretix/base/forms/user.py:77
#, fuzzy
#| msgid "Attendee email address"
msgid "Change email address"
msgstr "참석자 이메일 주소"
msgstr "이메일 주소 변경"
#: pretix/base/forms/user.py:83
msgid "Device name"
@@ -3518,16 +3433,12 @@ msgstr ""
"이 이메일 주소와 관련된 계정이 이미 있습니다. 다른 계정을 선택해 주세요."
#: pretix/base/forms/user.py:179
#, fuzzy
#| msgid "Email address"
msgid "Old email address"
msgstr "이메일 주소"
msgstr "이전 이메일 주소"
#: pretix/base/forms/user.py:180
#, fuzzy
#| msgid "Email address"
msgid "New email address"
msgstr "이메일 주소"
msgstr "이메일 주소"
#: pretix/base/forms/validators.py:51
msgid ""
@@ -3572,23 +3483,22 @@ msgstr "개별 고객"
#: pretix/base/invoicing/email.py:50
msgid "Email invoice directly to accounting department"
msgstr ""
msgstr "청구서를 회계 부서로 이메일로 바로 보내기"
#: pretix/base/invoicing/email.py:51
msgid ""
"If not selected, the invoice will be sent to you using the email address "
"listed above."
msgstr ""
msgstr "선택되지 않은 경우, 청구서는 위의 이메일 주소를 통해 당신에게 보내질 것 입니"
"다."
#: pretix/base/invoicing/email.py:55
#, fuzzy
#| msgid "Email address verified"
msgid "Email address for invoice"
msgstr "이메일 주소 확인"
msgstr "청구서용 이메일 주소"
#: pretix/base/invoicing/email.py:91
msgid "PDF via email"
msgstr ""
msgstr "이메일로 PDF"
#: pretix/base/invoicing/national.py:37
msgctxt "italian_invoice"
@@ -3613,11 +3523,9 @@ msgid "Address for certified electronic mail"
msgstr ""
#: pretix/base/invoicing/national.py:57
#, fuzzy
#| msgid "Recipient"
msgctxt "italian_invoice"
msgid "Recipient code"
msgstr "영수증"
msgstr "영수증 코드"
#: pretix/base/invoicing/national.py:81
msgctxt "italian_invoice"
@@ -3697,7 +3605,6 @@ msgid ""
"until {to_date}"
msgstr ""
"{from_date}\n"
"\n"
"{to_date}까지"
#: pretix/base/invoicing/pdf.py:609 pretix/base/services/mail.py:512
@@ -3777,10 +3684,10 @@ msgid "Single price: {net_price} net / {gross_price} gross"
msgstr "단일 가격: {net_price} 순 / {gross_price} 총합"
#: pretix/base/invoicing/pdf.py:901
#, fuzzy, python-brace-format
#, python-brace-format
msgctxt "invoice"
msgid "Single price: {price}"
msgstr "단일 가격: {가격}"
msgstr "단일 가격: {price}"
#: pretix/base/invoicing/pdf.py:944 pretix/base/invoicing/pdf.py:949
msgctxt "invoice"
@@ -3808,12 +3715,10 @@ msgid "Remaining amount"
msgstr "잔액"
#: pretix/base/invoicing/pdf.py:1009
#, fuzzy, python-brace-format
#| msgctxt "invoice"
#| msgid "Event date: {date_range}"
#, python-brace-format
msgctxt "invoice"
msgid "Invoice period: {daterange}"
msgstr "이벤트 날짜: {date_range}"
msgstr "청구서 기간: {daterange}"
#: pretix/base/invoicing/pdf.py:1040
msgctxt "invoice"
@@ -3836,23 +3741,21 @@ msgid "Included taxes"
msgstr "세금 포함"
#: pretix/base/invoicing/pdf.py:1100
#, fuzzy, python-brace-format
#, python-brace-format
msgctxt "invoice"
msgid ""
"Using the conversion rate of 1:{rate} as published by the {authority} on "
"{date}, this corresponds to:"
msgstr ""
"{날짜}에 {당국}에서 발표한 1:{세율}의 변환율을 사용하면 다음과 같습니다:"
msgstr "{date}에 {authority}에서 발표한 1:{rate}의 변환율을 사용하면 다음과 같습니다:"
#: pretix/base/invoicing/pdf.py:1115
#, fuzzy, python-brace-format
#, python-brace-format
msgctxt "invoice"
msgid ""
"Using the conversion rate of 1:{rate} as published by the {authority} on "
"{date}, the invoice total corresponds to {total}."
msgstr ""
"{날짜}에 {당국}에서 게시한 1:{세율}의 변환율을 사용하면 송장 총액이 {총합}에 "
"해당합니다."
msgstr "{date}에 {authority}에서 게시한 1:{rate}의 변환율을 사용하면 송장 총액이 "
"{total}에 해당합니다."
#: pretix/base/invoicing/pdf.py:1129
msgid "Default invoice renderer (European-style letter)"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-12-01 12:48+0000\n"
"Last-Translator: José Manuel Silva <prof.jmas@gmail.com>\n"
"PO-Revision-Date: 2025-12-02 16:47+0000\n"
"Last-Translator: Ana Rute Pacheco Vivas <rute.vivas@om.org>\n"
"Language-Team: Portuguese (Portugal) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_PT/>\n"
"Language: pt_PT\n"
@@ -13318,13 +13318,13 @@ msgid "Contact:"
msgstr "Contacto:"
#: pretix/base/templates/pretixbase/email/order_details.html:54
#, fuzzy, python-format
#| msgid ""
#| "You are receiving this email because you placed an order for {event}."
#, python-format
msgid ""
"You are receiving this email because you placed an order for "
"<strong>%(event)s</strong>."
msgstr "Estás a receber este email porque fizeste um pedido para o {event}."
msgstr ""
"Estás a receber este email porque fizeste um pedido para o <strong>%(event)"
"s</strong>."
#: pretix/base/templates/pretixbase/email/order_details.html:93
#: pretix/control/templates/pretixcontrol/organizers/customer.html:23

View File

@@ -82,6 +82,9 @@ $(function () {
if ('label' in options) {
dependent.closest(".form-group").find(".control-label").text(options.label);
}
if ('helptext_visible' in options) {
dependent.closest(".form-group").find(".help-block").toggle(options.helptext_visible);
}
const required = 'required' in options && visible && (
(options.required === 'if_any' && isAnyRequired) ||

View File

@@ -34,7 +34,7 @@ def test_no_invoice_address(client):
'data': [],
'state': {'label': 'State', 'required': False, 'visible': False},
'street': {'required': 'if_any'},
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -44,7 +44,7 @@ def test_no_invoice_address(client):
'data': [],
'state': {'label': 'State', 'required': False, 'visible': False},
'street': {'required': 'if_any'},
'vat_id': {'required': False, 'visible': False},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': False},
'zipcode': {'required': False}
}
@@ -98,7 +98,7 @@ def test_provider_only_email_available(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': False},
'transmission_types': [{'code': 'email', 'name': 'Email'}],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -123,7 +123,7 @@ def test_provider_italy_sdi_not_enforced_when_optional(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'it_sdi', 'name': 'Exchange System (SdI)'}],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID / P.IVA', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -148,7 +148,7 @@ def test_provider_italy_sdi_enforced_individual(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'it_sdi', 'name': 'Exchange System (SdI)'}],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID / P.IVA', 'required': False, 'visible': True},
'zipcode': {'required': True}
}
@@ -174,11 +174,37 @@ def test_provider_italy_sdi_enforced_business(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'it_sdi', 'name': 'Exchange System (SdI)'}],
'vat_id': {'required': True, 'visible': True},
'vat_id': {'helptext_visible': False, 'label': 'VAT ID / P.IVA', 'required': True, 'visible': True},
'zipcode': {'required': True}
}
@pytest.mark.django_db
def test_vat_id_enforced(client, event):
response = client.get(
'/js_helpers/address_form/?country=GR&invoice=true&organizer=org&event=ev'
'&is_business=business'
)
assert response.status_code == 200
d = response.json()
del d['data']
assert d == {
'city': {'required': 'if_any'},
'state': {'label': 'State', 'required': False, 'visible': False},
'street': {'required': 'if_any'},
'transmission_email_address': {'required': False, 'visible': False},
'transmission_email_other': {'required': False, 'visible': False},
'transmission_it_sdi_codice_fiscale': {'required': False, 'visible': False},
'transmission_it_sdi_pec': {'required': False, 'visible': False},
'transmission_it_sdi_recipient_code': {'required': False, 'visible': False},
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'email', 'name': 'Email'}, {'code': 'peppol', 'name': 'Peppol'}],
'vat_id': {'helptext_visible': False, 'label': 'VAT ID / TIN', 'required': True, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@pytest.mark.django_db
def test_email_peppol_choice(client, event):
response = client.get(
@@ -203,7 +229,7 @@ def test_email_peppol_choice(client, event):
{'code': 'email', 'name': 'Email'},
{'code': 'peppol', 'name': 'Peppol'},
],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -229,6 +255,6 @@ def test_email_peppol_choice(client, event):
{'code': 'email', 'name': 'Email'},
{'code': 'peppol', 'name': 'Peppol'},
],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': True}
}

View File

@@ -24,7 +24,7 @@ import responses
from requests import Timeout
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
VATIDFinalError, VATIDTemporaryError, normalize_vat_id, validate_vat_id,
)
@@ -51,6 +51,18 @@ def test_eu_country_mismatch():
validate_vat_id('AT12345', 'DE')
@responses.activate
def test_normalize():
assert normalize_vat_id('AT U 12345678', 'AT') == 'ATU12345678'
assert normalize_vat_id('U12345678', 'AT') == 'ATU12345678'
assert normalize_vat_id('IT.123.456.789.00', 'IT') == 'IT12345678900'
assert normalize_vat_id('12345678900', 'IT') == 'IT12345678900'
assert normalize_vat_id('123456789MVA', 'NO') == "NO123456789MVA"
assert normalize_vat_id('CHE 123456789 MWST', 'CH') == "CHE123456789"
# Bad combination is left for validation
assert normalize_vat_id('ATU12345678', 'IT') == 'ATU12345678'
@responses.activate
def test_eu_server_down():
def _callback(request):

View File

@@ -411,6 +411,69 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
assert ia.vat_id == "AT123456"
assert not ia.vat_id_validated
def test_reverse_charge_vatid_required(self):
self.event.settings.invoice_address_vatid = True
self.event.settings.invoice_address_vatid_required_countries = ["AT"]
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
resp = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
assert 'has-error' in resp.content.decode()
def test_reverse_charge_vatid_check_unavailable_but_required(self):
self.tr19.eu_reverse_charge = True
self.tr19.home_country = Country('DE')
self.tr19.save()
self.event.settings.invoice_address_vatid = True
self.event.settings.invoice_address_vatid_required_countries = ["AT"]
with scopes_disabled():
cr1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
with mock.patch('pretix.base.services.tax._validate_vat_id_EU') as mock_validate:
def raiser(*args, **kwargs):
raise VATIDTemporaryError('temp')
mock_validate.side_effect = raiser
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
assert cr1.price == Decimal('23.00')
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
assert ia.vat_id == "AT123456"
assert not ia.vat_id_validated
def test_reverse_charge_keep_gross(self):
@@ -448,6 +511,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
assert ia.vat_id == "AT123456"
assert ia.vat_id_validated
def test_custom_tax_rules(self):
@@ -1452,7 +1516,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'transmission_type': 'it_sdi',
'vat_id': '',
}, follow=True)
assert "This field is required for the selected type" in response.content.decode()
assert "This field is required" in response.content.decode()
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
@@ -1468,6 +1532,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'state': 'MI',
'email': 'admin@localhost',
'transmission_type': 'email',
'vat_id': 'IT01234567890',
}, follow=True)
assert "must be used for this country" in response.content.decode()