Compare commits

..
Author SHA1 Message Date
Richard Schreiber 4d18e8391a Fix mail headers being None 2026-02-03 11:16:58 +01:00
dependabot[bot] cead2898a7 Bump @babel/preset-env from 7.28.5 to 7.29.0 in /src/pretix/static/npm_dir (#5867)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.28.5 to 7.29.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.0/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 13:43:13 +01:00
Raphael Michel 6a594a6166 Metrics: Fix length and age of of queues (broken after #5513) (#5865) 2026-02-02 13:37:16 +01:00
Raphael Michel 0e7bb43a5a Manual payment: Fix using hidden method for existing order (#5850) 2026-02-02 12:32:53 +01:00
Richard Schreiber 3a3ae6e66c Fix custom pycountry_add index handling (#5869) 2026-02-02 09:41:32 +01:00
Raphael Michel 48aecb80f6 Mail compat layer: Disable scopes 2026-01-30 12:38:51 +01:00
Raphael MichelandRichard Schreiber d58a6e2503 Tax rounding: Allow to apply only for B2B (Z#23220106) (#5810)
* Tax rounding: Allow to apply only for B2B (Z#23220106)

Most effective in combination with #5807

* Update src/pretix/base/settings.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-01-30 11:53:38 +01:00
Raphael Michel 8c4e0bdb82 Outgoing mails: Fix cross-browser support 2026-01-30 11:37:10 +01:00
Raphael Michel c40e34af57 Model-based mail queuing 2026-01-30 10:43:02 +01:00
Richard Schreiber 1492ec51bf Limit organizer ical to 1000 entries 2026-01-30 08:59:34 +01:00
robbi5 7ca2a0c910 Remove duplicate device/revoke from api documentation 2026-01-29 20:44:47 +01:00
Ruud Hendrickx 0e41cb53a2 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate
2026-01-29 20:43:22 +01:00
Ruud Hendrickx 1d579d12c5 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 93.8% (5827 of 6207 strings)

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

powered by weblate
2026-01-29 20:43:22 +01:00
Jiří Pastrňák f3fa323351 Translations: Update Czech
Currently translated at 69.9% (4341 of 6207 strings)

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

powered by weblate
2026-01-29 20:43:22 +01:00
Ruud Hendrickx 67434bbe08 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 76.7% (4766 of 6207 strings)

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

powered by weblate
2026-01-29 20:43:22 +01:00
Nate Horst 1f38d48ab7 Translations: Update Thai
Currently translated at 99.2% (254 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/th/

powered by weblate
2026-01-29 20:43:22 +01:00
Nate Horst 0b99ab74a1 Translations: Update Thai
Currently translated at 20.3% (1263 of 6207 strings)

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

powered by weblate
2026-01-29 20:43:22 +01:00
Ruud Hendrickx 9508e13ea8 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 91.0% (233 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate
2026-01-29 20:43:22 +01:00
Ruud Hendrickx 7efac71d62 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 75.5% (4690 of 6207 strings)

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

powered by weblate
2026-01-29 20:43:22 +01:00
Raphael MichelandRichard Schreiber 26fdcc2872 Order changes: Do not allow to double-book add-ons (Z#23220592) (#5851)
* Order changes: Do not allow to double-book add-ons

* tests

* Update src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-01-29 20:42:43 +01:00
Richard Schreiber 0e5e2193ed Fix auto-quantity change on free-price input
* Fix auto-quantity change on free-price input

* do not use one()
2026-01-29 14:19:09 +01:00
Richard Schreiber 1e2900ad2a Markdown: fix double escaping URLs in safelink
* Markdown: fix double escaping URLs in safelink

* add tests

* fix isort
2026-01-29 12:14:12 +01:00
dependabot[bot] 4f521022f5 Update sentry-sdk requirement from ==2.50.* to ==2.51.*
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.50.0...2.51.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.51.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 09:25:47 +01:00
Ryo Tagami 5ce28ce258 Translations: Update Japanese
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ja/

powered by weblate
2026-01-29 09:25:39 +01:00
Ryo Tagami e51e765fcd Translations: Update Japanese
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx bb8301fbac Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 75.3% (193 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 5023081d6a Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 70.1% (4352 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Jiří Pastrňák f2bf8e01e1 Translations: Update Czech
Currently translated at 69.9% (4341 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 65645a7e93 Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Nate Horst 296b17fb7b Translations: Update Thai
Currently translated at 56.6% (145 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/th/

powered by weblate
2026-01-29 09:25:39 +01:00
Nate Horst fdc6de2a3d Translations: Update Thai
Currently translated at 12.6% (783 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 5c2c9c94c7 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 64.4% (165 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 277e63cce7 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 69.2% (4297 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx b0a031de93 Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ryo Tagami 6d770c66d6 Translations: Update Japanese
Currently translated at 99.2% (254 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ja/

powered by weblate
2026-01-29 09:25:39 +01:00
Yasunobu YesNo Kawaguchi d73155b69a Translations: Update Japanese
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ryo Tagami 839deabac3 Translations: Update Japanese
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 59c702588a Translations: Update Dutch
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl/

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx e1aaa422c9 Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ryo Tagami 27ae5ae018 Translations: Update Japanese
Currently translated at 99.7% (6189 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
CVZ-es 56c528795c Translations: Update Spanish
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/es/

powered by weblate
2026-01-29 09:25:39 +01:00
CVZ-es 6e70562839 Translations: Update Spanish
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
CVZ-es f7eff231ff Translations: Update French
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fr/

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 52f78157f3 Translations: Update Dutch
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl/

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx e9a2633b01 Translations: Update Dutch
Currently translated at 99.6% (6187 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
CVZ-es 40932685fe Translations: Update French
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 5e66f21193 Translations: Update Dutch
Currently translated at 99.6% (6186 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Hijiri Umemoto 48683ce11d Translations: Update Chinese (Traditional Han script)
Currently translated at 92.1% (5720 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Hijiri Umemoto 8fc719b483 Translations: Update Japanese
Currently translated at 99.7% (6189 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Renne Rocha c38859478c Translations: Update Portuguese (Brazil)
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/pt_BR/

powered by weblate
2026-01-29 09:25:39 +01:00
Renne Rocha 210115acef Translations: Update Portuguese (Brazil)
Currently translated at 89.9% (5586 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Ruud Hendrickx 4db2384e93 Translations: Update Dutch
Currently translated at 99.6% (6186 of 6207 strings)

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

powered by weblate
2026-01-29 09:25:39 +01:00
Richard Schreiber 803d0b1570 Fix missing locale in widget waitinglist 2026-01-26 16:52:37 +01:00
Raphael Michel 65fe7b3396 Bump version to 2026.2.0.dev0 2026-01-26 16:52:09 +01:00
97 changed files with 9882 additions and 9242 deletions
-14
View File
@@ -208,20 +208,6 @@ Additionally, when creating a device through the user interface or API, a user c
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
policies for official pretix apps like pretixSCAN and pretixPOS. policies for official pretix apps like pretixSCAN and pretixPOS.
Removing a device
-----------------
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
invalidate your API key. There is no way to reverse this operation.
.. sourcecode:: http
POST /api/v1/device/revoke HTTP/1.1
Host: pretix.eu
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
This can also be done by the user through the web interface.
Event selection Event selection
--------------- ---------------
+1 -1
View File
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*", "redis==7.1.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.50.*", "sentry-sdk==2.51.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2026.1.2" __version__ = "2026.2.0.dev0"
+1
View File
@@ -1743,6 +1743,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
rounding_mode = self.context["event"].settings.tax_rounding rounding_mode = self.context["event"].settings.tax_rounding
changed = apply_rounding( changed = apply_rounding(
rounding_mode, rounding_mode,
ia,
self.context["event"].currency, self.context["event"].currency,
[*pos_map.values(), *fees] [*pos_map.values(), *fees]
) )
+16 -19
View File
@@ -49,7 +49,7 @@ from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_ORGANIZER,
) )
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import mail
from pretix.base.settings import validate_organizer_settings from pretix.base.settings import validate_organizer_settings
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
@@ -363,24 +363,21 @@ class TeamInviteSerializer(serializers.ModelSerializer):
) )
def _send_invite(self, instance): def _send_invite(self, instance):
try: mail(
mail( instance.email,
instance.email, _('pretix account invitation'),
_('pretix account invitation'), 'pretixcontrol/email/invitation.txt',
'pretixcontrol/email/invitation.txt', {
{ 'user': self,
'user': self, 'organizer': self.context['organizer'].name,
'organizer': self.context['organizer'].name, 'team': instance.team.name,
'team': instance.team.name, 'url': build_global_uri('control:auth.invite', kwargs={
'url': build_global_uri('control:auth.invite', kwargs={ 'token': instance.token
'token': instance.token })
}) },
}, event=None,
event=None, locale=get_language_without_region() # TODO: expose?
locale=get_language_without_region() # TODO: expose? )
)
except SendMailException:
pass # Already logged
def create(self, validated_data): def create(self, validated_data):
if 'email' in validated_data: if 'email' in validated_data:
+1 -1
View File
@@ -1117,7 +1117,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'can_view_orders' permission = 'can_view_orders'
def get_queryset(self): def get_queryset(self):
qs = Checkin.all.filter(list__event=self.request.event).select_related( qs = Checkin.all.filter().select_related(
"position", "position",
"device", "device",
) )
+1 -11
View File
@@ -90,7 +90,6 @@ from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice, transmit_invoice, regenerate_invoice, transmit_invoice,
) )
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, _order_placed_email, OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order, _order_placed_email_attendee, approve_order, cancel_order, deny_order,
@@ -439,8 +438,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e: except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
return Response( return Response(
@@ -634,10 +631,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
order = self.get_object() order = self.get_object()
if not order.email: if not order.email:
return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST)
try: order.resend_link(user=self.request.user, auth=self.request.auth)
order.resend_link(user=self.request.user, auth=self.request.auth)
except SendMailException:
return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return Response( return Response(
status=status.HTTP_204_NO_CONTENT status=status.HTTP_204_NO_CONTENT
@@ -1616,8 +1610,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
) )
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
pass pass
except SendMailException:
pass
serializer = OrderPaymentSerializer(r, context=serializer.context) serializer = OrderPaymentSerializer(r, context=serializer.context)
@@ -1655,8 +1647,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e: except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST']) @action(detail=True, methods=['POST'])
+7 -9
View File
@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback, DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_callback, markdown_compile_email, truelink_callback,
) )
from pretix.helpers.format import FormattedString, SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext get_available_placeholders, PlaceholderContext
@@ -141,7 +141,6 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
return markdown_compile_email(plaintext, context=context) return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str: def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
apply_format_map = not isinstance(plain_body, FormattedString)
body_md = self.compile_markdown(plain_body, context) body_md = self.compile_markdown(plain_body, context)
if context: if context:
linker = bleach.Linker( linker = bleach.Linker(
@@ -150,13 +149,12 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback], callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True parse_email=True
) )
if apply_format_map: body_md = format_map(
body_md = format_map( body_md,
body_md, context=context,
context=context, mode=SafeFormatter.MODE_RICH_TO_HTML,
mode=SafeFormatter.MODE_RICH_TO_HTML, linkifier=linker
linkifier=linker )
)
htmlctx = { htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME, 'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL, 'site_url': settings.SITE_URL,
+35 -39
View File
@@ -33,7 +33,7 @@ from pretix.base.invoicing.transmission import (
transmission_types, transmission_types,
) )
from pretix.base.models import Invoice, InvoiceAddress from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.services.mail import SendMailException, mail, render_mail from pretix.base.services.mail import mail, render_mail
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
@@ -133,41 +133,37 @@ class EmailTransmissionProvider(TransmissionProvider):
template = invoice.order.event.settings.get('mail_text_order_invoice', as_type=LazyI18nString) template = invoice.order.event.settings.get('mail_text_order_invoice', as_type=LazyI18nString)
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString) subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
try: # Do not set to completed because that is done by the email sending task
# Do not set to completed because that is done by the email sending task subject = format_map(subject, context)
subject = format_map(subject, context) email_content = render_mail(template, context)
email_content = render_mail(template, context) mail(
mail( [recipient],
[recipient], subject,
subject, template,
template, context=context,
context=context, event=invoice.order.event,
event=invoice.order.event, locale=invoice.order.locale,
locale=invoice.order.locale, order=invoice.order,
order=invoice.order, invoices=[invoice],
invoices=[invoice], attach_tickets=False,
attach_tickets=False, auto_email=True,
auto_email=True, attach_ical=False,
attach_ical=False, plain_text_only=True,
plain_text_only=True, no_order_links=True,
no_order_links=True, )
) invoice.order.log_action(
except SendMailException: 'pretix.event.order.email.invoice',
raise user=None,
else: auth=None,
invoice.order.log_action( data={
'pretix.event.order.email.invoice', 'subject': subject,
user=None, 'message': email_content,
auth=None, 'position': None,
data={ 'recipient': recipient,
'subject': subject, 'invoices': [invoice.pk],
'message': email_content, 'attach_tickets': False,
'position': None, 'attach_ical': False,
'recipient': recipient, 'attach_other_files': [],
'invoices': [invoice.pk], 'attach_cached_files': [],
'attach_tickets': False, }
'attach_ical': False, )
'attach_other_files': [],
'attach_cached_files': [],
}
)
+21 -7
View File
@@ -294,14 +294,28 @@ def metric_values():
channel = app.broker_connection().channel() channel = app.broker_connection().channel()
if hasattr(channel, 'client') and channel.client is not None: if hasattr(channel, 'client') and channel.client is not None:
client = channel.client client = channel.client
priority_steps = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("priority_steps", [0])
sep = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("sep", ":")
for q in settings.CELERY_TASK_QUEUES: for q in settings.CELERY_TASK_QUEUES:
llen = client.llen(q.name) queue_lengths = []
lfirst = client.lindex(q.name, -1) queue_delays = []
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = llen for prio in priority_steps:
if lfirst: if prio:
ldata = json.loads(lfirst) qname = f"{q.name}{sep}{prio}"
dt = time.time() - ldata.get('created', 0) else:
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = dt qname = q.name
queue_length = client.llen(qname)
queue_lengths.append(queue_length)
oldest_queue_item = client.lindex(qname, -1)
if oldest_queue_item:
ldata = json.loads(oldest_queue_item)
oldest_item_age = time.time() - ldata.get('created', 0)
queue_delays.append(oldest_item_age)
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = sum(queue_lengths)
if queue_delays:
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = max(queue_delays)
else: else:
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0 metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0
@@ -0,0 +1,120 @@
# Generated by Django 4.2.26 on 2026-01-22 13:44
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.mail
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0296_invoice_invoice_from_state"),
]
operations = [
migrations.CreateModel(
name="OutgoingMail",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("guid", models.UUIDField(db_index=True, default=uuid.uuid4)),
("status", models.CharField(default="queued", max_length=200)),
("created", models.DateTimeField(auto_now_add=True)),
("sent", models.DateTimeField(blank=True, null=True)),
("inflight_since", models.DateTimeField(blank=True, null=True)),
("retry_after", models.DateTimeField(blank=True, null=True)),
("error", models.TextField(null=True)),
("error_detail", models.TextField(null=True)),
("sensitive", models.BooleanField(default=False)),
("subject", models.TextField()),
("body_plain", models.TextField()),
("body_html", models.TextField(null=True)),
("sender", models.CharField(max_length=500)),
("headers", models.JSONField(default=dict)),
("to", models.JSONField(default=list)),
("cc", models.JSONField(default=list)),
("bcc", models.JSONField(default=list)),
("recipient_count", models.IntegerField()),
("should_attach_tickets", models.BooleanField(default=False)),
("should_attach_ical", models.BooleanField(default=False)),
("should_attach_other_files", models.JSONField(default=list)),
("actual_attachments", models.JSONField(default=list)),
(
"customer",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.customer",
),
),
(
"event",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.event",
),
),
(
"order",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.order",
),
),
(
"orderposition",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.orderposition",
),
),
(
"organizer",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="outgoing_mails",
to="pretixbase.organizer",
),
),
(
"should_attach_cached_files",
models.ManyToManyField(
related_name="outgoing_mails", to="pretixbase.cachedfile"
),
),
(
"should_attach_invoices",
models.ManyToManyField(
related_name="outgoing_mails", to="pretixbase.invoice"
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="outgoing_mails",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ("-created",),
},
),
]
+1
View File
@@ -41,6 +41,7 @@ from .items import (
itempicture_upload_to, itempicture_upload_to,
) )
from .log import LogEntry from .log import LogEntry
from .mail import OutgoingMail
from .media import ReusableMedium from .media import ReusableMedium
from .memberships import Membership, MembershipType from .memberships import Membership, MembershipType
from .notifications import NotificationSetting from .notifications import NotificationSetting
+16 -19
View File
@@ -334,27 +334,24 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.email return self.email
def send_security_notice(self, messages, email=None): def send_security_notice(self, messages, email=None):
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import mail
try: with language(self.locale):
with language(self.locale): msg = '- ' + '\n- '.join(str(m) for m in messages)
msg = '- ' + '\n- '.join(str(m) for m in messages)
mail( mail(
email or self.email, email or self.email,
_('Account information changed'), _('Account information changed'),
'pretixcontrol/email/security_notice.txt', 'pretixcontrol/email/security_notice.txt',
{ {
'user': self, 'user': self,
'messages': msg, 'messages': msg,
'url': build_absolute_uri('control:user.settings') 'url': build_absolute_uri('control:user.settings')
}, },
event=None, event=None,
user=self, user=self,
locale=self.locale locale=self.locale
) )
except SendMailException:
pass # Already logged
def send_confirmation_code(self, session, reason, email=None, state=None): def send_confirmation_code(self, session, reason, email=None, state=None):
""" """
+1
View File
@@ -293,6 +293,7 @@ class Customer(LoggedModel):
locale=self.locale, locale=self.locale,
customer=self, customer=self,
organizer=self.organizer, organizer=self.organizer,
sensitive=True,
) )
def usable_gift_cards(self, used_cards=[]): def usable_gift_cards(self, used_cards=[]):
+222
View File
@@ -0,0 +1,222 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import uuid
from django.core.mail import get_connection
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled
def CASCADE_IF_QUEUED(collector, field, sub_objs, using):
# If the email is still queued and the thing it is related to vanishes, the email can vanish as well
cascade_objs = [
o for o in sub_objs if o.status == OutgoingMail.STATUS_QUEUED
]
if cascade_objs:
models.CASCADE(collector, field, cascade_objs, using)
# In all other cases, set to NULL to keep the email on record
models.SET_NULL(collector, field, [o for o in sub_objs if o not in cascade_objs], using)
class OutgoingMail(models.Model):
STATUS_QUEUED = "queued"
STATUS_WITHHELD = "withheld"
STATUS_INFLIGHT = "inflight"
STATUS_AWAITING_RETRY = "awaiting_retry"
STATUS_FAILED = "failed"
STATUS_SENT = "sent"
STATUS_BOUNCED = "bounced"
STATUS_ABORTED = "aborted"
STATUS_CHOICES = (
(STATUS_QUEUED, _("queued")),
(STATUS_INFLIGHT, _("being sent")),
(STATUS_AWAITING_RETRY, _("awaiting retry")),
(STATUS_WITHHELD, _("withheld")), # for plugin use
(STATUS_FAILED, _("failed")),
(STATUS_ABORTED, _("aborted")),
(STATUS_SENT, _("sent")),
(STATUS_BOUNCED, _("bounced")), # for plugin use
)
STATUS_LIST_ABORTABLE = {
STATUS_QUEUED,
STATUS_WITHHELD,
STATUS_AWAITING_RETRY,
}
STATUS_LIST_RETRYABLE = {
STATUS_FAILED,
STATUS_WITHHELD,
}
# The GUID is a globally unique ID for the email added to a header of the email for later tracing
# in bug reports etc. We could theoretically also use this as a basis for the Message-ID header, but
# we currently don't since we are unsure if some intermediary SMTP servers have opinions on setting
# their own Message-ID headers.
guid = models.UUIDField(db_index=True, default=uuid.uuid4)
status = models.CharField(max_length=200, choices=STATUS_CHOICES, default=STATUS_QUEUED)
created = models.DateTimeField(auto_now_add=True)
# sent will be the time the email was sent or the email failed
sent = models.DateTimeField(null=True, blank=True)
inflight_since = models.DateTimeField(null=True, blank=True)
retry_after = models.DateTimeField(null=True, blank=True)
error = models.TextField(null=True, blank=True)
error_detail = models.TextField(null=True, blank=True)
# There is a conflict here between the different purposes of the model. As a system administrator,
# one wants *all* emails to be persisted as long as possible to debug issues. This means that if
# e.g. the event or order is deleted, we want SET_NULL behavior. However, in that case, the email
# would be an "orphan" forever and there's no way to remove the personal information.
# We try to find a middle-ground with the following behaviour:
# - The email is always deleted if the entire organizer or user is deleted
# - The email is always deleted if it has not yet been sent
# - The email is kept in all other cases
# This is only an acceptable trade-off since emails are stored for a short period only, and because
# orders and customers are never deleted during normal operation. If we ever make this a long-term
# storage / email archive, we'd need to find another way to make sure personal information is removed
# if personal information of orders etc is removed.
organizer = models.ForeignKey(
'pretixbase.Organizer',
on_delete=models.CASCADE,
related_name='outgoing_mails',
null=True, blank=True,
)
event = models.ForeignKey(
'pretixbase.Event',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
order = models.ForeignKey(
'pretixbase.Order',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
orderposition = models.ForeignKey(
'pretixbase.OrderPosition',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
customer = models.ForeignKey(
'pretixbase.Customer',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
user = models.ForeignKey(
'pretixbase.User',
on_delete=models.CASCADE,
related_name='outgoing_mails',
null=True, blank=True,
)
sensitive = models.BooleanField(default=False)
subject = models.TextField()
body_plain = models.TextField()
body_html = models.TextField(null=True)
sender = models.CharField(max_length=500)
headers = models.JSONField(default=dict)
to = models.JSONField(default=list)
cc = models.JSONField(default=list)
bcc = models.JSONField(default=list)
recipient_count = models.IntegerField()
# We don't store the actual invoices, tickets or calendar invites, so if the email is re-sent at a later time, a
# newer version of the files might be used. We accept that risk to save on storage and also because the new
# version might actually be more useful.
should_attach_invoices = models.ManyToManyField(
'pretixbase.Invoice',
related_name='outgoing_mails'
)
should_attach_tickets = models.BooleanField(default=False)
should_attach_ical = models.BooleanField(default=False)
# clean_cached_files makes sure not to delete these as long as the email is in a retryable state
should_attach_cached_files = models.ManyToManyField(
'pretixbase.CachedFile',
related_name='outgoing_mails',
)
# This is used to send files stored in settings. In most cases, these aren't short-lived and should still be there
# if the email is sent. Otherwise, they will be skipped. We accept that risk.
should_attach_other_files = models.JSONField(default=list)
# [{name, type size}] of the attachments we actually setn
actual_attachments = models.JSONField(default=list)
class Meta:
ordering = ('-created',)
def get_mail_backend(self):
if self.event:
return self.event.get_mail_backend()
elif self.organizer:
return self.organizer.get_mail_backend()
else:
return get_connection(fail_silently=False)
def scope_manager(self):
if self.organizer:
return scope(organizer=self.organizer) # noqa
else:
return scopes_disabled() # noqa
@property
def is_failed(self):
return self.status in (
OutgoingMail.STATUS_FAILED,
OutgoingMail.STATUS_AWAITING_RETRY,
OutgoingMail.STATUS_BOUNCED,
)
def save(self, *args, **kwargs):
if self.orderposition_id and not self.order_id:
self.order = self.orderposition.order
if self.order_id and not self.event_id:
self.event = self.order.event
if self.event_id and not self.organizer_id:
self.organizer = self.event.organizer
if self.customer_id and not self.organizer_id:
self.organizer = self.customer.organizer
self.recipient_count = len(self.to) + len(self.cc) + len(self.bcc)
super().save(*args, **kwargs)
def log_parameters(self):
if self.order:
error_log_action_type = 'pretix.event.order.email.error'
log_target = self.order
elif self.customer:
error_log_action_type = 'pretix.customer.email.error'
log_target = self.customer
elif self.user:
error_log_action_type = 'pretix.user.email.error'
log_target = self.user
else:
error_log_action_type = 'pretix.email.error'
log_target = None
return log_target, error_log_action_type
+68 -92
View File
@@ -87,7 +87,7 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map from ...helpers.format import format_map
from ...helpers.names import build_name from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var from ...testutils.middleware import debugflags_var
from ._transactions import ( from ._transactions import (
@@ -1167,9 +1167,7 @@ class Order(LockModel, LoggedModel):
only be attached for this position and child positions, the link will only point to the only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available. position and the attendee email will be used if available.
""" """
from pretix.base.services.mail import ( from pretix.base.services.mail import mail, render_mail
SendMailException, mail, render_mail,
)
if not self.email and not (position and position.attendee_email): if not self.email and not (position and position.attendee_email):
return return
@@ -1179,36 +1177,31 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email: if position and position.attendee_email:
recipient = position.attendee_email recipient = position.attendee_email
try: email_content = render_mail(template, context)
email_content = render_mail(template, context) subject = format_map(subject, context)
if not isinstance(subject, FormattedString): mail(
subject = format_map(subject, context) recipient, subject, template, context,
mail( self.event, self.locale, self, headers=headers, sender=sender,
recipient, subject, template, context, invoices=invoices, attach_tickets=attach_tickets,
self.event, self.locale, self, headers=headers, sender=sender, position=position, auto_email=auto_email, attach_ical=attach_ical,
invoices=invoices, attach_tickets=attach_tickets, attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
position=position, auto_email=auto_email, attach_ical=attach_ical, )
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files, self.log_action(
) log_entry_type,
except SendMailException: user=user,
raise auth=auth,
else: data={
self.log_action( 'subject': subject,
log_entry_type, 'message': email_content,
user=user, 'position': position.positionid if position else None,
auth=auth, 'recipient': recipient,
data={ 'invoices': [i.pk for i in invoices] if invoices else [],
'subject': subject, 'attach_tickets': attach_tickets,
'message': email_content, 'attach_ical': attach_ical,
'position': position.positionid if position else None, 'attach_other_files': attach_other_files,
'recipient': recipient, 'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
'invoices': [i.pk for i in invoices] if invoices else [], }
'attach_tickets': attach_tickets, )
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
def resend_link(self, user=None, auth=None): def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region): with language(self.locale, self.event.settings.region):
@@ -2025,40 +2018,30 @@ class OrderPayment(models.Model):
transmit_invoice.apply_async(args=(self.order.event_id, invoice.pk, False)) transmit_invoice.apply_async(args=(self.order.event_id, invoice.pk, False))
def _send_paid_mail_attendee(self, position, user): def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
with language(self.order.locale, self.order.event.settings.region): with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid_attendee email_template = self.order.event.settings.mail_text_order_paid_attendee
email_subject = self.order.event.settings.mail_subject_order_paid_attendee email_subject = self.order.event.settings.mail_subject_order_paid_attendee
email_context = get_email_context(event=self.order.event, order=self.order, position=position) email_context = get_email_context(event=self.order.event, order=self.order, position=position)
try: position.send_mail(
position.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.order_paid', user,
'pretix.event.order.email.order_paid', user, invoices=[],
invoices=[], attach_tickets=True,
attach_tickets=True, attach_ical=self.order.event.settings.mail_attach_ical
attach_ical=self.order.event.settings.mail_attach_ical )
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text): def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
with language(self.order.locale, self.order.event.settings.region): with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid email_template = self.order.event.settings.mail_text_order_paid
email_subject = self.order.event.settings.mail_subject_order_paid email_subject = self.order.event.settings.mail_subject_order_paid
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text) email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
try: self.order.send_mail(
self.order.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.order_paid', user,
'pretix.event.order.email.order_paid', user, invoices=[invoice] if invoice else [],
invoices=[invoice] if invoice else [], attach_tickets=True,
attach_tickets=True, attach_ical=self.order.event.settings.mail_attach_ical
attach_ical=self.order.event.settings.mail_attach_ical )
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@property @property
def refunded_amount(self): def refunded_amount(self):
@@ -2916,46 +2899,39 @@ class OrderPosition(AbstractPosition):
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download :param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files :param attach_ical: Attach relevant ICS files
""" """
from pretix.base.services.mail import ( from pretix.base.services.mail import mail, render_mail
SendMailException, mail, render_mail,
)
if not self.attendee_email: if not self.attendee_email:
return return
with language(self.order.locale, self.order.event.settings.region): with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email recipient = self.attendee_email
try: email_content = render_mail(template, context)
email_content = render_mail(template, context) subject = format_map(subject, context)
if not isinstance(subject, FormattedString): mail(
subject = format_map(subject, context) recipient, subject, template, context,
mail( self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
recipient, subject, template, context, position=self,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender, invoices=invoices,
position=self, attach_tickets=attach_tickets,
invoices=invoices, attach_ical=attach_ical,
attach_tickets=attach_tickets, attach_other_files=attach_other_files,
attach_ical=attach_ical, )
attach_other_files=attach_other_files, self.order.log_action(
) log_entry_type,
except SendMailException: user=user,
raise auth=auth,
else: data={
self.order.log_action( 'subject': subject,
log_entry_type, 'message': email_content,
user=user, 'recipient': recipient,
auth=auth, 'invoices': [i.pk for i in invoices] if invoices else [],
data={ 'attach_tickets': attach_tickets,
'subject': subject, 'attach_ical': attach_ical,
'message': email_content, 'attach_other_files': attach_other_files,
'recipient': recipient, 'attach_cached_files': [],
'invoices': [i.pk for i in invoices] if invoices else [], }
'attach_tickets': attach_tickets, )
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [],
}
)
def resend_link(self, user=None, auth=None): def resend_link(self, user=None, auth=None):
+25 -29
View File
@@ -34,7 +34,7 @@ from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import User, Voucher from pretix.base.models import User, Voucher
from pretix.base.services.mail import SendMailException, mail, render_mail from pretix.base.services.mail import mail, render_mail
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from ...helpers.format import format_map from ...helpers.format import format_map
@@ -272,34 +272,30 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region): with language(self.locale, self.event.settings.region):
recipient = self.email recipient = self.email
try: email_content = render_mail(template, context)
email_content = render_mail(template, context) subject = format_map(subject, context)
subject = format_map(subject, context) mail(
mail( recipient, subject, template, context,
recipient, subject, template, context, self.event,
self.event, self.locale,
self.locale, headers=headers,
headers=headers, sender=sender,
sender=sender, auto_email=auto_email,
auto_email=auto_email, attach_other_files=attach_other_files,
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
attach_cached_files=attach_cached_files, )
) self.log_action(
except SendMailException: log_entry_type,
raise user=user,
else: auth=auth,
self.log_action( data={
log_entry_type, 'subject': subject,
user=user, 'message': email_content,
auth=auth, 'recipient': recipient,
data={ 'attach_other_files': attach_other_files,
'subject': subject, 'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
'message': email_content, }
'recipient': recipient, )
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
@staticmethod @staticmethod
def clean_itemvar(event, item, variation): def clean_itemvar(event, item, variation):
+2 -2
View File
@@ -1231,8 +1231,8 @@ class ManualPayment(BasePaymentProvider):
def is_allowed(self, request: HttpRequest, total: Decimal=None): def is_allowed(self, request: HttpRequest, total: Decimal=None):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().is_allowed(request, total) return 'pretix.plugins.manualpayment' in self.event.plugins and super().is_allowed(request, total)
def order_change_allowed(self, order: Order): def order_change_allowed(self, order: Order, request=None):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order) return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order, request)
@property @property
def public_name(self): def public_name(self):
+20 -29
View File
@@ -36,7 +36,7 @@ from pretix.base.models import (
SubEvent, TaxRule, User, WaitingListEntry, SubEvent, TaxRule, User, WaitingListEntry,
) )
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import mail
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund, OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
) )
@@ -53,17 +53,14 @@ logger = logging.getLogger(__name__)
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent): def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
with language(wle.locale, wle.event.settings.region): with language(wle.locale, wle.event.settings.region):
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event) email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
try: mail(
mail( wle.email,
wle.email, format_map(subject, email_context),
format_map(subject, email_context), message,
message, email_context,
email_context, wle.event,
wle.event, locale=wle.locale
locale=wle.locale )
)
except SendMailException:
logger.exception('Waiting list canceled email could not be sent')
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent, def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
@@ -77,14 +74,11 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount, email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event) order=order, position_or_address=ia, event=order.event)
real_subject = format_map(subject, email_context) real_subject = format_map(subject, email_context)
try: order.send_mail(
order.send_mail( real_subject, message, email_context,
real_subject, message, email_context, 'pretix.event.order.email.event_canceled',
'pretix.event.order.email.event_canceled', user,
user, )
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
for p in positions: for p in positions:
if subevent and p.subevent_id != subevent.id: if subevent and p.subevent_id != subevent.id:
@@ -97,15 +91,12 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
refund_amount=refund_amount, refund_amount=refund_amount,
position_or_address=p, position_or_address=p,
order=order, position=p) order=order, position=p)
try: order.send_mail(
order.send_mail( real_subject, message, email_context,
real_subject, message, email_context, 'pretix.event.order.email.event_canceled',
'pretix.event.order.email.event_canceled', position=p,
position=p, user=user
user=user )
)
except SendMailException:
logger.exception('Order canceled email could not be sent to attendee')
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
+2 -2
View File
@@ -1639,7 +1639,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment
if fee.tax_rule and not fee.tax_rule.pk: if fee.tax_rule and not fee.tax_rule.pk:
fee.tax_rule = None # TODO: deprecate fee.tax_rule = None # TODO: deprecate
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees])
if total != 0 and payments: if total != 0 and payments:
@@ -1679,7 +1679,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment
fees.append(pf) fees.append(pf)
# Re-apply rounding as grand total has changed # Re-apply rounding as grand total has changed
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees])
# Re-calculate to_pay as grand total has changed # Re-calculate to_pay as grand total has changed
+14 -2
View File
@@ -23,11 +23,12 @@ from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.db.models import Exists, OuterRef
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.timezone import now
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.models import CachedCombinedTicket, CachedTicket from pretix.base.models import CachedCombinedTicket, CachedTicket, OutgoingMail
from pretix.base.models.customers import CustomerSSOGrant from pretix.base.models.customers import CustomerSSOGrant
from ..models import CachedFile, CartPosition, InvoiceAddress from ..models import CachedFile, CartPosition, InvoiceAddress
@@ -49,7 +50,18 @@ def clean_cart_positions(sender, **kwargs):
@receiver(signal=periodic_task) @receiver(signal=periodic_task)
@scopes_disabled() @scopes_disabled()
def clean_cached_files(sender, **kwargs): def clean_cached_files(sender, **kwargs):
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()): has_queued_email = Exists(
OutgoingMail.objects.filter(
should_attach_cached_files__pk=OuterRef("pk"),
status__in=(
OutgoingMail.STATUS_QUEUED,
OutgoingMail.STATUS_INFLIGHT,
OutgoingMail.STATUS_AWAITING_RETRY,
OutgoingMail.STATUS_FAILED,
),
)
)
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()).exclude(has_queued_email):
cf.delete() cf.delete()
File diff suppressed because it is too large Load Diff
+23 -9
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import uuid
import css_inline import css_inline
from django.conf import settings from django.conf import settings
from django.template.loader import get_template from django.template.loader import get_template
@@ -26,7 +28,9 @@ from django.utils.timezone import override
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import LogEntry, NotificationSetting, User from pretix.base.models import (
LogEntry, NotificationSetting, OutgoingMail, User,
)
from pretix.base.notifications import Notification, get_all_notification_types from pretix.base.notifications import Notification, get_all_notification_types
from pretix.base.services.mail import mail_send_task from pretix.base.services.mail import mail_send_task
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
@@ -153,16 +157,26 @@ def send_notification_mail(notification: Notification, user: User):
tpl_plain = get_template('pretixbase/email/notification.txt') tpl_plain = get_template('pretixbase/email/notification.txt')
body_plain = tpl_plain.render(ctx) body_plain = tpl_plain.render(ctx)
mail_send_task.apply_async(kwargs={ guid = uuid.uuid4()
'to': [user.email], m = OutgoingMail.objects.create(
'subject': '[{}] {}: {}'.format( guid=guid,
user=user,
to=[user.email],
subject='[{}] {}: {}'.format(
settings.PRETIX_INSTANCE_NAME, settings.PRETIX_INSTANCE_NAME,
notification.event.settings.mail_prefix or notification.event.slug.upper(), notification.event.settings.mail_prefix or notification.event.slug.upper(),
notification.title notification.title
), ),
'body': body_plain, body_plain=body_plain,
'html': body_html, body_html=body_html,
'sender': settings.MAIL_FROM_NOTIFICATIONS, sender=settings.MAIL_FROM_NOTIFICATIONS,
'headers': {}, headers={
'user': user.pk 'X-Auto-Response-Suppress': 'OOF, NRN, AutoReply, RN',
'Auto-Submitted': 'auto-generated',
'X-Mailer': 'pretix',
'X-PX-Correlation': str(guid),
},
)
mail_send_task.apply_async(kwargs={
'outgoing_mail': m.pk,
}) })
+136 -144
View File
@@ -90,7 +90,6 @@ from pretix.base.services.invoices import (
from pretix.base.services.locking import ( from pretix.base.services.locking import (
LOCK_TRUST_WINDOW, LockTimeoutException, lock_objects, LOCK_TRUST_WINDOW, LockTimeoutException, lock_objects,
) )
from pretix.base.services.mail import SendMailException
from pretix.base.services.memberships import ( from pretix.base.services.memberships import (
create_membership, validate_memberships_in_order, create_membership, validate_memberships_in_order,
) )
@@ -438,33 +437,27 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee
email_context = get_email_context(event=order.event, order=order) email_context = get_email_context(event=order.event, order=order)
try: order.send_mail(
order.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.order_approved', user,
'pretix.event.order.email.order_approved', user, attach_tickets=True,
attach_tickets=True, attach_ical=order.event.settings.mail_attach_ical and (
attach_ical=order.event.settings.mail_attach_ical and ( not order.event.settings.mail_attach_ical_paid_only or
not order.event.settings.mail_attach_ical_paid_only or order.total == Decimal('0.00') or
order.total == Decimal('0.00') or order.valid_if_pending
order.valid_if_pending ),
), invoices=[invoice] if invoice and transmit_invoice_mail else []
invoices=[invoice] if invoice and transmit_invoice_mail else [] )
)
except SendMailException:
logger.exception('Order approved email could not be sent')
if email_attendees: if email_attendees:
for p in order.positions.all(): for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
email_attendee_context = get_email_context(event=order.event, order=order, position=p) email_attendee_context = get_email_context(event=order.event, order=order, position=p)
try: p.send_mail(
p.send_mail( email_attendee_subject, email_attendee_template, email_attendee_context,
email_attendee_subject, email_attendee_template, email_attendee_context, 'pretix.event.order.email.order_approved', user,
'pretix.event.order.email.order_approved', user, attach_tickets=True,
attach_tickets=True, )
)
except SendMailException:
logger.exception('Order approved email could not be sent to attendee')
return order.pk return order.pk
@@ -501,13 +494,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
email_template = order.event.settings.mail_text_order_denied email_template = order.event.settings.mail_text_order_denied
email_subject = order.event.settings.mail_subject_order_denied email_subject = order.event.settings.mail_subject_order_denied
email_context = get_email_context(event=order.event, order=order, comment=comment) email_context = get_email_context(event=order.event, order=order, comment=comment)
try: order.send_mail(
order.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.order_denied', user
'pretix.event.order.email.order_denied', user )
)
except SendMailException:
logger.exception('Order denied email could not be sent')
return order.pk return order.pk
@@ -660,14 +650,11 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
email_template = order.event.settings.mail_text_order_canceled email_template = order.event.settings.mail_text_order_canceled
email_subject = order.event.settings.mail_subject_order_canceled email_subject = order.event.settings.mail_subject_order_canceled
email_context = get_email_context(event=order.event, order=order, comment=comment or "") email_context = get_email_context(event=order.event, order=order, comment=comment or "")
try: order.send_mail(
order.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.order_canceled', user,
'pretix.event.order.email.order_canceled', user, invoices=transmit_invoices_mail,
invoices=transmit_invoices_mail, )
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)): for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)):
try: try:
@@ -968,7 +955,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
fee.tax_rule = None # TODO: deprecate fee.tax_rule = None # TODO: deprecate
# Apply rounding to get final total in case no payment fees will be added # Apply rounding to get final total in case no payment fees will be added
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees])
payments_assigned = Decimal("0.00") payments_assigned = Decimal("0.00")
@@ -995,7 +982,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
p['fee'] = pf p['fee'] = pf
# Re-apply rounding as grand total has changed # Re-apply rounding as grand total has changed
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees])
# Re-calculate to_pay as grand total has changed # Re-calculate to_pay as grand total has changed
@@ -1108,46 +1095,40 @@ def _order_placed_email(event: Event, order: Order, email_template, subject_temp
log_entry: str, invoice, payments: List[OrderPayment], is_free=False): log_entry: str, invoice, payments: List[OrderPayment], is_free=False):
email_context = get_email_context(event=event, order=order, payments=payments) email_context = get_email_context(event=event, order=order, payments=payments)
try: order.send_mail(
order.send_mail( subject_template, email_template, email_context,
subject_template, email_template, email_context, log_entry,
log_entry, invoices=[invoice] if invoice else [],
invoices=[invoice] if invoice else [], attach_tickets=True,
attach_tickets=True, attach_ical=event.settings.mail_attach_ical and (
attach_ical=event.settings.mail_attach_ical and ( not event.settings.mail_attach_ical_paid_only or
not event.settings.mail_attach_ical_paid_only or is_free or
is_free or order.valid_if_pending
order.valid_if_pending ),
), attach_other_files=[a for a in [
attach_other_files=[a for a in [ event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] ] if a],
] if a], )
)
except SendMailException:
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, subject_template, def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, subject_template,
log_entry: str, is_free=False): log_entry: str, is_free=False):
email_context = get_email_context(event=event, order=order, position=position) email_context = get_email_context(event=event, order=order, position=position)
try: position.send_mail(
position.send_mail( subject_template, email_template, email_context,
subject_template, email_template, email_context, log_entry,
log_entry, invoices=[],
invoices=[], attach_tickets=True,
attach_tickets=True, attach_ical=event.settings.mail_attach_ical and (
attach_ical=event.settings.mail_attach_ical and ( not event.settings.mail_attach_ical_paid_only or
not event.settings.mail_attach_ical_paid_only or is_free or
is_free or order.valid_if_pending
order.valid_if_pending ),
), attach_other_files=[a for a in [
attach_other_files=[a for a in [ event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] ] if a],
] if a], )
)
except SendMailException:
logger.exception('Order received email could not be sent to attendee')
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str], def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
@@ -1476,13 +1457,10 @@ def send_expiry_warnings(sender, **kwargs):
email_template = settings.mail_text_order_pending_warning email_template = settings.mail_text_order_pending_warning
email_subject = settings.mail_subject_order_pending_warning email_subject = settings.mail_subject_order_pending_warning
try: o.send_mail(
o.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.expire_warning_sent'
'pretix.event.order.email.expire_warning_sent' )
)
except SendMailException:
logger.exception('Reminder email could not be sent')
@receiver(signal=periodic_task) @receiver(signal=periodic_task)
@@ -1543,14 +1521,11 @@ def send_download_reminders(sender, **kwargs):
email_template = event.settings.mail_text_download_reminder email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o) email_context = get_email_context(event=event, order=o)
try: o.send_mail(
o.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.download_reminder_sent',
'pretix.event.order.email.download_reminder_sent', attach_tickets=True
attach_tickets=True )
)
except SendMailException:
logger.exception('Reminder email could not be sent')
if event.settings.mail_send_download_reminder_attendee: if event.settings.mail_send_download_reminder_attendee:
for p in positions: for p in positions:
@@ -1564,14 +1539,11 @@ def send_download_reminders(sender, **kwargs):
email_template = event.settings.mail_text_download_reminder_attendee email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p) email_context = get_email_context(event=event, order=o, position=p)
try: o.send_mail(
o.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.download_reminder_sent',
'pretix.event.order.email.download_reminder_sent', attach_tickets=True, position=p
attach_tickets=True, position=p )
)
except SendMailException:
logger.exception('Reminder email could not be sent to attendee')
def notify_user_changed_order(order, user=None, auth=None, invoices=[]): def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
@@ -1579,13 +1551,10 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
email_template = order.event.settings.mail_text_order_changed email_template = order.event.settings.mail_text_order_changed
email_context = get_email_context(event=order.event, order=order) email_context = get_email_context(event=order.event, order=order)
email_subject = order.event.settings.mail_subject_order_changed email_subject = order.event.settings.mail_subject_order_changed
try: order.send_mail(
order.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True,
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True, )
)
except SendMailException:
logger.exception('Order changed email could not be sent')
class OrderChangeManager: class OrderChangeManager:
@@ -1641,6 +1610,7 @@ class OrderChangeManager:
ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until')) ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until'))
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
class AddPositionResult: class AddPositionResult:
_position: Optional[OrderPosition] _position: Optional[OrderPosition]
@@ -1804,6 +1774,7 @@ class OrderChangeManager:
positions = self.order.positions.select_related('item', 'item__tax_rule') positions = self.order.positions.select_related('item', 'item__tax_rule')
ia = self._invoice_address ia = self._invoice_address
tax_rules = self._current_tax_rules() tax_rules = self._current_tax_rules()
self._operations.append(self.ForceRecomputeOperation())
for pos in positions: for pos in positions:
tax_rule = tax_rules.get(pos.pk, pos.tax_rule) tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
@@ -2094,6 +2065,43 @@ class OrderChangeManager:
) )
item_counts[item] += 1 item_counts[item] += 1
# Detect removed add-ons and create RemoveOperations
for cp, al in list(current_addons.items()):
for k, v in al.items():
input_num = input_addons[cp.id].get(k, 0)
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
# not mean it should be unremovable for others.
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or (
not a.item.all_sales_channels and
not a.item.limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
# "Re-select" add-on
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
continue
if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError(
error_messages['addon_already_checked_in'] % {
'addon': str(a.item.name),
}
)
self.cancel(a)
item_counts[a.item] -= 1
# Check constraints on the add-on combinations # Check constraints on the add-on combinations
for op in toplevel_op: for op in toplevel_op:
item = op.item item = op.item
@@ -2126,41 +2134,6 @@ class OrderChangeManager:
} }
) )
# Detect removed add-ons and create RemoveOperations
for cp, al in list(current_addons.items()):
for k, v in al.items():
input_num = input_addons[cp.id].get(k, 0)
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
# not mean it should be unremovable for others.
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or (
not item.all_sales_channels and
not item.limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
continue
if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError(
error_messages['addon_already_checked_in'] % {
'addon': str(a.item.name),
}
)
self.cancel(a)
item_counts[a.item] -= 1
for item, count in item_counts.items(): for item, count in item_counts.items():
if count == 0: if count == 0:
continue continue
@@ -2640,6 +2613,10 @@ class OrderChangeManager:
except BlockedTicketSecret.DoesNotExist: except BlockedTicketSecret.DoesNotExist:
pass pass
# todo: revoke list handling # todo: revoke list handling
elif isinstance(op, self.ForceRecomputeOperation):
self.order.log_action('pretix.event.order.changed.recomputed', user=self.user, auth=self.auth, data={})
else:
raise TypeError(f"Unknown operation {type(op)}")
for p in secret_dirty: for p in secret_dirty:
assign_ticket_secret( assign_ticket_secret(
@@ -2694,7 +2671,10 @@ class OrderChangeManager:
fees.append(new_fee) fees.append(new_fee)
changed_by_rounding = set(apply_rounding( changed_by_rounding = set(apply_rounding(
self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees self.order.tax_rounding_mode,
self._invoice_address,
self.event.currency,
[p for p in split_positions if not p.canceled] + fees
)) ))
split_order.total = sum([p.price for p in split_positions if not p.canceled]) split_order.total = sum([p.price for p in split_positions if not p.canceled])
@@ -2716,7 +2696,10 @@ class OrderChangeManager:
fee.delete() fee.delete()
changed_by_rounding |= set(apply_rounding( changed_by_rounding |= set(apply_rounding(
self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees self.order.tax_rounding_mode,
self._invoice_address,
self.event.currency,
[p for p in split_positions if not p.canceled] + fees
)) ))
split_order.total = sum([p.price for p in split_positions if not p.canceled]) + sum([f.value for f in fees]) split_order.total = sum([p.price for p in split_positions if not p.canceled]) + sum([f.value for f in fees])
@@ -2833,7 +2816,12 @@ class OrderChangeManager:
if fee_changed: if fee_changed:
fees = list(self.order.fees.all()) fees = list(self.order.fees.all())
changed = apply_rounding(self.order.tax_rounding_mode, self.order.event.currency, [*positions, *fees]) changed = apply_rounding(
self.order.tax_rounding_mode,
self._invoice_address,
self.order.event.currency,
[*positions, *fees]
)
for l in changed: for l in changed:
if isinstance(l, OrderPosition): if isinstance(l, OrderPosition):
l.save(update_fields=[ l.save(update_fields=[
@@ -3269,8 +3257,12 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
positions = list(order.positions.all()) positions = list(order.positions.all())
fees = list(order.fees.all()) fees = list(order.fees.all())
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
rounding_changed = set(apply_rounding( rounding_changed = set(apply_rounding(
order.tax_rounding_mode, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]] order.tax_rounding_mode, ia, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]]
)) ))
total_without_fee = sum(c.price for c in positions) + sum(f.value for f in fees if f.pk != fee.pk) total_without_fee = sum(c.price for c in positions) + sum(f.value for f in fees if f.pk != fee.pk)
pending_sum_without_fee = max(Decimal("0.00"), total_without_fee - already_paid) pending_sum_without_fee = max(Decimal("0.00"), total_without_fee - already_paid)
@@ -3295,7 +3287,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
fee = None fee = None
rounding_changed |= set(apply_rounding( rounding_changed |= set(apply_rounding(
order.tax_rounding_mode, order.event.currency, [*positions, *fees] order.tax_rounding_mode, ia, order.event.currency, [*positions, *fees]
)) ))
for l in rounding_changed: for l in rounding_changed:
if isinstance(l, OrderPosition): if isinstance(l, OrderPosition):
+9 -2
View File
@@ -211,7 +211,8 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)] return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)]
def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep_gross"], currency: str, def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only_business", "sum_by_net_keep_gross"],
invoice_address: Optional[InvoiceAddress], currency: str,
lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list: lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list:
""" """
Given a list of order positions / cart positions / order fees (may be mixed), applies the given rounding mode Given a list of order positions / cart positions / order fees (may be mixed), applies the given rounding mode
@@ -226,11 +227,17 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep
When rounding mode is set to ``"sum_by_net"``, the gross prices and tax values of the individual lines will be When rounding mode is set to ``"sum_by_net"``, the gross prices and tax values of the individual lines will be
adjusted such that the per-taxrate/taxcode subtotal is rounded correctly. The net prices will stay constant. adjusted such that the per-taxrate/taxcode subtotal is rounded correctly. The net prices will stay constant.
:param rounding_mode: One of ``"line"``, ``"sum_by_net"``, or ``"sum_by_net_keep_gross"``. :param rounding_mode: One of ``"line"``, ``"sum_by_net"``, ``"sum_by_net_only_business"``, or ``"sum_by_net_keep_gross"``.
:param invoice_address: The invoice address, or ``None``
:param currency: Currency that will be used to determine rounding precision :param currency: Currency that will be used to determine rounding precision
:param lines: List of order/cart contents :param lines: List of order/cart contents
:return: Collection of ``lines`` members that have been changed and may need to be persisted to the database. :return: Collection of ``lines`` members that have been changed and may need to be persisted to the database.
""" """
if rounding_mode == "sum_by_net_only_business":
if invoice_address and invoice_address.is_business:
rounding_mode = "sum_by_net"
else:
rounding_mode = "line"
def _key(line): def _key(line):
return (line.tax_rate, line.tax_code or "") return (line.tax_rate, line.tax_code or "")
+16 -19
View File
@@ -48,7 +48,7 @@ from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name from pretix.base.models import CachedFile, Event, User, cachedfile_name
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import mail
from pretix.base.services.tasks import ProfiledEventTask from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.shredder import ShredError from pretix.base.shredder import ShredError
from pretix.celery_app import app from pretix.celery_app import app
@@ -171,21 +171,18 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
if user: if user:
with language(user.locale): with language(user.locale):
try: mail(
mail( user.email,
user.email, _('Data shredding completed'),
_('Data shredding completed'), 'pretixbase/email/shred_completed.txt',
'pretixbase/email/shred_completed.txt', {
{ 'user': user,
'user': user, 'organizer': event.organizer.name,
'organizer': event.organizer.name, 'event': str(event.name),
'event': str(event.name), 'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'), 'shredders': ', '.join([str(s.verbose_name) for s in shredders])
'shredders': ', '.join([str(s.verbose_name) for s in shredders]) },
}, event=None,
event=None, user=user,
user=user, locale=user.locale,
locale=user.locale, )
)
except SendMailException:
pass # Already logged
+1
View File
@@ -81,6 +81,7 @@ from pretix.helpers.countries import CachedCountries, pycountry_add
ROUNDING_MODES = ( ROUNDING_MODES = (
('line', _('Compute taxes for every line individually')), ('line', _('Compute taxes for every line individually')),
('sum_by_net', _('Compute taxes based on net total')), ('sum_by_net', _('Compute taxes based on net total')),
('sum_by_net_only_business', _('For business customers, compute taxes based on net total. For individuals, use line-based rounding')),
('sum_by_net_keep_gross', _('Compute taxes based on net total with stable gross prices')), ('sum_by_net_keep_gross', _('Compute taxes based on net total with stable gross prices')),
# We could also have sum_by_gross, but we're not aware of any use-cases for it # We could also have sum_by_gross, but we're not aware of any use-cases for it
) )
+5 -1
View File
@@ -51,7 +51,7 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.i18n import LazyLocaleException from pretix.base.i18n import LazyLocaleException
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment, CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment,
OrderPosition, OrderRefund, QuestionAnswer, OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
) )
from pretix.base.services.invoices import invoice_pdf_task from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import register_data_shredders from pretix.base.signals import register_data_shredders
@@ -329,6 +329,10 @@ class EmailAddressShredder(BaseDataShredder):
sleep_time=2, sleep_time=2,
) )
slow_delete(
OutgoingMail.objects.filter(event=self.event)
)
for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total): for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total):
changed = bool(o.email) or bool(o.customer) changed = bool(o.email) or bool(o.customer)
o.email = None o.email = None
+10 -2
View File
@@ -944,32 +944,40 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
email_filter = EventPluginSignal() email_filter = EventPluginSignal()
""" """
Arguments: ``message``, ``order``, ``user`` Arguments: ``message``, ``order``, ``user``, ``outgoing_mail``
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you. return a (possibly modified) copy of the message object passed to you.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event. As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
The ``message`` argument will contain an ``EmailMultiAlternatives`` object. The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object
might have newer information if a previous plugin already modified the email.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``. it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``. well, otherwise it will be ``None``.
You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting.
""" """
global_email_filter = GlobalSignal() global_email_filter = GlobalSignal()
""" """
Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer`` Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer``, ``outgoing_mail``
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you. return a (possibly modified) copy of the message object passed to you.
This signal is called on all events and even if there is no known event. ``sender`` is an event or None. This signal is called on all events and even if there is no known event. ``sender`` is an event or None.
The ``message`` argument will contain an ``EmailMultiAlternatives`` object. The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object
might have newer information if a previous plugin already modified the email.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``. it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``. well, otherwise it will be ``None``.
You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting.
""" """
+1 -1
View File
@@ -156,7 +156,7 @@ def safelink_callback(attrs, new=False):
Makes sure that all links to a different domain are passed through a redirection handler Makes sure that all links to a different domain are passed through a redirection handler
to ensure there's no passing of referers with secrets inside them. to ensure there's no passing of referers with secrets inside them.
""" """
url = attrs.get((None, 'href'), '/') url = html.unescape(attrs.get((None, 'href'), '/'))
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'): if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect') signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
+5
View File
@@ -867,6 +867,11 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm):
"The gross price of some products may be changed to ensure correct rounding, while the net " "The gross price of some products may be changed to ensure correct rounding, while the net "
"prices will be kept as configured. This may cause the actual payment amount to differ." "prices will be kept as configured. This may cause the actual payment amount to differ."
), ),
"sum_by_net_only_business": _(
"Same as above, but only applied to business customers. Line-based rounding will be used for consumers. "
"Recommended when e-invoicing is only used for business customers and consumers do not receive "
"invoices. This can cause the payment amount to change when the invoice address is changed."
),
"sum_by_net_keep_gross": _( "sum_by_net_keep_gross": _(
"Recommended for e-invoicing when you primarily sell to consumers. " "Recommended for e-invoicing when you primarily sell to consumers. "
"The gross or net price of some products may be changed automatically to ensure correct " "The gross or net price of some products may be changed automatically to ensure correct "
+61 -2
View File
@@ -57,8 +57,9 @@ from pretix.base.forms.widgets import (
from pretix.base.models import ( from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue, Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition, Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel, OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher, SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
Voucher,
) )
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.base.timeframes import ( from pretix.base.timeframes import (
@@ -2815,3 +2816,61 @@ class DeviceFilterForm(FilterForm):
qs = qs.order_by('-device_id') qs = qs.order_by('-device_id')
return qs return qs
class OutgoingMailFilterForm(FilterForm):
orders = {
'date': 'd',
'-date': '-d',
}
query = forms.CharField(
label=_('Search email address or subject'),
widget=forms.TextInput(attrs={
'placeholder': _('Search email address or subject'),
}),
required=False
)
event = forms.ModelChoiceField(
queryset=Event.objects.none(),
label=_('Event'),
empty_label=_('All events'),
required=False,
)
status = forms.ChoiceField(
label=_('Status'),
choices=[
('', _('All')),
*OutgoingMail.STATUS_CHOICES,
],
required=False
)
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
self.fields['event'].queryset = request.organizer.events.all()
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(to__containsstring=query.lower())
| Q(cc__containsstring=query.lower())
| Q(bcc__containsstring=query.lower())
| Q(subject__icontains=query)
)
if fdata.get('event'):
qs = qs.filter(event=fdata['event'])
if fdata.get('status'):
qs = qs.filter(status=fdata['status'])
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by("-created", "-pk")
return qs
+1
View File
@@ -585,6 +585,7 @@ class MailSettingsForm(SettingsForm):
help_text=''.join([ help_text=''.join([
str(_("All emails will be sent to this address as a Bcc copy.")), str(_("All emails will be sent to this address as a Bcc copy.")),
str(_("You can specify multiple recipients separated by commas.")), str(_("You can specify multiple recipients separated by commas.")),
str(_("Sensitive emails like password resets will not be sent in Bcc.")),
]), ]),
validators=[multimail_validate], validators=[multimail_validate],
required=False, required=False,
+8
View File
@@ -170,6 +170,12 @@ class OrderFeeAdded(OrderChangeLogEntryType):
plain = _('A fee has been added') plain = _('A fee has been added')
@log_entry_types.new()
class OrderRecomputed(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.recomputed'
plain = _('Taxes and rounding have been recomputed')
@log_entry_types.new() @log_entry_types.new()
class OrderFeeChanged(OrderChangeLogEntryType): class OrderFeeChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.feevalue' action_type = 'pretix.event.order.changed.feevalue'
@@ -699,6 +705,8 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'), 'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'), 'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'), 'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'), 'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'), 'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'), 'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
+9
View File
@@ -679,6 +679,15 @@ def get_organizer_navigation(request):
'active': (url.url_name == 'organizer.datasync.failedjobs'), 'active': (url.url_name == 'organizer.datasync.failedjobs'),
}]) }])
nav.append({
'label': _('Outgoing emails'),
'url': reverse('control:organizer.outgoingmails', kwargs={
'organizer': request.organizer.slug,
}),
'active': 'organizer.outgoingmail' in url.url_name,
'icon': 'send',
})
merge_in(nav, sorted( merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)), sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
[]), []),
@@ -0,0 +1,222 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% load icon %}
{% load compress %}
{% load static %}
{% block inner %}
<h1>
{% trans "Outgoing email" %}
</h1>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Email details" %}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-7 col-md-12">
<dl class="dl-horizontal">
<dt>{% trans "From" context "email" %}</dt>
<dd>{{ mail.sender }}</dd>
<dt>{% trans "To" context "email" %}</dt>
<dd>{{ mail.to|join:", " }}</dd>
{% if mail.cc %}
<dt>{% trans "Cc" context "email" %}</dt>
<dd>{{ mail.cc|join:", " }}</dd>
{% endif %}
{% if mail.bcc %}
<dt>{% trans "Bcc" context "email" %}</dt>
<dd>{{ mail.bcc|join:", " }}</dd>
{% endif %}
<dt>{% trans "Subject" %}</dt>
<dd>{{ mail.subject }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if mail.status == "queued" %}
<span class="label label-info">{% icon "clock-o" %} {% trans "queued" %}</span>
{% elif mail.status == "inflight" %}
<span class="label label-info">{% icon "send" %} {% trans "being sent" %}</span>
{% elif mail.status == "awaiting_retry" %}
<span class="label label-warning">{% icon "repeat" %} {% trans "will be retried" %}</span>
{% elif mail.status == "failed" %}
<span class="label label-danger">{% icon "warning" %} {% trans "failed" %}</span>
{% elif mail.status == "bounced" %}
<span class="label label-danger">{% icon "exclamation-circle" %} {% trans "bounced" %}</span>
{% elif mail.status == "withheld" %}
<span class="label label-warning">{% icon "ban" %} {% trans "withheld" %}</span>
{% elif mail.status == "aborted" %}
<span class="label label-danger">{% icon "ban" %} {% trans "aborted" %}</span>
{% elif mail.status == "sent" %}
<span class="label label-success">{% icon "check" %} {% trans "sent" %}</span>
{% endif %}
</dd>
<dt>{% trans "Creation" %}</dt>
<dd>{{ mail.created|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% if mail.sent %}
<dt>{% trans "Sent" %}</dt>
<dd>{{ mail.sent|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if mail.retry_after and mail.status == "awaiting_retry" %}
<dt>{% trans "Next attempt (estimate)" %}</dt>
<dd>{{ mail.retry_after|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if mail.event %}
<dt>{% trans "Event" %}</dt>
<dd>
<a href="{% url "control:event.index" organizer=request.organizer.slug event=mail.event.slug %}">
{{ mail.event }}
</a>
</dd>
{% endif %}
{% if mail.order %}
<dt>{% trans "Order" %}</dt>
<dd>
<a href="{% url "control:event.order" organizer=request.organizer.slug event=mail.event.slug code=mail.order.code %}">
{{ mail.order.code }}</a>{% if mail.orderposition %}-
{{ mail.orderposition.positionid }}{% endif %}
</dd>
{% endif %}
{% if mail.customer %}
<dt>{% trans "Customer" %}</dt>
<dd>
{% icon "user fa-fw" %}
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=mail.customer.identifier %}">
{{ mail.customer }}
</a>
</dd>
{% endif %}
</dl>
</div>
{% if mail.actual_attachments %}
<div class="col-lg-5 col-md-12">
<strong>{% trans "Attachments" %}</strong><br>
<ul class="list-unstyled">
{% for a in mail.actual_attachments %}
<li>
{% if a.type == "text/calendar" %}
{% icon "calendar-plus-o fa-fw" %}
{% elif a.type == "application/pdf" %}
{% icon "file-pdf-o fa-fw" %}
{% elif "image/" in a.type %}
{% icon "file-image-o fa-fw" %}
{% elif "msword" in a.type or "document" in a.type %}
{% icon "file-word-o fa-fw" %}
{% elif "excel" in a.type or "spreadsheet" in a.type %}
{% icon "file-excel-o fa-fw" %}
{% elif "powerpoint" in a.type or "presentation" in a.type %}
{% icon "file-powerpoint-o fa-fw" %}
{% elif "pkpass" in a.type %}
{% icon "qrcode fa-fw" %}
{% else %}
{% icon "file-o fa-fw" %}
{% endif %}
{{ a.name }}
<span class="text-muted">
({{ a.size|filesizeformat }})
</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
<div>
<ul class="nav nav-tabs" role="tablist">
{% if mail.is_failed %}
<li role="presentation" class="active">
<a href="#tab-error" role="tab" data-toggle="tab">
<span class="fa fa-warning"></span>
{% trans "Error" %}
</a>
</li>
{% endif %}
{% if mail.body_html %}
<li role="presentation"
{% if not mail.is_failed %}class="active"{% endif %}>
<a href="#tab-html" role="tab" data-toggle="tab">
<span class="fa fa-eye"></span>
{% trans "HTML content" %}
</a>
</li>
{% endif %}
<li role="presentation"
{% if not mail.is_failed and not mail.body_html %}class="active"{% endif %}>
<a href="#tab-text" role="tab" data-toggle="tab">
<span class="fa fa-file-text-o"></span>
{% trans "Text content" %}
</a>
</li>
<li role="presentation">
<a href="#tab-headers" role="tab" data-toggle="tab">
<span class="fa fa-code"></span>
{% trans "Headers" %}
</a>
</li>
</ul>
<div class="tab-content">
{% if mail.is_failed %}
<div role="tabpanel" class="tab-pane active" id="tab-error">
<strong>
{{ mail.error }}
</strong>
<pre>{{ mail.error_detail }}</pre>
</div>
{% endif %}
{% if mail.body_html %}
<div role="tabpanel"
class="tab-pane {% if not mail.is_failed %}active{% endif %}"
id="tab-html">
{% if mail.sensitive %}
<div class="empty-collection">
<p>
{% icon "eye-slash fa-4x" %}
</p>
<p>
{% blocktrans trimmed %}
Sensitive content not shown for security reasons
{% endblocktrans %}
</p>
</div>
{% else %}
{{ data_url|json_script:"mail_body_html" }}
{% endif %}
</div>
{% endif %}
<div role="tabpanel"
class="tab-pane {% if not mail.is_failed and not mail.body_html %}active{% endif %}"
id="tab-text">
{% if mail.sensitive %}
<div class="empty-collection">
<p>
{% icon "eye-slash fa-4x" %}
</p>
<p>
{% blocktrans trimmed %}
Sensitive content not shown for security reasons
{% endblocktrans %}
</p>
</div>
{% else %}
<pre><code>{{ mail.body_plain }}</code></pre>
{% endif %}
</div>
<div role="tabpanel"
class="tab-pane"
id="tab-headers">
<pre><code>{% for k, v in mail.headers.items %}{{ k }}: {{ v }}<br>{% endfor %}</code></pre>
<p class="text-muted">
{% trans "Additional headers will be added by the mail server and are not visible here." %}
</p>
</div>
</div>
</div>
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/outgoingmail.js" %}"></script>
{% endcompress %}
{% endblock %}
@@ -0,0 +1,185 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% load icon %}
{% block inner %}
<h1>
{% trans "Outgoing emails" %}
</h1>
<p>
{% blocktrans trimmed with days=days %}
This is an overview of all emails sent by your organizer account in the last {{ days }} days.
{% endblocktrans %}
</p>
{% if mails|length == 0 and not filter_form.filtered %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't sent any emails recently.
{% endblocktrans %}
</p>
</div>
{% else %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Filter" %}</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-4 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status %}
</div>
<div class="col-md-5 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.event %}
</div>
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<form action="{% url "control:organizer.outgoingmails.bulk_action" organizer=request.organizer.slug %}" method="post">
{% csrf_token %}
{% for field in filter_form %}
{{ field.as_hidden }}
{% endfor %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-quotas">
<thead>
<tr>
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
<th>{% trans "Subject" %}</th>
<th>{% trans "Recipients" %}</th>
<th>{% trans "Context" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Date" %}
<a href="?{% url_replace request 'ordering' '-date' %}"><i
class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th></th>
</tr>
{% if page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all"
data-results-total="{{ page_obj.paginator.count }}">
</td>
<td colspan="7">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead>
<tbody>
{% for m in mails %}
<tr>
<td>
<label aria-label="{% trans "select row for batch-operation" %}"
class="batch-select-label"><input type="checkbox" name="outgoingmail"
class="batch-select-checkbox"
value="{{ m.pk }}"/></label>
</td>
<td>
<a href="{% url "control:organizer.outgoingmail" organizer=request.organizer.slug mail=m.id %}">
{{ m.subject }}
</a>
{% if m.sensitive %}
<span class="text-muted">{% icon "eye-slash" %}</span>
{% endif %}
</td>
<td>
{{ m.to|join:", " }}
{% if m.cc %}
<br><small class="text-muted">{% trans "Cc" context "email" %}: {{ m.cc|join:", " }}</small>
{% endif %}
{% if m.bcc %}
<br><small class="text-muted">{% trans "Bcc" context "email" %}: {{ m.bcc|join:", " }}</small>
{% endif %}
</td>
<td>
{% if m.event %}
<div>
{% icon "calendar fa-fw" %}
<a href="{% url "control:event.index" organizer=request.organizer.slug event=m.event.slug %}">
{{ m.event }}
</a>
</div>
{% endif %}
{% if m.order %}
<div>
{% icon "shopping-cart fa-fw" %}
<a href="{% url "control:event.order" organizer=request.organizer.slug event=m.event.slug code=m.order.code %}">
{{ m.order.code }}</a>{% if m.orderposition %}-{{ m.orderposition.positionid }}{% endif %}
</div>
{% endif %}
{% if m.customer %}
<div>
{% icon "user fa-fw" %}
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=m.customer.identifier %}">
{{ m.customer }}
</a>
</div>
{% endif %}
</td>
<td>
{% if m.status == "queued" %}
<span class="label label-info">{% icon "clock-o" %} {% trans "queued" %}</span>
{% elif m.status == "inflight" %}
<span class="label label-info">{% icon "send" %} {% trans "being sent" %}</span>
{% elif m.status == "awaiting_retry" %}
<span class="label label-warning">{% icon "repeat" %} {% trans "will be retried" %}</span>
{% elif m.status == "failed" %}
<span class="label label-danger">{% icon "warning" %} {% trans "failed" %}</span>
{% elif m.status == "bounced" %}
<span class="label label-danger">{% icon "exclamation-circle" %} {% trans "bounced" %}</span>
{% elif m.status == "withheld" %}
<span class="label label-warning">{% icon "ban" %} {% trans "withheld" %}</span>
{% elif m.status == "aborted" %}
<span class="label label-danger">{% icon "ban" %} {% trans "aborted" %}</span>
{% elif m.status == "sent" %}
<span class="label label-success">{% icon "check" %} {% trans "sent" %}</span>
{% endif %}
</td>
<td>
{{ m.created|date:"SHORT_DATETIME_FORMAT" }}
{% if m.sent %}
<br>
<small class="text-muted">{% trans "Sent:" %} {{ m.sent|date:"SHORT_DATETIME_FORMAT" }}</small>
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.outgoingmail" organizer=request.organizer.slug mail=m.id %}"
class="btn btn-default btn-sm">{% icon "eye" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="batch-select-actions">
<button type="submit" class="btn btn-primary btn-save" name="action" value="retry">
{% icon "repeat" %}
{% trans "Retry (if failed or withheld)" %}
</button>
<button type="submit" class="btn btn-danger btn-save" name="action" value="abort">
{% icon "ban" %}
{% trans "Abort (if queued, awaiting retry or withheld)" %}
</button>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}
+6 -2
View File
@@ -38,8 +38,9 @@ from django.views.generic.base import RedirectView
from pretix.control.views import ( from pretix.control.views import (
auth, checkin, dashboards, datasync, discounts, event, geo, auth, checkin, dashboards, datasync, discounts, event, geo,
global_settings, item, main, modelimport, oauth, orders, organizer, pdf, global_settings, item, mail, main, modelimport, oauth, orders, organizer,
search, shredder, subevents, typeahead, user, users, vouchers, waitinglist, pdf, search, shredder, subevents, typeahead, user, users, vouchers,
waitinglist,
) )
urlpatterns = [ urlpatterns = [
@@ -240,6 +241,9 @@ urlpatterns = [
name='organizer.gate.edit'), name='organizer.gate.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/gate/(?P<gate>[^/]+)/delete$', organizer.GateDeleteView.as_view(), re_path(r'^organizer/(?P<organizer>[^/]+)/gate/(?P<gate>[^/]+)/delete$', organizer.GateDeleteView.as_view(),
name='organizer.gate.delete'), name='organizer.gate.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmails$', mail.OutgoingMailListView.as_view(), name='organizer.outgoingmails'),
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmail/bulk_action$', mail.OutgoingMailBulkAction.as_view(), name='organizer.outgoingmails.bulk_action'),
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmail/(?P<mail>[0-9]+)/$', mail.OutgoingMailDetailView.as_view(), name='organizer.outgoingmail'),
re_path(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'), re_path(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
re_path(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'), re_path(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
re_path(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(), re_path(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
-4
View File
@@ -66,7 +66,6 @@ from pretix.base.forms.auth import (
) )
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.base.services.mail import SendMailException
from pretix.helpers.http import get_client_ip, redirect_to_url from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.security import handle_login_source from pretix.helpers.security import handle_login_source
@@ -347,9 +346,6 @@ class Forgot(TemplateView):
except User.DoesNotExist: except User.DoesNotExist:
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.') logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
except SendMailException:
logger.exception('Sending password reset email to \"' + email + '\" failed.')
except RepeatedResetDenied: except RepeatedResetDenied:
pass pass
+186
View File
@@ -0,0 +1,186 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import base64
import logging
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import BadRequest
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ngettext
from django.views import View
from django.views.generic import DetailView, ListView
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
from pretix.base.models import OutgoingMail
from pretix.base.services.mail import mail_send_task
from pretix.control.forms.filter import OutgoingMailFilterForm
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.views.organizer import OrganizerDetailViewMixin
logger = logging.getLogger(__name__)
class OutgoingMailQueryMixin:
@cached_property
def request_data(self):
if self.request.method == "POST":
d = self.request.POST
else:
d = self.request.GET
d = d.copy()
return d
@cached_property
def filter_form(self):
return OutgoingMailFilterForm(
data=self.request_data,
request=self.request,
)
def get_queryset(self):
qs = self.request.organizer.outgoing_mails.select_related(
'event', 'order', 'orderposition', 'customer'
)
if 'outgoingmail' in self.request_data and '__ALL' not in self.request_data:
qs = qs.filter(
id__in=self.request_data.getlist('outgoingmail')
)
elif self.request.method == 'GET' or '__ALL' in self.request_data:
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
else:
raise BadRequest("No mails selected")
return qs
class OutgoingMailListView(OutgoingMailQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = OutgoingMail
template_name = 'pretixcontrol/organizers/outgoing_mails.html'
# Assume "the highest" permission level for now because emails could belog to any event, order, or customer.
# We plan to add a special permissoin in the future
permission = 'can_change_organizer_settings'
context_object_name = 'mails'
paginate_by = 100
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['days'] = int(settings.OUTGOING_MAIL_RETENTION / (24 * 3600))
return ctx
class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = OutgoingMail
template_name = 'pretixcontrol/organizers/outgoing_mail.html'
permission = 'can_change_organizer_settings'
context_object_name = 'mail'
def get_object(self, queryset=None):
return get_object_or_404(OutgoingMail, organizer=self.request.organizer, pk=self.kwargs.get('mail'))
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if 'Content-Security-Policy' in response:
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
csps = {
'frame-src': ['data:'],
# Unfortuantely, we can't avoid unsafe-inline for style here.
# See outgoingmail.js for the protection measures we take.
'style-src': ["'unsafe-inline'"],
}
_merge_csp(h, csps)
response['Content-Security-Policy'] = _render_csp(h)
return response
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if self.object.body_html:
ctx['data_url'] = "data:text/html;charset=utf-8;base64," + base64.b64encode(self.object.body_html.encode()).decode()
return ctx
class OutgoingMailBulkAction(OutgoingMailQueryMixin, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, View):
permission = 'can_change_organizer_settings'
@transaction.atomic
def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'retry':
ids = set(
self.get_queryset().filter(status__in=OutgoingMail.STATUS_LIST_RETRYABLE).values_list("pk", flat=True)
)
with transaction.atomic():
OutgoingMail.objects.filter(pk__in=ids).update(
status=OutgoingMail.STATUS_QUEUED,
sent=None,
)
self.request.organizer.log_action(
'pretix.organizer.outgoingmails.retried', user=self.request.user, data={
'mails': list(ids)
}, save=False
)
for i in ids:
mail_send_task.apply_async(kwargs={"outgoing_mail": i})
messages.success(request, ngettext(
"A retry of one email was scheduled.",
"A retry of {num} emails was scheduled.",
len(ids),
).format(num=len(ids)))
elif request.POST.get('action') == 'abort':
ids = set(
self.get_queryset().filter(
status__in=(OutgoingMail.STATUS_QUEUED, OutgoingMail.STATUS_AWAITING_RETRY)
).values_list("pk", flat=True)
)
with transaction.atomic():
OutgoingMail.objects.filter(pk__in=ids).update(
status=OutgoingMail.STATUS_ABORTED,
sent=None,
)
self.request.organizer.log_action(
'pretix.organizer.outgoingmails.aborted', user=self.request.user, data={
'mails': list(ids)
}, save=False
)
for i in ids:
mail_send_task.apply_async(kwargs={"outgoing_mail": i})
messages.success(request, ngettext(
"One email was aborted and will not be sent.",
"{num} emails were aborted and will not be sent.",
len(ids),
).format(num=len(ids)))
return redirect(self.get_success_url())
def get_success_url(self) -> str:
return reverse('control:organizer.outgoingmails', kwargs={
'organizer': self.request.organizer.slug,
})
+31 -54
View File
@@ -98,9 +98,7 @@ from pretix.base.services.invoices import (
invoice_qualified, regenerate_invoice, transmit_invoice, invoice_qualified, regenerate_invoice, transmit_invoice,
) )
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import ( from pretix.base.services.mail import prefix_subject, render_mail
SendMailException, prefix_subject, render_mail,
)
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order, OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded, extend_order, mark_order_expired, mark_order_refunded,
@@ -1066,10 +1064,6 @@ class OrderPaymentConfirm(OrderView):
messages.error(self.request, str(e)) messages.error(self.request, str(e))
except PaymentException as e: except PaymentException as e:
messages.error(self.request, str(e)) messages.error(self.request, str(e))
except SendMailException:
messages.warning(self.request,
_('The payment has been marked as complete, but we were unable to send a '
'confirmation mail.'))
else: else:
messages.success(self.request, _('The payment has been marked as complete.')) messages.success(self.request, _('The payment has been marked as complete.'))
else: else:
@@ -1540,9 +1534,6 @@ class OrderTransition(OrderView):
'message': str(e) 'message': str(e)
}) })
messages.error(self.request, str(e)) messages.error(self.request, str(e))
except SendMailException:
messages.warning(self.request, _('The order has been marked as paid, but we were unable to send a '
'confirmation mail.'))
else: else:
messages.success(self.request, _('The payment has been created successfully.')) messages.success(self.request, _('The payment has been created successfully.'))
elif self.order.cancel_allowed() and to == 'c': elif self.order.cancel_allowed() and to == 'c':
@@ -1781,15 +1772,11 @@ class OrderResendLink(OrderView):
permission = 'can_change_orders' permission = 'can_change_orders'
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
try: if 'position' in kwargs:
if 'position' in kwargs: p = get_object_or_404(self.order.positions, pk=kwargs['position'])
p = get_object_or_404(self.order.positions, pk=kwargs['position']) p.resend_link(user=self.request.user)
p.resend_link(user=self.request.user) else:
else: self.order.resend_link(user=self.request.user)
self.order.resend_link(user=self.request.user)
except SendMailException:
messages.error(self.request, _('There was an error sending the mail. Please try again later.'))
return redirect(self.get_order_url())
messages.success(self.request, _('The email has been queued to be sent.')) messages.success(self.request, _('The email has been queued to be sent.'))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
@@ -2433,24 +2420,18 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
} }
return self.get(self.request, *self.args, **self.kwargs) return self.get(self.request, *self.args, **self.kwargs)
else: else:
try: order.send_mail(
order.send_mail( form.cleaned_data['subject'], email_template,
form.cleaned_data['subject'], email_template, email_context, 'pretix.event.order.email.custom_sent',
email_context, 'pretix.event.order.email.custom_sent', self.request.user, auto_email=False,
self.request.user, auto_email=False, attach_tickets=form.cleaned_data.get('attach_tickets', False),
attach_tickets=form.cleaned_data.get('attach_tickets', False), invoices=form.cleaned_data.get('attach_invoices', []),
invoices=form.cleaned_data.get('attach_invoices', []), attach_other_files=[a for a in [
attach_other_files=[a for a in [ self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] ] if a] if form.cleaned_data.get('attach_new_order', False) else [],
] if a] if form.cleaned_data.get('attach_new_order', False) else [], )
) messages.success(self.request,
messages.success(self.request, _('Your message has been queued and will be sent to {}.'.format(order.email)))
_('Your message has been queued and will be sent to {}.'.format(order.email)))
except SendMailException:
messages.error(
self.request,
_('Failed to send mail to the following user: {}'.format(order.email))
)
return super(OrderSendMail, self).form_valid(form) return super(OrderSendMail, self).form_valid(form)
def get_success_url(self): def get_success_url(self):
@@ -2503,23 +2484,19 @@ class OrderPositionSendMail(OrderSendMail):
} }
return self.get(self.request, *self.args, **self.kwargs) return self.get(self.request, *self.args, **self.kwargs)
else: else:
try: position.send_mail(
position.send_mail( form.cleaned_data['subject'],
form.cleaned_data['subject'], email_template,
email_template, email_context,
email_context, 'pretix.event.order.position.email.custom_sent',
'pretix.event.order.position.email.custom_sent', self.request.user,
self.request.user, attach_tickets=form.cleaned_data.get('attach_tickets', False),
attach_tickets=form.cleaned_data.get('attach_tickets', False), attach_other_files=[a for a in [
attach_other_files=[a for a in [ self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] ] if a] if form.cleaned_data.get('attach_new_order', False) else [],
] if a] if form.cleaned_data.get('attach_new_order', False) else [], )
) messages.success(self.request,
messages.success(self.request, _('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))
_('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))
except SendMailException:
messages.error(self.request,
_('Failed to send mail to the following user: {}'.format(position.attendee_email)))
return super(OrderSendMail, self).form_valid(form) return super(OrderSendMail, self).form_valid(form)
+17 -19
View File
@@ -103,7 +103,7 @@ from pretix.base.plugins import (
PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_ORGANIZER,
) )
from pretix.base.services.export import multiexport, scheduled_organizer_export from pretix.base.services.export import multiexport, scheduled_organizer_export
from pretix.base.services.mail import SendMailException, mail, prefix_subject from pretix.base.services.mail import mail, prefix_subject
from pretix.base.signals import register_multievent_data_exporters from pretix.base.signals import register_multievent_data_exporters
from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
@@ -1037,24 +1037,21 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
return ctx return ctx
def _send_invite(self, instance): def _send_invite(self, instance):
try: mail(
mail( instance.email,
instance.email, _('pretix account invitation'),
_('pretix account invitation'), 'pretixcontrol/email/invitation.txt',
'pretixcontrol/email/invitation.txt', {
{ 'user': self,
'user': self, 'organizer': self.request.organizer.name,
'organizer': self.request.organizer.name, 'team': instance.team.name,
'team': instance.team.name, 'url': build_global_uri('control:auth.invite', kwargs={
'url': build_global_uri('control:auth.invite', kwargs={ 'token': instance.token
'token': instance.token })
}) },
}, event=None,
event=None, locale=self.request.LANGUAGE_CODE
locale=self.request.LANGUAGE_CODE )
)
except SendMailException:
pass # Already logged
@transaction.atomic @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -3027,6 +3024,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
locale=self.customer.locale, locale=self.customer.locale,
customer=self.customer, customer=self.customer,
organizer=self.request.organizer, organizer=self.request.organizer,
sensitive=True,
) )
messages.success( messages.success(
self.request, self.request,
+1 -6
View File
@@ -41,7 +41,6 @@ from hijack import signals
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.models import User from pretix.base.models import User
from pretix.base.services.mail import SendMailException
from pretix.control.forms.filter import UserFilterForm from pretix.control.forms.filter import UserFilterForm
from pretix.control.forms.users import UserEditForm from pretix.control.forms.users import UserEditForm
from pretix.control.permissions import AdministratorPermissionRequiredMixin from pretix.control.permissions import AdministratorPermissionRequiredMixin
@@ -139,11 +138,7 @@ class UserResetView(AdministratorPermissionRequiredMixin, RecentAuthenticationRe
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id")) self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
try: self.object.send_password_reset()
self.object.send_password_reset()
except SendMailException:
messages.error(request, _('There was an error sending the mail. Please try again later.'))
return redirect(self.get_success_url())
self.object.log_action('pretix.control.auth.user.forgot_password.mail_sent', self.object.log_action('pretix.control.auth.user.forgot_password.mail_sent',
user=request.user) user=request.user)
+4 -1
View File
@@ -148,4 +148,7 @@ def pycountry_add(db, **kw):
continue continue
value = value.lower() value = value.lower()
index = db.indices.setdefault(key, {}) index = db.indices.setdefault(key, {})
index.setdefault(value, set()).add(obj) if key in ["country_code"]:
index.setdefault(value, set()).add(obj)
else:
index[value] = obj
+14 -1
View File
@@ -25,7 +25,7 @@ from django.conf import settings
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import ( from django.db.models import (
Aggregate, Expression, F, Field, Lookup, OrderBy, Value, Aggregate, Expression, F, Field, JSONField, Lookup, OrderBy, Value,
) )
from django.utils.functional import lazy from django.utils.functional import lazy
@@ -154,6 +154,19 @@ class NotEqual(Lookup):
return '%s <> %s' % (lhs, rhs), params return '%s <> %s' % (lhs, rhs), params
@JSONField.register_lookup
class ContainsString(Lookup):
lookup_name = 'containsstring'
def as_sql(self, compiler, connection):
if connection.vendor != "postgresql":
raise NotImplementedError("Lookup in JSON Array not supported on this database")
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return '%s ? %s' % (lhs, rhs), params
class PostgresWindowFrame(Expression): class PostgresWindowFrame(Expression):
template = "%(frame_type)s BETWEEN %(start)s AND %(end)s" template = "%(frame_type)s BETWEEN %(start)s AND %(end)s"
+2 -25
View File
@@ -22,7 +22,6 @@
import logging import logging
from string import Formatter from string import Formatter
from django.core.exceptions import SuspiciousOperation
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,17 +37,6 @@ class PlainHtmlAlternativeString:
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')" return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
class FormattedString(str):
"""
A str subclass that has been specifically marked as "already formatted" for email rendering
purposes to avoid duplicate formatting.
"""
__slots__ = ()
def __str__(self):
return self
class SafeFormatter(Formatter): class SafeFormatter(Formatter):
""" """
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
@@ -89,19 +77,8 @@ class SafeFormatter(Formatter):
# Ignore format_spec # Ignore format_spec
return super().format_field(self._prepare_value(value), '') return super().format_field(self._prepare_value(value), '')
def convert_field(self, value, conversion):
# Ignore any conversions
if conversion is None:
return value
else:
return str(value)
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
if isinstance(template, FormattedString):
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
if not isinstance(template, str): if not isinstance(template, str):
template = str(template) template = str(template)
return FormattedString( return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
)
+16 -19
View File
@@ -32,7 +32,7 @@ from django_countries.fields import Country
from geoip2.errors import AddressNotFoundError from geoip2.errors import AddressNotFoundError
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import mail
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
@@ -159,21 +159,18 @@ def handle_login_source(user, request):
}) })
if user.known_login_sources.count() > 1: if user.known_login_sources.count() > 1:
# Do not send on first login or first login after introduction of this feature: # Do not send on first login or first login after introduction of this feature:
try: with language(user.locale):
with language(user.locale): mail(
mail( user.email,
user.email, _('Login from new source detected'),
_('Login from new source detected'), 'pretixcontrol/email/login_notice.txt',
'pretixcontrol/email/login_notice.txt', {
{ 'source': src,
'source': src, 'country': Country(str(country)).name if country else _('Unknown country'),
'country': Country(str(country)).name if country else _('Unknown country'), 'instance': settings.PRETIX_INSTANCE_NAME,
'instance': settings.PRETIX_INSTANCE_NAME, 'url': build_absolute_uri('control:user.settings')
'url': build_absolute_uri('control:user.settings') },
}, event=None,
event=None, user=user,
user=user, locale=user.locale
locale=user.locale )
)
except SendMailException:
pass # Not much we can do
+9 -9
View File
@@ -8,10 +8,10 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n" "POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-01-21 21:00+0000\n" "PO-Revision-Date: 2026-01-29 19:42+0000\n"
"Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n" "Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/>"
">\n" "\n"
"Language: cs\n" "Language: cs\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -2848,7 +2848,7 @@ msgstr "Stavy platby"
#: pretix/base/exporters/orderlist.py:1089 #: pretix/base/exporters/orderlist.py:1089
msgid "Refund states" msgid "Refund states"
msgstr "Stav vrácení peněz" msgstr "Stavy vrácení peněz"
#: pretix/base/exporters/orderlist.py:1132 #: pretix/base/exporters/orderlist.py:1132
#: pretix/base/exporters/orderlist.py:1274 #: pretix/base/exporters/orderlist.py:1274
@@ -13119,13 +13119,13 @@ msgid "Contact:"
msgstr "Kontakt:" msgstr "Kontakt:"
#: pretix/base/templates/pretixbase/email/order_details.html:54 #: pretix/base/templates/pretixbase/email/order_details.html:54
#, fuzzy, python-format #, python-format
#| msgid ""
#| "You are receiving this email because you placed an order for {event}."
msgid "" msgid ""
"You are receiving this email because you placed an order for " "You are receiving this email because you placed an order for "
"<strong>%(event)s</strong>." "<strong>%(event)s</strong>."
msgstr "Tento e-mail jste obdrželi, protože jste zadali objednávku na {event}." msgstr ""
"Tento e-mail jste obdrželi, protože jste zadali objednávku na <strong>%"
"(event)s</strong>."
#: pretix/base/templates/pretixbase/email/order_details.html:93 #: pretix/base/templates/pretixbase/email/order_details.html:93
#: pretix/control/templates/pretixcontrol/organizers/customer.html:23 #: pretix/control/templates/pretixcontrol/organizers/customer.html:23
@@ -14841,7 +14841,7 @@ msgstr "Jakýkoli produkt v kvótě \"{quota}\""
#: pretix/control/forms/filter.py:2439 #: pretix/control/forms/filter.py:2439
msgid "Refund status" msgid "Refund status"
msgstr "Stav náhrady" msgstr "Stav vrácení peněz"
#: pretix/control/forms/filter.py:2441 #: pretix/control/forms/filter.py:2441
msgid "All open refunds" msgid "All open refunds"
+60 -69
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n" "POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-01-14 00:00+0000\n" "PO-Revision-Date: 2026-01-27 14:51+0000\n"
"Last-Translator: Mario Montes <mario@t3chfest.es>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n" "es/>\n"
"Language: es\n" "Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.1\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -2309,6 +2309,10 @@ msgid ""
"contain at least one position of this product. The order totals etc. still " "contain at least one position of this product. The order totals etc. still "
"include all products contained in the order." "include all products contained in the order."
msgstr "" msgstr ""
"Si no se selecciona ninguno, se incluyen todos los productos. Se incluyen "
"los pedidos que contengan al menos una posición de este producto. Los "
"totales del pedido, etc., siguen incluyendo todos los productos contenidos "
"en el pedido."
#: pretix/base/exporters/orderlist.py:283 #: pretix/base/exporters/orderlist.py:283
#: pretix/base/exporters/orderlist.py:478 #: pretix/base/exporters/orderlist.py:478
@@ -2572,10 +2576,8 @@ msgid "Voucher"
msgstr "Vale de compra" msgstr "Vale de compra"
#: pretix/base/exporters/orderlist.py:653 #: pretix/base/exporters/orderlist.py:653
#, fuzzy
#| msgid "Voucher code used:"
msgid "Voucher budget usage" msgid "Voucher budget usage"
msgstr "Código de vale de compra utilizado:" msgstr "Uso del presupuesto del vale"
#: pretix/base/exporters/orderlist.py:654 #: pretix/base/exporters/orderlist.py:654
msgid "Pseudonymization ID" msgid "Pseudonymization ID"
@@ -5540,14 +5542,6 @@ msgid "Only show after sellout of"
msgstr "Mostrar sólo tras la venta completa de" msgstr "Mostrar sólo tras la venta completa de"
#: pretix/base/models/items.py:596 #: pretix/base/models/items.py:596
#, fuzzy
#| msgid ""
#| "If you select a product here, this product will only be shown when that "
#| "product is sold out. If combined with the option to hide sold-out "
#| "products, this allows you to swap out products for more expensive ones "
#| "once the cheaper option is sold out. There might be a short period in "
#| "which both products are visible while all tickets of the referenced "
#| "product are reserved, but not yet sold."
msgid "" msgid ""
"If you select a product here, this product will only be shown when that " "If you select a product here, this product will only be shown when that "
"product is no longer available. This will happen either because the other " "product is no longer available. This will happen either because the other "
@@ -5558,12 +5552,14 @@ msgid ""
"products are visible while all tickets of the referenced product are " "products are visible while all tickets of the referenced product are "
"reserved, but not yet sold." "reserved, but not yet sold."
msgstr "" msgstr ""
"Si selecciona un producto aquí, este producto solo se mostrará cuando esté " "Si selecciona un producto aquí, este solo se mostrará cuando ya no esté "
"agotado. Si se combina con la opción de ocultar productos agotados, esto le " "disponible. Esto sucederá porque el otro producto se haya agotado o porque "
"permite cambiar productos por otros más caros una vez que la opción más " "el tiempo esté fuera del plazo de venta del otro producto. Si se combina con "
"barata se agote. Puede haber un breve período en el que ambos productos " "la opción de ocultar los productos agotados, esto le permite cambiar los "
"estén visibles mientras todas las entradas del producto al que se hace " "productos por otros más caros una vez que se haya agotado la opción más "
"referencia están reservadas, pero aún no vendidas." "barata. Puede haber un breve periodo en el que ambos productos estén "
"visibles mientras todas las entradas del producto de referencia están "
"reservadas, pero aún no vendidas."
#: pretix/base/models/items.py:611 #: pretix/base/models/items.py:611
msgid "" msgid ""
@@ -9797,6 +9793,8 @@ msgstr ""
#: pretix/base/settings.py:189 #: pretix/base/settings.py:189
msgid "Require login to access order confirmation pages" msgid "Require login to access order confirmation pages"
msgstr "" msgstr ""
"Es necesario iniciar sesión para acceder a las páginas de confirmación de "
"pedidos"
#: pretix/base/settings.py:190 #: pretix/base/settings.py:190
msgid "" msgid ""
@@ -9805,6 +9803,11 @@ msgid ""
"placing an order, the restriction only becomes active after the customer " "placing an order, the restriction only becomes active after the customer "
"account is activated." "account is activated."
msgstr "" msgstr ""
"Si está habilitada, los usuarios que hayan iniciado sesión en el momento de "
"la compra también deberán iniciar sesión para acceder a la información de su "
"pedido. Si se crea una cuenta de cliente al realizar un pedido, la "
"restricción solo se activará después de que se haya activado la cuenta de "
"cliente."
#: pretix/base/settings.py:202 #: pretix/base/settings.py:202
msgid "Match orders based on email address" msgid "Match orders based on email address"
@@ -10603,10 +10606,8 @@ msgstr ""
"algún cambio." "algún cambio."
#: pretix/base/settings.py:1234 #: pretix/base/settings.py:1234
#, fuzzy
#| msgid "Restrict to business customers"
msgid "Only issue invoices to business customers" msgid "Only issue invoices to business customers"
msgstr "Restringir a clientes empresariales" msgstr "Emitir facturas únicamente a clientes empresariales"
#: pretix/base/settings.py:1243 #: pretix/base/settings.py:1243
msgid "Address line" msgid "Address line"
@@ -24650,10 +24651,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/export_form.html:46 #: pretix/control/templates/pretixcontrol/orders/export_form.html:46
#: pretix/control/templates/pretixcontrol/organizers/export_form.html:47 #: pretix/control/templates/pretixcontrol/organizers/export_form.html:47
#, fuzzy
#| msgid "Sample company"
msgid "Save copy" msgid "Save copy"
msgstr "Compañía de ejemplo" msgstr "Guardar copia"
#: pretix/control/templates/pretixcontrol/orders/export_form.html:56 #: pretix/control/templates/pretixcontrol/orders/export_form.html:56
#: pretix/control/templates/pretixcontrol/organizers/export_form.html:57 #: pretix/control/templates/pretixcontrol/organizers/export_form.html:57
@@ -28139,10 +28138,10 @@ msgid "Please try again."
msgstr "Por favor, inténtalo de nuevo." msgstr "Por favor, inténtalo de nuevo."
#: pretix/control/views/auth.py:544 #: pretix/control/views/auth.py:544
#, fuzzy
#| msgid "Two-factor authentication is required to log in"
msgid "A recovery code for two-factor authentification was used to log in." msgid "A recovery code for two-factor authentification was used to log in."
msgstr "Autenticación de 2 pasos es necesaria para iniciar sesión" msgstr ""
"Se utilizó un código de recuperación para la autenticación de dos factores "
"para iniciar sesión."
#: pretix/control/views/auth.py:560 #: pretix/control/views/auth.py:560
msgid "Invalid code, please try again." msgid "Invalid code, please try again."
@@ -28200,11 +28199,11 @@ msgstr "Se ha borrado la lista seleccionada."
#: pretix/control/views/dashboards.py:114 #: pretix/control/views/dashboards.py:114
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "Asistentes (pedidos)" msgstr "Asistentes (por orden alfabético)"
#: pretix/control/views/dashboards.py:124 #: pretix/control/views/dashboards.py:124
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "Asistentes (pagados)" msgstr "Asistentes (de pago)"
#: pretix/control/views/dashboards.py:136 #: pretix/control/views/dashboards.py:136
#, python-brace-format #, python-brace-format
@@ -29164,10 +29163,8 @@ msgid "The invoice has been scheduled for retransmission."
msgstr "La factura se ha programado para su reenvío." msgstr "La factura se ha programado para su reenvío."
#: pretix/control/views/orders.py:1751 #: pretix/control/views/orders.py:1751
#, fuzzy
#| msgid "The invoice has already been canceled."
msgid "The invoice has been canceled." msgid "The invoice has been canceled."
msgstr "La factura ya se ha anulado." msgstr "La factura ha sido cancelada."
#: pretix/control/views/orders.py:1794 #: pretix/control/views/orders.py:1794
msgid "The email has been queued to be sent." msgid "The email has been queued to be sent."
@@ -29877,6 +29874,9 @@ msgid ""
"This will usually happen if you lost access to your two-factor credentials " "This will usually happen if you lost access to your two-factor credentials "
"and requested a reset of the credentials." "and requested a reset of the credentials."
msgstr "" msgstr ""
"El administrador del sistema ha generado un código de emergencia de dos "
"factores. Esto suele ocurrir si ha perdido el acceso a sus credenciales de "
"dos factores y ha solicitado un restablecimiento de las mismas."
#: pretix/control/views/users.py:174 #: pretix/control/views/users.py:174
#, python-brace-format #, python-brace-format
@@ -30783,14 +30783,8 @@ msgid "Reference code (important):"
msgstr "Código de referencia (importante):" msgstr "Código de referencia (importante):"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30
#, fuzzy
#| msgid ""
#| "We will assign you a personal reference code to use after you completed "
#| "the order."
msgid "We will assign you a personal reference code in the next step." msgid "We will assign you a personal reference code in the next step."
msgstr "" msgstr "En el siguiente paso le asignaremos un código de referencia personal."
"Le asignaremos un código de referencia personal para que lo utilice después "
"de completar el pedido."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36
msgid "" msgid ""
@@ -32126,11 +32120,8 @@ msgid "#"
msgstr "#" msgstr "#"
#: pretix/plugins/reports/exporters.py:483 #: pretix/plugins/reports/exporters.py:483
#, fuzzy
#| msgctxt "skip-to-main-nav"
#| msgid "Skip link"
msgid "Skip empty lines" msgid "Skip empty lines"
msgstr "Saltar enlace" msgstr "Omitir líneas vacías"
#: pretix/plugins/reports/exporters.py:492 #: pretix/plugins/reports/exporters.py:492
msgid "Tax split list (PDF)" msgid "Tax split list (PDF)"
@@ -32760,24 +32751,20 @@ msgid "Orders by day"
msgstr "Pedidos por día" msgstr "Pedidos por día"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:25 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:25
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Orders paid in multiple payments are shown with the date of their last " "Orders paid in multiple payments are shown with the date of their last "
"payment. Placed orders include all orders (pending, paid, canceled, and " "payment. Placed orders include all orders (pending, paid, canceled, and "
"expired); paid orders include only paid orders and exclude all canceled " "expired); paid orders include only paid orders and exclude all canceled "
"orders." "orders."
msgstr "" msgstr ""
"Sólo se cuentan los pedidos pagados en su totalidad. Los pedidos pagados en " "Los pedidos pagados en varios plazos se muestran con la fecha de su último "
"múltiples pagos se muestran con la fecha de su último pago." "pago. Los pedidos realizados incluyen todos los pedidos (pendientes, "
"pagados, cancelados y caducados); los pedidos pagados incluyen solo los "
"pedidos pagados y excluyen todos los pedidos cancelados."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:36 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:36
#, fuzzy
#| msgid "Attendee badges"
msgid "Attendees by day" msgid "Attendees by day"
msgstr "Insignias de participante" msgstr "Asistentes por día"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:42 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:42
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:62 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:62
@@ -32789,12 +32776,18 @@ msgid ""
"(pending, paid, canceled, and expired); attendees in paid orders include " "(pending, paid, canceled, and expired); attendees in paid orders include "
"only those from paid orders and exclude those from canceled orders." "only those from paid orders and exclude those from canceled orders."
msgstr "" msgstr ""
"Los asistentes en pedidos pagados en varios plazos se muestran utilizando la "
"fecha del último pago. Las fechas de los pedidos reflejan cuándo se realizó "
"el pedido por primera vez; los asistentes añadidos posteriormente a través "
"de posiciones de pedido adicionales siguen utilizando la fecha del pedido "
"original. Los asistentes en pedidos realizados incluyen los de todos los "
"estados de los pedidos (pendientes, pagados, cancelados y caducados); los "
"asistentes en pedidos pagados incluyen solo los de pedidos pagados y "
"excluyen los de pedidos cancelados."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:56 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:56
#, fuzzy
#| msgid "Attendee name"
msgid "Attendees by time" msgid "Attendees by time"
msgstr "Nombre del asistente" msgstr "Asistentes por hora"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:76 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:76
msgid "Revenue over time" msgid "Revenue over time"
@@ -32810,36 +32803,34 @@ msgstr ""
"mostrarán aquí, ya que es posible que no esté claro a qué fecha pertenecen." "mostrarán aquí, ya que es posible que no esté claro a qué fecha pertenecen."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:91 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:91
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Only fully paid orders are counted. Orders paid in multiple payments are " "Only fully paid orders are counted. Orders paid in multiple payments are "
"shown with the date of their last payment. Revenue excludes all fees, " "shown with the date of their last payment. Revenue excludes all fees, "
"including cancellation fees." "including cancellation fees."
msgstr "" msgstr ""
"Sólo se cuentan los pedidos pagados en su totalidad. Los pedidos pagados en " "Solo se contabilizan los pedidos pagados en su totalidad. Los pedidos "
"múltiples pagos se muestran con la fecha de su último pago." "pagados en varios plazos se muestran con la fecha del último pago. Los "
"ingresos excluyen todas las tasas, incluidas las tasas de cancelación."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:97 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:97
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Only fully paid orders are counted. Orders paid in multiple payments are " "Only fully paid orders are counted. Orders paid in multiple payments are "
"shown with the date of their last payment. Revenue includes all fees, " "shown with the date of their last payment. Revenue includes all fees, "
"including cancellation fees from canceled orders." "including cancellation fees from canceled orders."
msgstr "" msgstr ""
"Sólo se cuentan los pedidos pagados en su totalidad. Los pedidos pagados en " "Solo se contabilizan los pedidos pagados en su totalidad. Los pedidos "
"múltiples pagos se muestran con la fecha de su último pago." "pagados en varios plazos se muestran con la fecha del último pago. Los "
"ingresos incluyen todas las tarifas, incluidas las tarifas de cancelación de "
"los pedidos cancelados."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:115 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:115
msgid "" msgid ""
"Placed orders include all orders (pending, paid, canceled, and expired); " "Placed orders include all orders (pending, paid, canceled, and expired); "
"paid orders include only paid orders and exclude all canceled orders." "paid orders include only paid orders and exclude all canceled orders."
msgstr "" msgstr ""
"Los pedidos realizados incluyen todos los pedidos (pendientes, pagados, "
"cancelados y caducados); los pedidos pagados incluyen solo los pedidos "
"pagados y excluyen todos los pedidos cancelados."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:126 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:126
msgid "Seating Overview" msgid "Seating Overview"
+4 -4
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-01-14 00:00+0000\n" "PO-Revision-Date: 2026-01-27 14:51+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n" "js/es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.1\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -162,12 +162,12 @@ msgstr "Órdenes pagadas"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "Asistentes (por orden alfabético)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "Asistentes (de pago)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
+63 -73
View File
@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n" "POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-01-06 23:00+0000\n" "PO-Revision-Date: 2026-01-27 14:51+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/" "Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
">\n" "fr/>\n"
"Language: fr\n" "Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.15.1\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -2310,6 +2310,10 @@ msgid ""
"contain at least one position of this product. The order totals etc. still " "contain at least one position of this product. The order totals etc. still "
"include all products contained in the order." "include all products contained in the order."
msgstr "" msgstr ""
"Si aucun n'est sélectionné, tous les produits sont inclus. Les commandes "
"sont incluses si elles contiennent au moins un article de ce produit. Les "
"totaux des commandes, etc. incluent toujours tous les produits contenus dans "
"la commande."
#: pretix/base/exporters/orderlist.py:283 #: pretix/base/exporters/orderlist.py:283
#: pretix/base/exporters/orderlist.py:478 #: pretix/base/exporters/orderlist.py:478
@@ -2573,10 +2577,8 @@ msgid "Voucher"
msgstr "Bon" msgstr "Bon"
#: pretix/base/exporters/orderlist.py:653 #: pretix/base/exporters/orderlist.py:653
#, fuzzy
#| msgid "Voucher code used:"
msgid "Voucher budget usage" msgid "Voucher budget usage"
msgstr "Code de réduction utilisé :" msgstr "Utilisation du budget des bons"
#: pretix/base/exporters/orderlist.py:654 #: pretix/base/exporters/orderlist.py:654
msgid "Pseudonymization ID" msgid "Pseudonymization ID"
@@ -5560,14 +5562,6 @@ msgid "Only show after sellout of"
msgstr "Afficher uniquement après la vente de" msgstr "Afficher uniquement après la vente de"
#: pretix/base/models/items.py:596 #: pretix/base/models/items.py:596
#, fuzzy
#| msgid ""
#| "If you select a product here, this product will only be shown when that "
#| "product is sold out. If combined with the option to hide sold-out "
#| "products, this allows you to swap out products for more expensive ones "
#| "once the cheaper option is sold out. There might be a short period in "
#| "which both products are visible while all tickets of the referenced "
#| "product are reserved, but not yet sold."
msgid "" msgid ""
"If you select a product here, this product will only be shown when that " "If you select a product here, this product will only be shown when that "
"product is no longer available. This will happen either because the other " "product is no longer available. This will happen either because the other "
@@ -5578,12 +5572,14 @@ msgid ""
"products are visible while all tickets of the referenced product are " "products are visible while all tickets of the referenced product are "
"reserved, but not yet sold." "reserved, but not yet sold."
msgstr "" msgstr ""
"Si vous sélectionnez un produit ici, celui-ci ne sera affiché que lorsqu'il " "Si vous sélectionnez un produit ici, celui-ci ne s'affichera que lorsqu'il "
"sera épuisé. Combinée à l'option permettant de masquer les produits épuisés, " "ne sera plus disponible. Cela se produira soit parce que l'autre produit est "
"cette option vous permet de remplacer des produits par des produits plus " "épuisé, soit parce que la période de vente de l'autre produit est terminée. "
"chers lorsque l'option la moins chère est épuisée. Il peut y avoir une " "Si vous combinez cette option avec celle permettant de masquer les produits "
"courte période pendant laquelle les deux produits sont visibles alors que " "épuisés, vous pourrez remplacer les produits par des produits plus chers une "
"tous les billets du produit référencé sont réservés, mais pas encore vendus." "fois que les moins chers seront épuisés. Il peut y avoir une courte période "
"pendant laquelle les deux produits sont visibles alors que tous les billets "
"du produit référencé sont réservés, mais pas encore vendus."
#: pretix/base/models/items.py:611 #: pretix/base/models/items.py:611
msgid "" msgid ""
@@ -9862,7 +9858,7 @@ msgstr ""
#: pretix/base/settings.py:189 #: pretix/base/settings.py:189
msgid "Require login to access order confirmation pages" msgid "Require login to access order confirmation pages"
msgstr "" msgstr "Connexion requise pour accéder aux pages de confirmation de commande"
#: pretix/base/settings.py:190 #: pretix/base/settings.py:190
msgid "" msgid ""
@@ -9871,6 +9867,11 @@ msgid ""
"placing an order, the restriction only becomes active after the customer " "placing an order, the restriction only becomes active after the customer "
"account is activated." "account is activated."
msgstr "" msgstr ""
"Si cette option est activée, les utilisateurs qui étaient connectés au "
"moment de l'achat doivent également se connecter pour accéder aux "
"informations relatives à leur commande. Si un compte client est créé lors de "
"la passation d'une commande, la restriction ne devient active qu'après "
"l'activation du compte client."
#: pretix/base/settings.py:202 #: pretix/base/settings.py:202
msgid "Match orders based on email address" msgid "Match orders based on email address"
@@ -10679,10 +10680,8 @@ msgstr ""
"si un changement doit être apporté." "si un changement doit être apporté."
#: pretix/base/settings.py:1234 #: pretix/base/settings.py:1234
#, fuzzy
#| msgid "Restrict to business customers"
msgid "Only issue invoices to business customers" msgid "Only issue invoices to business customers"
msgstr "Restreindre aux clients professionnels" msgstr "N'émettez des factures qu'aux clients professionnels"
#: pretix/base/settings.py:1243 #: pretix/base/settings.py:1243
msgid "Address line" msgid "Address line"
@@ -24835,10 +24834,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/export_form.html:46 #: pretix/control/templates/pretixcontrol/orders/export_form.html:46
#: pretix/control/templates/pretixcontrol/organizers/export_form.html:47 #: pretix/control/templates/pretixcontrol/organizers/export_form.html:47
#, fuzzy
#| msgid "Sample company"
msgid "Save copy" msgid "Save copy"
msgstr "Exemple de société" msgstr "Enregistrer une copie"
#: pretix/control/templates/pretixcontrol/orders/export_form.html:56 #: pretix/control/templates/pretixcontrol/orders/export_form.html:56
#: pretix/control/templates/pretixcontrol/organizers/export_form.html:57 #: pretix/control/templates/pretixcontrol/organizers/export_form.html:57
@@ -28351,10 +28348,10 @@ msgid "Please try again."
msgstr "Veuillez réessayer." msgstr "Veuillez réessayer."
#: pretix/control/views/auth.py:544 #: pretix/control/views/auth.py:544
#, fuzzy
#| msgid "Two-factor authentication is required to log in"
msgid "A recovery code for two-factor authentification was used to log in." msgid "A recovery code for two-factor authentification was used to log in."
msgstr "L'authentification à deux facteurs est actuellement activée" msgstr ""
"Un code de récupération pour l'authentification à deux facteurs a été "
"utilisé pour se connecter."
#: pretix/control/views/auth.py:560 #: pretix/control/views/auth.py:560
msgid "Invalid code, please try again." msgid "Invalid code, please try again."
@@ -28413,11 +28410,11 @@ msgstr "La liste sélectionnée a été supprimée."
#: pretix/control/views/dashboards.py:114 #: pretix/control/views/dashboards.py:114
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "Participants (commandés)" msgstr "Participants (par ordre alphabétique)"
#: pretix/control/views/dashboards.py:124 #: pretix/control/views/dashboards.py:124
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "Participants (payés)" msgstr "Participants (payants)"
#: pretix/control/views/dashboards.py:136 #: pretix/control/views/dashboards.py:136
#, python-brace-format #, python-brace-format
@@ -29388,10 +29385,8 @@ msgid "The invoice has been scheduled for retransmission."
msgstr "La facture a été programmée pour être renvoyée." msgstr "La facture a été programmée pour être renvoyée."
#: pretix/control/views/orders.py:1751 #: pretix/control/views/orders.py:1751
#, fuzzy
#| msgid "The invoice has already been canceled."
msgid "The invoice has been canceled." msgid "The invoice has been canceled."
msgstr "La facture a déjà été annulée." msgstr "La facture a été annulée."
#: pretix/control/views/orders.py:1794 #: pretix/control/views/orders.py:1794
msgid "The email has been queued to be sent." msgid "The email has been queued to be sent."
@@ -30110,6 +30105,9 @@ msgid ""
"This will usually happen if you lost access to your two-factor credentials " "This will usually happen if you lost access to your two-factor credentials "
"and requested a reset of the credentials." "and requested a reset of the credentials."
msgstr "" msgstr ""
"Un code d'urgence à deux facteurs a été généré par un administrateur "
"système. Cela se produit généralement si vous avez perdu l'accès à vos "
"identifiants à deux facteurs et demandé leur réinitialisation."
#: pretix/control/views/users.py:174 #: pretix/control/views/users.py:174
#, python-brace-format #, python-brace-format
@@ -31022,14 +31020,9 @@ msgid "Reference code (important):"
msgstr "Code de référence (important) :" msgstr "Code de référence (important) :"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30
#, fuzzy
#| msgid ""
#| "We will assign you a personal reference code to use after you completed "
#| "the order."
msgid "We will assign you a personal reference code in the next step." msgid "We will assign you a personal reference code in the next step."
msgstr "" msgstr ""
"Nous vous assignerons un code de référence personnel à utiliser après avoir " "Nous vous attribuerons un code de référence personnel à l'étape suivante."
"complété la commande."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36
msgid "" msgid ""
@@ -32385,11 +32378,8 @@ msgid "#"
msgstr "#" msgstr "#"
#: pretix/plugins/reports/exporters.py:483 #: pretix/plugins/reports/exporters.py:483
#, fuzzy
#| msgctxt "skip-to-main-nav"
#| msgid "Skip link"
msgid "Skip empty lines" msgid "Skip empty lines"
msgstr "Passer le lien" msgstr "Ignorer les lignes vides"
#: pretix/plugins/reports/exporters.py:492 #: pretix/plugins/reports/exporters.py:492
msgid "Tax split list (PDF)" msgid "Tax split list (PDF)"
@@ -33018,25 +33008,21 @@ msgid "Orders by day"
msgstr "Commandes par jour" msgstr "Commandes par jour"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:25 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:25
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Orders paid in multiple payments are shown with the date of their last " "Orders paid in multiple payments are shown with the date of their last "
"payment. Placed orders include all orders (pending, paid, canceled, and " "payment. Placed orders include all orders (pending, paid, canceled, and "
"expired); paid orders include only paid orders and exclude all canceled " "expired); paid orders include only paid orders and exclude all canceled "
"orders." "orders."
msgstr "" msgstr ""
"Seules les commandes entièrement payées sont comptabilisées. Les commandes " "Les commandes réglées en plusieurs versements sont indiquées avec la date de "
"payées en paiements multiples sont indiquées avec la date de leur dernier " "leur dernier paiement. Les commandes passées comprennent toutes les "
"paiement." "commandes (en attente, payées, annulées et expirées) ; les commandes payées "
"comprennent uniquement les commandes payées et excluent toutes les commandes "
"annulées."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:36 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:36
#, fuzzy
#| msgid "Attendee badges"
msgid "Attendees by day" msgid "Attendees by day"
msgstr "Badges de participant" msgstr "Participants par jour"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:42 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:42
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:62 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:62
@@ -33048,12 +33034,19 @@ msgid ""
"(pending, paid, canceled, and expired); attendees in paid orders include " "(pending, paid, canceled, and expired); attendees in paid orders include "
"only those from paid orders and exclude those from canceled orders." "only those from paid orders and exclude those from canceled orders."
msgstr "" msgstr ""
"Les participants dont les commandes ont été réglées en plusieurs versements "
"sont affichés en fonction de la date du dernier paiement. Les dates de "
"commande correspondent à la date à laquelle la commande a été passée pour la "
"première fois ; les participants ajoutés ultérieurement via des positions de "
"commande supplémentaires utilisent toujours la date de commande d'origine. "
"Les participants figurant dans les commandes passées comprennent ceux de "
"tous les statuts de commande (en attente, payée, annulée et expirée) ; les "
"participants figurant dans les commandes payées comprennent uniquement ceux "
"des commandes payées et excluent ceux des commandes annulées."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:56 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:56
#, fuzzy
#| msgid "Attendee name"
msgid "Attendees by time" msgid "Attendees by time"
msgstr "Nom du participant" msgstr "Participants par heure"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:76 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:76
msgid "Revenue over time" msgid "Revenue over time"
@@ -33069,38 +33062,35 @@ msgstr ""
"listés ici car il se peut que la date correspondante ne soit pas claire." "listés ici car il se peut que la date correspondante ne soit pas claire."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:91 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:91
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Only fully paid orders are counted. Orders paid in multiple payments are " "Only fully paid orders are counted. Orders paid in multiple payments are "
"shown with the date of their last payment. Revenue excludes all fees, " "shown with the date of their last payment. Revenue excludes all fees, "
"including cancellation fees." "including cancellation fees."
msgstr "" msgstr ""
"Seules les commandes entièrement payées sont comptabilisées. Les commandes " "Seules les commandes entièrement payées sont prises en compte. Les commandes "
"payées en paiements multiples sont indiquées avec la date de leur dernier " "payées en plusieurs fois sont indiquées avec la date de leur dernier "
"paiement." "paiement. Le chiffre d'affaires exclut tous les frais, y compris les frais "
"d'annulation."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:97 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:97
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Only fully paid orders are counted. Orders paid in multiple payments are " "Only fully paid orders are counted. Orders paid in multiple payments are "
"shown with the date of their last payment. Revenue includes all fees, " "shown with the date of their last payment. Revenue includes all fees, "
"including cancellation fees from canceled orders." "including cancellation fees from canceled orders."
msgstr "" msgstr ""
"Seules les commandes entièrement payées sont comptabilisées. Les commandes " "Seules les commandes entièrement payées sont prises en compte. Les commandes "
"payées en paiements multiples sont indiquées avec la date de leur dernier " "payées en plusieurs fois sont indiquées avec la date de leur dernier "
"paiement." "paiement. Les revenus comprennent tous les frais, y compris les frais "
"d'annulation des commandes annulées."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:115 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:115
msgid "" msgid ""
"Placed orders include all orders (pending, paid, canceled, and expired); " "Placed orders include all orders (pending, paid, canceled, and expired); "
"paid orders include only paid orders and exclude all canceled orders." "paid orders include only paid orders and exclude all canceled orders."
msgstr "" msgstr ""
"Les commandes passées comprennent toutes les commandes (en attente, payées, "
"annulées et expirées) ; les commandes payées comprennent uniquement les "
"commandes payées et excluent toutes les commandes annulées."
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:126 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:126
msgid "Seating Overview" msgid "Seating Overview"
+4 -4
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: French\n" "Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2025-10-22 16:00+0000\n" "PO-Revision-Date: 2026-01-27 14:51+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n" "fr/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.13.3\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -161,12 +161,12 @@ msgstr "Commandes payées"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "Participants (par ordre alphabétique)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "Participants (payants)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
+129 -149
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n" "POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-01-12 17:00+0000\n" "PO-Revision-Date: 2026-01-28 18:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n" "ja/>\n"
"Language: ja\n" "Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.15.1\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -1090,7 +1090,7 @@ msgstr "チケット価格"
#: pretix/control/forms/filter.py:220 pretix/control/forms/filter.py:1242 #: pretix/control/forms/filter.py:220 pretix/control/forms/filter.py:1242
#: pretix/control/forms/modelimport.py:90 #: pretix/control/forms/modelimport.py:90
msgid "Order status" msgid "Order status"
msgstr "注文ステータス" msgstr "注文の状態"
#: pretix/base/datasync/sourcefields.py:500 #: pretix/base/datasync/sourcefields.py:500
msgid "Ticket status" msgid "Ticket status"
@@ -1304,7 +1304,7 @@ msgstr "SSOプロバイダー"
#: pretix/control/templates/pretixcontrol/organizers/customers.html:65 #: pretix/control/templates/pretixcontrol/organizers/customers.html:65
#: pretix/control/templates/pretixcontrol/users/form.html:49 #: pretix/control/templates/pretixcontrol/users/form.html:49
msgid "External identifier" msgid "External identifier"
msgstr "外部アイデンティファイラー" msgstr "外部識別子"
#: pretix/base/exporters/customers.py:68 pretix/base/exporters/orderlist.py:284 #: pretix/base/exporters/customers.py:68 pretix/base/exporters/orderlist.py:284
#: pretix/base/exporters/orderlist.py:483 #: pretix/base/exporters/orderlist.py:483
@@ -2194,7 +2194,7 @@ msgstr ""
#: pretix/base/exporters/mail.py:76 pretix/plugins/reports/exporters.py:502 #: pretix/base/exporters/mail.py:76 pretix/plugins/reports/exporters.py:502
#: pretix/plugins/reports/exporters.py:685 #: pretix/plugins/reports/exporters.py:685
msgid "Filter by status" msgid "Filter by status"
msgstr "ステータスによる絞り込み" msgstr "状態で絞り込み"
#: pretix/base/exporters/orderlist.py:89 #: pretix/base/exporters/orderlist.py:89
msgid "" msgid ""
@@ -2289,6 +2289,9 @@ msgid ""
"contain at least one position of this product. The order totals etc. still " "contain at least one position of this product. The order totals etc. still "
"include all products contained in the order." "include all products contained in the order."
msgstr "" msgstr ""
"何も選択されていない場合、すべての製品が含まれます。注文は、この製品の"
"ポジションを1つ以上含む場合に含まれます。注文合計などには、注文に含まれるすべ"
"ての製品が引き続き含まれます。"
#: pretix/base/exporters/orderlist.py:283 #: pretix/base/exporters/orderlist.py:283
#: pretix/base/exporters/orderlist.py:478 #: pretix/base/exporters/orderlist.py:478
@@ -2340,7 +2343,7 @@ msgstr "イベントのスラッグ"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_inspect.html:34 #: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_inspect.html:34
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:79 #: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:79
msgid "Status" msgid "Status"
msgstr "ステータス" msgstr "状態"
#: pretix/base/exporters/orderlist.py:284 #: pretix/base/exporters/orderlist.py:284
#: pretix/base/exporters/orderlist.py:484 #: pretix/base/exporters/orderlist.py:484
@@ -2552,10 +2555,8 @@ msgid "Voucher"
msgstr "バウチャー" msgstr "バウチャー"
#: pretix/base/exporters/orderlist.py:653 #: pretix/base/exporters/orderlist.py:653
#, fuzzy
#| msgid "Voucher code used:"
msgid "Voucher budget usage" msgid "Voucher budget usage"
msgstr "使用するバウチャーコード:" msgstr "バウチャー予算の使用状況"
#: pretix/base/exporters/orderlist.py:654 #: pretix/base/exporters/orderlist.py:654
msgid "Pseudonymization ID" msgid "Pseudonymization ID"
@@ -2830,7 +2831,7 @@ msgstr "完了日"
#: pretix/base/exporters/orderlist.py:1133 #: pretix/base/exporters/orderlist.py:1133
msgid "Status code" msgid "Status code"
msgstr "ステータスコード" msgstr "状態コード"
#: pretix/base/exporters/orderlist.py:1133 #: pretix/base/exporters/orderlist.py:1133
#: pretix/base/exporters/orderlist.py:1272 #: pretix/base/exporters/orderlist.py:1272
@@ -3278,7 +3279,7 @@ msgstr ""
#: pretix/base/forms/auth.py:67 pretix/base/forms/auth.py:286 #: pretix/base/forms/auth.py:67 pretix/base/forms/auth.py:286
msgid "This account is inactive." msgid "This account is inactive."
msgstr "このアカウントは非アクティブです。" msgstr "このアカウントは無効です。"
#: pretix/base/forms/auth.py:156 #: pretix/base/forms/auth.py:156
msgid "" msgid ""
@@ -4048,7 +4049,7 @@ msgstr "複数の一致する製品が見つかりました。"
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257 #: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257
#: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:100 #: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:100
msgid "Product variation" msgid "Product variation"
msgstr "製品のバリエーション" msgstr "製品バリエーション"
#: pretix/base/modelimport_orders.py:161 #: pretix/base/modelimport_orders.py:161
msgid "The variation can be specified by its internal ID or full name." msgid "The variation can be specified by its internal ID or full name."
@@ -4092,7 +4093,7 @@ msgstr "有効な州を入力してください。"
#: pretix/base/modelimport_orders.py:359 pretix/control/forms/filter.py:687 #: pretix/base/modelimport_orders.py:359 pretix/control/forms/filter.py:687
msgid "Attendee email address" msgid "Attendee email address"
msgstr "出席者のメールアドレス" msgstr "参加者のメールアドレス"
#: pretix/base/modelimport_orders.py:375 pretix/base/modelimport_orders.py:386 #: pretix/base/modelimport_orders.py:375 pretix/base/modelimport_orders.py:386
#: pretix/base/modelimport_orders.py:397 pretix/base/modelimport_orders.py:408 #: pretix/base/modelimport_orders.py:397 pretix/base/modelimport_orders.py:408
@@ -4296,7 +4297,7 @@ msgstr ""
#: pretix/base/models/auth.py:251 #: pretix/base/models/auth.py:251
msgid "Is active" msgid "Is active"
msgstr "アクティブです" msgstr "有効"
#: pretix/base/models/auth.py:253 #: pretix/base/models/auth.py:253
msgid "Is site admin" msgid "Is site admin"
@@ -4470,7 +4471,7 @@ msgstr "自動的に誰もをチェックアウトします"
#: pretix/base/models/checkin.py:336 #: pretix/base/models/checkin.py:336
msgid "Entry" msgid "Entry"
msgstr "エントリー" msgstr "入場"
#: pretix/base/models/checkin.py:337 #: pretix/base/models/checkin.py:337
msgid "Exit" msgid "Exit"
@@ -4490,7 +4491,7 @@ msgstr "慣習のルールによって禁止されています"
#: pretix/base/models/checkin.py:359 #: pretix/base/models/checkin.py:359
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "チケットコードが取り消され/変更されました" msgstr "チケットコードが取り消し/変更されました"
#: pretix/base/models/checkin.py:360 #: pretix/base/models/checkin.py:360
msgid "Information required" msgid "Information required"
@@ -4518,7 +4519,7 @@ msgstr "チケットがブロックされました"
#: pretix/base/models/checkin.py:366 #: pretix/base/models/checkin.py:366
msgid "Order not approved" msgid "Order not approved"
msgstr "注文が承認されませんでした" msgstr "未承認の注文"
#: pretix/base/models/checkin.py:367 #: pretix/base/models/checkin.py:367
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
@@ -4526,7 +4527,7 @@ msgstr "このチケットは現在有効ではありません"
#: pretix/base/models/checkin.py:368 #: pretix/base/models/checkin.py:368
msgid "Check-in annulled" msgid "Check-in annulled"
msgstr "チェックイン取消" msgstr "チェックインキャンセル"
#: pretix/base/models/customers.py:62 #: pretix/base/models/customers.py:62
msgid "Provider name" msgid "Provider name"
@@ -5075,7 +5076,7 @@ msgstr ""
#: pretix/base/models/event.py:1788 pretix/control/forms/organizer.py:269 #: pretix/base/models/event.py:1788 pretix/control/forms/organizer.py:269
#: pretix/control/forms/organizer.py:273 #: pretix/control/forms/organizer.py:273
msgid "Public name" msgid "Public name"
msgstr "公共の名前" msgstr "公開名"
#: pretix/base/models/event.py:1792 #: pretix/base/models/event.py:1792
#: pretix/control/templates/pretixcontrol/organizers/properties.html:40 #: pretix/control/templates/pretixcontrol/organizers/properties.html:40
@@ -5179,7 +5180,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/giftcard.html:39 #: pretix/control/templates/pretixcontrol/organizers/giftcard.html:39
msgctxt "giftcard" msgctxt "giftcard"
msgid "Special terms and conditions" msgid "Special terms and conditions"
msgstr "特別なアイテムと条件" msgstr "特別な利用条件"
#: pretix/base/models/giftcards.py:227 pretix/base/models/giftcards.py:231 #: pretix/base/models/giftcards.py:227 pretix/base/models/giftcards.py:231
msgid "Manual transaction" msgid "Manual transaction"
@@ -5204,7 +5205,7 @@ msgstr "失敗した"
#: pretix/base/models/invoices.py:126 #: pretix/base/models/invoices.py:126
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:56 #: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:56
msgid "unknown" msgid "unknown"
msgstr "未知" msgstr "不明"
#: pretix/base/models/invoices.py:127 #: pretix/base/models/invoices.py:127
msgid "not transmitted due to test mode" msgid "not transmitted due to test mode"
@@ -5315,7 +5316,7 @@ msgstr "この製品は指定された日付を過ぎた後には販売されま
#: pretix/base/models/items.py:436 #: pretix/base/models/items.py:436
msgid "Event validity (default)" msgid "Event validity (default)"
msgstr "イベント開始日" msgstr "イベント有効期間(デフォルト)"
#: pretix/base/models/items.py:437 #: pretix/base/models/items.py:437
msgid "Fixed time frame" msgid "Fixed time frame"
@@ -5391,7 +5392,7 @@ msgstr ""
#: pretix/base/models/items.py:511 pretix/base/models/items.py:1176 #: pretix/base/models/items.py:511 pretix/base/models/items.py:1176
msgid "Suggested price" msgid "Suggested price"
msgstr "正味金額" msgstr "参考価格"
#: pretix/base/models/items.py:512 pretix/base/models/items.py:1177 #: pretix/base/models/items.py:512 pretix/base/models/items.py:1177
msgid "" msgid ""
@@ -5464,14 +5465,6 @@ msgid "Only show after sellout of"
msgstr "売り切れ後にのみ表示" msgstr "売り切れ後にのみ表示"
#: pretix/base/models/items.py:596 #: pretix/base/models/items.py:596
#, fuzzy
#| msgid ""
#| "If you select a product here, this product will only be shown when that "
#| "product is sold out. If combined with the option to hide sold-out "
#| "products, this allows you to swap out products for more expensive ones "
#| "once the cheaper option is sold out. There might be a short period in "
#| "which both products are visible while all tickets of the referenced "
#| "product are reserved, but not yet sold."
msgid "" msgid ""
"If you select a product here, this product will only be shown when that " "If you select a product here, this product will only be shown when that "
"product is no longer available. This will happen either because the other " "product is no longer available. This will happen either because the other "
@@ -5482,11 +5475,12 @@ msgid ""
"products are visible while all tickets of the referenced product are " "products are visible while all tickets of the referenced product are "
"reserved, but not yet sold." "reserved, but not yet sold."
msgstr "" msgstr ""
"ここで製品を選択すると、その製品は売り切れの場合にのみ表示されます。売り切れ" "ここで製品を選択すると、その製品が利用できなくなった場合にのみ、この製品が表"
"の製品を非表示にするオプションと組み合わせると、安いオプションが売り切れた後" "示されます。これは、選択した製品が売り切れになった場合、または現在の時刻がそ"
"により高価な製品に交換することができます。参照製品のすべてのチケットが予約さ" "の製品の販売期間外である場合に発生します。売り切れ製品を非表示にする"
"れているがまだ売れていない短い期間があるかもしれませんが、その間に両方の製品" "オプションと組み合わせると、安価なオプションが売り切れた際に、より高価な製品"
"が表示されます。" "に入れ替えることができます。参照先の製品のすべてのチケットが予約済みだがまだ"
"販売されていない間、両方の製品が一時的に表示される場合があります。"
#: pretix/base/models/items.py:611 #: pretix/base/models/items.py:611
msgid "" msgid ""
@@ -6840,7 +6834,7 @@ msgstr "税金免除(理由なし)"
#: pretix/base/models/tax.py:176 #: pretix/base/models/tax.py:176
msgctxt "tax_code" msgctxt "tax_code"
msgid "Zero-rated goods" msgid "Zero-rated goods"
msgstr "ゼロ税率商品" msgstr "ゼロ税率製品"
#: pretix/base/models/tax.py:179 #: pretix/base/models/tax.py:179
msgctxt "tax_code" msgctxt "tax_code"
@@ -7861,7 +7855,7 @@ msgstr ""
#: pretix/base/pdf.py:198 #: pretix/base/pdf.py:198
msgid "Attendee street" msgid "Attendee street"
msgstr "出席者通" msgstr "参加者の住所(番地)"
#: pretix/base/pdf.py:203 #: pretix/base/pdf.py:203
msgid "Attendee ZIP code" msgid "Attendee ZIP code"
@@ -7869,11 +7863,11 @@ msgstr "参加者の郵便番号"
#: pretix/base/pdf.py:208 #: pretix/base/pdf.py:208
msgid "Attendee city" msgid "Attendee city"
msgstr "出席者の都市" msgstr "参加者の都市"
#: pretix/base/pdf.py:213 #: pretix/base/pdf.py:213
msgid "Attendee state" msgid "Attendee state"
msgstr "出席者の状態" msgstr "参加者の州"
#: pretix/base/pdf.py:218 #: pretix/base/pdf.py:218
msgid "Attendee country" msgid "Attendee country"
@@ -8060,7 +8054,7 @@ msgstr "購入時間"
#: pretix/base/pdf.py:446 #: pretix/base/pdf.py:446
msgid "Validity start date" msgid "Validity start date"
msgstr "イベント開始日" msgstr "有効期間開始日"
#: pretix/base/pdf.py:454 #: pretix/base/pdf.py:454
msgid "Validity start date and time" msgid "Validity start date and time"
@@ -9265,7 +9259,7 @@ msgstr "この操作を許可する定義されたクォータはありません
#: pretix/base/services/orders.py:1596 #: pretix/base/services/orders.py:1596
msgid "The selected product is not active or has no price set." msgid "The selected product is not active or has no price set."
msgstr "選択された製品はアクティブではないか、価格が設定されていません。" msgstr "選択された製品は有効ではないか、価格が設定されていません。"
#: pretix/base/services/orders.py:1597 #: pretix/base/services/orders.py:1597
msgid "" msgid ""
@@ -9558,7 +9552,7 @@ msgstr ""
#: pretix/base/settings.py:189 #: pretix/base/settings.py:189
msgid "Require login to access order confirmation pages" msgid "Require login to access order confirmation pages"
msgstr "" msgstr "注文確認ページへのアクセスにログインを必須にする"
#: pretix/base/settings.py:190 #: pretix/base/settings.py:190
msgid "" msgid ""
@@ -9567,6 +9561,9 @@ msgid ""
"placing an order, the restriction only becomes active after the customer " "placing an order, the restriction only becomes active after the customer "
"account is activated." "account is activated."
msgstr "" msgstr ""
"有効にすると、購入時にログインしていたユーザーは、注文情報にアクセスする際に"
"もログインが必要になります。注文時に顧客アカウントが作成された場合、この制限"
"は顧客アカウントが有効化された後にのみ適用されます。"
#: pretix/base/settings.py:202 #: pretix/base/settings.py:202
msgid "Match orders based on email address" msgid "Match orders based on email address"
@@ -9639,7 +9636,7 @@ msgstr ""
#: pretix/base/settings.py:360 #: pretix/base/settings.py:360
msgid "Hide prices on attendee ticket page" msgid "Hide prices on attendee ticket page"
msgstr "出席者のチケットページで価格を非表示にしてください" msgstr "参加者のチケットページで価格を非表示にする"
#: pretix/base/settings.py:361 #: pretix/base/settings.py:361
msgid "" msgid ""
@@ -9654,7 +9651,7 @@ msgstr ""
#: pretix/base/settings.py:379 #: pretix/base/settings.py:379
msgid "Ask for attendee names" msgid "Ask for attendee names"
msgstr "出席者の名前を尋ねる" msgstr "参加者の名前を尋ねる"
#: pretix/base/settings.py:380 #: pretix/base/settings.py:380
msgid "Ask for a name for all personalized tickets." msgid "Ask for a name for all personalized tickets."
@@ -9662,7 +9659,7 @@ msgstr "すべてのパーソナライズチケットで名前を尋ねる。"
#: pretix/base/settings.py:389 #: pretix/base/settings.py:389
msgid "Require attendee names" msgid "Require attendee names"
msgstr "出席者の名前の要求" msgstr "参加者の名前を必須にする"
#: pretix/base/settings.py:390 #: pretix/base/settings.py:390
msgid "Require customers to fill in the names of all attendees." msgid "Require customers to fill in the names of all attendees."
@@ -10014,7 +10011,7 @@ msgstr "製品がカートに追加された後、チェックアウトに直接
#: pretix/base/settings.py:887 #: pretix/base/settings.py:887
msgid "End of presale text" msgid "End of presale text"
msgstr "前売り終了" msgstr "前売り終了時のテキスト"
#: pretix/base/settings.py:890 #: pretix/base/settings.py:890
msgid "" msgid ""
@@ -10040,7 +10037,7 @@ msgstr ""
#: pretix/base/settings.py:916 pretix/base/settings.py:925 #: pretix/base/settings.py:916 pretix/base/settings.py:925
msgid "in days" msgid "in days"
msgstr "数日間" msgstr "日単位"
#: pretix/base/settings.py:917 pretix/base/settings.py:926 #: pretix/base/settings.py:917 pretix/base/settings.py:926
msgid "in minutes" msgid "in minutes"
@@ -10314,10 +10311,8 @@ msgstr ""
"することをお勧めします。" "することをお勧めします。"
#: pretix/base/settings.py:1234 #: pretix/base/settings.py:1234
#, fuzzy
#| msgid "Restrict to business customers"
msgid "Only issue invoices to business customers" msgid "Only issue invoices to business customers"
msgstr "ビジネス顧客に制限" msgstr "法人顧客にのみ請求書を発行する"
#: pretix/base/settings.py:1243 #: pretix/base/settings.py:1243
msgid "Address line" msgid "Address line"
@@ -10846,9 +10841,9 @@ msgid ""
"order contains tickets for multiple event dates, the earliest date will be " "order contains tickets for multiple event dates, the earliest date will be "
"used." "used."
msgstr "" msgstr ""
"出席者の名前や質問への回答など、ユーザーが注文の詳細を変更できる最終日。イベ" "参加者の名前や質問への回答など、ユーザーが注文の詳細を変更できる最終日。"
"ントシリーズ機能を使用し、注文に複数のイベント日付のチケットが含まれている場" "イベントシリーズ機能を使用し、注文に複数のイベント日付のチケットが含まれてい"
"合、最も早い日付が使用されます。" "る場合、最も早い日付が使用されます。"
#: pretix/base/settings.py:1906 #: pretix/base/settings.py:1906
msgid "Customers can change the variation of the products they purchased" msgid "Customers can change the variation of the products they purchased"
@@ -12703,7 +12698,7 @@ msgstr ""
#: pretix/base/shredder.py:421 #: pretix/base/shredder.py:421
msgid "Attendee info" msgid "Attendee info"
msgstr "出席者情報" msgstr "参加者情報"
#: pretix/base/shredder.py:423 #: pretix/base/shredder.py:423
msgid "" msgid ""
@@ -13231,7 +13226,7 @@ msgstr "四半期ごと"
#: pretix/base/timeframes.py:193 #: pretix/base/timeframes.py:193
msgctxt "reporting_timeframe" msgctxt "reporting_timeframe"
msgid "Current quarter to date" msgid "Current quarter to date"
msgstr "イベント開始日" msgstr "四半期初来"
#: pretix/base/timeframes.py:202 #: pretix/base/timeframes.py:202
msgctxt "reporting_timeframe" msgctxt "reporting_timeframe"
@@ -13257,7 +13252,7 @@ msgstr "年ごとに"
#: pretix/base/timeframes.py:231 #: pretix/base/timeframes.py:231
msgctxt "reporting_timeframe" msgctxt "reporting_timeframe"
msgid "Current year to date" msgid "Current year to date"
msgstr "イベント開始日" msgstr "年初来"
#: pretix/base/timeframes.py:240 #: pretix/base/timeframes.py:240
msgctxt "reporting_timeframe" msgctxt "reporting_timeframe"
@@ -13956,13 +13951,13 @@ msgstr ""
#: pretix/control/forms/event.py:1186 pretix/control/forms/event.py:1344 #: pretix/control/forms/event.py:1186 pretix/control/forms/event.py:1344
#: pretix/control/forms/event.py:1389 pretix/control/forms/event.py:1419 #: pretix/control/forms/event.py:1389 pretix/control/forms/event.py:1419
msgid "Subject sent to attendees" msgid "Subject sent to attendees"
msgstr "出席者への送信事項" msgstr "参加者への件名"
#: pretix/control/forms/event.py:1137 pretix/control/forms/event.py:1164 #: pretix/control/forms/event.py:1137 pretix/control/forms/event.py:1164
#: pretix/control/forms/event.py:1191 pretix/control/forms/event.py:1349 #: pretix/control/forms/event.py:1191 pretix/control/forms/event.py:1349
#: pretix/control/forms/event.py:1394 pretix/control/forms/event.py:1424 #: pretix/control/forms/event.py:1394 pretix/control/forms/event.py:1424
msgid "Text sent to attendees" msgid "Text sent to attendees"
msgstr "出席者へのテキスト" msgstr "参加者へのテキスト"
#: pretix/control/forms/event.py:1202 pretix/control/forms/event.py:1276 #: pretix/control/forms/event.py:1202 pretix/control/forms/event.py:1276
#: pretix/control/forms/event.py:1286 pretix/control/forms/event.py:1296 #: pretix/control/forms/event.py:1286 pretix/control/forms/event.py:1296
@@ -14317,7 +14312,7 @@ msgstr "キャンセル済み (全額)"
#: pretix/control/forms/filter.py:231 #: pretix/control/forms/filter.py:231
msgid "Canceled (fully or with paid fee)" msgid "Canceled (fully or with paid fee)"
msgstr "キャンセル(全額返金または手数料を支払って)" msgstr "キャンセル(全額払い戻しまたは手数料を支払って)"
#: pretix/control/forms/filter.py:232 #: pretix/control/forms/filter.py:232
msgid "Canceled (at least one position)" msgid "Canceled (at least one position)"
@@ -14497,7 +14492,7 @@ msgstr "公開中かつ前売り実施中です"
#: pretix/control/forms/filter.py:1361 pretix/control/forms/filter.py:2165 #: pretix/control/forms/filter.py:1361 pretix/control/forms/filter.py:2165
msgid "Inactive" msgid "Inactive"
msgstr "非アクティブ" msgstr "無効"
#: pretix/control/forms/filter.py:1362 pretix/control/forms/filter.py:1829 #: pretix/control/forms/filter.py:1362 pretix/control/forms/filter.py:1829
#: pretix/control/templates/pretixcontrol/events/index.html:153 #: pretix/control/templates/pretixcontrol/events/index.html:153
@@ -14601,7 +14596,7 @@ msgstr "過去の単一のイベント"
#: pretix/control/forms/filter.py:2031 pretix/control/forms/filter.py:2033 #: pretix/control/forms/filter.py:2031 pretix/control/forms/filter.py:2033
msgid "Search attendee…" msgid "Search attendee…"
msgstr "出席者を検索…" msgstr "参加者を検索…"
#: pretix/control/forms/filter.py:2038 #: pretix/control/forms/filter.py:2038
#: pretix/plugins/checkinlists/exporters.py:106 #: pretix/plugins/checkinlists/exporters.py:106
@@ -14611,7 +14606,7 @@ msgstr "チェックインの状況"
#: pretix/control/forms/filter.py:2040 #: pretix/control/forms/filter.py:2040
#: pretix/plugins/checkinlists/exporters.py:108 #: pretix/plugins/checkinlists/exporters.py:108
msgid "All attendees" msgid "All attendees"
msgstr "すべての出席者" msgstr "すべての参加者"
#: pretix/control/forms/filter.py:2041 #: pretix/control/forms/filter.py:2041
#: pretix/control/templates/pretixcontrol/checkin/index.html:183 #: pretix/control/templates/pretixcontrol/checkin/index.html:183
@@ -14800,7 +14795,7 @@ msgstr "デバイスの状態"
#: pretix/control/forms/filter.py:2770 #: pretix/control/forms/filter.py:2770
msgid "Active devices" msgid "Active devices"
msgstr "アクティブなデバイス" msgstr "有効なデバイス"
#: pretix/control/forms/filter.py:2771 #: pretix/control/forms/filter.py:2771
msgid "Revoked devices" msgid "Revoked devices"
@@ -15297,7 +15292,7 @@ msgstr "バンドル製品"
#: pretix/control/forms/item.py:1246 pretix/control/forms/orders.py:379 #: pretix/control/forms/item.py:1246 pretix/control/forms/orders.py:379
#: pretix/control/forms/orders.py:568 #: pretix/control/forms/orders.py:568
msgid "inactive" msgid "inactive"
msgstr "非アクティブ" msgstr "無効"
#: pretix/control/forms/item.py:1331 #: pretix/control/forms/item.py:1331
msgid "Program times" msgid "Program times"
@@ -16419,19 +16414,18 @@ msgid ""
"Position #{posid}: {old_item} ({old_price}) changed to {new_item} " "Position #{posid}: {old_item} ({old_price}) changed to {new_item} "
"({new_price})." "({new_price})."
msgstr "" msgstr ""
"座席番号 #{posid}: {old_item} ({old_price}) は、 {new_item} ({new_price})に変" "注文明細 #{posid}: {old_item} ({old_price}) は {new_item} ({new_price})に変更"
"更。" "されました。"
#: pretix/control/logdisplay.py:111 #: pretix/control/logdisplay.py:111
#, python-brace-format #, python-brace-format
msgid "Position #{posid}: Used membership changed." msgid "Position #{posid}: Used membership changed."
msgstr "座席番号 #{posid}:使用されたメンバーシップが変更されました。" msgstr "注文明細 #{posid}: 使用されたメンバーシップが変更されました。"
#: pretix/control/logdisplay.py:117 #: pretix/control/logdisplay.py:117
#, python-brace-format #, python-brace-format
msgid "Position #{posid}: Seat \"{old_seat}\" changed to \"{new_seat}\"." msgid "Position #{posid}: Seat \"{old_seat}\" changed to \"{new_seat}\"."
msgstr "" msgstr "ポジション #{posid}: シート\"{old_seat}\"が\"{new_seat}\"に変更されました。"
"ポジション#{posid}:シート\"{old_seat}\"が\"{new_seat}\"に変更されました。"
#: pretix/control/logdisplay.py:127 #: pretix/control/logdisplay.py:127
#, python-brace-format #, python-brace-format
@@ -16439,18 +16433,18 @@ msgid ""
"Position #{posid}: Event date \"{old_event}\" ({old_price}) changed to " "Position #{posid}: Event date \"{old_event}\" ({old_price}) changed to "
"\"{new_event}\" ({new_price})." "\"{new_event}\" ({new_price})."
msgstr "" msgstr ""
"ポジション#{posid}:イベント日\"{old_event}\"({old_price})が\"{new_event}" "注文明細 #{posid}: イベント日\"{old_event}\"({old_price})が\"{new_event}\"("
"\"({new_price})に変更されました。" "{new_price})に変更されました。"
#: pretix/control/logdisplay.py:141 #: pretix/control/logdisplay.py:141
#, python-brace-format #, python-brace-format
msgid "Price of position #{posid} changed from {old_price} to {new_price}." msgid "Price of position #{posid} changed from {old_price} to {new_price}."
msgstr "ポジション#{posid}の価格が{old_price}から{new_price}に変更されました。" msgstr "注文明細#{posid}の価格が{old_price}から{new_price}に変更されました。"
#: pretix/control/logdisplay.py:154 #: pretix/control/logdisplay.py:154
#, python-brace-format #, python-brace-format
msgid "Tax rule of position #{posid} changed from {old_rule} to {new_rule}." msgid "Tax rule of position #{posid} changed from {old_rule} to {new_rule}."
msgstr "ポジション#{posid}の税規則が{old_rule}から{new_rule}に変更されました。" msgstr "注文明細#{posid}の税規則が{old_rule}から{new_rule}に変更されました。"
#: pretix/control/logdisplay.py:160 #: pretix/control/logdisplay.py:160
#, python-brace-format #, python-brace-format
@@ -16474,7 +16468,7 @@ msgstr "料金{old_price}が削除されました。"
#: pretix/control/logdisplay.py:202 #: pretix/control/logdisplay.py:202
#, python-brace-format #, python-brace-format
msgid "Position #{posid} ({old_item}, {old_price}) canceled." msgid "Position #{posid} ({old_item}, {old_price}) canceled."
msgstr "座席番号 #{posid}({old_item}、{old_price})がキャンセルされました。" msgstr "注文明細 #{posid} ({old_item}、{old_price})がキャンセルされました。"
#: pretix/control/logdisplay.py:219 #: pretix/control/logdisplay.py:219
#, python-brace-format #, python-brace-format
@@ -16482,13 +16476,13 @@ msgid ""
"Position #{posid} created: {item} ({price}) as an add-on to position " "Position #{posid} created: {item} ({price}) as an add-on to position "
"#{addon_to}." "#{addon_to}."
msgstr "" msgstr ""
"ポジション#{posid}が作成されました: {item} ({price})、ポジション#{addon_to}の" "注文明細 #{posid} が作成されました: {item} ({price}) が注文明細 #{addon_to} "
"アドオンとして。" "のアドオンとして。"
#: pretix/control/logdisplay.py:225 #: pretix/control/logdisplay.py:225
#, python-brace-format #, python-brace-format
msgid "Position #{posid} created: {item} ({price})." msgid "Position #{posid} created: {item} ({price})."
msgstr "座席番号 #{posid} が作成されました: {item} ({price})。" msgstr "注文明細 #{posid} が作成されました: {item} ({price})。"
#: pretix/control/logdisplay.py:235 #: pretix/control/logdisplay.py:235
#, python-brace-format #, python-brace-format
@@ -16499,13 +16493,13 @@ msgstr "座席番号#{posid}のために新しい秘密が生成されました
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The validity start date for position #{posid} has been changed to {value}." "The validity start date for position #{posid} has been changed to {value}."
msgstr "ポジション#{posid}の有効開始日が{value}に変更されました。" msgstr "注文明細#{posid}の有効開始日が{value}に変更されました。"
#: pretix/control/logdisplay.py:255 #: pretix/control/logdisplay.py:255
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The validity end date for position #{posid} has been changed to {value}." "The validity end date for position #{posid} has been changed to {value}."
msgstr "ポジション#{posid}の有効期限終了日が{value}に変更されました。" msgstr "注文明細#{posid}の有効期限終了日が{value}に変更されました。"
#: pretix/control/logdisplay.py:264 #: pretix/control/logdisplay.py:264
#, python-brace-format #, python-brace-format
@@ -17664,7 +17658,7 @@ msgstr "この製品からバンドルされたアイテムが削除されまし
#: pretix/control/logdisplay.py:893 #: pretix/control/logdisplay.py:893
msgid "A bundled item has been changed on this product." msgid "A bundled item has been changed on this product."
msgstr "この製品のバンドル商品が変更されました。" msgstr "この製品のバンドル製品が変更されました。"
#: pretix/control/logdisplay.py:894 #: pretix/control/logdisplay.py:894
msgid "A program time has been added to this product." msgid "A program time has been added to this product."
@@ -20490,7 +20484,7 @@ msgstr "変更が保存されました。"
#: pretix/control/templates/pretixcontrol/event/plugins.html:34 #: pretix/control/templates/pretixcontrol/event/plugins.html:34
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:34 #: pretix/control/templates/pretixcontrol/organizers/plugins.html:34
msgid "Search results" msgid "Search results"
msgstr "結果を検索する" msgstr "検索結果"
#: pretix/control/templates/pretixcontrol/event/plugins.html:56 #: pretix/control/templates/pretixcontrol/event/plugins.html:56
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:56 #: pretix/control/templates/pretixcontrol/organizers/plugins.html:56
@@ -21519,7 +21513,7 @@ msgstr "今すぐ更新を確認してください"
#: pretix/control/templates/pretixcontrol/global_update.html:26 #: pretix/control/templates/pretixcontrol/global_update.html:26
msgid "The last update check was not successful." msgid "The last update check was not successful."
msgstr "最後のアップデートチェックは成功しませんでした。" msgstr "最後の更新チェックは成功しませんでした。"
#: pretix/control/templates/pretixcontrol/global_update.html:28 #: pretix/control/templates/pretixcontrol/global_update.html:28
msgid "The pretix.eu server returned an error code." msgid "The pretix.eu server returned an error code."
@@ -22822,8 +22816,8 @@ msgid ""
"change the price or item, the discount will still be calculated from the " "change the price or item, the discount will still be calculated from the "
"original price at the time of purchase." "original price at the time of purchase."
msgstr "" msgstr ""
"このポジションは限られた予算のバウチャーで作成されました。価格やアイテムを変" "この注文明細は限られた予算のバウチャーで作成されました。価格やアイテムを変更"
"更しても、割引は購入時の通常価格から計算されます。" "しても、割引は購入時の通常価格から計算されます。"
#: pretix/control/templates/pretixcontrol/order/change.html:101 #: pretix/control/templates/pretixcontrol/order/change.html:101
#: pretix/control/templates/pretixcontrol/order/change.html:413 #: pretix/control/templates/pretixcontrol/order/change.html:413
@@ -22846,9 +22840,9 @@ msgid ""
"will not affect the membership. Memberships can be managed in the customer " "will not affect the membership. Memberships can be managed in the customer "
"account." "account."
msgstr "" msgstr ""
"このポジションの販売によりメンバーシップが作成されました。ここで製品を変更し" "この注文明細の販売によりメンバーシップが作成されました。ここで製品を変更して"
"てもメンバーシップには影響しません。メンバーシップは顧客アカウントで管理でき" "もメンバーシップには影響しません。メンバーシップは顧客アカウントで管理できま"
"ます。" "す。"
#: pretix/control/templates/pretixcontrol/order/change.html:220 #: pretix/control/templates/pretixcontrol/order/change.html:220
msgid "Ticket block" msgid "Ticket block"
@@ -22894,9 +22888,9 @@ msgid ""
"ticket here will not affect the membership. Memberships can be managed in " "ticket here will not affect the membership. Memberships can be managed in "
"the customer account." "the customer account."
msgstr "" msgstr ""
"このポジションの販売によってメンバーシップが作成されました。ここでチケットの" "この注文明細の販売によってメンバーシップが作成されました。ここでチケットの有"
"有効期限を変更してもメンバーシップには影響しません。メンバーシップは顧客アカ" "効期限を変更してもメンバーシップには影響しません。メンバーシップは顧客"
"ウントで管理することができます。" "アカウントで管理することができます。"
#: pretix/control/templates/pretixcontrol/order/change.html:296 #: pretix/control/templates/pretixcontrol/order/change.html:296
msgid "" msgid ""
@@ -23965,10 +23959,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/export_form.html:46 #: pretix/control/templates/pretixcontrol/orders/export_form.html:46
#: pretix/control/templates/pretixcontrol/organizers/export_form.html:47 #: pretix/control/templates/pretixcontrol/organizers/export_form.html:47
#, fuzzy
#| msgid "Sample company"
msgid "Save copy" msgid "Save copy"
msgstr "サンプル会社" msgstr "コピーを保存"
#: pretix/control/templates/pretixcontrol/orders/export_form.html:56 #: pretix/control/templates/pretixcontrol/orders/export_form.html:56
#: pretix/control/templates/pretixcontrol/organizers/export_form.html:57 #: pretix/control/templates/pretixcontrol/organizers/export_form.html:57
@@ -24110,7 +24102,7 @@ msgstr "キャンセル済み(支払い済み手数料)"
#: pretix/control/templates/pretixcontrol/orders/import_start.html:4 #: pretix/control/templates/pretixcontrol/orders/import_start.html:4
#: pretix/control/templates/pretixcontrol/orders/import_start.html:6 #: pretix/control/templates/pretixcontrol/orders/import_start.html:6
msgid "Import attendees" msgid "Import attendees"
msgstr "出席者をインポートします" msgstr "参加者をインポート"
#: pretix/control/templates/pretixcontrol/orders/import_process.html:13 #: pretix/control/templates/pretixcontrol/orders/import_process.html:13
#: pretix/control/templates/pretixcontrol/vouchers/import_process.html:13 #: pretix/control/templates/pretixcontrol/vouchers/import_process.html:13
@@ -25176,7 +25168,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:81 #: pretix/control/templates/pretixcontrol/organizers/plugins.html:81
msgid "Active (all events)" msgid "Active (all events)"
msgstr "アクティブ(全イベント)" msgstr "有効(全イベント)"
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:86 #: pretix/control/templates/pretixcontrol/organizers/plugins.html:86
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:95 #: pretix/control/templates/pretixcontrol/organizers/plugins.html:95
@@ -27352,10 +27344,8 @@ msgid "Please try again."
msgstr "もう一度お試しください。" msgstr "もう一度お試しください。"
#: pretix/control/views/auth.py:544 #: pretix/control/views/auth.py:544
#, fuzzy
#| msgid "Two-factor authentication is required to log in"
msgid "A recovery code for two-factor authentification was used to log in." msgid "A recovery code for two-factor authentification was used to log in."
msgstr "ログインするには、二要素認証が必要です" msgstr "二要素認証のリカバリーコードがログインに使用されました。"
#: pretix/control/views/auth.py:560 #: pretix/control/views/auth.py:560
msgid "Invalid code, please try again." msgid "Invalid code, please try again."
@@ -27412,11 +27402,11 @@ msgstr "選択したリストは削除されました。"
#: pretix/control/views/dashboards.py:114 #: pretix/control/views/dashboards.py:114
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "参加者(注文済み)" msgstr "参加者 (注文済み)"
#: pretix/control/views/dashboards.py:124 #: pretix/control/views/dashboards.py:124
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "参加者(有料)" msgstr "参加者 (有料)"
#: pretix/control/views/dashboards.py:136 #: pretix/control/views/dashboards.py:136
#, python-brace-format #, python-brace-format
@@ -27425,7 +27415,7 @@ msgstr "総収入 ({currency})"
#: pretix/control/views/dashboards.py:147 #: pretix/control/views/dashboards.py:147
msgid "Active products" msgid "Active products"
msgstr "アクティブな製品" msgstr "有効な製品"
#: pretix/control/views/dashboards.py:212 #: pretix/control/views/dashboards.py:212
msgid "available to give to people on waiting list" msgid "available to give to people on waiting list"
@@ -28282,7 +28272,7 @@ msgstr "支払いは正常に作成されました。"
msgid "" msgid ""
"The order has been canceled. You can now select how you want to transfer the " "The order has been canceled. You can now select how you want to transfer the "
"money back to the user." "money back to the user."
msgstr "注文はキャンセルされました。今、ユーザーに返金する方法を選択できます。" msgstr "注文はキャンセルされました。払い戻し方法を選択してください。"
#: pretix/control/views/orders.py:1632 pretix/control/views/orders.py:1636 #: pretix/control/views/orders.py:1632 pretix/control/views/orders.py:1636
msgid "No VAT ID specified." msgid "No VAT ID specified."
@@ -28350,10 +28340,8 @@ msgid "The invoice has been scheduled for retransmission."
msgstr "請求書の再送信が予定されました。" msgstr "請求書の再送信が予定されました。"
#: pretix/control/views/orders.py:1751 #: pretix/control/views/orders.py:1751
#, fuzzy
#| msgid "The invoice has already been canceled."
msgid "The invoice has been canceled." msgid "The invoice has been canceled."
msgstr "請求書はすでにキャンセルされています。" msgstr "請求書が取り消されました。"
#: pretix/control/views/orders.py:1794 #: pretix/control/views/orders.py:1794
msgid "The email has been queued to be sent." msgid "The email has been queued to be sent."
@@ -29043,6 +29031,9 @@ msgid ""
"This will usually happen if you lost access to your two-factor credentials " "This will usually happen if you lost access to your two-factor credentials "
"and requested a reset of the credentials." "and requested a reset of the credentials."
msgstr "" msgstr ""
"システム管理者によって二要素認証の緊急コードが生成されました。これは通常、二"
"要素認証の資格情報にアクセスできなくなり、資格情報のリセットを依頼した場合に"
"発生します。"
#: pretix/control/views/users.py:174 #: pretix/control/views/users.py:174
#, python-brace-format #, python-brace-format
@@ -29932,12 +29923,8 @@ msgid "Reference code (important):"
msgstr "リファレンス・コード (重要):" msgstr "リファレンス・コード (重要):"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30
#, fuzzy
#| msgid ""
#| "We will assign you a personal reference code to use after you completed "
#| "the order."
msgid "We will assign you a personal reference code in the next step." msgid "We will assign you a personal reference code in the next step."
msgstr "ご注文完了後に、個人用の参照コードを割り当てます。" msgstr "次のステップで個人用の参照コードを割り当てます。"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36
msgid "" msgid ""
@@ -31019,7 +31006,7 @@ msgstr ""
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/control.html:24 #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/control.html:24
msgid "Capture status" msgid "Capture status"
msgstr "ステータスによる絞り込み" msgstr "キャプチャの状態"
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/control.html:27 #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/control.html:27
msgid "" msgid ""
@@ -31238,11 +31225,8 @@ msgid "#"
msgstr "#" msgstr "#"
#: pretix/plugins/reports/exporters.py:483 #: pretix/plugins/reports/exporters.py:483
#, fuzzy
#| msgctxt "skip-to-main-nav"
#| msgid "Skip link"
msgid "Skip empty lines" msgid "Skip empty lines"
msgstr "リンクをスキップする" msgstr "空行をスキップ"
#: pretix/plugins/reports/exporters.py:492 #: pretix/plugins/reports/exporters.py:492
msgid "Tax split list (PDF)" msgid "Tax split list (PDF)"
@@ -31438,7 +31422,7 @@ msgstr "支払いが遅れている保留中"
#: pretix/plugins/sendmail/forms.py:242 #: pretix/plugins/sendmail/forms.py:242
msgctxt "sendmail_form" msgctxt "sendmail_form"
msgid "Restrict to orders with status" msgid "Restrict to orders with status"
msgstr "ステータスを備えた注文に限定" msgstr "注文の状態で限定"
#: pretix/plugins/sendmail/forms.py:267 pretix/plugins/sendmail/forms.py:271 #: pretix/plugins/sendmail/forms.py:267 pretix/plugins/sendmail/forms.py:271
msgctxt "sendmail_form" msgctxt "sendmail_form"
@@ -31472,7 +31456,7 @@ msgstr "相対的、イベント終了後"
#: pretix/plugins/sendmail/forms.py:395 #: pretix/plugins/sendmail/forms.py:395
msgctxt "sendmail_from" msgctxt "sendmail_from"
msgid "Restrict to orders with status" msgid "Restrict to orders with status"
msgstr "ステータスを備えた注文に限定" msgstr "注文の状態で限定"
#: pretix/plugins/sendmail/forms.py:410 #: pretix/plugins/sendmail/forms.py:410
msgid "Please specify the send date" msgid "Please specify the send date"
@@ -31520,11 +31504,11 @@ msgstr "製品を制限します"
#: pretix/plugins/sendmail/models.py:255 #: pretix/plugins/sendmail/models.py:255
msgid "Restrict to orders with status" msgid "Restrict to orders with status"
msgstr "ステータスを備えた注文に限定" msgstr "注文の状態で限定"
#: pretix/plugins/sendmail/models.py:260 #: pretix/plugins/sendmail/models.py:260
msgid "Restrict to check-in status" msgid "Restrict to check-in status"
msgstr "チェックインステータスに限定" msgstr "チェックインの状態で限定"
#: pretix/plugins/sendmail/models.py:274 #: pretix/plugins/sendmail/models.py:274
msgid "Send date" msgid "Send date"
@@ -31844,24 +31828,19 @@ msgid "Orders by day"
msgstr "日別の注文" msgstr "日別の注文"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:25 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:25
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Orders paid in multiple payments are shown with the date of their last " "Orders paid in multiple payments are shown with the date of their last "
"payment. Placed orders include all orders (pending, paid, canceled, and " "payment. Placed orders include all orders (pending, paid, canceled, and "
"expired); paid orders include only paid orders and exclude all canceled " "expired); paid orders include only paid orders and exclude all canceled "
"orders." "orders."
msgstr "" msgstr ""
"支払いが完了した注文のみがカウントされます。複数回の支払いで支払われた注文" "複数回の支払いで決済された注文は、最後の支払い日で表示されます。注文済みには"
"は、最後の支払い日付とともに表示されます。" "すべての注文(保留中、支払い済み、取消し済み、期限切れ)が含まれます。支払い"
"済みには支払い済みの注文のみが含まれ、取消し済みの注文はすべて除外されます。"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:36 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:36
#, fuzzy
#| msgid "Attendee badges"
msgid "Attendees by day" msgid "Attendees by day"
msgstr "参加者のバッジ" msgstr "日別参加者数"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:42 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:42
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:62 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:62
@@ -31873,12 +31852,16 @@ msgid ""
"(pending, paid, canceled, and expired); attendees in paid orders include " "(pending, paid, canceled, and expired); attendees in paid orders include "
"only those from paid orders and exclude those from canceled orders." "only those from paid orders and exclude those from canceled orders."
msgstr "" msgstr ""
"複数回の支払いで決済された注文の参加者は、最後の支払い日で表示されます。注文"
"日は注文が最初に行われた日を反映します。後から追加の注文ポジションで追加され"
"た参加者も、元の注文日が使用されます。注文済みの参加者には、すべての注文"
"ステータス(保留中、支払い済み、取消し済み、期限切れ)の参加者が含まれます。"
"支払い済みの参加者には支払い済みの注文の参加者のみが含まれ、取消し済みの注文"
"の参加者は除外されます。"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:56 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:56
#, fuzzy
#| msgid "Attendee name"
msgid "Attendees by time" msgid "Attendees by time"
msgstr "参加者の名前" msgstr "時間別参加者数"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:76 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:76
msgid "Revenue over time" msgid "Revenue over time"
@@ -31894,36 +31877,33 @@ msgstr ""
"ここには表示されません。" "ここには表示されません。"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:91 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:91
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Only fully paid orders are counted. Orders paid in multiple payments are " "Only fully paid orders are counted. Orders paid in multiple payments are "
"shown with the date of their last payment. Revenue excludes all fees, " "shown with the date of their last payment. Revenue excludes all fees, "
"including cancellation fees." "including cancellation fees."
msgstr "" msgstr ""
"支払いが完了した注文のみがカウントされます。複数回の支払いで支払われた注文" "完全に支払い済みの注文のみがカウントされます。複数回の支払いで決済された注文"
"は、最後の支払い日付とともに表示されます。" "は、最後の支払い日で表示されます。収益にはキャンセル料を含むすべての手数料が"
"除外されます。"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:97 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:97
#, fuzzy
#| msgid ""
#| "Only fully paid orders are counted. Orders paid in multiple payments are "
#| "shown with the date of their last payment."
msgid "" msgid ""
"Only fully paid orders are counted. Orders paid in multiple payments are " "Only fully paid orders are counted. Orders paid in multiple payments are "
"shown with the date of their last payment. Revenue includes all fees, " "shown with the date of their last payment. Revenue includes all fees, "
"including cancellation fees from canceled orders." "including cancellation fees from canceled orders."
msgstr "" msgstr ""
"支払いが完了した注文のみがカウントされます。複数回の支払いで支払われた注文" "完全に支払い済みの注文のみがカウントされます。複数回の支払いで決済された注文"
"は、最後の支払い日付とともに表示されます。" "は、最後の支払い日で表示されます。収益には、取消し済み注文のキャンセル料を含"
"むすべての手数料が含まれます。"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:115 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:115
msgid "" msgid ""
"Placed orders include all orders (pending, paid, canceled, and expired); " "Placed orders include all orders (pending, paid, canceled, and expired); "
"paid orders include only paid orders and exclude all canceled orders." "paid orders include only paid orders and exclude all canceled orders."
msgstr "" msgstr ""
"注文済みにはすべての注文(保留中、支払い済み、取消し済み、期限切れ)が含まれ"
"ます。支払い済みには支払い済みの注文のみが含まれ、取消し済みの注文はすべて除"
"外されます。"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:126 #: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:126
msgid "Seating Overview" msgid "Seating Overview"
@@ -32656,7 +32636,7 @@ msgstr "紛争解決の手続きが更新されました。理由:{}"
#: pretix/plugins/stripe/signals.py:111 #: pretix/plugins/stripe/signals.py:111
#, python-brace-format #, python-brace-format
msgid "Dispute closed. Status: {}" msgid "Dispute closed. Status: {}"
msgstr "紛争解決手続きは終了しました。ステータス:{}" msgstr "紛争解決手続きは終了しました。状態:{}"
#: pretix/plugins/stripe/signals.py:114 #: pretix/plugins/stripe/signals.py:114
#, python-brace-format #, python-brace-format
@@ -34167,7 +34147,7 @@ msgstr[0] "このチケットは%(count)s回使用されています。"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:166 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:166
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "出席者の名前が提供されていません" msgstr "参加者の名前が入力されていません"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:219 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:219
msgid "The image you previously uploaded" msgid "The image you previously uploaded"
@@ -34917,7 +34897,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html:199 #: pretix/presale/templates/pretixpresale/event/order.html:199
#: pretix/presale/templates/pretixpresale/event/position.html:33 #: pretix/presale/templates/pretixpresale/event/position.html:33
msgid "Change ordered items" msgid "Change ordered items"
msgstr "注文した商品を変更" msgstr "注文した製品を変更"
#: pretix/presale/templates/pretixpresale/event/order.html:200 #: pretix/presale/templates/pretixpresale/event/order.html:200
#: pretix/presale/templates/pretixpresale/event/order.html:291 #: pretix/presale/templates/pretixpresale/event/order.html:291
@@ -35849,7 +35829,7 @@ msgstr "本当にアカウントから次のプロフィールを削除します
#: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html:11 #: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html:11
#: pretix/presale/views/customer.py:384 #: pretix/presale/views/customer.py:384
msgid "Attendee profiles" msgid "Attendee profiles"
msgstr "出席者のプロフィール" msgstr "参加者プロフィール"
#: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html:37 #: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html:37
msgid "You don’t have any attendee profiles in your account yet." msgid "You don’t have any attendee profiles in your account yet."
+35 -35
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2025-11-18 17:00+0000\n" "PO-Revision-Date: 2026-01-28 18:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n" "js/ja/>\n"
"Language: ja\n" "Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.14.3\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -152,7 +152,7 @@ msgstr "支払い方法が利用できません"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63
msgid "Placed orders" msgid "Placed orders"
msgstr "受注状況" msgstr "受注件数"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63
@@ -162,12 +162,12 @@ msgstr "支払い済みの注文"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "参加者 (注文済み)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "参加者 (有料)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
@@ -191,7 +191,7 @@ msgstr "チェックインリストを選択してください"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found." msgid "No active check-in lists found."
msgstr "アクティブなチェックインリストは見つかりません。" msgstr "有効なチェックインリストが見つかりません。"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list" msgid "Switch check-in list"
@@ -199,7 +199,7 @@ msgstr "チェックインリストを切り替え"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results" msgid "Search results"
msgstr "結果を検索する" msgstr "検索結果"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
msgid "No tickets found" msgid "No tickets found"
@@ -219,7 +219,7 @@ msgstr "方向転換"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
msgid "Entry" msgid "Entry"
msgstr "エントリー" msgstr "入場"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
msgid "Exit" msgid "Exit"
@@ -281,7 +281,7 @@ msgstr "有効なチケット"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
msgstr "記録を保存" msgstr "退出を記録しました"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
@@ -301,11 +301,11 @@ msgstr "この種類のチケットは使用できません"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
msgstr "入力できません" msgstr "入場できません"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "チケットコードのブロック/変更" msgstr "チケットコードが取り消し/変更されました"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Ticket blocked" msgid "Ticket blocked"
@@ -325,7 +325,7 @@ msgstr "リストのチケットコードは曖昧です"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "承認されない注文" msgstr "未承認の注文"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
@@ -463,7 +463,7 @@ msgstr "製品"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "Product variation" msgid "Product variation"
msgstr "商品の種類" msgstr "製品バリエーション"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
msgid "Gate" msgid "Gate"
@@ -479,47 +479,47 @@ msgstr "現在の曜日 (1 = 月曜日, 7 = 日曜日)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
msgid "Current entry status" msgid "Current entry status"
msgstr "現在の登録ステータス" msgstr "現在の入場状態"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
msgid "Number of previous entries" msgid "Number of previous entries"
msgstr "これまでの入力件数" msgstr "これまでの入場回数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
msgid "Number of previous entries since midnight" msgid "Number of previous entries since midnight"
msgstr "0時から現在までの入力件数" msgstr "0時から現在までの入場回数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:131 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:131
msgid "Number of previous entries since" msgid "Number of previous entries since"
msgstr "この時点から今までの入力件数" msgstr "この時点から今までの入場回数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:135 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:135
msgid "Number of previous entries before" msgid "Number of previous entries before"
msgstr "この時点より前に入力された件数" msgstr "この時点より前の入場回数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:139 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:139
msgid "Number of days with a previous entry" msgid "Number of days with a previous entry"
msgstr "これまでの入力日数" msgstr "これまでの入場日数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:143 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:143
msgid "Number of days with a previous entry since" msgid "Number of days with a previous entry since"
msgstr "この時点より後に入力が行われた日数" msgstr "この時点より後に入場があった日数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:147 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:147
msgid "Number of days with a previous entry before" msgid "Number of days with a previous entry before"
msgstr "この時点より前に入力が行われた日数" msgstr "この時点より前に入場があった日数"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:151 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:151
msgid "Minutes since last entry (-1 on first entry)" msgid "Minutes since last entry (-1 on first entry)"
msgstr "最後の登録からの経過分数(最初の登録は-1)" msgstr "最後の入場からの経過分数(最初の入場は-1)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:155 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:155
msgid "Minutes since first entry (-1 on first entry)" msgid "Minutes since first entry (-1 on first entry)"
msgstr "最初のとうろくからの経過分数(最初の登録は-1)" msgstr "最初の入場からの経過分数(最初の入場は-1)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
msgid "All of the conditions below (AND)" msgid "All of the conditions below (AND)"
msgstr "以下全ての条件(と)" msgstr "以下のすべての条件(AND)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:183 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:183
msgid "At least one of the conditions below (OR)" msgid "At least one of the conditions below (OR)"
@@ -539,11 +539,11 @@ msgstr "イベントの入場"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:187 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:187
msgid "custom date and time" msgid "custom date and time"
msgstr "日時確定" msgstr "カスタム日時"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:188 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:188
msgid "custom time" msgid "custom time"
msgstr "時刻確定" msgstr "カスタム時刻"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:189 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:189
msgid "Tolerance (minutes)" msgid "Tolerance (minutes)"
@@ -755,8 +755,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still " "The items in your cart are no longer reserved for you. You can still "
"complete your order as long as they're available." "complete your order as long as they're available."
msgstr "" msgstr ""
"カートに入っている商品はお取り置きできません。在庫があれば、このまま注文を進" "カート内の商品の予約期限が切れました。在庫があれば、このまま注文を完了するこ"
"めることができます。" "とができます。"
#: pretix/static/pretixpresale/js/ui/cart.js:87 #: pretix/static/pretixpresale/js/ui/cart.js:87
msgid "Do you want to renew the reservation period?" msgid "Do you want to renew the reservation period?"
@@ -843,7 +843,7 @@ msgstr "%sを選択"
#, javascript-format #, javascript-format
msgctxt "widget" msgctxt "widget"
msgid "Select variant %s" msgid "Select variant %s"
msgstr "バリアント %sを選択" msgstr "バリエーション %sを選択"
#: pretix/static/pretixpresale/js/widget/widget.js:27 #: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget" msgctxt "widget"
@@ -853,7 +853,7 @@ msgstr "売り切れ"
#: pretix/static/pretixpresale/js/widget/widget.js:28 #: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget" msgctxt "widget"
msgid "Buy" msgid "Buy"
msgstr "カート内" msgstr "購入"
#: pretix/static/pretixpresale/js/widget/widget.js:29 #: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget" msgctxt "widget"
@@ -910,7 +910,7 @@ msgstr "現在%s使用可能"
#: pretix/static/pretixpresale/js/widget/widget.js:39 #: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget" msgctxt "widget"
msgid "Only available with a voucher" msgid "Only available with a voucher"
msgstr "クーポンをお持ちの方のみ" msgstr "バウチャーをお持ちの方のみ"
#: pretix/static/pretixpresale/js/widget/widget.js:40 #: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43 #: pretix/static/pretixpresale/js/widget/widget.js:43
@@ -999,7 +999,7 @@ msgstr "チェックアウトを続行する"
#: pretix/static/pretixpresale/js/widget/widget.js:58 #: pretix/static/pretixpresale/js/widget/widget.js:58
msgctxt "widget" msgctxt "widget"
msgid "Redeem a voucher" msgid "Redeem a voucher"
msgstr "クーポンを使用する" msgstr "バウチャーを使用する"
#: pretix/static/pretixpresale/js/widget/widget.js:59 #: pretix/static/pretixpresale/js/widget/widget.js:59
msgctxt "widget" msgctxt "widget"
@@ -1009,7 +1009,7 @@ msgstr "使用する"
#: pretix/static/pretixpresale/js/widget/widget.js:60 #: pretix/static/pretixpresale/js/widget/widget.js:60
msgctxt "widget" msgctxt "widget"
msgid "Voucher code" msgid "Voucher code"
msgstr "クーポンコード" msgstr "バウチャーコード"
#: pretix/static/pretixpresale/js/widget/widget.js:61 #: pretix/static/pretixpresale/js/widget/widget.js:61
msgctxt "widget" msgctxt "widget"
@@ -1095,7 +1095,7 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76 #: pretix/static/pretixpresale/js/widget/widget.js:76
msgctxt "widget" msgctxt "widget"
msgid "Load more" msgid "Load more"
msgstr "さらに読み込む" msgstr "もっと見る"
#: pretix/static/pretixpresale/js/widget/widget.js:78 #: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "Mo" msgid "Mo"
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-01-24 01:00+0000\n" "PO-Revision-Date: 2026-01-28 00:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n" "Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n" "nl/>\n"
@@ -161,12 +161,12 @@ msgstr "Betaalde bestellingen"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "Deelnemers (besteld)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "Deelnemers (betaald)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
@@ -1004,7 +1004,7 @@ msgstr "Doorgaan met afrekenen"
#: pretix/static/pretixpresale/js/widget/widget.js:58 #: pretix/static/pretixpresale/js/widget/widget.js:58
msgctxt "widget" msgctxt "widget"
msgid "Redeem a voucher" msgid "Redeem a voucher"
msgstr "Voucher inwisselen" msgstr "Een voucher inwisselen"
#: pretix/static/pretixpresale/js/widget/widget.js:59 #: pretix/static/pretixpresale/js/widget/widget.js:59
msgctxt "widget" msgctxt "widget"
File diff suppressed because it is too large Load Diff
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2021-08-05 04:00+0000\n" "PO-Revision-Date: 2026-01-29 19:43+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n" "Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/" "Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix-js/nl_Informal/>\n" "pretix-js/nl_Informal/>\n"
"Language: nl_Informal\n" "Language: nl_Informal\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.6\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -31,106 +31,104 @@ msgstr "Opmerking:"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
msgid "PayPal" msgid "PayPal"
msgstr "" msgstr "PayPal"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
msgid "Venmo" msgid "Venmo"
msgstr "" msgstr "Venmo"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
#: pretix/static/pretixpresale/js/walletdetection.js:38 #: pretix/static/pretixpresale/js/walletdetection.js:38
msgid "Apple Pay" msgid "Apple Pay"
msgstr "" msgstr "Apple Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
msgid "Itaú" msgid "Itaú"
msgstr "" msgstr "Itaú"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
msgid "PayPal Credit" msgid "PayPal Credit"
msgstr "" msgstr "PayPal-krediet"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
msgid "Credit Card" msgid "Credit Card"
msgstr "" msgstr "Creditcard"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
msgid "PayPal Pay Later" msgid "PayPal Pay Later"
msgstr "" msgstr "PayPal Later betalen"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL" msgid "iDEAL"
msgstr "" msgstr "iDEAL"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit" msgid "SEPA Direct Debit"
msgstr "" msgstr "SEPA-incasso"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
msgid "Bancontact" msgid "Bancontact"
msgstr "" msgstr "Bancontact"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
msgid "giropay" msgid "giropay"
msgstr "" msgstr "giropay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
msgid "SOFORT" msgid "SOFORT"
msgstr "" msgstr "SOFORT"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
#, fuzzy
#| msgid "Yes"
msgid "eps" msgid "eps"
msgstr "Ja" msgstr "eps"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
msgid "MyBank" msgid "MyBank"
msgstr "" msgstr "MyBank"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
msgid "Przelewy24" msgid "Przelewy24"
msgstr "" msgstr "Przelewy24"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
msgid "Verkkopankki" msgid "Verkkopankki"
msgstr "" msgstr "Verkkopankki"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:50 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:50
msgid "PayU" msgid "PayU"
msgstr "" msgstr "PayU"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51
msgid "BLIK" msgid "BLIK"
msgstr "" msgstr "BLIK"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52
msgid "Trustly" msgid "Trustly"
msgstr "" msgstr "Trustly"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:53 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:53
msgid "Zimpler" msgid "Zimpler"
msgstr "" msgstr "Zimpler"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:54 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:54
msgid "Maxima" msgid "Maxima"
msgstr "" msgstr "Maxima"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:55 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:55
msgid "OXXO" msgid "OXXO"
msgstr "" msgstr "OXXO"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:56 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:56
msgid "Boleto" msgid "Boleto"
msgstr "" msgstr "Boleto"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57
msgid "WeChat Pay" msgid "WeChat Pay"
msgstr "" msgstr "WeChat Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58
msgid "Mercado Pago" msgid "Mercado Pago"
msgstr "" msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
@@ -149,7 +147,7 @@ msgstr "Betaling bevestigen …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:254 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:254
msgid "Payment method unavailable" msgid "Payment method unavailable"
msgstr "" msgstr "Betaalmethode niet beschikbaar"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63
@@ -164,12 +162,12 @@ msgstr "Betaalde bestellingen"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "Deelnemers (geordend)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "Deelnemers (betaald)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
@@ -189,15 +187,15 @@ msgstr "Verbinding maken met je bank …"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
msgid "Select a check-in list" msgid "Select a check-in list"
msgstr "Kies een inchecklijst" msgstr "Kies een check-inlijst"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found." msgid "No active check-in lists found."
msgstr "Geen actieve inchecklijsten gevonden." msgstr "Geen actieve check-inlijsten gevonden."
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list" msgid "Switch check-in list"
msgstr "Andere inchecklijst kiezen" msgstr "Andere check-inlijst kiezen"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results" msgid "Search results"
@@ -205,7 +203,7 @@ msgstr "Zoekresultaten"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
msgid "No tickets found" msgid "No tickets found"
msgstr "Geen kaartjes gevonden" msgstr "Geen tickets gevonden"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
msgid "Result" msgid "Result"
@@ -213,7 +211,7 @@ msgstr "Resultaat"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
msgid "This ticket requires special attention" msgid "This ticket requires special attention"
msgstr "Dit kaartje heeft speciale aandacht nodig" msgstr "Dit ticket heeft speciale aandacht nodig"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction" msgid "Switch direction"
@@ -229,7 +227,7 @@ msgstr "Vertrek"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
msgid "Scan a ticket or search and press return…" msgid "Scan a ticket or search and press return…"
msgstr "Scan een kaartje of voer een zoekterm in en druk op Enter…" msgstr "Scan een ticket of voer een zoekterm in en druk op Enter…"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
msgid "Load more" msgid "Load more"
@@ -250,15 +248,15 @@ msgstr "Geannuleerd"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Confirmed" msgid "Confirmed"
msgstr "" msgstr "Bevestigd"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending" msgid "Approval pending"
msgstr "" msgstr "Goedkeuring in behandeling"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed" msgid "Redeemed"
msgstr "Gebruikt" msgstr "Ingewisseld"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
@@ -267,19 +265,19 @@ msgstr "Annuleren"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid" msgid "Ticket not paid"
msgstr "Kaartje niet betaald" msgstr "Ticket niet betaald"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "This ticket is not yet paid. Do you want to continue anyways?" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "Dit kaartje is nog niet betaald. Wil je toch doorgaan?" msgstr "Dit ticket is nog niet betaald. Wil je toch doorgaan?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Additional information required" msgid "Additional information required"
msgstr "Extra informatie nodig" msgstr "Aanvullende informatie vereist"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Valid ticket" msgid "Valid ticket"
msgstr "Geldig kaartje" msgstr "Geldig ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
@@ -287,7 +285,7 @@ msgstr "Vertrek opgeslagen"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
msgstr "Kaartje al gebruikt" msgstr "Ticket al gebruikt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required" msgid "Information required"
@@ -295,11 +293,11 @@ msgstr "Informatie nodig"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket" msgid "Unknown ticket"
msgstr "Onbekend kaartje" msgstr "Onbekend ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here" msgid "Ticket type not allowed here"
msgstr "Kaartjestype hier niet toegestaan" msgstr "Dit type ticket is hier niet toegestaan"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
@@ -307,19 +305,15 @@ msgstr "Binnenkomst niet toegestaan"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "Kaartjescode ingetrokken/veranderd" msgstr "Ticketcode ingetrokken/veranderd"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "Kaartje niet betaald" msgstr "Ticket geblokkeerd"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "Kaartje niet betaald" msgstr "Ticket niet geldig op dit tijdstip"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled" msgid "Order canceled"
@@ -327,11 +321,11 @@ msgstr "Bestelling geannuleerd"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list" msgid "Ticket code is ambiguous on list"
msgstr "" msgstr "Ticketcode is dubbelzinnig op lijst"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "" msgstr "Bestelling niet goedgekeurd"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
@@ -358,11 +352,8 @@ msgid "No"
msgstr "Nee" msgstr "Nee"
#: pretix/static/lightbox/js/lightbox.js:96 #: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close" msgid "close"
msgstr "Sluiten" msgstr "sluiten"
#: pretix/static/pretixbase/js/addressform.js:101 #: pretix/static/pretixbase/js/addressform.js:101
#: pretix/static/pretixpresale/js/ui/main.js:529 #: pretix/static/pretixpresale/js/ui/main.js:529
@@ -434,7 +425,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:276 #: pretix/static/pretixbase/js/asynctask.js:276
msgid "If this takes longer than a few minutes, please contact us." msgid "If this takes longer than a few minutes, please contact us."
msgstr "" msgstr "Als dit langer dan een paar minuten duurt, neem dan contact met ons op."
#: pretix/static/pretixbase/js/asynctask.js:331 #: pretix/static/pretixbase/js/asynctask.js:331
msgid "Close message" msgid "Close message"
@@ -464,7 +455,7 @@ msgstr "is na"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
msgid "=" msgid "="
msgstr "" msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Product" msgid "Product"
@@ -476,7 +467,7 @@ msgstr "Productvariant"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
msgid "Gate" msgid "Gate"
msgstr "" msgstr "Ingang"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:111 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:111
msgid "Current date and time" msgid "Current date and time"
@@ -484,11 +475,11 @@ msgstr "Huidige datum en tijd"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:115 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:115
msgid "Current day of the week (1 = Monday, 7 = Sunday)" msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr "" msgstr "Huidige dag van de week (1 = maandag, 7 = zondag)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
msgid "Current entry status" msgid "Current entry status"
msgstr "" msgstr "Huidige toegangsstatus"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
msgid "Number of previous entries" msgid "Number of previous entries"
@@ -499,40 +490,32 @@ msgid "Number of previous entries since midnight"
msgstr "Aantal eerdere binnenkomsten sinds middernacht" msgstr "Aantal eerdere binnenkomsten sinds middernacht"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:131 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:131
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries since" msgid "Number of previous entries since"
msgstr "Aantal eerdere binnenkomsten" msgstr "Aantal eerdere binnenkomsten sinds"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:135 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:135
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries before" msgid "Number of previous entries before"
msgstr "Aantal eerdere binnenkomsten" msgstr "Aantal eerdere binnenkomsten vóór"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:139 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:139
msgid "Number of days with a previous entry" msgid "Number of days with a previous entry"
msgstr "Aantal dagen met een eerdere binnenkomst" msgstr "Aantal dagen met een eerdere binnenkomst"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:143 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:143
#, fuzzy
#| msgid "Number of days with a previous entry"
msgid "Number of days with a previous entry since" msgid "Number of days with a previous entry since"
msgstr "Aantal dagen met een eerdere binnenkomst" msgstr "Aantal dagen met een eerdere binnenkomst sinds"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:147 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:147
#, fuzzy
#| msgid "Number of days with a previous entry"
msgid "Number of days with a previous entry before" msgid "Number of days with a previous entry before"
msgstr "Aantal dagen met een eerdere binnenkomst" msgstr "Aantal dagen met een eerdere binnenkomst voor"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:151 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:151
msgid "Minutes since last entry (-1 on first entry)" msgid "Minutes since last entry (-1 on first entry)"
msgstr "" msgstr "Minuten sinds laatste binnenkomst (-1 bij eerste binnenkomst)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:155 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:155
msgid "Minutes since first entry (-1 on first entry)" msgid "Minutes since first entry (-1 on first entry)"
msgstr "" msgstr "Minuten sinds eerste binnenkomst (-1 bij eerste binnenkomst)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
msgid "All of the conditions below (AND)" msgid "All of the conditions below (AND)"
@@ -576,25 +559,25 @@ msgstr "minuten"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:192 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:192
msgid "Duplicate" msgid "Duplicate"
msgstr "" msgstr "Duplicaat"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
msgctxt "entry_status" msgctxt "entry_status"
msgid "present" msgid "present"
msgstr "" msgstr "aanwezig"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
msgctxt "entry_status" msgctxt "entry_status"
msgid "absent" msgid "absent"
msgstr "" msgstr "afwezig"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:289 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:289
msgid "Error: Product not found!" msgid "Error: Product not found!"
msgstr "" msgstr "Fout: Product niet gevonden!"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:296 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:296
msgid "Error: Variation not found!" msgid "Error: Variation not found!"
msgstr "" msgstr "Fout: Variant niet gevonden!"
#: pretix/static/pretixcontrol/js/ui/editor.js:171 #: pretix/static/pretixcontrol/js/ui/editor.js:171
msgid "Check-in QR" msgid "Check-in QR"
@@ -610,16 +593,12 @@ msgid "Group of objects"
msgstr "Groep van objecten" msgstr "Groep van objecten"
#: pretix/static/pretixcontrol/js/ui/editor.js:909 #: pretix/static/pretixcontrol/js/ui/editor.js:909
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)" msgid "Text object (deprecated)"
msgstr "Tekstobject" msgstr "Tekstobject (verouderd)"
#: pretix/static/pretixcontrol/js/ui/editor.js:911 #: pretix/static/pretixcontrol/js/ui/editor.js:911
#, fuzzy
#| msgid "Text object"
msgid "Text box" msgid "Text box"
msgstr "Tekstobject" msgstr "Tekstveld"
#: pretix/static/pretixcontrol/js/ui/editor.js:913 #: pretix/static/pretixcontrol/js/ui/editor.js:913
msgid "Barcode area" msgid "Barcode area"
@@ -667,25 +646,26 @@ msgid "Unknown error."
msgstr "Onbekende fout." msgstr "Onbekende fout."
#: pretix/static/pretixcontrol/js/ui/main.js:309 #: pretix/static/pretixcontrol/js/ui/main.js:309
#, fuzzy
#| msgid "Your color has great contrast and is very easy to read!"
msgid "Your color has great contrast and will provide excellent accessibility." msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "Je kleur heeft een goed contrast, en is gemakkelijk te lezen!" msgstr ""
"Je kleur heeft een groot contrast en zorgt voor een uitstekende "
"toegankelijkheid."
#: pretix/static/pretixcontrol/js/ui/main.js:313 #: pretix/static/pretixcontrol/js/ui/main.js:313
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid "" msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility " "Your color has decent contrast and is sufficient for minimum accessibility "
"requirements." "requirements."
msgstr "" msgstr ""
"Je kleur heeft een redelijk contrast, en is waarschijnlijk goed te lezen!" "Je kleur heeft een behoorlijk contrast en voldoet aan de minimale "
"toegankelijkheidseisen."
#: pretix/static/pretixcontrol/js/ui/main.js:317 #: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "" msgid ""
"Your color has insufficient contrast to white. Accessibility of your site " "Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted." "will be impacted."
msgstr "" msgstr ""
"Je kleur heeft onvoldoende contrast met wit. Dit heeft invloed op de "
"toegankelijkheid van je website."
#: pretix/static/pretixcontrol/js/ui/main.js:443 #: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463 #: pretix/static/pretixcontrol/js/ui/main.js:463
@@ -706,11 +686,11 @@ msgstr "Alleen geselecteerde"
#: pretix/static/pretixcontrol/js/ui/main.js:839 #: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr "Voer een paginanummer in tussen 1 en %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js:842 #: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr "Ongeldig paginanummer."
#: pretix/static/pretixcontrol/js/ui/main.js:1000 #: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally" msgid "Use a different name internally"
@@ -729,10 +709,8 @@ msgid "Calculating default price…"
msgstr "Standaardprijs berekenen…" msgstr "Standaardprijs berekenen…"
#: pretix/static/pretixcontrol/js/ui/plugins.js:69 #: pretix/static/pretixcontrol/js/ui/plugins.js:69
#, fuzzy
#| msgid "Search results"
msgid "No results" msgid "No results"
msgstr "Zoekresultaten" msgstr "Geen resultaten"
#: pretix/static/pretixcontrol/js/ui/question.js:41 #: pretix/static/pretixcontrol/js/ui/question.js:41
msgid "Others" msgid "Others"
@@ -740,7 +718,7 @@ msgstr "Andere"
#: pretix/static/pretixcontrol/js/ui/question.js:81 #: pretix/static/pretixcontrol/js/ui/question.js:81
msgid "Count" msgid "Count"
msgstr "Aantal" msgstr "Tellen"
#: pretix/static/pretixcontrol/js/ui/subevent.js:112 #: pretix/static/pretixcontrol/js/ui/subevent.js:112
msgid "(one more date)" msgid "(one more date)"
@@ -749,12 +727,12 @@ msgstr[0] "(één andere datum)"
msgstr[1] "({num} andere datums)" msgstr[1] "({num} andere datums)"
#: pretix/static/pretixpresale/js/ui/cart.js:47 #: pretix/static/pretixpresale/js/ui/cart.js:47
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid "" msgid ""
"The items in your cart are no longer reserved for you. You can still " "The items in your cart are no longer reserved for you. You can still "
"complete your order as long as they’re available." "complete your order as long as they’re available."
msgstr "De items in je winkelwagen zijn niet meer voor je gereserveerd." msgstr ""
"De artikelen in je winkelwagen zijn niet langer voor je gereserveerd. Je "
"kunt je bestelling nog steeds voltooien zolang ze beschikbaar zijn."
#: pretix/static/pretixpresale/js/ui/cart.js:49 #: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired" msgid "Cart expired"
@@ -763,41 +741,34 @@ msgstr "Winkelwagen is verlopen"
#: pretix/static/pretixpresale/js/ui/cart.js:58 #: pretix/static/pretixpresale/js/ui/cart.js:58
#: pretix/static/pretixpresale/js/ui/cart.js:84 #: pretix/static/pretixpresale/js/ui/cart.js:84
msgid "Your cart is about to expire." msgid "Your cart is about to expire."
msgstr "" msgstr "Je winkelwagen verloopt bijna."
#: pretix/static/pretixpresale/js/ui/cart.js:62 #: pretix/static/pretixpresale/js/ui/cart.js:62
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute." msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes." msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "" msgstr[0] "De artikelen in je winkelwagen worden één minuut voor je gereserveerd."
"De items in je winkelwagen zijn nog één minuut voor je gereserveerd."
msgstr[1] "" msgstr[1] ""
"De items in je winkelwagen zijn nog {num} minuten voor je gereserveerd." "De artikelen in je winkelwagen worden {num} minuten voor je gereserveerd."
#: pretix/static/pretixpresale/js/ui/cart.js:83 #: pretix/static/pretixpresale/js/ui/cart.js:83
#, fuzzy
#| msgid "Cart expired"
msgid "Your cart has expired." msgid "Your cart has expired."
msgstr "Winkelwagen is verlopen" msgstr "Je winkelwagen is verlopen."
#: pretix/static/pretixpresale/js/ui/cart.js:86 #: pretix/static/pretixpresale/js/ui/cart.js:86
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid "" msgid ""
"The items in your cart are no longer reserved for you. You can still " "The items in your cart are no longer reserved for you. You can still "
"complete your order as long as they're available." "complete your order as long as they're available."
msgstr "De items in je winkelwagen zijn niet meer voor je gereserveerd." msgstr ""
"De artikelen in je winkelwagen zijn niet langer voor je gereserveerd. Je "
"kunt je bestelling nog steeds voltooien zolang ze beschikbaar zijn."
#: pretix/static/pretixpresale/js/ui/cart.js:87 #: pretix/static/pretixpresale/js/ui/cart.js:87
msgid "Do you want to renew the reservation period?" msgid "Do you want to renew the reservation period?"
msgstr "" msgstr "Wil je de reserveringsperiode verlengen?"
#: pretix/static/pretixpresale/js/ui/cart.js:90 #: pretix/static/pretixpresale/js/ui/cart.js:90
msgid "Renew reservation" msgid "Renew reservation"
msgstr "" msgstr "Reservering verlengen"
#: pretix/static/pretixpresale/js/ui/main.js:194 #: pretix/static/pretixpresale/js/ui/main.js:194
msgid "The organizer keeps %(currency)s %(amount)s" msgid "The organizer keeps %(currency)s %(amount)s"
@@ -817,71 +788,66 @@ msgstr "Je lokale tijd:"
#: pretix/static/pretixpresale/js/walletdetection.js:39 #: pretix/static/pretixpresale/js/walletdetection.js:39
msgid "Google Pay" msgid "Google Pay"
msgstr "" msgstr "Google Pay"
#: pretix/static/pretixpresale/js/widget/widget.js:16 #: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget" msgctxt "widget"
msgid "Quantity" msgid "Quantity"
msgstr "" msgstr "Hoeveelheid"
#: pretix/static/pretixpresale/js/widget/widget.js:17 #: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget" msgctxt "widget"
msgid "Decrease quantity" msgid "Decrease quantity"
msgstr "" msgstr "Hoeveelheid verminderen"
#: pretix/static/pretixpresale/js/widget/widget.js:18 #: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget" msgctxt "widget"
msgid "Increase quantity" msgid "Increase quantity"
msgstr "" msgstr "Hoeveelheid verhogen"
#: pretix/static/pretixpresale/js/widget/widget.js:19 #: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget" msgctxt "widget"
msgid "Filter events by" msgid "Filter events by"
msgstr "" msgstr "Filter evenementen op"
#: pretix/static/pretixpresale/js/widget/widget.js:20 #: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget" msgctxt "widget"
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Filteren"
#: pretix/static/pretixpresale/js/widget/widget.js:21 #: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget" msgctxt "widget"
msgid "Price" msgid "Price"
msgstr "" msgstr "Prijs"
#: pretix/static/pretixpresale/js/widget/widget.js:22 #: pretix/static/pretixpresale/js/widget/widget.js:22
#, javascript-format #, javascript-format
msgctxt "widget" msgctxt "widget"
msgid "Original price: %s" msgid "Original price: %s"
msgstr "" msgstr "Oorspronkelijke prijs: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:23 #: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format #, javascript-format
msgctxt "widget" msgctxt "widget"
msgid "New price: %s" msgid "New price: %s"
msgstr "" msgstr "Nieuwe prijs: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:24 #: pretix/static/pretixpresale/js/widget/widget.js:24
#, fuzzy
#| msgid "Selected only"
msgctxt "widget" msgctxt "widget"
msgid "Select" msgid "Select"
msgstr "Alleen geselecteerde" msgstr "Selecteren"
#: pretix/static/pretixpresale/js/widget/widget.js:25 #: pretix/static/pretixpresale/js/widget/widget.js:25
#, fuzzy, javascript-format #, javascript-format
#| msgid "Selected only"
msgctxt "widget" msgctxt "widget"
msgid "Select %s" msgid "Select %s"
msgstr "Alleen geselecteerde" msgstr "Selecteer %s"
#: pretix/static/pretixpresale/js/widget/widget.js:26 #: pretix/static/pretixpresale/js/widget/widget.js:26
#, fuzzy, javascript-format #, javascript-format
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget" msgctxt "widget"
msgid "Select variant %s" msgid "Select variant %s"
msgstr "Zie variaties" msgstr "Selecteer variant %s"
#: pretix/static/pretixpresale/js/widget/widget.js:27 #: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget" msgctxt "widget"
@@ -917,7 +883,7 @@ msgstr "vanaf %(currency)s %(price)s"
#, javascript-format #, javascript-format
msgctxt "widget" msgctxt "widget"
msgid "Image of %s" msgid "Image of %s"
msgstr "" msgstr "Afbeelding van %s"
#: pretix/static/pretixpresale/js/widget/widget.js:34 #: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget" msgctxt "widget"
@@ -952,25 +918,19 @@ msgstr "Alleen beschikbaar met een voucher"
#: pretix/static/pretixpresale/js/widget/widget.js:40 #: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43 #: pretix/static/pretixpresale/js/widget/widget.js:43
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget" msgctxt "widget"
msgid "Not yet available" msgid "Not yet available"
msgstr "nu beschikbaar: %s" msgstr "Nog niet beschikbaar"
#: pretix/static/pretixpresale/js/widget/widget.js:41 #: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget" msgctxt "widget"
msgid "Not available anymore" msgid "Not available anymore"
msgstr "" msgstr "Niet meer beschikbaar"
#: pretix/static/pretixpresale/js/widget/widget.js:42 #: pretix/static/pretixpresale/js/widget/widget.js:42
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget" msgctxt "widget"
msgid "Currently not available" msgid "Currently not available"
msgstr "nu beschikbaar: %s" msgstr "Momenteel niet beschikbaar"
#: pretix/static/pretixpresale/js/widget/widget.js:44 #: pretix/static/pretixpresale/js/widget/widget.js:44
#, javascript-format #, javascript-format
@@ -1003,12 +963,9 @@ msgid "Open ticket shop"
msgstr "Open de kaartjeswinkel" msgstr "Open de kaartjeswinkel"
#: pretix/static/pretixpresale/js/widget/widget.js:50 #: pretix/static/pretixpresale/js/widget/widget.js:50
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget" msgctxt "widget"
msgid "Checkout" msgid "Checkout"
msgstr "Doorgaan met afrekenen" msgstr "Afrekenen"
#: pretix/static/pretixpresale/js/widget/widget.js:51 #: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget" msgctxt "widget"
@@ -1067,17 +1024,14 @@ msgid "Close"
msgstr "Sluiten" msgstr "Sluiten"
#: pretix/static/pretixpresale/js/widget/widget.js:62 #: pretix/static/pretixpresale/js/widget/widget.js:62
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget" msgctxt "widget"
msgid "Close checkout" msgid "Close checkout"
msgstr "Doorgaan met afrekenen" msgstr "Afrekening afsluiten"
#: pretix/static/pretixpresale/js/widget/widget.js:63 #: pretix/static/pretixpresale/js/widget/widget.js:63
msgctxt "widget" msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish." msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr "" msgstr "Je kunt deze bewerking niet annuleren. Wacht tot het laden voltooid is."
#: pretix/static/pretixpresale/js/widget/widget.js:64 #: pretix/static/pretixpresale/js/widget/widget.js:64
msgctxt "widget" msgctxt "widget"
@@ -1085,20 +1039,14 @@ msgid "Continue"
msgstr "Ga verder" msgstr "Ga verder"
#: pretix/static/pretixpresale/js/widget/widget.js:65 #: pretix/static/pretixpresale/js/widget/widget.js:65
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget" msgctxt "widget"
msgid "Show variants" msgid "Show variants"
msgstr "Zie variaties" msgstr "Varianten weergeven"
#: pretix/static/pretixpresale/js/widget/widget.js:66 #: pretix/static/pretixpresale/js/widget/widget.js:66
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget" msgctxt "widget"
msgid "Hide variants" msgid "Hide variants"
msgstr "Zie variaties" msgstr "Varianten verbergen"
#: pretix/static/pretixpresale/js/widget/widget.js:67 #: pretix/static/pretixpresale/js/widget/widget.js:67
msgctxt "widget" msgctxt "widget"
@@ -1147,6 +1095,9 @@ msgid ""
"add yourself to the waiting list. We will then notify if seats are available " "add yourself to the waiting list. We will then notify if seats are available "
"again." "again."
msgstr "" msgstr ""
"Sommige of alle ticketcategorieën zijn momenteel uitverkocht. Als je dat "
"wilt, kun je je op de wachtlijst laten plaatsen. Wij zullen je dan op de "
"hoogte brengen als er weer plaatsen beschikbaar zijn."
#: pretix/static/pretixpresale/js/widget/widget.js:76 #: pretix/static/pretixpresale/js/widget/widget.js:76
msgctxt "widget" msgctxt "widget"
@@ -1183,79 +1134,79 @@ msgstr "Zo"
#: pretix/static/pretixpresale/js/widget/widget.js:85 #: pretix/static/pretixpresale/js/widget/widget.js:85
msgid "Monday" msgid "Monday"
msgstr "" msgstr "maandag"
#: pretix/static/pretixpresale/js/widget/widget.js:86 #: pretix/static/pretixpresale/js/widget/widget.js:86
msgid "Tuesday" msgid "Tuesday"
msgstr "" msgstr "dinsdag"
#: pretix/static/pretixpresale/js/widget/widget.js:87 #: pretix/static/pretixpresale/js/widget/widget.js:87
msgid "Wednesday" msgid "Wednesday"
msgstr "" msgstr "woensdag"
#: pretix/static/pretixpresale/js/widget/widget.js:88 #: pretix/static/pretixpresale/js/widget/widget.js:88
msgid "Thursday" msgid "Thursday"
msgstr "" msgstr "donderdag"
#: pretix/static/pretixpresale/js/widget/widget.js:89 #: pretix/static/pretixpresale/js/widget/widget.js:89
msgid "Friday" msgid "Friday"
msgstr "" msgstr "vrijdag"
#: pretix/static/pretixpresale/js/widget/widget.js:90 #: pretix/static/pretixpresale/js/widget/widget.js:90
msgid "Saturday" msgid "Saturday"
msgstr "" msgstr "zaterdag"
#: pretix/static/pretixpresale/js/widget/widget.js:91 #: pretix/static/pretixpresale/js/widget/widget.js:91
msgid "Sunday" msgid "Sunday"
msgstr "" msgstr "zondag"
#: pretix/static/pretixpresale/js/widget/widget.js:94 #: pretix/static/pretixpresale/js/widget/widget.js:94
msgid "January" msgid "January"
msgstr "Januari" msgstr "januari"
#: pretix/static/pretixpresale/js/widget/widget.js:95 #: pretix/static/pretixpresale/js/widget/widget.js:95
msgid "February" msgid "February"
msgstr "Februari" msgstr "februari"
#: pretix/static/pretixpresale/js/widget/widget.js:96 #: pretix/static/pretixpresale/js/widget/widget.js:96
msgid "March" msgid "March"
msgstr "Maart" msgstr "maart"
#: pretix/static/pretixpresale/js/widget/widget.js:97 #: pretix/static/pretixpresale/js/widget/widget.js:97
msgid "April" msgid "April"
msgstr "April" msgstr "april"
#: pretix/static/pretixpresale/js/widget/widget.js:98 #: pretix/static/pretixpresale/js/widget/widget.js:98
msgid "May" msgid "May"
msgstr "Mei" msgstr "mei"
#: pretix/static/pretixpresale/js/widget/widget.js:99 #: pretix/static/pretixpresale/js/widget/widget.js:99
msgid "June" msgid "June"
msgstr "Juni" msgstr "juni"
#: pretix/static/pretixpresale/js/widget/widget.js:100 #: pretix/static/pretixpresale/js/widget/widget.js:100
msgid "July" msgid "July"
msgstr "Juli" msgstr "juli"
#: pretix/static/pretixpresale/js/widget/widget.js:101 #: pretix/static/pretixpresale/js/widget/widget.js:101
msgid "August" msgid "August"
msgstr "Augustus" msgstr "augustus"
#: pretix/static/pretixpresale/js/widget/widget.js:102 #: pretix/static/pretixpresale/js/widget/widget.js:102
msgid "September" msgid "September"
msgstr "September" msgstr "september"
#: pretix/static/pretixpresale/js/widget/widget.js:103 #: pretix/static/pretixpresale/js/widget/widget.js:103
msgid "October" msgid "October"
msgstr "Oktober" msgstr "oktober"
#: pretix/static/pretixpresale/js/widget/widget.js:104 #: pretix/static/pretixpresale/js/widget/widget.js:104
msgid "November" msgid "November"
msgstr "November" msgstr "november"
#: pretix/static/pretixpresale/js/widget/widget.js:105 #: pretix/static/pretixpresale/js/widget/widget.js:105
msgid "December" msgid "December"
msgstr "December" msgstr "december"
#~ msgid "Time zone:" #~ msgid "Time zone:"
#~ msgstr "Tijdzone:" #~ msgstr "Tijdzone:"
+11 -4
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n" "POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2025-12-18 01:00+0000\n" "PO-Revision-Date: 2026-01-26 22:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n" "Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n" "pretix/pretix/pt_BR/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.15\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -4366,6 +4366,8 @@ msgid ""
"to confirm changing your email address from {old_email}\n" "to confirm changing your email address from {old_email}\n"
"to {new_email}, use the following code:" "to {new_email}, use the following code:"
msgstr "" msgstr ""
"para confirmar a alteração do seu endereço de email de {old_email}\n"
"para {new_email}, use o código a seguir:"
#: pretix/base/models/auth.py:377 #: pretix/base/models/auth.py:377
#, python-brace-format #, python-brace-format
@@ -4373,6 +4375,8 @@ msgid ""
"to confirm that your email address {email} belongs to your pretix account, " "to confirm that your email address {email} belongs to your pretix account, "
"use the following code:" "use the following code:"
msgstr "" msgstr ""
"para confirmar que o seu endereço de email {email} pertence a sua conta do "
"pretix, use o código a seguir:"
#: pretix/base/models/auth.py:391 #: pretix/base/models/auth.py:391
msgid "pretix confirmation code" msgid "pretix confirmation code"
@@ -8185,6 +8189,9 @@ msgid ""
"2017-05-31 14:00 – 16:00\n" "2017-05-31 14:00 – 16:00\n"
"2017-05-31 14:00 – 2017-06-01 14:00" "2017-05-31 14:00 – 2017-06-01 14:00"
msgstr "" msgstr ""
"2017-05-31 10:00 – 12:00\n"
"2017-05-31 14:00 – 16:00\n"
"2017-05-31 14:00 – 2017-06-01 14:00"
#: pretix/base/pdf.py:500 #: pretix/base/pdf.py:500
msgid "Reusable Medium ID" msgid "Reusable Medium ID"
@@ -16267,7 +16274,7 @@ msgstr ""
#: pretix/control/forms/orders.py:1037 #: pretix/control/forms/orders.py:1037
msgid "I understand that this is not reversible and want to continue" msgid "I understand that this is not reversible and want to continue"
msgstr "" msgstr "Eu entendo que esta ação não é reversível e eu quero continuar"
#: pretix/control/forms/orders.py:1042 #: pretix/control/forms/orders.py:1042
msgid "" msgid ""
@@ -27627,7 +27634,7 @@ msgstr "A lista selecionada foi apagada."
#: pretix/control/views/dashboards.py:114 #: pretix/control/views/dashboards.py:114
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "Participantes (com pedidos)" msgstr "Participantes (com pedido)"
#: pretix/control/views/dashboards.py:124 #: pretix/control/views/dashboards.py:124
msgid "Attendees (paid)" msgid "Attendees (paid)"
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2025-12-09 00:47+0000\n" "PO-Revision-Date: 2026-01-26 22:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n" "Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/pt_BR/>\n" "pretix/pretix-js/pt_BR/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.14.3\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -162,12 +162,12 @@ msgstr "Pedidos pagos"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "Participantes (com pedido)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "Participantes (pago)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n" "POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-01-12 17:00+0000\n" "PO-Revision-Date: 2026-01-26 22:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/" "Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/"
"projects/pretix/pretix/zh_Hant/>\n" "projects/pretix/pretix/zh_Hant/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.15.1\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -9583,17 +9583,13 @@ msgid "Ask for VAT ID"
msgstr "詢問增值稅號" msgstr "詢問增值稅號"
#: pretix/base/settings.py:645 #: pretix/base/settings.py:645
#, fuzzy, python-brace-format #, python-brace-format
#| msgid ""
#| "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}"
msgid "" msgid ""
"Only works if an invoice address is asked for. VAT ID is only requested from " "Only works if an invoice address is asked for. VAT ID is only requested from "
"business customers in the following countries: {countries}." "business customers in the following countries: {countries}."
msgstr "" msgstr ""
"僅當要求提供發票位址時才有效。增值稅號從來都不是必需的,並且僅向以下國家/地區" "僅當要求提供發票地址時才有效。 僅以下國家的企業客戶要求提供增值稅 ID:"
"的企業客戶請求:{countries}" "{countries}。"
#: pretix/base/settings.py:664 #: pretix/base/settings.py:664
#, fuzzy #, fuzzy
@@ -26554,10 +26550,8 @@ msgid "Please try again."
msgstr "請重試。" msgstr "請重試。"
#: pretix/control/views/auth.py:544 #: pretix/control/views/auth.py:544
#, fuzzy
#| msgid "Two-factor authentication is required to log in"
msgid "A recovery code for two-factor authentification was used to log in." msgid "A recovery code for two-factor authentification was used to log in."
msgstr "登入需要兩步驟驗證" msgstr "用於雙因素身份驗證的恢復程式用於登入。"
#: pretix/control/views/auth.py:560 #: pretix/control/views/auth.py:560
msgid "Invalid code, please try again." msgid "Invalid code, please try again."
+4 -11
View File
@@ -56,7 +56,6 @@ from pretix.base.models import (
) )
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import change_payment_provider from pretix.base.services.orders import change_payment_provider
from pretix.base.services.tasks import TransactionAwareTask from pretix.base.services.tasks import TransactionAwareTask
from pretix.celery_app import app from pretix.celery_app import app
@@ -72,13 +71,10 @@ def notify_incomplete_payment(o: Order):
email_context = get_email_context(event=o.event, order=o, pending_sum=o.pending_sum) email_context = get_email_context(event=o.event, order=o, pending_sum=o.pending_sum)
email_subject = o.event.settings.mail_subject_order_incomplete_payment email_subject = o.event.settings.mail_subject_order_incomplete_payment
try: o.send_mail(
o.send_mail( email_subject, email_template, email_context,
email_subject, email_template, email_context, 'pretix.event.order.email.expire_warning_sent'
'pretix.event.order.email.expire_warning_sent' )
)
except SendMailException:
logger.exception('Reminder email could not be sent')
def cancel_old_payments(order): def cancel_old_payments(order):
@@ -288,9 +284,6 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, regex_match_to_s
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
# payment confirmed but order status could not be set, no longer problem of this plugin # payment confirmed but order status could not be set, no longer problem of this plugin
cancel_old_payments(order) cancel_old_payments(order)
except SendMailException:
# payment confirmed but order status could not be set, no longer problem of this plugin
cancel_old_payments(order)
else: else:
cancel_old_payments(order) cancel_old_payments(order)
-6
View File
@@ -58,7 +58,6 @@ from localflavor.generic.forms import BICFormField, IBANFormField
from pretix.base.forms.widgets import DatePickerWidget from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.control.permissions import ( from pretix.control.permissions import (
@@ -160,11 +159,6 @@ class ActionView(View):
p.confirm(user=self.request.user) p.confirm(user=self.request.user)
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
pass pass
except SendMailException:
return JsonResponse({
'status': 'error',
'message': _('Problem sending email.')
})
trans.state = BankTransaction.STATE_VALID trans.state = BankTransaction.STATE_VALID
trans.save() trans.save()
trans.order.payments.filter( trans.order.payments.filter(
-4
View File
@@ -57,7 +57,6 @@ from pretix.base.decimal import round_decimal
from pretix.base.forms import SecretKeySettingsField from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.paypal.api import Api from pretix.plugins.paypal.api import Api
@@ -468,9 +467,6 @@ class Paypal(BasePaymentProvider):
payment_obj.confirm() payment_obj.confirm()
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
raise PaymentException(str(e)) raise PaymentException(str(e))
except SendMailException:
messages.warning(request, _('There was an error sending the confirmation mail.'))
return None return None
def payment_pending_render(self, request, payment) -> str: def payment_pending_render(self, request, payment) -> str:
-4
View File
@@ -54,7 +54,6 @@ from pretix.base.forms import SecretKeySettingsField
from pretix.base.forms.questions import guess_country from pretix.base.forms.questions import guess_country
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import build_absolute_uri as build_global_uri
@@ -821,9 +820,6 @@ class PaypalMethod(BasePaymentProvider):
payment.confirm() payment.confirm()
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
raise PaymentException(str(e)) raise PaymentException(str(e))
except SendMailException:
messages.warning(request, _('There was an error sending the confirmation mail.'))
finally: finally:
if 'payment_paypal_oid' in request.session: if 'payment_paypal_oid' in request.session:
del request.session['payment_paypal_oid'] del request.session['payment_paypal_oid']
+26 -33
View File
@@ -38,7 +38,6 @@ from pretix.base.models import (
fields, fields,
) )
from pretix.base.models.base import LoggingMixin from pretix.base.models.base import LoggingMixin
from pretix.base.services.mail import SendMailException
class ScheduledMail(models.Model): class ScheduledMail(models.Model):
@@ -180,13 +179,10 @@ class ScheduledMail(models.Model):
invoice_address=ia, invoice_address=ia,
event_or_subevent=self.subevent or e, event_or_subevent=self.subevent or e,
) )
try: o.send_mail(self.rule.subject, self.rule.template, email_ctx,
o.send_mail(self.rule.subject, self.rule.template, email_ctx, attach_ical=self.rule.attach_ical,
attach_ical=self.rule.attach_ical, log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent') o_sent = True
o_sent = True
except SendMailException:
... # ¯\_(ツ)_/¯
if send_to_attendees: if send_to_attendees:
if not self.rule.all_products: if not self.rule.all_products:
@@ -195,31 +191,28 @@ class ScheduledMail(models.Model):
positions = [p for p in positions if p.subevent_id == self.subevent_id] positions = [p for p in positions if p.subevent_id == self.subevent_id]
for p in positions: for p in positions:
try: if p.attendee_email and (p.attendee_email != o.email or not o_sent):
if p.attendee_email and (p.attendee_email != o.email or not o_sent): email_ctx = get_email_context(
email_ctx = get_email_context( event=e,
event=e, order=o,
order=o, invoice_address=ia,
invoice_address=ia, position=p,
position=p, event_or_subevent=self.subevent or e,
event_or_subevent=self.subevent or e, )
) p.send_mail(self.rule.subject, self.rule.template, email_ctx,
p.send_mail(self.rule.subject, self.rule.template, email_ctx, attach_ical=self.rule.attach_ical,
attach_ical=self.rule.attach_ical, log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent') elif not o_sent and o.email:
elif not o_sent and o.email: email_ctx = get_email_context(
email_ctx = get_email_context( event=e,
event=e, order=o,
order=o, invoice_address=ia,
invoice_address=ia, event_or_subevent=self.subevent or e,
event_or_subevent=self.subevent or e, )
) o.send_mail(self.rule.subject, self.rule.template, email_ctx,
o.send_mail(self.rule.subject, self.rule.template, email_ctx, attach_ical=self.rule.attach_ical,
attach_ical=self.rule.attach_ical, log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent') o_sent = True
o_sent = True
except SendMailException:
... # ¯\_(ツ)_/¯
self.last_successful_order_id = o.pk self.last_successful_order_id = o.pk
+37 -44
View File
@@ -41,7 +41,7 @@ from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Checkin, Event, InvoiceAddress, Order, User, CachedFile, Checkin, Event, InvoiceAddress, Order, User,
) )
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import mail
from pretix.base.services.tasks import ProfiledEventTask from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
@@ -61,7 +61,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
recipients: str, filter_checkins: bool, not_checked_in: bool, checkin_lists: list, recipients: str, filter_checkins: bool, not_checked_in: bool, checkin_lists: list,
attachments: list = None, attach_tickets: bool = False, attachments: list = None, attach_tickets: bool = False,
attach_ical: bool = False) -> None: attach_ical: bool = False) -> None:
failures = []
user = User.objects.get(pk=user) if user else None user = User.objects.get(pk=user) if user else None
subject = LazyI18nString(subject) subject = LazyI18nString(subject)
message = LazyI18nString(message) message = LazyI18nString(message)
@@ -121,70 +120,64 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
if subevents_to and p.subevent.date_from >= subevents_to: if subevents_to and p.subevent.date_from >= subevents_to:
continue continue
try:
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
mail(
p.attendee_email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o,
position=p,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_cached_files=attachments
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data={
'position': p.positionid,
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': p.attendee_email,
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': [],
'attach_cached_files': attachments_for_log,
}
)
except SendMailException:
failures.append(p.attendee_email)
if send_to_order and o.email:
try:
with language(o.locale, event.settings.region): with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia) email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
mail( mail(
o.email, p.attendee_email,
subject, subject,
message, message,
email_context, email_context,
event, event,
locale=o.locale, locale=o.locale,
order=o, order=o,
position=p,
attach_tickets=attach_tickets, attach_tickets=attach_tickets,
attach_ical=attach_ical, attach_ical=attach_ical,
attach_cached_files=attachments, attach_cached_files=attachments
) )
o.log_action( o.log_action(
'pretix.plugins.sendmail.order.email.sent', 'pretix.plugins.sendmail.order.email.sent.attendee',
user=user, user=user,
data={ data={
'position': p.positionid,
'subject': format_map(subject.localize(o.locale), email_context), 'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context), 'message': format_map(message.localize(o.locale), email_context),
'recipient': o.email, 'recipient': p.attendee_email,
'attach_tickets': attach_tickets, 'attach_tickets': attach_tickets,
'attach_ical': attach_ical, 'attach_ical': attach_ical,
'attach_other_files': [], 'attach_other_files': [],
'attach_cached_files': attachments_for_log, 'attach_cached_files': attachments_for_log,
} }
) )
except SendMailException:
failures.append(o.email) if send_to_order and o.email:
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia)
mail(
o.email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_cached_files=attachments,
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': o.email,
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': [],
'attach_cached_files': attachments_for_log,
}
)
for chunk in _chunks(objects, 1000): for chunk in _chunks(objects, 1000):
orders = Order.objects.filter(pk__in=chunk, event=event) orders = Order.objects.filter(pk__in=chunk, event=event)
-4
View File
@@ -71,7 +71,6 @@ from pretix.base.payment import (
BasePaymentProvider, PaymentException, WalletQueries, BasePaymentProvider, PaymentException, WalletQueries,
) )
from pretix.base.plugins import get_all_plugins from pretix.base.plugins import get_all_plugins
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
@@ -1000,9 +999,6 @@ class StripeMethod(BasePaymentProvider):
payment.confirm() payment.confirm()
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
raise PaymentException(str(e)) raise PaymentException(str(e))
except SendMailException:
raise PaymentException(_('There was an error sending the confirmation mail.'))
elif intent.status == 'processing': elif intent.status == 'processing':
if request: if request:
messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the '
-5
View File
@@ -1654,11 +1654,6 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
order = Order.objects.get(id=value) order = Order.objects.get(id=value)
return self.get_order_url(order) return self.get_order_url(order)
def get_error_message(self, exception):
if exception.__class__.__name__ == 'SendMailException':
return _('There was an error sending the confirmation mail. Please try again later.')
return super().get_error_message(exception)
def get_error_url(self): def get_error_url(self):
return self.get_step_url(self.request) return self.get_step_url(self.request)
@@ -372,6 +372,19 @@
</article> </article>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if c.items_missing %}
<div class="product-appendix-row">
<p class="text-muted">
{% trans "There are products selected in this add-on category that currently cannot be changed because they are not on sale:" %}
</p>
<ul class="text-muted">
{% for itemvar, count in c.items_missing.items %}
<li>{{ count }}x {{ itemvar.0 }}{% if itemvar.1 %} – {{ itemvar.1 }}{% endif %}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</fieldset> </fieldset>
{% endwith %} {% endwith %}
{% empty %} {% empty %}
@@ -493,7 +493,18 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if cart.show_rounding_info %}
<div class="text-muted">
<small>
{% icon "info-circle" %}
{% blocktrans trimmed %}
Since you entered a business address, your price was computed from the
VAT-exclusive price. Due to rounding, this caused a small change to the
total price.
{% endblocktrans %}
</small>
</div>
{% endif %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% if not cart.is_ordered %} {% if not cart.is_ordered %}
+8 -3
View File
@@ -167,7 +167,7 @@ class CartMixin:
fees = [] fees = []
if not order: if not order:
apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*lcp, *fees]) apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees])
total = sum([c.price for c in lcp]) + sum([f.value for f in fees]) total = sum([c.price for c in lcp]) + sum([f.value for f in fees])
net_total = sum(p.price - p.tax_value for p in lcp) + sum([f.net_value for f in fees]) net_total = sum(p.price - p.tax_value for p in lcp) + sum([f.net_value for f in fees])
@@ -264,6 +264,11 @@ class CartMixin:
'max_expiry_extend': max_expiry_extend, 'max_expiry_extend': max_expiry_extend,
'is_ordered': bool(order), 'is_ordered': bool(order),
'itemcount': sum(c.count for c in positions if not c.addon_to), 'itemcount': sum(c.count for c in positions if not c.addon_to),
'show_rounding_info': (
self.request.event.settings.tax_rounding == "sum_by_net_only_business" and
not self.request.event.settings.display_net_prices and
sum(c.price_includes_rounding_correction for c in positions) + sum(f.price_includes_rounding_correction for f in fees)
),
'itemvarsums': itemvarsums, 'itemvarsums': itemvarsums,
'current_selected_payments': [ 'current_selected_payments': [
p for p in self.current_selected_payments(lcp, fees, self.invoice_address) p for p in self.current_selected_payments(lcp, fees, self.invoice_address)
@@ -275,7 +280,7 @@ class CartMixin:
raw_payments = copy.deepcopy(self.cart_session.get('payments', [])) raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here
apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*positions, *fees]) apply_rounding(self.request.event.settings.tax_rounding, invoice_address, self.request.event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees])
payments = [] payments = []
@@ -329,7 +334,7 @@ class CartMixin:
fees.append(pf) fees.append(pf)
# Re-apply rounding as grand total has changed # Re-apply rounding as grand total has changed
apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*positions, *fees]) apply_rounding(self.request.event.settings.tax_rounding, invoice_address, self.request.event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees])
# Re-calculate to_pay as grand total has changed # Re-calculate to_pay as grand total has changed
+1
View File
@@ -325,6 +325,7 @@ class ResetPasswordView(FormView):
locale=customer.locale, locale=customer.locale,
customer=customer, customer=customer,
organizer=self.request.organizer, organizer=self.request.organizer,
sensitive=True,
) )
messages.success( messages.success(
self.request, self.request,
+8 -7
View File
@@ -40,7 +40,7 @@ import logging
import mimetypes import mimetypes
import os import os
import re import re
from collections import OrderedDict, defaultdict from collections import Counter, OrderedDict, defaultdict
from decimal import Decimal from decimal import Decimal
from urllib.parse import quote from urllib.parse import quote
@@ -77,7 +77,6 @@ from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task, generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
invoice_qualified, invoice_qualified,
) )
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, _try_auto_refund, cancel_order, OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
change_payment_provider, error_messages, change_payment_provider, error_messages,
@@ -656,10 +655,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
amount=Decimal('0.00'), amount=Decimal('0.00'),
fee=None fee=None
) )
try: p.confirm()
p.confirm()
except SendMailException:
pass
else: else:
p._mark_order_paid( p._mark_order_paid(
payment_refund_sum=self.order.payment_refund_sum payment_refund_sum=self.order.payment_refund_sum
@@ -1431,11 +1427,13 @@ class OrderChangeMixin:
'categories': [] 'categories': []
} }
current_addon_products = defaultdict(list) current_addon_products = defaultdict(list)
current_addon_products_missing = Counter()
for a in p.addons.all(): for a in p.addons.all():
if a.canceled: if a.canceled:
continue continue
if not a.is_bundled: if not a.is_bundled:
current_addon_products[a.item_id, a.variation_id].append(a) current_addon_products[a.item_id, a.variation_id].append(a)
current_addon_products_missing[a.item, a.variation] += 1
for iao in p.item.addons.all(): for iao in p.item.addons.all():
ckey = '{}-{}'.format(p.subevent.pk if p.subevent else 0, iao.addon_category.pk) ckey = '{}-{}'.format(p.subevent.pk if p.subevent else 0, iao.addon_category.pk)
@@ -1473,6 +1471,7 @@ class OrderChangeMixin:
if i.has_variations: if i.has_variations:
for v in i.available_variations: for v in i.available_variations:
v.initial = len(current_addon_products[i.pk, v.pk]) v.initial = len(current_addon_products[i.pk, v.pk])
current_addon_products_missing[i, v] = 0
if v.initial and i.free_price: if v.initial and i.free_price:
a = current_addon_products[i.pk, v.pk][0] a = current_addon_products[i.pk, v.pk][0]
v.initial_price = TaxedPrice( v.initial_price = TaxedPrice(
@@ -1488,6 +1487,7 @@ class OrderChangeMixin:
i.expand = any(v.initial for v in i.available_variations) i.expand = any(v.initial for v in i.available_variations)
else: else:
i.initial = len(current_addon_products[i.pk, None]) i.initial = len(current_addon_products[i.pk, None])
current_addon_products_missing[i, None] = 0
if i.initial and i.free_price: if i.initial and i.free_price:
a = current_addon_products[i.pk, None][0] a = current_addon_products[i.pk, None][0]
i.initial_price = TaxedPrice( i.initial_price = TaxedPrice(
@@ -1509,7 +1509,8 @@ class OrderChangeMixin:
'min_count': iao.min_count, 'min_count': iao.min_count,
'max_count': iao.max_count, 'max_count': iao.max_count,
'iao': iao, 'iao': iao,
'items': [i for i in items if not i.require_voucher] 'items': [i for i in items if not i.require_voucher],
'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
}) })
return positions return positions
+7 -2
View File
@@ -1300,6 +1300,8 @@ class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
class OrganizerIcalDownload(OrganizerViewMixin, View): class OrganizerIcalDownload(OrganizerViewMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cutoff = now() - timedelta(days=31) cutoff = now() - timedelta(days=31)
# generally limit to 1000 entries as this seems to be a limitation on ics-files for some calendar software
limit = 1000
events = list( events = list(
filter_qs_by_attr( filter_qs_by_attr(
self.request.organizer.events.filter( self.request.organizer.events.filter(
@@ -1318,7 +1320,7 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
'organizer', 'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects') queryset=Organizer.objects.prefetch_related('_settings_objects')
) )
) )[:limit]
) )
events += list( events += list(
filter_qs_by_attr( filter_qs_by_attr(
@@ -1346,8 +1348,11 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
) )
).order_by( ).order_by(
'date_from' 'date_from'
) )[:limit]
) )
if len(events) > limit:
events.sort(key=lambda e: e.date_from)
events = events[:limit]
if 'locale' in request.GET and request.GET.get('locale') in dict(settings.LANGUAGES): if 'locale' in request.GET and request.GET.get('locale') in dict(settings.LANGUAGES):
with language(request.GET.get('locale'), self.request.organizer.settings.region): with language(request.GET.get('locale'), self.request.organizer.settings.region):
+4 -11
View File
@@ -32,8 +32,6 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import logging
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -42,7 +40,7 @@ from django.views import View
from django.views.generic import TemplateView from django.views.generic import TemplateView
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.services.mail import INVALID_ADDRESS, SendMailException, mail from pretix.base.services.mail import INVALID_ADDRESS, mail
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.user import ResendLinkForm from pretix.presale.forms.user import ResendLinkForm
@@ -83,13 +81,7 @@ class ResendLinkView(EventViewMixin, TemplateView):
subject = self.request.event.settings.mail_subject_resend_all_links subject = self.request.event.settings.mail_subject_resend_all_links
template = self.request.event.settings.mail_text_resend_all_links template = self.request.event.settings.mail_text_resend_all_links
context = get_email_context(event=self.request.event, orders=orders) context = get_email_context(event=self.request.event, orders=orders)
try: mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE)
mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE)
except SendMailException:
logger = logging.getLogger('pretix.presale.user')
logger.exception('A mail resending order links to {} could not be sent.'.format(user))
messages.error(self.request, _('We have trouble sending emails right now, please check back later.'))
return self.get(request, *args, **kwargs)
messages.success(self.request, _('If there were any orders by this user, they will receive an email with their order codes.')) messages.success(self.request, _('If there were any orders by this user, they will receive an email with their order codes.'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))
@@ -105,7 +97,8 @@ class UnlockHashView(EventViewMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
hashes = request.session.get('pretix_unlock_hashes', []) hashes = request.session.get('pretix_unlock_hashes', [])
hashes.append(kwargs.get('hash')) if kwargs.get('hash') not in hashes:
hashes.append(kwargs.get('hash'))
request.session['pretix_unlock_hashes'] = hashes request.session['pretix_unlock_hashes'] = hashes
if 'voucher' in request.GET: if 'voucher' in request.GET:
+2
View File
@@ -855,6 +855,8 @@ COUNTRIES_OVERRIDE = {
DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000 DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB
OUTGOING_MAIL_RETENTION = 14 * 24 * 3600 # 14 days in seonds
# File sizes are in MiB # File sizes are in MiB
FILE_UPLOAD_MAX_SIZE_IMAGE = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_image", fallback=10) FILE_UPLOAD_MAX_SIZE_IMAGE = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_image", fallback=10)
FILE_UPLOAD_MAX_SIZE_FAVICON = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_favicon", fallback=1) FILE_UPLOAD_MAX_SIZE_FAVICON = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_favicon", fallback=1)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,7 +5,7 @@
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"@babel/core": "^7.28.5", "@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5", "@babel/preset-env": "^7.29.0",
"@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-node-resolve": "^16.0.3",
"vue": "^2.7.16", "vue": "^2.7.16",
@@ -674,10 +674,11 @@ var editor = {
$("#toolbox").find("button[data-action=middle]").toggleClass('active', o.verticalAlign === 'middle'); $("#toolbox").find("button[data-action=middle]").toggleClass('active', o.verticalAlign === 'middle');
$("#toolbox").find("button[data-action=bottom]").toggleClass('active', o.verticalAlign === 'bottom'); $("#toolbox").find("button[data-action=bottom]").toggleClass('active', o.verticalAlign === 'bottom');
console.log("_update_toolbox_values", o.scaleY, o.scaleX)
if (o.scaleY !== 1 || o.scaleX !== 1) { if (o.scaleY !== 1 || o.scaleX !== 1) {
o.set({ o.set({
height: o.height * o.scaleY, height: Math.max(o.height * o.scaleY, editor._mm2px(10.01)),
width: o.width * o.scaleX, width: Math.max(o.width * o.scaleX, editor._mm2px(10.01)),
scaleX: 1, scaleX: 1,
scaleY: 1 scaleY: 1
}); });
@@ -0,0 +1,43 @@
function is_sandbox_supported() {
const iframe = document.createElement('iframe');
return 'sandbox' in iframe;
}
function safe_render(url, parent) {
// Estimate the height that prevents the user from having to scroll on two levels to see the full email
const height = (
Math.max(400, window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector("footer").getBoundingClientRect().height - 20)
) + "px";
const iframe = (
// Per the HTML spec, a data: URL in an iframe is treated as its own origin:
// https://github.com/whatwg/html/pull/1756
// It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild:
// https://github.com/whatwg/html/issues/12091
// Together with the sandbox attribute disallowing all JavaScript, and the fact
// that we sanitize the HTML before we even save it to the database, this should
// still be the safest way to render HTML in the context of our backend.
$("<iframe>")
.height(height)
.attr("class", "html-email")
.attr("src", url)
.attr("sandbox", "allow-popups allow-popups-to-escape-sandbox")
.attr("csp", "script-src 'none'; font-src 'none'; connect-src 'none'; form-action 'none'; style-src 'unsafe-inline'") // respected only by chrome
.prop("credentialless", true) // respected only by chrome
);
console.log(parent, iframe);
parent.append(iframe);
}
$(function () {
const script_element = $("#mail_body_html");
if (!script_element.length) return;
if (!is_sandbox_supported()) {
// Browser is too old for <iframe sandbox>
$(script_element.parent()).text("Please switch to a modern browser to view HTML content safely.");
return;
}
safe_render(JSON.parse(script_element.html()), script_element.parent());
});
@@ -91,3 +91,8 @@ div.mail-preview {
text-align: right; text-align: right;
} }
} }
iframe.html-email {
border: 0;
width: 100%;
}
@@ -716,7 +716,7 @@ $(function () {
// free-range price input auto-check checkbox/set count-input to 1 if 0 // free-range price input auto-check checkbox/set count-input to 1 if 0
$("[data-checked-onchange]").each(function() { $("[data-checked-onchange]").each(function() {
var countInput = this; var countInput = this;
$("#" + this.getAttribute("data-checked-onchange")).on("change", function() { $("#" + this.getAttribute("data-checked-onchange")).on("input", function() {
if (countInput.type === "checkbox") { if (countInput.type === "checkbox") {
if (countInput.checked) return; if (countInput.checked) return;
countInput.checked = true; countInput.checked = true;
@@ -301,15 +301,14 @@ Vue.component('availbox', {
return this.avail[0] < 100 && this.$root.waiting_list_enabled && this.item.allow_waitinglist; return this.avail[0] < 100 && this.$root.waiting_list_enabled && this.item.allow_waitinglist;
}, },
waiting_list_url: function () { waiting_list_url: function () {
var u var u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?locale=' + lang + '&item=' + this.item.id
if (this.item.has_variations) { if (this.item.has_variations) {
u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter; u += '&var=' + this.variation.id
} else {
u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter;
} }
if (this.$root.subevent) { if (this.$root.subevent) {
u += '&subevent=' + this.$root.subevent u += '&subevent=' + this.$root.subevent
} }
u += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter
return u return u
} }
}, },
@@ -99,6 +99,15 @@
} }
} }
.product-appendix-row {
border-top: 1px solid $table-border-color;
border-bottom: 1px solid $table-border-color;
padding: 1.25*$line-height-computed 0;
& > :last-child {
margin-bottom: 0;
}
}
article.item-with-variations { article.item-with-variations {
margin: 0 -15px; margin: 0 -15px;
padding: 0 15px; padding: 0 15px;
+5
View File
@@ -29,3 +29,8 @@ class FailingEmailBackend(EmailBackend):
raise smtplib.SMTPRecipientsRefused({ raise smtplib.SMTPRecipientsRefused({
'recipient@example.org': (450, b'Recipient unknown') 'recipient@example.org': (450, b'Recipient unknown')
}) })
class PermanentlyFailingEmailBackend(EmailBackend):
def send_messages(self, email_messages):
raise smtplib.SMTPNotSupportedError()
+119 -206
View File
@@ -32,24 +32,22 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import datetime
import os import os
import re import re
from decimal import Decimal
from email.mime.text import MIMEText from email.mime.text import MIMEText
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.core import mail as djmail from django.core import mail as djmail
from django.utils.html import escape from django.test import override_settings
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled from django_scopes import scope
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.models import Event, InvoiceAddress, Order, Organizer, User from pretix.base.models import Event, Organizer, OutgoingMail, User
from pretix.base.services.mail import mail from pretix.base.services.mail import mail, mail_send_task
@pytest.fixture @pytest.fixture
@@ -70,45 +68,6 @@ def env():
yield event, user, o yield event, user, o
@pytest.fixture
@scopes_disabled()
def item(env):
return env[0].items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
@scopes_disabled()
def order(env, item):
event, _, _ = env
o = Order.objects.create(
code="FOO",
event=event,
email="dummy@dummy.test",
status=Order.STATUS_PENDING,
secret="k24fiuwvu8kxz3y1",
sales_channel=event.organizer.sales_channels.get(identifier="web"),
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.UTC),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.UTC),
total=23,
locale="en",
)
o.positions.create(
order=o,
item=item,
variation=None,
price=Decimal("23"),
attendee_email="peter@example.org",
attendee_name_parts={"given_name": "Peter", "family_name": "Miller"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL",
)
InvoiceAddress.objects.create(
order=o,
name_parts={"given_name": "Peter", "family_name": "Miller"},
)
return o
@pytest.mark.django_db @pytest.mark.django_db
def test_send_mail_with_prefix(env): def test_send_mail_with_prefix(env):
djmail.outbox = [] djmail.outbox = []
@@ -204,6 +163,101 @@ def test_send_mail_with_user_locale(env):
assert 'The language code used for rendering this email is de.' in djmail.outbox[0].body assert 'The language code used for rendering this email is de.' in djmail.outbox[0].body
@pytest.mark.django_db
def test_queue_state_sent(env):
m = OutgoingMail.objects.create(
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
assert m.status == OutgoingMail.STATUS_QUEUED
mail_send_task.apply(kwargs={
'outgoing_mail': m.pk,
}, max_retries=0)
m.refresh_from_db()
assert m.status == OutgoingMail.STATUS_SENT
@pytest.mark.django_db
@override_settings(EMAIL_BACKEND='pretix.testutils.mail.PermanentlyFailingEmailBackend')
def test_queue_state_permanent_failure(env):
m = OutgoingMail.objects.create(
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
assert m.status == OutgoingMail.STATUS_QUEUED
mail_send_task.apply(kwargs={
'outgoing_mail': m.pk,
}, max_retries=0)
m.refresh_from_db()
assert m.status == OutgoingMail.STATUS_FAILED
@pytest.mark.django_db
@override_settings(EMAIL_BACKEND='pretix.testutils.mail.FailingEmailBackend')
def test_queue_state_retry_failure(env, monkeypatch):
def retry(*args, **kwargs):
raise Exception()
monkeypatch.setattr('celery.app.task.Task.retry', retry, raising=True)
m = OutgoingMail.objects.create(
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
assert m.status == OutgoingMail.STATUS_QUEUED
mail_send_task.apply(kwargs={
'outgoing_mail': m.pk,
}, max_retries=0)
m.refresh_from_db()
assert m.status == OutgoingMail.STATUS_AWAITING_RETRY
assert m.retry_after > now()
@pytest.mark.django_db
def test_queue_state_foreign_key_handling():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now()
)
mail_queued = OutgoingMail.objects.create(
organizer=o,
event=event,
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
mail_sent = OutgoingMail.objects.create(
organizer=o,
event=event,
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
status=OutgoingMail.STATUS_SENT,
)
event.delete()
assert not OutgoingMail.objects.filter(pk=mail_queued.pk).exists()
assert OutgoingMail.objects.get(pk=mail_sent.pk).event is None
o.delete()
assert not OutgoingMail.objects.filter(pk=mail_sent.pk).exists()
@pytest.mark.django_db @pytest.mark.django_db
def test_sendmail_placeholder(env): def test_sendmail_placeholder(env):
djmail.outbox = [] djmail.outbox = []
@@ -230,7 +284,7 @@ def _extract_html(mail):
def test_placeholder_html_rendering_from_template(env): def test_placeholder_html_rendering_from_template(env):
djmail.outbox = [] djmail.outbox = []
event, user, organizer = env event, user, organizer = env
event.name = "<strong>event & co. kg</strong> {currency}" event.name = "<strong>event & co. kg</strong>"
event.save() event.save()
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context( mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
event=event, event=event,
@@ -239,26 +293,25 @@ def test_placeholder_html_rendering_from_template(env):
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email] assert djmail.outbox[0].to == [user.email]
# Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body assert '**Meta**: *Beep*' in djmail.outbox[0].body
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
# todo: assert '&lt;' not in djmail.outbox[0].body assert '&lt;' not in djmail.outbox[0].body
# todo: assert '&amp;' not in djmail.outbox[0].body assert '&amp;' not in djmail.outbox[0].body
assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
assert 'EUR' not in djmail.outbox[0].body
html = _extract_html(djmail.outbox[0]) html = _extract_html(djmail.outbox[0])
assert '<strong>event' not in html assert '<strong>event' not in html
assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}' in html assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt;' in html
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
assert '<strong>Meta</strong>: <em>Beep</em>' in html assert '<strong>Meta</strong>: <em>Beep</em>' in html
assert 'Unevaluated placeholder: {currency}' in html
assert 'EUR' not in html
assert re.search( assert re.search(
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">' r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>', html
)
assert re.search(
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
html html
) )
@@ -276,7 +329,7 @@ def test_placeholder_html_rendering_from_string(env):
}) })
djmail.outbox = [] djmail.outbox = []
event, user, organizer = env event, user, organizer = env
event.name = "<strong>event & co. kg</strong> {currency}" event.name = "<strong>event & co. kg</strong>"
event.save() event.save()
ctx = get_email_context( ctx = get_email_context(
event=event, event=event,
@@ -287,9 +340,9 @@ def test_placeholder_html_rendering_from_string(env):
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email] assert djmail.outbox[0].to == [user.email]
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong> {currency}](https://example.com)' in djmail.outbox[0].body assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
assert '**Meta**: *Beep*' in djmail.outbox[0].body assert '**Meta**: *Beep*' in djmail.outbox[0].body
assert 'URL: https://google.com' in djmail.outbox[0].body assert 'URL: https://google.com' in djmail.outbox[0].body
@@ -304,13 +357,11 @@ def test_placeholder_html_rendering_from_string(env):
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
assert '<strong>Meta</strong>: <em>Beep</em>' in html assert '<strong>Meta</strong>: <em>Beep</em>' in html
assert re.search( assert re.search(
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">' r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html html
) )
assert re.search( assert re.search(
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">' r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html html
) )
assert re.search( assert re.search(
@@ -331,141 +382,3 @@ def test_placeholder_html_rendering_from_string(env):
r'style="[^"]+" target="_blank">Link &amp; Text</a>', r'style="[^"]+" target="_blank">Link &amp; Text</a>',
html html
) )
@pytest.mark.django_db
def test_nested_placeholder_inclusion_full_process(env, order):
# Test that it is not possible to sneak in a placeholder like {url_cancel} inside a user-controlled
# placeholder value like {invoice_company}
event, user, organizer = env
position = order.positions.get()
order.invoice_address.company = "{url_cancel} Corp"
order.invoice_address.save()
event.settings.mail_text_resend_link = LazyI18nString({"en": "Ticket for {invoice_company}"})
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": "Ticket for {invoice_company}"})
djmail.outbox = []
position.resend_link()
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [position.attendee_email]
assert "Ticket for {url_cancel} Corp" == djmail.outbox[0].subject
assert "/cancel" not in djmail.outbox[0].body
assert "/order" not in djmail.outbox[0].body
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain):
assert "Ticket for {url_cancel} Corp" in part
assert "/order/" not in part
assert "/cancel" not in part
@pytest.mark.django_db
def test_nested_placeholder_inclusion_mail_service(env):
# test that it is not possible to have placeholders within the values of placeholders when
# the mail() function is called directly
template = LazyI18nString("Event name: {event}")
djmail.outbox = []
event, user, organizer = env
event.name = "event & {currency} co. kg"
event.slug = "event-co-ag-slug"
event.save()
mail(
"dummy@dummy.dummy",
"{event} Test subject",
template,
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
),
event,
)
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "event & {currency} co. kg" in part or "event &amp; {currency} co. kg" in part
assert "EUR" not in part
@pytest.mark.django_db
@pytest.mark.parametrize("tpl", [
"Event: {event.__class__}",
"Event: {{event.__class__}}",
"Event: {{{event.__class__}}}",
])
def test_variable_inclusion_from_string_full_process(env, tpl, order):
# Test that it is not possible to use placeholders that leak system information in templates
# when run through system processes
event, user, organizer = env
event.name = "event & co. kg"
event.save()
position = order.positions.get()
event.settings.mail_text_resend_link = LazyI18nString({"en": tpl})
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": tpl})
position.resend_link()
assert len(djmail.outbox) == 1
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "{event.__class__}" in part
assert "LazyI18nString" not in part
@pytest.mark.django_db
@pytest.mark.parametrize("tpl", [
"Event: {event.__class__}",
"Event: {{event.__class__}}",
"Event: {{{event.__class__}}}",
])
def test_variable_inclusion_from_string_mail_service(env, tpl):
# Test that it is not possible to use placeholders that leak system information in templates
# when run through mail() directly
event, user, organizer = env
event.name = "event & co. kg"
event.save()
djmail.outbox = []
mail(
"dummy@dummy.dummy",
tpl,
LazyI18nString(tpl),
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456\n" + tpl,
),
event,
)
assert len(djmail.outbox) == 1
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "{event.__class__}" in part
assert "LazyI18nString" not in part
@pytest.mark.django_db
def test_escaped_braces_mail_services(env):
# Test that braces can be escaped by doubling
template = LazyI18nString("Event name: -{{currency}}-")
djmail.outbox = []
event, user, organizer = env
event.name = "event & co. kg"
event.save()
mail(
"dummy@dummy.dummy",
"-{{currency}}- Test subject",
template,
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
),
event,
)
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "EUR" not in part
assert "-{currency}-" in part
+28 -11
View File
@@ -43,7 +43,7 @@ def _validate_sample_lines(sample_lines, rounding_mode):
(line.tax_value_includes_rounding_correction, line.price_includes_rounding_correction) (line.tax_value_includes_rounding_correction, line.price_includes_rounding_correction)
for line in sample_lines for line in sample_lines
] ]
changed = apply_rounding(rounding_mode, "EUR", sample_lines) changed = apply_rounding(rounding_mode, None, "EUR", sample_lines)
for line, original in zip(sample_lines, corrections): for line, original in zip(sample_lines, corrections):
if (line.tax_value_includes_rounding_correction, line.price_includes_rounding_correction) != original: if (line.tax_value_includes_rounding_correction, line.price_includes_rounding_correction) != original:
assert line in changed assert line in changed
@@ -108,7 +108,7 @@ def test_revert_net_rounding_to_single_line(sample_lines):
tax_rate=Decimal("19.00"), tax_rate=Decimal("19.00"),
tax_code="S", tax_code="S",
) )
apply_rounding("sum_by_net", "EUR", [l]) apply_rounding("sum_by_net", None, "EUR", [l])
assert l.price == Decimal("100.00") assert l.price == Decimal("100.00")
assert l.price_includes_rounding_correction == Decimal("0.00") assert l.price_includes_rounding_correction == Decimal("0.00")
assert l.tax_value == Decimal("15.97") assert l.tax_value == Decimal("15.97")
@@ -126,7 +126,7 @@ def test_revert_net_keep_gross_rounding_to_single_line(sample_lines):
tax_rate=Decimal("19.00"), tax_rate=Decimal("19.00"),
tax_code="S", tax_code="S",
) )
apply_rounding("sum_by_net_keep_gross", "EUR", [l]) apply_rounding("sum_by_net_keep_gross", None, "EUR", [l])
assert l.price == Decimal("100.00") assert l.price == Decimal("100.00")
assert l.price_includes_rounding_correction == Decimal("0.00") assert l.price_includes_rounding_correction == Decimal("0.00")
assert l.tax_value == Decimal("15.97") assert l.tax_value == Decimal("15.97")
@@ -144,7 +144,7 @@ def test_rounding_of_impossible_gross_price(rounding_mode):
price=Decimal("23.00"), price=Decimal("23.00"),
) )
l._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress()) l._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress())
apply_rounding(rounding_mode, "EUR", [l]) apply_rounding(rounding_mode, None, "EUR", [l])
assert l.price == Decimal("23.01") assert l.price == Decimal("23.01")
assert l.price_includes_rounding_correction == Decimal("0.01") assert l.price_includes_rounding_correction == Decimal("0.01")
assert l.tax_value == Decimal("1.51") assert l.tax_value == Decimal("1.51")
@@ -164,12 +164,12 @@ def test_round_down():
assert sum(l.tax_value for l in lines) == Decimal("79.85") assert sum(l.tax_value for l in lines) == Decimal("79.85")
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.15") assert sum(l.price - l.tax_value for l in lines) == Decimal("420.15")
apply_rounding("sum_by_net", "EUR", lines) apply_rounding("sum_by_net", None, "EUR", lines)
assert sum(l.price for l in lines) == Decimal("499.98") assert sum(l.price for l in lines) == Decimal("499.98")
assert sum(l.tax_value for l in lines) == Decimal("79.83") assert sum(l.tax_value for l in lines) == Decimal("79.83")
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.15") assert sum(l.price - l.tax_value for l in lines) == Decimal("420.15")
apply_rounding("sum_by_net_keep_gross", "EUR", lines) apply_rounding("sum_by_net_keep_gross", None, "EUR", lines)
assert sum(l.price for l in lines) == Decimal("500.00") assert sum(l.price for l in lines) == Decimal("500.00")
assert sum(l.tax_value for l in lines) == Decimal("79.83") assert sum(l.tax_value for l in lines) == Decimal("79.83")
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.17") assert sum(l.price - l.tax_value for l in lines) == Decimal("420.17")
@@ -187,12 +187,12 @@ def test_round_up():
assert sum(l.tax_value for l in lines) == Decimal("79.80") assert sum(l.tax_value for l in lines) == Decimal("79.80")
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.10") assert sum(l.price - l.tax_value for l in lines) == Decimal("420.10")
apply_rounding("sum_by_net", "EUR", lines) apply_rounding("sum_by_net", None, "EUR", lines)
assert sum(l.price for l in lines) == Decimal("499.92") assert sum(l.price for l in lines) == Decimal("499.92")
assert sum(l.tax_value for l in lines) == Decimal("79.82") assert sum(l.tax_value for l in lines) == Decimal("79.82")
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.10") assert sum(l.price - l.tax_value for l in lines) == Decimal("420.10")
apply_rounding("sum_by_net_keep_gross", "EUR", lines) apply_rounding("sum_by_net_keep_gross", None, "EUR", lines)
assert sum(l.price for l in lines) == Decimal("499.90") assert sum(l.price for l in lines) == Decimal("499.90")
assert sum(l.tax_value for l in lines) == Decimal("79.82") assert sum(l.tax_value for l in lines) == Decimal("79.82")
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.08") assert sum(l.price - l.tax_value for l in lines) == Decimal("420.08")
@@ -210,12 +210,12 @@ def test_round_currency_without_decimals():
assert sum(l.tax_value for l in lines) == Decimal("7980.00") assert sum(l.tax_value for l in lines) == Decimal("7980.00")
assert sum(l.price - l.tax_value for l in lines) == Decimal("42010.00") assert sum(l.price - l.tax_value for l in lines) == Decimal("42010.00")
apply_rounding("sum_by_net", "JPY", lines) apply_rounding("sum_by_net", None, "JPY", lines)
assert sum(l.price for l in lines) == Decimal("49992.00") assert sum(l.price for l in lines) == Decimal("49992.00")
assert sum(l.tax_value for l in lines) == Decimal("7982.00") assert sum(l.tax_value for l in lines) == Decimal("7982.00")
assert sum(l.price - l.tax_value for l in lines) == Decimal("42010.00") assert sum(l.price - l.tax_value for l in lines) == Decimal("42010.00")
apply_rounding("sum_by_net_keep_gross", "JPY", lines) apply_rounding("sum_by_net_keep_gross", None, "JPY", lines)
assert sum(l.price for l in lines) == Decimal("49990.00") assert sum(l.price for l in lines) == Decimal("49990.00")
assert sum(l.tax_value for l in lines) == Decimal("7982.00") assert sum(l.tax_value for l in lines) == Decimal("7982.00")
assert sum(l.price - l.tax_value for l in lines) == Decimal("42008.00") assert sum(l.price - l.tax_value for l in lines) == Decimal("42008.00")
@@ -235,7 +235,7 @@ def test_do_not_touch_free(rounding_mode):
price=Decimal("23.00"), price=Decimal("23.00"),
) )
l2._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress()) l2._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress())
apply_rounding(rounding_mode, "EUR", [l1, l2]) apply_rounding(rounding_mode, None, "EUR", [l1, l2])
assert l2.price == Decimal("23.01") assert l2.price == Decimal("23.01")
assert l2.price_includes_rounding_correction == Decimal("0.01") assert l2.price_includes_rounding_correction == Decimal("0.01")
assert l2.tax_value == Decimal("1.51") assert l2.tax_value == Decimal("1.51")
@@ -245,3 +245,20 @@ def test_do_not_touch_free(rounding_mode):
assert l1.price_includes_rounding_correction == Decimal("0.00") assert l1.price_includes_rounding_correction == Decimal("0.00")
assert l1.tax_value == Decimal("0.00") assert l1.tax_value == Decimal("0.00")
assert l1.tax_value_includes_rounding_correction == Decimal("0.00") assert l1.tax_value_includes_rounding_correction == Decimal("0.00")
@pytest.mark.django_db
def test_only_business():
lines = [OrderPosition(
price=Decimal("100.00"),
tax_value=Decimal("15.97"),
tax_rate=Decimal("19.00"),
tax_code="S",
) for _ in range(5)]
assert sum(l.price for l in lines) == Decimal("500.00")
apply_rounding("sum_by_net_only_business", None, "EUR", lines)
assert sum(l.price for l in lines) == Decimal("500.00")
apply_rounding("sum_by_net_only_business", InvoiceAddress(is_business=True), "EUR", lines)
assert sum(l.price for l in lines) == Decimal("499.98")
+42
View File
@@ -19,7 +19,11 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import html
import urllib.parse
import pytest import pytest
from django.core import signing
from pretix.base.templatetags.rich_text import ( from pretix.base.templatetags.rich_text import (
ALLOWED_ATTRIBUTES, ALLOWED_TAGS, markdown_compile_email, rich_text, ALLOWED_ATTRIBUTES, ALLOWED_TAGS, markdown_compile_email, rich_text,
@@ -43,6 +47,10 @@ from pretix.base.templatetags.rich_text import (
"[Foo](/foo)", "[Foo](/foo)",
'<a href="http://example.com/foo" rel="noopener" target="_blank">Foo</a>', '<a href="http://example.com/foo" rel="noopener" target="_blank">Foo</a>',
), ),
(
"[Foo](/foo?bar&baz)",
'<a href="http://example.com/foo?bar&amp;baz" rel="noopener" target="_blank">Foo</a>',
),
("mail@example.org", '<a href="mailto:mail@example.org">mail@example.org</a>'), ("mail@example.org", '<a href="mailto:mail@example.org">mail@example.org</a>'),
# Test truelink_callback # Test truelink_callback
( (
@@ -111,6 +119,40 @@ def test_linkify_abs(link):
assert markdown_compile_email(input) == f"<p>{output}</p>" assert markdown_compile_email(input) == f"<p>{output}</p>"
signer = signing.Signer(salt='safe-redirect')
@pytest.mark.parametrize(
"url,result",
[
('http://example.com/foo', '<a href="/redirect/?url={}" rel="noopener" target="_blank">{}</a>'),
('http://example.com/foo?bar&baz', '<a href="/redirect/?url={}" rel="noopener" target="_blank">{}</a>'),
('http://example.com/foo?bar&baz>', '<a href="/redirect/?url={}" rel="noopener" target="_blank">{}</a>'),
(
'http://example.com/foo?bar&baz">',
'<a href="/redirect/?url={}" rel="noopener" target="_blank">{}</a>"&gt;'.format(
urllib.parse.quote(signer.sign('http://example.com/foo?bar&baz')),
html.escape('http://example.com/foo?bar&baz'),
)
),
(
'http://example.com/foo?bar&baz\\">',
'<a href="/redirect/?url={}" rel="noopener" target="_blank">{}</a>\\"&gt;'.format(
urllib.parse.quote(signer.sign('http://example.com/foo?bar&baz')),
html.escape('http://example.com/foo?bar&baz'),
)
),
],
)
def test_linkify_safelinks(url, result):
output = result.format(
urllib.parse.quote(signer.sign(url)),
html.escape(url),
)
assert rich_text_snippet(url, safelinks=True) == output
assert rich_text(url, safelinks=True) == f"<p>{output}</p>"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"content,result", "content,result",
[ [
+11 -1
View File
@@ -31,7 +31,7 @@ from django_scopes import scope
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order, CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
OrderPayment, OrderPosition, Organizer, QuestionAnswer, OrderPayment, OrderPosition, Organizer, OutgoingMail, QuestionAnswer,
) )
from pretix.base.services.invoices import generate_invoice, invoice_pdf_task from pretix.base.services.invoices import generate_invoice, invoice_pdf_task
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
@@ -111,6 +111,15 @@ def test_email_shredder(event, order):
'new_email': 'foo@bar.com', 'new_email': 'foo@bar.com',
} }
) )
m = OutgoingMail.objects.create(
event=event,
order=order,
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
s = EmailAddressShredder(event) s = EmailAddressShredder(event)
f = list(s.generate_files()) f = list(s.generate_files())
@@ -129,6 +138,7 @@ def test_email_shredder(event, order):
assert 'Foo' not in l1.data assert 'Foo' not in l1.data
l2.refresh_from_db() l2.refresh_from_db()
assert '@' not in l2.data assert '@' not in l2.data
assert not OutgoingMail.objects.filter(pk=m.pk).exists()
@pytest.mark.django_db @pytest.mark.django_db
+110 -1
View File
@@ -24,12 +24,13 @@ from smtplib import SMTPResponseException
import pytest import pytest
import responses import responses
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.test.utils import override_settings from django.test.utils import override_settings
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from tests.base import SoupTest, extract_form_fields from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, Organizer, Team, User from pretix.base.models import Event, Organizer, OutgoingMail, Team, User
@pytest.fixture @pytest.fixture
@@ -453,3 +454,111 @@ class OrganizerTest(SoupTest):
self.event1.refresh_from_db() self.event1.refresh_from_db()
assert 'pretix.plugins.banktransfer' not in self.event1.get_plugins() assert 'pretix.plugins.banktransfer' not in self.event1.get_plugins()
assert 'pretix.plugins.stripe' in self.event1.get_plugins() assert 'pretix.plugins.stripe' in self.event1.get_plugins()
def test_outgoing_mails_list_and_detail(self):
m1 = OutgoingMail.objects.create(
organizer=self.orga1,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
m2 = OutgoingMail.objects.create(
organizer=self.orga2,
to=['wrongrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
resp = self.client.get('/control/organizer/%s/outgoingmails' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
assert b"wrongrecipient@example.com" not in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmails?status=queued' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmails?status=sent' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" not in resp.content
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
resp = self.client.get('/control/organizer/%s/outgoingmails?query=RIGHTrecipient@example.com' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmails?query=wrongrecipient@example.com' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" not in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmail/%d/' % (self.orga1.slug, m1.pk))
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmail/%d/' % (self.orga1.slug, m2.pk))
assert resp.status_code == 404
def test_outgoing_mails_retry(self):
m1 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_SENT,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
m2 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_FAILED,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
resp = self.client.post(
'/control/organizer/%s/outgoingmail/bulk_action' % self.orga1.slug,
data={
"action": "retry",
"outgoingmail": [m1.pk, m2.pk]
}
)
assert resp.status_code == 302
m1.refresh_from_db()
m2.refresh_from_db()
assert m1.status == OutgoingMail.STATUS_SENT
assert m2.status in (OutgoingMail.STATUS_SENT, OutgoingMail.STATUS_QUEUED)
def test_outgoing_mails_abort(self):
m1 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_SENT,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
m2 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_QUEUED,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
resp = self.client.post(
'/control/organizer/%s/outgoingmail/bulk_action' % self.orga1.slug,
data={
"action": "abort",
"__ALL": "on",
}
)
assert resp.status_code == 302
m1.refresh_from_db()
m2.refresh_from_db()
assert m1.status == OutgoingMail.STATUS_SENT
assert m2.status == OutgoingMail.STATUS_ABORTED
+6
View File
@@ -192,6 +192,9 @@ organizer_urls = [
'organizer/abc/team/1/edit', 'organizer/abc/team/1/edit',
'organizer/abc/team/1/delete', 'organizer/abc/team/1/delete',
'organizer/abc/team/add', 'organizer/abc/team/add',
'organizer/abc/outgoingmails',
'organizer/abc/outgoingmail/bulk_action',
'organizer/abc/outgoingmail/1/',
'organizer/abc/devices', 'organizer/abc/devices',
'organizer/abc/device/add', 'organizer/abc/device/add',
'organizer/abc/device/bulk_edit', 'organizer/abc/device/bulk_edit',
@@ -528,6 +531,9 @@ organizer_permission_urls = [
("can_change_organizer_settings", "organizer/dummy/settings/plugins/pretix.plugins.sendmail/events", 200), ("can_change_organizer_settings", "organizer/dummy/settings/plugins/pretix.plugins.sendmail/events", 200),
("can_change_organizer_settings", "organizer/dummy/settings/email", 200), ("can_change_organizer_settings", "organizer/dummy/settings/email", 200),
("can_change_organizer_settings", "organizer/dummy/settings/email/setup", 200), ("can_change_organizer_settings", "organizer/dummy/settings/email/setup", 200),
("can_change_organizer_settings", "organizer/dummy/outgoingmails", 200),
("can_change_organizer_settings", "organizer/dummy/outgoingmail/1/", 404),
("can_change_organizer_settings", "organizer/dummy/outgoingmail/bulk_action", 405),
("can_change_organizer_settings", "organizer/dummy/devices", 200), ("can_change_organizer_settings", "organizer/dummy/devices", 200),
("can_change_organizer_settings", "organizer/dummy/devices/select2", 200), ("can_change_organizer_settings", "organizer/dummy/devices/select2", 200),
("can_change_organizer_settings", "organizer/dummy/device/add", 200), ("can_change_organizer_settings", "organizer/dummy/device/add", 200),
-2
View File
@@ -29,8 +29,6 @@ def test_format_map():
assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}" assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}"
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}" assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3" assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {bar!r}", {"bar": '3'}) == "Foo 3"
assert format_map("Foo {bar!a}", {"bar": '3'}) == "Foo 3"
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3" assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"
+35 -1
View File
@@ -1015,7 +1015,8 @@ class OrderChangeAddonsTest(BaseOrdersTest):
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret) '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
) )
assert response.status_code == 200 assert response.status_code == 200
assert 'Workshop 1' not in response.content.decode() assert '<li>1x Workshop 1</li>' in response.content.decode()
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
response = self.client.post( response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
@@ -1035,6 +1036,39 @@ class OrderChangeAddonsTest(BaseOrdersTest):
with scopes_disabled(): with scopes_disabled():
assert self.ticket_pos.addons.count() == 2 assert self.ticket_pos.addons.count() == 2
def test_do_not_overbook_unavailable_on_adding(self):
self.iao.max_count = 1
self.iao.save()
self.workshop1.available_until = now() - datetime.timedelta(days=1)
self.workshop1.save()
with scopes_disabled():
OrderPosition.objects.create(
order=self.order,
item=self.workshop1,
variation=None,
price=Decimal("12"),
addon_to=self.ticket_pos,
attendee_name_parts={'full_name': "Peter"}
)
self.order.total += Decimal("12")
self.order.save()
response = self.client.get(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
assert '<li>1x Workshop 1</li>' in response.content.decode()
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
{
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
},
follow=True
)
assert 'alert-danger' in response.content.decode()
def test_remove_addon_checked_in(self): def test_remove_addon_checked_in(self):
with scopes_disabled(): with scopes_disabled():
self.event.settings.change_allow_user_if_checked_in = True self.event.settings.change_allow_user_if_checked_in = True
+5 -5
View File
@@ -1,13 +1,13 @@
{% load i18n %} {% load i18n %}
This is a test file for sending mails. This is a test file for sending mails.
Event name: {{ event }} Event name: {event}
Unevaluated placeholder: {currency}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
The language code used for rendering this email is {{ LANGUAGE_CODE }}. The language code used for rendering this email is {{ LANGUAGE_CODE }}.
Payment info: Payment info:
{{ payment_info }} {payment_info}
**Meta**: {{ meta_Test }} **Meta**: {meta_Test}
Event website: [{{event}}](https://example.org/{{event_slug}}) Event website: [{event}](https://example.org/{event_slug})
Other website: [{event}]({meta_Website})