mirror of
https://github.com/pretix/pretix.git
synced 2025-12-07 22:42:26 +00:00
Compare commits
26 Commits
v2025.8.2
...
fix-datasy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6baf11c6d | ||
|
|
cfbe00d24d | ||
|
|
22f351cb89 | ||
|
|
2611ff74a5 | ||
|
|
cc1c7e1c23 | ||
|
|
e2eedac93b | ||
|
|
432064c3ae | ||
|
|
457115f4ca | ||
|
|
9d5563018e | ||
|
|
425f4da1f1 | ||
|
|
aa0ea27d6c | ||
|
|
5a2219124a | ||
|
|
f79813ea32 | ||
|
|
ba62db7a19 | ||
|
|
aa8b699b89 | ||
|
|
6adabd54dc | ||
|
|
e61f8035d3 | ||
|
|
fc4a9406e1 | ||
|
|
9d2ef94389 | ||
|
|
c060322f2f | ||
|
|
7629bbbb6a | ||
|
|
21d084c3fa | ||
|
|
b8d09a15e2 | ||
|
|
af2d35cc5a | ||
|
|
cbe18608e4 | ||
|
|
37d0a0de22 |
@@ -80,17 +80,12 @@ lines list of objects The actual invo
|
||||
for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a fee (e.g. a product).
|
||||
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees).
|
||||
├ event_date_to datetime End date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no end date set.
|
||||
├ period_start datetime Start date of the service or delivery period of the invoice line.
|
||||
Can be ``null`` if not known.
|
||||
├ period_end datetime End date of the service or delivery period of the invoice line.
|
||||
Can be ``null`` if not known.
|
||||
├ event_date_from datetime Deprecated alias of ``period_start``.
|
||||
├ event_date_to datetime Deprecated alias of ``period_end``.
|
||||
├ event_location string Location of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
@@ -274,6 +269,8 @@ List of all invoices
|
||||
"fee_internal_type": null,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"period_start": "2017-12-27T10:00:00Z",
|
||||
"period_end": "2017-12-27T10:00:00Z",
|
||||
"event_location": "Heidelberg",
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
@@ -420,6 +417,8 @@ Fetching individual invoices
|
||||
"fee_internal_type": null,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"period_start": "2017-12-27T10:00:00Z",
|
||||
"period_end": "2017-12-27T10:00:00Z",
|
||||
"event_location": "Heidelberg",
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
|
||||
@@ -23,7 +23,7 @@ There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, build_invoice_data, invoice_line_text
|
||||
|
||||
Check-ins
|
||||
"""""""""
|
||||
|
||||
@@ -28,7 +28,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||
"babel",
|
||||
"BeautifulSoup4==4.13.*",
|
||||
"BeautifulSoup4==4.14.*",
|
||||
"bleach==6.2.*",
|
||||
"celery==5.5.*",
|
||||
"chardet==5.2.*",
|
||||
@@ -113,7 +113,7 @@ dev = [
|
||||
"fakeredis==2.31.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==6.0.*",
|
||||
"isort==6.1.*",
|
||||
"pep8-naming==0.15.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=0.24",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "2025.8.2"
|
||||
__version__ = "2025.9.0.dev0"
|
||||
|
||||
@@ -805,6 +805,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_reissue_after_modify',
|
||||
'invoice_include_free',
|
||||
'invoice_generate',
|
||||
'invoice_period',
|
||||
'invoice_numbers_consecutive',
|
||||
'invoice_numbers_prefix',
|
||||
'invoice_numbers_prefix_cancellations',
|
||||
|
||||
@@ -1757,12 +1757,14 @@ class LinePositionField(serializers.IntegerField):
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
position = LinePositionField(read_only=True)
|
||||
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
||||
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
||||
'fee_internal_type', 'event_location')
|
||||
'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
|
||||
'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -743,7 +743,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
)
|
||||
order_placed.send(self.request.event, order=order)
|
||||
order_placed.send(self.request.event, order=order, bulk=False)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(self.request.event, order=order)
|
||||
order.log_action(
|
||||
|
||||
@@ -106,7 +106,7 @@ class OutboundSyncProvider:
|
||||
return str(cls.identifier)
|
||||
|
||||
@classmethod
|
||||
def enqueue_order(cls, order, triggered_by, not_before=None):
|
||||
def enqueue_order(cls, order, triggered_by, not_before=None, immediate=False):
|
||||
"""
|
||||
Adds an order to the sync queue. May only be called on derived classes which define an ``identifier`` attribute.
|
||||
|
||||
@@ -119,10 +119,14 @@ class OutboundSyncProvider:
|
||||
:param order: the Order that should be synced
|
||||
:param triggered_by: the reason why the order should be synced, e.g. name of the signal
|
||||
(currently only used internally for logging)
|
||||
:param immediate: whether a new sync task should run immediately for this order, instead
|
||||
of waiting for the next periodic_task interval
|
||||
:return: Return a tuple (queue_item, created), where created is a boolean
|
||||
specifying whether a new queue item was created.
|
||||
"""
|
||||
if not hasattr(cls, 'identifier'):
|
||||
raise TypeError('Call this method on a derived class that defines an "identifier" attribute.')
|
||||
OrderSyncQueue.objects.update_or_create(
|
||||
queue_item, created = OrderSyncQueue.objects.update_or_create(
|
||||
order=order,
|
||||
sync_provider=cls.identifier,
|
||||
in_flight=False,
|
||||
@@ -133,6 +137,10 @@ class OutboundSyncProvider:
|
||||
"need_manual_retry": None,
|
||||
},
|
||||
)
|
||||
if immediate:
|
||||
from pretix.base.services.datasync import sync_single
|
||||
sync_single.apply_async(args=(queue_item.pk,))
|
||||
return queue_item, created
|
||||
|
||||
@classmethod
|
||||
def get_external_link_info(cls, event, external_link_href, external_link_display_name):
|
||||
@@ -383,7 +391,7 @@ class OutboundSyncProvider:
|
||||
def sync_order(self, order):
|
||||
if not self.should_sync_order(order):
|
||||
logger.debug("Skipping order %r", order)
|
||||
return
|
||||
return {}
|
||||
|
||||
logger.debug("Syncing order %r", order)
|
||||
positions = list(
|
||||
|
||||
@@ -24,7 +24,6 @@ from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar
|
||||
|
||||
import bleach
|
||||
import css_inline
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
@@ -35,10 +34,7 @@ from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.templatetags.rich_text import (
|
||||
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
@@ -137,24 +133,13 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def template_name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def compile_markdown(self, plaintext, context=None):
|
||||
return markdown_compile_email(plaintext, context=context)
|
||||
def compile_markdown(self, plaintext):
|
||||
return markdown_compile_email(plaintext)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
body_md = self.compile_markdown(plain_body, context)
|
||||
body_md = self.compile_markdown(plain_body)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
@@ -526,6 +527,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
def _date_range_in_header(self):
|
||||
if self.invoice.event.has_subevents or not self.invoice.event.settings.show_dates_on_frontpage:
|
||||
return None, None
|
||||
tz = self.invoice.event.timezone
|
||||
show_end_date = (
|
||||
self.invoice.event.settings.show_date_to and
|
||||
self.invoice.event.date_to and
|
||||
self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
|
||||
)
|
||||
if show_end_date:
|
||||
return self.invoice.event.date_from.astimezone(tz).date(), self.invoice.event.date_to.astimezone(tz).date()
|
||||
else:
|
||||
return self.invoice.event.date_from.astimezone(tz).date(), None
|
||||
|
||||
def _draw_event(self, canvas):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
@@ -539,25 +554,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
return txt
|
||||
|
||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||
tz = self.invoice.event.timezone
|
||||
show_end_date = (
|
||||
self.invoice.event.settings.show_date_to and
|
||||
self.invoice.event.date_to and
|
||||
self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
|
||||
d_from, d_to = self._date_range_in_header()
|
||||
if d_from and d_to:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' +
|
||||
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
from_date=date_format(d_from, "DATE_FORMAT"),
|
||||
to_date=date_format(d_to, "DATE_FORMAT"),
|
||||
)
|
||||
)
|
||||
if show_end_date:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' +
|
||||
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
from_date=self.invoice.event.get_date_from_display(show_times=False),
|
||||
to_date=self.invoice.event.get_date_to_display(show_times=False)
|
||||
)
|
||||
)
|
||||
else:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display(show_times=False)
|
||||
)
|
||||
elif d_from:
|
||||
p_str = shorten(self.invoice.event.name) + '\n' + date_format(d_from, "DATE_FORMAT")
|
||||
else:
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
@@ -685,7 +692,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
return story
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all()) or self.invoice.reverse_charge
|
||||
all_lines = list(self.invoice.lines.all())
|
||||
has_taxes = any(il.tax_value for il in all_lines) or self.invoice.reverse_charge
|
||||
header_dates = self._date_range_in_header()
|
||||
tz = self.invoice.event.timezone
|
||||
has_multiple_service_dates = len(set(
|
||||
(il.period_start, il.period_end) for il in all_lines
|
||||
)) > 1
|
||||
request_show_service_date = False
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
@@ -729,15 +743,75 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
)]
|
||||
|
||||
def _group_key(line):
|
||||
return (line.description, line.tax_rate, line.tax_name, line.net_value, line.gross_value, line.subevent_id,
|
||||
line.event_date_from, line.event_date_to)
|
||||
return (line.description, line.tax_rate, line.tax_name, line.net_value, line.gross_value, line.subevent,
|
||||
line.period_start, line.period_end)
|
||||
|
||||
def day(dt: datetime.datetime) -> datetime.date:
|
||||
if dt is None:
|
||||
return None
|
||||
return dt.astimezone(tz).date()
|
||||
|
||||
total = Decimal('0.00')
|
||||
for (description, tax_rate, tax_name, net_value, gross_value, *ignored), lines in addon_aware_groupby(
|
||||
self.invoice.lines.all(),
|
||||
for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby(
|
||||
all_lines,
|
||||
key=_group_key,
|
||||
is_addon=lambda l: l.description.startswith(" +"),
|
||||
):
|
||||
# Try to be clever and figure out when organizers would want to show the period. This heuristic is
|
||||
# not perfect and the only "fully correct" way would be to include the period on every line always,
|
||||
# however this will cause confusion (a) due to useless repetition of the same date all over the invoice
|
||||
# (b) due to not respecting the show_date_to setting of events in cases where we could have respected it.
|
||||
# Still, we want to show the date explicitly if its different to the event or invoice date.
|
||||
period_start_day = day(period_start)
|
||||
period_end_day = day(period_end)
|
||||
if period_start and period_end and period_end_day != period_start_day:
|
||||
# It's a multi-day period, such as the validity of the ticket or an event date period
|
||||
|
||||
if period_start_day == header_dates[0] and period_end_day == header_dates[1]:
|
||||
# This is the exact event period we already printed in the header, no need to repeat it.
|
||||
period_line = ""
|
||||
|
||||
elif (self.event.has_subevents and subevent and day(subevent.date_from) == period_start_day and
|
||||
day(subevent.date_to) == period_end_day):
|
||||
# For subevents, build_invoice already includes the date in the description in the event-default format.
|
||||
period_line = ""
|
||||
|
||||
else:
|
||||
period_line = f"{date_format(period_start_day, 'SHORT_DATE_FORMAT')} – {date_format(period_end_day, 'SHORT_DATE_FORMAT')}"
|
||||
|
||||
elif period_start or period_end:
|
||||
# It's a single-day period
|
||||
|
||||
delivery_day = period_end_day or period_start_day
|
||||
if delivery_day in header_dates:
|
||||
# This is the event date we already printed in the header, no need to repeat it.
|
||||
period_line = ""
|
||||
|
||||
elif self.event.has_subevents and subevent and delivery_day in (day(subevent.date_from), day(subevent.date_to)):
|
||||
# For subevents, build_invoice already includes the date in the description in the event-default format.
|
||||
period_line = ""
|
||||
|
||||
elif (delivery_day == self.invoice.date) and header_dates[0] is None:
|
||||
# This is a shop that doesn't show the date of the event in the header, and the period is the invoice
|
||||
# date. We assume that this is an 'everything is executed immediately' situation and do not want to
|
||||
# confuse with showing additional dates on the invoice. This is the case that is not guaranteed to be
|
||||
# correct in all cases and might need to change in the future. If customers have legal concerns, a
|
||||
# quick fix is including a sentence like "Delivery date is the invoice date unless otherwise indicated:"
|
||||
# in a custom text on the invoice.
|
||||
period_line = ""
|
||||
|
||||
else:
|
||||
period_line = date_format(delivery_day, 'SHORT_DATE_FORMAT')
|
||||
else:
|
||||
# No period known
|
||||
period_line = ""
|
||||
|
||||
if not has_multiple_service_dates and period_line:
|
||||
# Group together at the end of the invoice
|
||||
request_show_service_date = period_line
|
||||
elif period_line:
|
||||
description += "\n" + period_line
|
||||
|
||||
lines = list(lines)
|
||||
if has_taxes:
|
||||
if len(lines) > 1:
|
||||
@@ -746,6 +820,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
||||
)
|
||||
description = description + "\n" + single_price_line
|
||||
|
||||
tdata.append((
|
||||
FontFallbackParagraph(
|
||||
self._clean_text(description, tags=['br']),
|
||||
@@ -850,6 +925,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
if request_show_service_date:
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(self.invoice.payment_provider_text),
|
||||
|
||||
78
src/pretix/base/migrations/0289_invoiceline_period.py
Normal file
78
src/pretix/base/migrations/0289_invoiceline_period.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# Generated by Django 4.2.17 on 2025-09-08 08:14
|
||||
from django.core.cache import cache
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_invoice_period(apps, schema_editor):
|
||||
EventSettingsStore = apps.get_model("pretixbase", "Event_SettingsStore")
|
||||
ev_seen = set()
|
||||
insert_queue = []
|
||||
flush_queue = []
|
||||
|
||||
def store():
|
||||
EventSettingsStore.objects.bulk_create(
|
||||
insert_queue,
|
||||
update_conflicts=True,
|
||||
update_fields=["value"],
|
||||
unique_fields=["object", "key"],
|
||||
)
|
||||
cache.delete_many(flush_queue)
|
||||
flush_queue.clear()
|
||||
insert_queue.clear()
|
||||
|
||||
# Existing events that use pretix-zugferd and have explicitly disabled delivery dates
|
||||
for setting in EventSettingsStore.objects.filter(key="zugferd_include_delivery_date", value="False"):
|
||||
flush_queue.append("hierarkey_{}_{}".format("event", setting.object_id))
|
||||
insert_queue.append(
|
||||
EventSettingsStore(
|
||||
object_id=setting.object_id,
|
||||
key="invoice_period",
|
||||
value="invoice_date",
|
||||
)
|
||||
)
|
||||
ev_seen.add(setting.object_id)
|
||||
|
||||
if len(insert_queue) > 1000:
|
||||
store()
|
||||
|
||||
# Existing events that previously hid their date on invoices
|
||||
for setting in EventSettingsStore.objects.filter(key="show_dates_on_frontpage", value="False"):
|
||||
if setting.object_id in ev_seen:
|
||||
continue
|
||||
|
||||
flush_queue.append("hierarkey_{}_{}".format("event", setting.object_id))
|
||||
insert_queue.append(
|
||||
EventSettingsStore(
|
||||
object_id=setting.object_id,
|
||||
key="invoice_period",
|
||||
value="auto_no_event",
|
||||
)
|
||||
)
|
||||
|
||||
if len(insert_queue) > 1000:
|
||||
store()
|
||||
|
||||
store()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0288_invoice_transmission"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="invoiceline",
|
||||
old_name="event_date_to",
|
||||
new_name="period_end",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="invoiceline",
|
||||
old_name="event_date_from",
|
||||
new_name="period_start",
|
||||
),
|
||||
migrations.RunPython(
|
||||
set_invoice_period,
|
||||
migrations.RunPython.noop,
|
||||
)
|
||||
]
|
||||
18
src/pretix/base/migrations/0290_invoice_plugin_data.py
Normal file
18
src/pretix/base/migrations/0290_invoice_plugin_data.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.17 on 2025-09-09 09:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0289_invoiceline_period"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="invoice",
|
||||
name="plugin_data",
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
||||
@@ -195,20 +195,21 @@ class GiftCardTransaction(models.Model):
|
||||
return response
|
||||
|
||||
if self.order_id:
|
||||
if not self.text:
|
||||
if not customer_facing:
|
||||
return format_html(
|
||||
'<a href="{}">{}</a>',
|
||||
reverse(
|
||||
"control:event.order",
|
||||
kwargs={
|
||||
"event": self.order.event.slug,
|
||||
"organizer": self.order.event.organizer.slug,
|
||||
"code": self.order.code,
|
||||
}
|
||||
),
|
||||
self.order.full_code
|
||||
)
|
||||
if not customer_facing:
|
||||
return format_html(
|
||||
'<a href="{}">{}</a> {}',
|
||||
reverse(
|
||||
"control:event.order",
|
||||
kwargs={
|
||||
"event": self.order.event.slug,
|
||||
"organizer": self.order.event.organizer.slug,
|
||||
"code": self.order.code,
|
||||
}
|
||||
),
|
||||
self.order.full_code,
|
||||
self.text or "",
|
||||
)
|
||||
elif not self.text:
|
||||
return self.order.full_code
|
||||
else:
|
||||
return self.text
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import string
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
@@ -201,6 +202,7 @@ class Invoice(models.Model):
|
||||
transmission_info = models.JSONField(null=True, blank=True)
|
||||
|
||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||
plugin_data = models.JSONField(default=dict)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
@@ -404,10 +406,10 @@ class InvoiceLine(models.Model):
|
||||
:type tax_name: str
|
||||
:param subevent: The subevent this line refers to
|
||||
:type subevent: SubEvent
|
||||
:param event_date_from: Event date of the (sub)event at the time the invoice was created
|
||||
:type event_date_from: datetime
|
||||
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
|
||||
:type event_date_to: datetime
|
||||
:param period_start: Start if service period invoiced
|
||||
:type period_start: datetime
|
||||
:param period_end: End of service period invoiced
|
||||
:type period_end: datetime
|
||||
:param event_location: Event location of the (sub)event at the time the invoice was created
|
||||
:type event_location: str
|
||||
:param item: The item this line refers to
|
||||
@@ -426,8 +428,8 @@ class InvoiceLine(models.Model):
|
||||
tax_name = models.CharField(max_length=190)
|
||||
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event_date_from = models.DateTimeField(null=True)
|
||||
event_date_to = models.DateTimeField(null=True)
|
||||
period_start = models.DateTimeField(null=True)
|
||||
period_end = models.DateTimeField(null=True)
|
||||
event_location = models.TextField(null=True, blank=True)
|
||||
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
|
||||
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
|
||||
@@ -444,3 +446,35 @@ class InvoiceLine(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return 'Line {} of invoice {}'.format(self.position, self.invoice)
|
||||
|
||||
@property
|
||||
def event_date_from(self):
|
||||
warnings.warn(
|
||||
'InvoiceLine.event_date_from is deprecated, use period_start instead,',
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
return self.period_start
|
||||
|
||||
@event_date_from.setter
|
||||
def event_date_from(self, value):
|
||||
warnings.warn(
|
||||
'InvoiceLine.event_date_from is deprecated, use period_start instead,',
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
self.period_start = value
|
||||
|
||||
@property
|
||||
def event_date_to(self):
|
||||
warnings.warn(
|
||||
'InvoiceLine.event_date_to is deprecated, use period_end instead,',
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
return self.period_end
|
||||
|
||||
@event_date_to.setter
|
||||
def event_date_to(self, value):
|
||||
warnings.warn(
|
||||
'InvoiceLine.event_date_to is deprecated, use period_end instead,',
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
self.period_to = value
|
||||
|
||||
@@ -1627,6 +1627,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
order=refund.order,
|
||||
refund=refund,
|
||||
acceptor=self.event.organizer,
|
||||
text=refund.comment,
|
||||
)
|
||||
refund.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
|
||||
@@ -61,7 +61,9 @@ from pretix.base.models.tax import EU_CURRENCIES
|
||||
from pretix.base.services.tasks import (
|
||||
TransactionAwareProfiledEventTask, TransactionAwareTask,
|
||||
)
|
||||
from pretix.base.signals import invoice_line_text, periodic_task
|
||||
from pretix.base.signals import (
|
||||
build_invoice_data, invoice_line_text, periodic_task,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.database import OF_SELF, rolledback_transaction
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -82,6 +84,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
|
||||
lp = invoice.order.payments.last()
|
||||
|
||||
min_period_start = None
|
||||
max_period_end = None
|
||||
now_dt = now()
|
||||
|
||||
with (language(invoice.locale, invoice.event.settings.region)):
|
||||
invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
|
||||
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
|
||||
@@ -208,7 +214,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
positions = list(
|
||||
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
|
||||
addon_c=Count('addons')
|
||||
).prefetch_related('answers', 'answers__options', 'answers__question').order_by('positionid', 'id')
|
||||
).prefetch_related(
|
||||
'answers', 'answers__options', 'answers__question', 'granted_memberships',
|
||||
).order_by('positionid', 'id')
|
||||
)
|
||||
|
||||
reverse_charge = False
|
||||
@@ -267,6 +275,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
location=_location_oneliner(location)
|
||||
)
|
||||
|
||||
period_start, period_end = _service_period_for_position(invoice, p, now_dt)
|
||||
min_period_start = min(min_period_start or period_start, period_start)
|
||||
max_period_end = min(max_period_end or period_end, period_end)
|
||||
|
||||
InvoiceLine.objects.create(
|
||||
position=i,
|
||||
invoice=invoice,
|
||||
@@ -277,8 +289,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
item=p.item,
|
||||
variation=p.variation,
|
||||
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
|
||||
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
event_location=location if invoice.event.settings.invoice_event_location else None,
|
||||
tax_rate=p.tax_rate,
|
||||
tax_code=p.tax_code,
|
||||
@@ -301,13 +313,29 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
fee_title = _(fee.get_fee_type_display())
|
||||
if fee.description:
|
||||
fee_title += " - " + fee.description
|
||||
|
||||
if min_period_start and max_period_end:
|
||||
# Consider fees to have the same service period as the products sold
|
||||
period_start = min_period_start
|
||||
period_end = max_period_end
|
||||
else:
|
||||
# Usually can only happen if everything except a cancellation fee is removed
|
||||
if invoice.event.settings.invoice_period in ("auto", "auto_no_event", "event_date") and not invoice.event.has_subevents:
|
||||
# Non-series event, let's be backwards-compatible and tag everything with the event period
|
||||
period_start = invoice.event.date_from
|
||||
period_end = invoice.event.date_to
|
||||
else:
|
||||
# We could try to work from the canceled positions, but it doesn't really make sense. A cancellation
|
||||
# fee is not "delivered" at the event date, it is rather effective right now.
|
||||
period_start = period_end = now()
|
||||
|
||||
InvoiceLine.objects.create(
|
||||
position=i + offset,
|
||||
invoice=invoice,
|
||||
description=fee_title,
|
||||
gross_value=fee.value,
|
||||
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
event_location=(
|
||||
None if invoice.event.has_subevents
|
||||
else (str(invoice.event.location)
|
||||
@@ -336,6 +364,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice.reverse_charge = reverse_charge
|
||||
invoice.save()
|
||||
|
||||
build_invoice_data.send(sender=invoice.event, invoice=invoice)
|
||||
return invoice
|
||||
|
||||
|
||||
@@ -351,6 +380,55 @@ def build_cancellation(invoice: Invoice):
|
||||
return invoice
|
||||
|
||||
|
||||
def _service_period_for_position(invoice, position, invoice_dt):
|
||||
if invoice.event.settings.invoice_period in ("auto", "auto_no_event"):
|
||||
if position.valid_from and position.valid_until:
|
||||
period_start = position.valid_from
|
||||
period_end = position.valid_until
|
||||
elif position.valid_from:
|
||||
period_start = position.valid_from
|
||||
period_end = position.valid_from # weird, but we have nothing else to base this on
|
||||
elif position.valid_until:
|
||||
period_start = min(invoice.order.datetime, position.valid_until)
|
||||
period_end = position.valid_until
|
||||
elif memberships := list(position.granted_memberships.all()):
|
||||
period_start = min(m.date_start for m in memberships)
|
||||
period_end = max(m.date_end for m in memberships)
|
||||
elif invoice.event.has_subevents:
|
||||
if position.subevent:
|
||||
period_start = position.subevent.date_from
|
||||
period_end = position.subevent.date_to
|
||||
else:
|
||||
# Currently impossible case, but might not be in the future and never makes
|
||||
# sense to use the event date here
|
||||
period_start = invoice_dt
|
||||
period_end = invoice_dt
|
||||
elif invoice.event.settings.invoice_period == "auto_no_event":
|
||||
period_start = invoice_dt
|
||||
period_end = invoice_dt
|
||||
else:
|
||||
period_start = invoice.event.date_from
|
||||
period_end = invoice.event.date_to
|
||||
elif invoice.event.settings.invoice_period == "order_date":
|
||||
period_start = invoice.order.datetime
|
||||
period_end = invoice.order.datetime
|
||||
elif invoice.event.settings.invoice_period == "event_date":
|
||||
if position.subevent:
|
||||
period_start = position.subevent.date_from
|
||||
period_end = position.subevent.date_to
|
||||
else:
|
||||
period_start = invoice.event.date_from
|
||||
period_end = invoice.event.date_to
|
||||
elif invoice.event.settings.invoice_period == "invoice_date":
|
||||
period_start = period_end = invoice_dt
|
||||
else:
|
||||
raise ValueError(f"Invalid invoice period setting '{invoice.event.settings.invoice_period}'")
|
||||
|
||||
if not period_end:
|
||||
period_end = period_start
|
||||
return period_start, period_end
|
||||
|
||||
|
||||
def generate_cancellation(invoice: Invoice, trigger_pdf=True):
|
||||
if invoice.canceled:
|
||||
raise ValueError("Invoice should not be canceled twice.")
|
||||
@@ -456,6 +534,12 @@ def build_preview_invoice_pdf(event):
|
||||
if not locale or locale == '__user__':
|
||||
locale = event.settings.locale
|
||||
|
||||
if event.settings.invoice_period in ("auto", "auto_no_event", "event_date"):
|
||||
period_start = event.date_from
|
||||
period_end = event.date_to or event.date_from
|
||||
else:
|
||||
period_start = period_end = timezone.now()
|
||||
|
||||
with rolledback_transaction(), language(locale, event.settings.region):
|
||||
order = event.orders.create(
|
||||
status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||
@@ -506,8 +590,8 @@ def build_preview_invoice_pdf(event):
|
||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||
gross_value=tax.gross, tax_value=tax.tax,
|
||||
tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code,
|
||||
event_date_from=event.date_from,
|
||||
event_date_to=event.date_to,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
event_location=event.settings.invoice_event_location,
|
||||
)
|
||||
else:
|
||||
@@ -515,8 +599,8 @@ def build_preview_invoice_pdf(event):
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice, description=_("Sample product A"),
|
||||
gross_value=100, tax_value=0, tax_rate=0, tax_code=None,
|
||||
event_date_from=event.date_from,
|
||||
event_date_to=event.date_to,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
event_location=event.settings.invoice_event_location,
|
||||
)
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
'invoice_company': ''
|
||||
})
|
||||
renderer = ClassicMailRenderer(None, organizer)
|
||||
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
content_plain = body_plain = render_mail(template, context)
|
||||
subject = str(subject).format_map(TolerantDict(context))
|
||||
sender = (
|
||||
sender or
|
||||
@@ -316,7 +316,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
|
||||
with override(timezone):
|
||||
try:
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
if plain_text_only:
|
||||
body_html = None
|
||||
elif 'context' in inspect.signature(renderer.render).parameters:
|
||||
@@ -750,11 +749,11 @@ def mail_send(*args, **kwargs):
|
||||
mail_send_task.apply_async(args=args, kwargs=kwargs)
|
||||
|
||||
|
||||
def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN):
|
||||
def render_mail(template, context):
|
||||
if isinstance(template, LazyI18nString):
|
||||
body = str(template)
|
||||
if context and placeholder_mode:
|
||||
body = format_map(body, context, mode=placeholder_mode)
|
||||
if context:
|
||||
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
body = tpl.render(context)
|
||||
|
||||
@@ -221,7 +221,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
|
||||
for o in orders:
|
||||
with language(o.locale, event.settings.region):
|
||||
order_placed.send(event, order=o)
|
||||
order_placed.send(event, order=o, bulk=True)
|
||||
if o.status == Order.STATUS_PAID:
|
||||
order_paid.send(event, order=o)
|
||||
|
||||
|
||||
@@ -1091,7 +1091,7 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
|
||||
for msg in meta_info.get('confirm_messages', []):
|
||||
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
return order, payments
|
||||
|
||||
|
||||
@@ -2825,7 +2825,7 @@ class OrderChangeManager:
|
||||
def _check_complete_cancel(self):
|
||||
current = self.order.positions.count()
|
||||
cancels = sum([
|
||||
1 + o.position.addons.count() for o in self._operations if isinstance(o, self.CancelOperation)
|
||||
1 + o.position.addons.filter(canceled=False).count() for o in self._operations if isinstance(o, self.CancelOperation)
|
||||
]) + len([
|
||||
o for o in self._operations if isinstance(o, self.SplitOperation)
|
||||
])
|
||||
|
||||
@@ -26,7 +26,7 @@ from decimal import Decimal
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape, mark_safe
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -123,10 +123,6 @@ class BaseRichTextPlaceholder(BaseTextPlaceholder):
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def allowed_in_plain_content(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
return self._args
|
||||
@@ -198,33 +194,6 @@ class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
|
||||
return f'{text}: {url}'
|
||||
|
||||
|
||||
class MarkdownTextPlaceholder(BaseRichTextPlaceholder):
|
||||
def __init__(self, identifier, args, func, sample, inline):
|
||||
super().__init__(identifier, args)
|
||||
self._func = func
|
||||
self._sample = sample
|
||||
self._snippet = inline
|
||||
|
||||
@property
|
||||
def allowed_in_plain_content(self):
|
||||
return self._snippet
|
||||
|
||||
def render_plain(self, **context):
|
||||
return self._func(**{k: context[k] for k in self._args})
|
||||
|
||||
def render_html(self, **context):
|
||||
return mark_safe(markdown_compile_email(self.render_plain(**context), snippet=self._snippet))
|
||||
|
||||
def render_sample_plain(self, event):
|
||||
if callable(self._sample):
|
||||
return self._sample(event)
|
||||
else:
|
||||
return self._sample
|
||||
|
||||
def render_sample_html(self, event):
|
||||
return mark_safe(markdown_compile_email(self.render_sample_plain(event), snippet=self._snippet))
|
||||
|
||||
|
||||
class PlaceholderContext(SafeFormatter):
|
||||
"""
|
||||
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||
@@ -605,7 +574,7 @@ def base_placeholders(sender, **kwargs):
|
||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||
_('Sample Corporation')
|
||||
),
|
||||
MarkdownTextPlaceholder(
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
order.full_code,
|
||||
@@ -635,7 +604,6 @@ def base_placeholders(sender, **kwargs):
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||
]
|
||||
),
|
||||
inline=False,
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||
@@ -650,13 +618,12 @@ def base_placeholders(sender, **kwargs):
|
||||
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
||||
'68CYU2H6ZTP3WLK5'
|
||||
),
|
||||
MarkdownTextPlaceholder(
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
||||
'68CYU2H6ZTP3WLK5 \n7MB94KKPVEPSMVF2',
|
||||
inline=False,
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
MarkdownTextPlaceholder(
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||
'voucher_url_list', ['event', 'voucher_list'],
|
||||
lambda event, voucher_list: ' \n'.join([
|
||||
@@ -671,7 +638,6 @@ def base_placeholders(sender, **kwargs):
|
||||
) + '?voucher=' + c
|
||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||
]),
|
||||
inline=False,
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
@@ -690,13 +656,13 @@ def base_placeholders(sender, **kwargs):
|
||||
'comment', ['comment'], lambda comment: comment,
|
||||
_('An individual text with a reason can be inserted here.'),
|
||||
),
|
||||
MarkdownTextPlaceholder(
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'payment_info', ['order', 'payments'], _placeholder_payments,
|
||||
_('The amount has been charged to your card.'), inline=False,
|
||||
_('The amount has been charged to your card.'),
|
||||
),
|
||||
MarkdownTextPlaceholder(
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'), inline=False,
|
||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||
@@ -753,13 +719,13 @@ def base_placeholders(sender, **kwargs):
|
||||
))
|
||||
|
||||
for k, v in sender.meta_data.items():
|
||||
ph.append(MarkdownTextPlaceholder(
|
||||
ph.append(SimpleFunctionalTextPlaceholder(
|
||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||
v, inline=True,
|
||||
v
|
||||
))
|
||||
ph.append(MarkdownTextPlaceholder(
|
||||
ph.append(SimpleFunctionalTextPlaceholder(
|
||||
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
||||
v, inline=True,
|
||||
v
|
||||
))
|
||||
|
||||
return ph
|
||||
@@ -787,7 +753,7 @@ def get_available_placeholders(event, base_parameters, rich=False):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if isinstance(v, BaseRichTextPlaceholder) and not rich and not v.allowed_in_plain_content:
|
||||
if isinstance(v, BaseRichTextPlaceholder) and not rich:
|
||||
continue
|
||||
if all(rp in base_parameters for rp in v.required_context):
|
||||
params[v.identifier] = v
|
||||
@@ -809,13 +775,13 @@ def get_sample_context(event, context_parameters, rich=True):
|
||||
)
|
||||
)
|
||||
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
|
||||
context_dict[k] = mark_safe('<div class="placeholder" title="{}">{}</div>'.format(
|
||||
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
lbl,
|
||||
markdown_compile_email(str(sample))
|
||||
))
|
||||
)
|
||||
else:
|
||||
context_dict[k] = mark_safe('<span class="placeholder" title="{}">{}</span>'.format(
|
||||
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
lbl,
|
||||
escape(sample)
|
||||
))
|
||||
)
|
||||
return context_dict
|
||||
|
||||
@@ -1098,6 +1098,35 @@ DEFAULTS = {
|
||||
help_text=_("Invoices will never be automatically generated for free orders.")
|
||||
)
|
||||
},
|
||||
'invoice_period': {
|
||||
'default': 'auto',
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=(
|
||||
('auto', _('Automatic based on ticket-specific validity, membership validity, event series date, or event date')),
|
||||
('auto_no_event', _('Automatic, but prefer invoice date over event date')),
|
||||
('event_date', _('Event date')),
|
||||
('order_date', _('Order date')),
|
||||
('invoice_date', _('Invoice date')),
|
||||
),
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Date of service"),
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('auto', _('Automatic based on ticket-specific validity, membership validity, event series date, or event date')),
|
||||
('auto_no_event', _('Automatic, but prefer invoice date over event date')),
|
||||
('event_date', _('Event date')),
|
||||
('order_date', _('Order date')),
|
||||
('invoice_date', _('Invoice date')),
|
||||
),
|
||||
help_text=_("This controls what dates are shown on the invoice, but is especially important for "
|
||||
"electronic invoicing."),
|
||||
required=True,
|
||||
)
|
||||
},
|
||||
'invoice_reissue_after_modify': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
|
||||
@@ -596,6 +596,18 @@ multiple events. Receivers should return a subclass of pretix.base.exporter.Base
|
||||
The ``sender`` keyword argument will contain an organizer.
|
||||
"""
|
||||
|
||||
build_invoice_data = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``invoice``
|
||||
|
||||
This signal is sent out every time an invoice is built, after the invoice model was created
|
||||
and filled and before the PDF generation task is started. You can use this to make changes
|
||||
to the invoice, but we recommend to mostly use it to add content to ``Invoice.plugin_data``.
|
||||
You are responsible for saving any changes to the database.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
validate_order = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
||||
@@ -653,12 +665,13 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
||||
|
||||
order_placed = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``
|
||||
Arguments: ``order``, ``bulk``
|
||||
|
||||
This signal is sent out every time an order is placed. The order object is given
|
||||
as the first argument. This signal is *not* sent out if an order is created through
|
||||
splitting an existing order, so you can not expect to see all orders by listening
|
||||
to this signal.
|
||||
as the first argument. The ``bulk`` argument specifies whether the order was placed
|
||||
as part of a bulk action, e.g. an import from a file.
|
||||
This signal is *not* sent out if an order is created through splitting an existing order,
|
||||
so you can not expect to see all orders by listening to this signal.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -44,7 +44,6 @@ from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from markdown import Extension
|
||||
@@ -53,8 +52,6 @@ from markdown.postprocessors import Postprocessor
|
||||
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||
from tlds import tld_set
|
||||
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
register = template.Library()
|
||||
|
||||
ALLOWED_TAGS_SNIPPET = {
|
||||
@@ -297,44 +294,27 @@ class LinkifyAndCleanExtension(Extension):
|
||||
)
|
||||
|
||||
|
||||
def markdown_compile_email(source, allowed_tags=None, allowed_attributes=ALLOWED_ATTRIBUTES, snippet=False, context=None):
|
||||
if allowed_tags is None:
|
||||
allowed_tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
|
||||
|
||||
context_callbacks = []
|
||||
if context:
|
||||
# This is a workaround to fix placeholders in URL targets
|
||||
def context_callback(attrs, new=False):
|
||||
if (None, "href") in attrs and "{" in attrs[None, "href"]:
|
||||
# Do not use MODE_RICH_TO_HTML to avoid recursive linkification
|
||||
attrs[None, "href"] = escape(format_map(attrs[None, "href"], context=context, mode=SafeFormatter.MODE_RICH_TO_PLAIN))
|
||||
return attrs
|
||||
|
||||
context_callbacks.append(context_callback)
|
||||
|
||||
def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes=ALLOWED_ATTRIBUTES):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=context_callbacks + DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
exts = [
|
||||
'markdown.extensions.sane_lists',
|
||||
'markdown.extensions.tables',
|
||||
EmailNl2BrExtension(),
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
tags=set(allowed_tags),
|
||||
attributes=allowed_attributes,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
strip=snippet,
|
||||
)
|
||||
]
|
||||
if snippet:
|
||||
exts.append(SnippetExtension())
|
||||
return markdown.markdown(
|
||||
source,
|
||||
extensions=exts
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
'markdown.extensions.tables',
|
||||
EmailNl2BrExtension(),
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
tags=set(allowed_tags),
|
||||
attributes=allowed_attributes,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
strip=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -857,6 +857,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'invoice_show_payments',
|
||||
'invoice_reissue_after_modify',
|
||||
'invoice_generate',
|
||||
'invoice_period',
|
||||
'invoice_attendee_name',
|
||||
'invoice_event_location',
|
||||
'invoice_include_expire_date',
|
||||
|
||||
@@ -308,8 +308,8 @@ class VoucherBulkForm(VoucherForm):
|
||||
)
|
||||
Recipient = namedtuple('Recipient', 'email number name tag')
|
||||
|
||||
def _set_field_placeholders(self, fn, base_parameters, rich=False):
|
||||
placeholders = get_available_placeholders(self.instance.event, base_parameters, rich=rich)
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
placeholders = get_available_placeholders(self.instance.event, base_parameters)
|
||||
ht = format_placeholders_help_text(placeholders, self.instance.event)
|
||||
|
||||
if self.fields[fn].help_text:
|
||||
@@ -345,7 +345,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._set_field_placeholders('send_subject', ['event', 'name'])
|
||||
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'], rich=True)
|
||||
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
|
||||
|
||||
with language(self.instance.event.settings.locale, self.instance.event.settings.region):
|
||||
for f in ("send_subject", "send_message"):
|
||||
|
||||
@@ -15,6 +15,19 @@
|
||||
{% bootstrap_field form.invoice_email_attachment layout="control" %}
|
||||
{% bootstrap_field form.invoice_email_organizer layout="control" %}
|
||||
{% bootstrap_field form.invoice_language layout="control" %}
|
||||
{% bootstrap_field form.invoice_period layout="control" %}
|
||||
|
||||
{% if not request.event.settings.show_dates_on_frontpage %}
|
||||
<div data-display-dependency="input[name=invoice_period][value=auto],input[name=invoice_period][value=event_date]">
|
||||
<div class="alert alert-warning dynamic">
|
||||
{% blocktrans trimmed %}
|
||||
You configured that your shop is not an event and the event date should not be shown.
|
||||
Therefore, we recommend that you set the date of service to a different option.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_field form.invoice_include_free layout="control" %}
|
||||
{% bootstrap_field form.invoice_show_payments layout="control" %}
|
||||
{% bootstrap_field form.invoice_reissue_after_modify layout="control" %}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
import logging
|
||||
from string import Formatter
|
||||
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -42,14 +40,14 @@ class SafeFormatter(Formatter):
|
||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
||||
"""
|
||||
MODE_IGNORE_RICH = 0
|
||||
MODE_RICH_TO_PLAIN = 1
|
||||
MODE_RICH_TO_HTML = 2
|
||||
|
||||
def __init__(self, context, raise_on_missing=False, mode=MODE_RICH_TO_PLAIN, linkifier=None):
|
||||
def __init__(self, context, raise_on_missing=False, mode=MODE_IGNORE_RICH):
|
||||
self.context = context
|
||||
self.raise_on_missing = raise_on_missing
|
||||
self.mode = mode
|
||||
self.linkifier = linkifier
|
||||
|
||||
def get_field(self, field_name, args, kwargs):
|
||||
return self.get_value(field_name, args, kwargs), field_name
|
||||
@@ -57,28 +55,22 @@ class SafeFormatter(Formatter):
|
||||
def get_value(self, key, args, kwargs):
|
||||
if not self.raise_on_missing and key not in self.context:
|
||||
return '{' + str(key) + '}'
|
||||
return self.context[key]
|
||||
|
||||
def _prepare_value(self, value):
|
||||
if isinstance(value, PlainHtmlAlternativeString):
|
||||
if self.mode == self.MODE_RICH_TO_PLAIN:
|
||||
return value.plain
|
||||
r = self.context[key]
|
||||
if isinstance(r, PlainHtmlAlternativeString):
|
||||
if self.mode == self.MODE_IGNORE_RICH:
|
||||
return '{' + str(key) + '}'
|
||||
elif self.mode == self.MODE_RICH_TO_PLAIN:
|
||||
return r.plain
|
||||
elif self.mode == self.MODE_RICH_TO_HTML:
|
||||
return value.html
|
||||
else:
|
||||
value = str(value)
|
||||
if self.mode == self.MODE_RICH_TO_HTML:
|
||||
value = conditional_escape(value)
|
||||
if self.linkifier:
|
||||
value = self.linkifier.linkify(value)
|
||||
return value
|
||||
return r.html
|
||||
return r
|
||||
|
||||
def format_field(self, value, format_spec):
|
||||
# Ignore format_spec
|
||||
return super().format_field(self._prepare_value(value), '')
|
||||
return super().format_field(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_IGNORE_RICH):
|
||||
if not isinstance(template, str):
|
||||
template = str(template)
|
||||
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-04-22 15:36+0000\n"
|
||||
"Last-Translator: Paul Berschick <paul@plainschwarz.com>\n"
|
||||
"PO-Revision-Date: 2025-09-29 07:39+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ca/>\n"
|
||||
"Language: ca\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.11\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -35318,11 +35318,6 @@ msgstr ""
|
||||
"Tingueu en compte que heu de fer el pagament per a finalitzar el procés."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:55
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Please bookmark or save the link to this exact page if you want to access "
|
||||
#| "your order later. We also sent you an email containing the link to the "
|
||||
#| "address you specified."
|
||||
msgid ""
|
||||
"Please bookmark or save the link to this exact page if you want to access "
|
||||
"your order later. We also sent you an email to the address you specified "
|
||||
|
||||
@@ -5,7 +5,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-09-23 16:13+0000\n"
|
||||
"PO-Revision-Date: 2025-09-26 13:02+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
@@ -808,28 +808,23 @@ msgstr ""
|
||||
"Sie zunächst die E-Mail-Adresse in Ihrem Kundenkonto."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:255
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" does not exist. Please check your {provider_name} "
|
||||
"settings."
|
||||
msgstr ""
|
||||
"Feld \"{field_name}\" ist nicht gültig für {available_inputs}. Bitte prüfen "
|
||||
"Sie die Einstellungen für {provider_name}."
|
||||
"Feld \"{field_name}\" existiert nicht. Bitte prüfen Sie die Einstellungen "
|
||||
"für {provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:262
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" requires {required_input}, but only got "
|
||||
"{available_inputs}. Please check your {provider_name} settings."
|
||||
msgstr ""
|
||||
"Feld \"{field_name}\" ist nicht gültig für {available_inputs}. Bitte prüfen "
|
||||
"Sie die Einstellungen für {provider_name}."
|
||||
"Feld \"{field_name}\" erfordert, {required_input}, aber nur "
|
||||
"{available_inputs} sind vorhanden. Bitte prüfen Sie die Einstellungen für "
|
||||
"{provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:273
|
||||
#, python-brace-format
|
||||
@@ -3548,6 +3543,10 @@ msgid ""
|
||||
"in accordance with the procedures and terms set forth in No. 89757/2018 of "
|
||||
"April 30, 2018, issued by the Director of the Revenue Agency."
|
||||
msgstr ""
|
||||
"Diese PDF-Datei ist eine Sichtkopie der Rechnung und stellt keine Rechnung "
|
||||
"für Umsatzsteuerzwecke dar. Die Rechnung wurde im XML-Format ausgestellt und "
|
||||
"im Einklang mit den Regularien aus dem Dekret Nr. 89757/2018 vom 30. April "
|
||||
"2018 des Direktors der Finanzbehörde übermittelt."
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:141
|
||||
#, python-format
|
||||
@@ -3817,12 +3816,9 @@ msgid "Peppol participant ID"
|
||||
msgstr "Peppol-Teilnehmer-ID"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:170
|
||||
#, fuzzy
|
||||
#| msgctxt "italian_invoice"
|
||||
#| msgid "Fiscal code"
|
||||
msgctxt "peppol_invoice"
|
||||
msgid "Visual copy"
|
||||
msgstr "Steuernummer"
|
||||
msgstr "Sichtkopie"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:175
|
||||
msgctxt "peppol_invoice"
|
||||
@@ -3831,6 +3827,9 @@ msgid ""
|
||||
"invoice for VAT purposes. The original invoice is issued in XML format and "
|
||||
"transmitted through the Peppol network."
|
||||
msgstr ""
|
||||
"Diese PDF-Datei ist eine Sichtkopie der Rechnung und stellt keine Rechnung "
|
||||
"für Umsatzsteuerzwecke dar. Die Originalrechnung wurde im XML-Format "
|
||||
"ausgestellt und über das Peppol-Netzwerk übertragen."
|
||||
|
||||
#: pretix/base/logentrytype_registry.py:43
|
||||
msgid ""
|
||||
@@ -14406,10 +14405,8 @@ msgid "Canceled (fully or with paid fee)"
|
||||
msgstr "Storniert (komplett oder mit Gebühr)"
|
||||
|
||||
#: pretix/control/forms/filter.py:228
|
||||
#, fuzzy
|
||||
#| msgid "Cancel this position"
|
||||
msgid "Canceled (at least one position)"
|
||||
msgstr "Diese Position stornieren"
|
||||
msgstr "storniert (mindestens eine Position)"
|
||||
|
||||
#: pretix/control/forms/filter.py:229
|
||||
msgid "Cancellation requested"
|
||||
@@ -17149,10 +17146,9 @@ msgid "The voucher has been deleted."
|
||||
msgstr "Der Gutschein wurde gelöscht."
|
||||
|
||||
#: pretix/control/logdisplay.py:584
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The voucher has been sent to {email} through the waiting list."
|
||||
#, python-brace-format
|
||||
msgid "The voucher has been assigned to {email} through the waiting list."
|
||||
msgstr "Der Gutschein wurde über die Warteliste an {email} verschickt."
|
||||
msgstr "Der Gutschein wurde über die Warteliste an {email} zugewiesen."
|
||||
|
||||
#: pretix/control/logdisplay.py:593
|
||||
#, python-brace-format
|
||||
@@ -27691,11 +27687,11 @@ msgstr ""
|
||||
"ausgeführt."
|
||||
|
||||
#: pretix/control/views/datasync.py:90 pretix/control/views/datasync.py:104
|
||||
#, fuzzy
|
||||
#| msgid "The voucher \"{voucher}\" has been used in the meantime."
|
||||
msgid ""
|
||||
"The sync job could not be found. It may have been processed in the meantime."
|
||||
msgstr "Der Gutschein \"{voucher}\" wurde zwischenzeitlich verwendet."
|
||||
msgstr ""
|
||||
"Der Synchronisationsauftrag wurde nicht gefunden. Möglicherweise wurde er "
|
||||
"zwischenzeitlich bearbeitet."
|
||||
|
||||
#: pretix/control/views/datasync.py:93 pretix/control/views/datasync.py:107
|
||||
msgid "The sync job is already in progress."
|
||||
@@ -28689,12 +28685,12 @@ msgstr ""
|
||||
"im Anhang dieser E-Mail finden Sie einen neuen Bericht für {name}."
|
||||
|
||||
#: pretix/control/views/orders.py:2849 pretix/control/views/organizer.py:2207
|
||||
#, fuzzy
|
||||
#| msgid "You do not have sufficient permission to perform this export."
|
||||
msgid ""
|
||||
"Your user account does not have sufficient permission to run this report, "
|
||||
"therefore you cannot schedule it."
|
||||
msgstr "Sie haben keine Berechtigung, um diesen Export durchzuführen."
|
||||
msgstr ""
|
||||
"Sie haben nicht genug Berechtigungen um diesen Bericht auszuführen, daher "
|
||||
"können Sie ihn nicht einplanen."
|
||||
|
||||
#: pretix/control/views/orders.py:2902 pretix/control/views/organizer.py:2259
|
||||
msgid ""
|
||||
@@ -29061,17 +29057,13 @@ msgstr ""
|
||||
"Der Termin kann nicht gelöscht werden, da es bereits Bestellungen dafür gibt."
|
||||
|
||||
#: pretix/control/views/subevents.py:203
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The channel could not be deleted as some constraints (e.g. data created "
|
||||
#| "by plug-ins) did not allow it."
|
||||
msgctxt "subevent"
|
||||
msgid ""
|
||||
"The date could not be deleted as some constraints (e.g. data created by plug-"
|
||||
"ins) did not allow it. The date was disabled instead."
|
||||
msgstr ""
|
||||
"Der Verkaufskanal konnte nicht gelöscht werden, da einige Bedingungen (z.B. "
|
||||
"von Plugins erstellte Daten) es nicht erlauben."
|
||||
"Der Termin konnte nicht gelöscht werden, da einige Bedingungen (z.B. Daten "
|
||||
"von Plugins) es verhindert haben. Der Termin wurde stattdessen deaktiviert."
|
||||
|
||||
#: pretix/control/views/subevents.py:207
|
||||
msgctxt "subevent"
|
||||
@@ -33875,18 +33867,12 @@ msgstr ""
|
||||
"Bestellung bestätigt werden kann und einen gültigen Vertrag darstellt."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:192
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "After you submitted your order using the button below, it will require "
|
||||
#| "approval by the event organizer before it can be confirmed and forms a "
|
||||
#| "valid contract."
|
||||
msgid ""
|
||||
"After you submitted your order using the button below, it will require "
|
||||
"approval by the event organizer."
|
||||
msgstr ""
|
||||
"Nachdem Sie Ihre Bestellung mit dem untenstehenden Button abgeschickt haben, "
|
||||
"erfordert sie noch eine Freigabe durch den Veranstalter, bevor die "
|
||||
"Bestellung bestätigt werden kann und einen gültigen Vertrag darstellt."
|
||||
"erfordert sie noch eine Freigabe durch den Veranstalter."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:195
|
||||
msgid ""
|
||||
@@ -36491,10 +36477,8 @@ msgid "The selected date does not exist in this event series."
|
||||
msgstr "Der ausgewählte Termin gehört nicht zu dieser Veranstaltungsreihe."
|
||||
|
||||
#: pretix/presale/views/widget.py:412
|
||||
#, fuzzy
|
||||
#| msgid "The selected seat \"{seat}\" is not available."
|
||||
msgid "The selected date is not available."
|
||||
msgstr "Der ausgewählte Sitzplatz \"{seat}\" ist nicht verfügbar."
|
||||
msgstr "Der gewählte Termin ist nicht verfügbar."
|
||||
|
||||
#: pretix/presale/views/widget.py:476
|
||||
#, python-format
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-09-23 16:13+0000\n"
|
||||
"PO-Revision-Date: 2025-09-26 13:02+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
@@ -809,28 +809,23 @@ msgstr ""
|
||||
"zunächst die E-Mail-Adresse in deinem Kundenkonto."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:255
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" does not exist. Please check your {provider_name} "
|
||||
"settings."
|
||||
msgstr ""
|
||||
"Feld \"{field_name}\" ist nicht gültig für {available_inputs}. Bitte prüfe "
|
||||
"die Einstellungen für {provider_name}."
|
||||
"Feld \"{field_name}\" existiert nicht. Bitte prüfe die Einstellungen für "
|
||||
"{provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:262
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" requires {required_input}, but only got "
|
||||
"{available_inputs}. Please check your {provider_name} settings."
|
||||
msgstr ""
|
||||
"Feld \"{field_name}\" ist nicht gültig für {available_inputs}. Bitte prüfe "
|
||||
"die Einstellungen für {provider_name}."
|
||||
"Feld \"{field_name}\" erfordert {required_input}, aber hat nur "
|
||||
"{available_inputs} sind verfügbar. Bitte prüfe die Einstellungen für "
|
||||
"{provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:273
|
||||
#, python-brace-format
|
||||
@@ -3548,6 +3543,10 @@ msgid ""
|
||||
"in accordance with the procedures and terms set forth in No. 89757/2018 of "
|
||||
"April 30, 2018, issued by the Director of the Revenue Agency."
|
||||
msgstr ""
|
||||
"Diese PDF-Datei ist eine Sichtkopie der Rechnung und stellt keine Rechnung "
|
||||
"für Umsatzsteuerzwecke dar. Die Rechnung wurde im XML-Format ausgestellt und "
|
||||
"im Einklang mit den Regularien aus dem Dekret Nr. 89757/2018 vom 30. April "
|
||||
"2018 des Direktors der Finanzbehörde übermittelt."
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:141
|
||||
#, python-format
|
||||
@@ -3817,12 +3816,9 @@ msgid "Peppol participant ID"
|
||||
msgstr "Peppol-Teilnehmer-ID"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:170
|
||||
#, fuzzy
|
||||
#| msgctxt "italian_invoice"
|
||||
#| msgid "Fiscal code"
|
||||
msgctxt "peppol_invoice"
|
||||
msgid "Visual copy"
|
||||
msgstr "Steuernummer"
|
||||
msgstr "Sichtkopie"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:175
|
||||
msgctxt "peppol_invoice"
|
||||
@@ -3831,6 +3827,9 @@ msgid ""
|
||||
"invoice for VAT purposes. The original invoice is issued in XML format and "
|
||||
"transmitted through the Peppol network."
|
||||
msgstr ""
|
||||
"Diese PDF-Datei ist eine Sichtkopie der Rechnung und stellt keine Rechnung "
|
||||
"für Umsatzsteuerzwecke dar. Die Originalrechnung wurde im XML-Format "
|
||||
"ausgestellt und über das Peppol-Netzwerk übertragen."
|
||||
|
||||
#: pretix/base/logentrytype_registry.py:43
|
||||
msgid ""
|
||||
@@ -14382,10 +14381,8 @@ msgid "Canceled (fully or with paid fee)"
|
||||
msgstr "Storniert (komplett oder mit Gebühr)"
|
||||
|
||||
#: pretix/control/forms/filter.py:228
|
||||
#, fuzzy
|
||||
#| msgid "Cancel this position"
|
||||
msgid "Canceled (at least one position)"
|
||||
msgstr "Diese Position stornieren"
|
||||
msgstr "Storniert (mindestens eine Position)"
|
||||
|
||||
#: pretix/control/forms/filter.py:229
|
||||
msgid "Cancellation requested"
|
||||
@@ -17125,10 +17122,9 @@ msgid "The voucher has been deleted."
|
||||
msgstr "Der Gutschein wurde gelöscht."
|
||||
|
||||
#: pretix/control/logdisplay.py:584
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The voucher has been sent to {email} through the waiting list."
|
||||
#, python-brace-format
|
||||
msgid "The voucher has been assigned to {email} through the waiting list."
|
||||
msgstr "Der Gutschein wurde über die Warteliste an {email} verschickt."
|
||||
msgstr "Der Gutschein wurde über die Warteliste an {email} zugewiesen."
|
||||
|
||||
#: pretix/control/logdisplay.py:593
|
||||
#, python-brace-format
|
||||
@@ -27650,11 +27646,11 @@ msgstr ""
|
||||
"ausgeführt."
|
||||
|
||||
#: pretix/control/views/datasync.py:90 pretix/control/views/datasync.py:104
|
||||
#, fuzzy
|
||||
#| msgid "The voucher \"{voucher}\" has been used in the meantime."
|
||||
msgid ""
|
||||
"The sync job could not be found. It may have been processed in the meantime."
|
||||
msgstr "Der Gutschein \"{voucher}\" wurde zwischenzeitlich verwendet."
|
||||
msgstr ""
|
||||
"Der Synchronisationsauftrag wurde nicht gefunden. Möglicherweise wurde er "
|
||||
"zwischenzeitlich bearbeitet."
|
||||
|
||||
#: pretix/control/views/datasync.py:93 pretix/control/views/datasync.py:107
|
||||
msgid "The sync job is already in progress."
|
||||
@@ -28645,12 +28641,12 @@ msgstr ""
|
||||
"im Anhang dieser E-Mail findest du einen neuen Bericht für {name}."
|
||||
|
||||
#: pretix/control/views/orders.py:2849 pretix/control/views/organizer.py:2207
|
||||
#, fuzzy
|
||||
#| msgid "You do not have sufficient permission to perform this export."
|
||||
msgid ""
|
||||
"Your user account does not have sufficient permission to run this report, "
|
||||
"therefore you cannot schedule it."
|
||||
msgstr "Du hast nicht die nötige Berechtigung, um diesen Export durchzuführen."
|
||||
msgstr ""
|
||||
"Du hast nicht genug Berechtigungen um diesen Bericht auszuführen, daher "
|
||||
"kannst du ihn nicht einplanen."
|
||||
|
||||
#: pretix/control/views/orders.py:2902 pretix/control/views/organizer.py:2259
|
||||
msgid ""
|
||||
@@ -29017,17 +29013,13 @@ msgstr ""
|
||||
"Der Termin kann nicht gelöscht werden, da es bereits Bestellungen dafür gibt."
|
||||
|
||||
#: pretix/control/views/subevents.py:203
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The channel could not be deleted as some constraints (e.g. data created "
|
||||
#| "by plug-ins) did not allow it."
|
||||
msgctxt "subevent"
|
||||
msgid ""
|
||||
"The date could not be deleted as some constraints (e.g. data created by plug-"
|
||||
"ins) did not allow it. The date was disabled instead."
|
||||
msgstr ""
|
||||
"Der Verkaufskanal konnte nicht gelöscht werden, da einige Bedingungen (z.B. "
|
||||
"von Plugins erstellte Daten) es nicht erlauben."
|
||||
"Der Termin konnte nicht gelöscht werden, da einige Bedingungen (z.B. Daten "
|
||||
"von Plugins) es verhindert haben. Der Termin wurde stattdessen deaktiviert."
|
||||
|
||||
#: pretix/control/views/subevents.py:207
|
||||
msgctxt "subevent"
|
||||
@@ -33820,18 +33812,12 @@ msgstr ""
|
||||
"Bestellung bestätigt werden kann und einen gültigen Vertrag darstellt."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:192
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "After you submitted your order using the button below, it will require "
|
||||
#| "approval by the event organizer before it can be confirmed and forms a "
|
||||
#| "valid contract."
|
||||
msgid ""
|
||||
"After you submitted your order using the button below, it will require "
|
||||
"approval by the event organizer."
|
||||
msgstr ""
|
||||
"Nachdem du deine Bestellung mit dem untenstehenden Button abgeschickt hast, "
|
||||
"erfordert sie noch eine Freigabe durch den Veranstalter, bevor die "
|
||||
"Bestellung bestätigt werden kann und einen gültigen Vertrag darstellt."
|
||||
"erfordert sie noch eine Freigabe durch den Veranstalter."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:195
|
||||
msgid ""
|
||||
@@ -36431,10 +36417,8 @@ msgid "The selected date does not exist in this event series."
|
||||
msgstr "Der ausgewählte Termin gehört nicht zu dieser Veranstaltungsreihe."
|
||||
|
||||
#: pretix/presale/views/widget.py:412
|
||||
#, fuzzy
|
||||
#| msgid "The selected seat \"{seat}\" is not available."
|
||||
msgid "The selected date is not available."
|
||||
msgstr "Der ausgewählte Sitzplatz \"{seat}\" ist nicht verfügbar."
|
||||
msgstr "Der gewählte Termin ist nicht verfügbar."
|
||||
|
||||
#: pretix/presale/views/widget.py:476
|
||||
#, python-format
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-08-30 02:00+0000\n"
|
||||
"PO-Revision-Date: 2025-09-30 16:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.13\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -810,28 +810,23 @@ msgstr ""
|
||||
"Primero verifique la dirección de correo electrónico en su cuenta de cliente."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:255
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" does not exist. Please check your {provider_name} "
|
||||
"settings."
|
||||
msgstr ""
|
||||
"El campo «{field_name}» no es válido para {available_inputs}. Comprueba la "
|
||||
"configuración de {provider_name}."
|
||||
"El campo «{field_name}» no existe. Comprueba la configuración de "
|
||||
"{provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:262
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" requires {required_input}, but only got "
|
||||
"{available_inputs}. Please check your {provider_name} settings."
|
||||
msgstr ""
|
||||
"El campo «{field_name}» no es válido para {available_inputs}. Comprueba la "
|
||||
"configuración de {provider_name}."
|
||||
"El campo «{field_name}» requiere {required_input}, pero solo se ha "
|
||||
"introducido {available_inputs}. Comprueba la configuración de "
|
||||
"{provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:273
|
||||
#, python-brace-format
|
||||
@@ -3559,6 +3554,11 @@ msgid ""
|
||||
"in accordance with the procedures and terms set forth in No. 89757/2018 of "
|
||||
"April 30, 2018, issued by the Director of the Revenue Agency."
|
||||
msgstr ""
|
||||
"Este documento PDF es una copia visual de la factura y no constituye una "
|
||||
"factura a efectos del IVA. La factura se emite en formato XML, transmitida "
|
||||
"de acuerdo con los procedimientos y términos establecidos en el n.º 89757/"
|
||||
"2018, de 30 de abril de 2018, emitido por el Director de la Agencia "
|
||||
"Tributaria."
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:141
|
||||
#, python-format
|
||||
@@ -3829,12 +3829,9 @@ msgid "Peppol participant ID"
|
||||
msgstr "Identificador de participante Peppol"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:170
|
||||
#, fuzzy
|
||||
#| msgctxt "italian_invoice"
|
||||
#| msgid "Fiscal code"
|
||||
msgctxt "peppol_invoice"
|
||||
msgid "Visual copy"
|
||||
msgstr "Código fiscal"
|
||||
msgstr "Copia visual"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:175
|
||||
msgctxt "peppol_invoice"
|
||||
@@ -3843,6 +3840,9 @@ msgid ""
|
||||
"invoice for VAT purposes. The original invoice is issued in XML format and "
|
||||
"transmitted through the Peppol network."
|
||||
msgstr ""
|
||||
"Este documento PDF es una copia visual de la factura y no constituye una "
|
||||
"factura a efectos del IVA. La factura original se emite en formato XML y se "
|
||||
"transmite a través de la red Peppol."
|
||||
|
||||
#: pretix/base/logentrytype_registry.py:43
|
||||
msgid ""
|
||||
@@ -14387,10 +14387,8 @@ msgid "Canceled (fully or with paid fee)"
|
||||
msgstr "Cancelado (totalmente o con tarifa pagada)"
|
||||
|
||||
#: pretix/control/forms/filter.py:228
|
||||
#, fuzzy
|
||||
#| msgid "Cancel this position"
|
||||
msgid "Canceled (at least one position)"
|
||||
msgstr "Cancelar posición"
|
||||
msgstr "Cancelado (al menos una posición)"
|
||||
|
||||
#: pretix/control/forms/filter.py:229
|
||||
msgid "Cancellation requested"
|
||||
@@ -17130,10 +17128,9 @@ msgid "The voucher has been deleted."
|
||||
msgstr "El vale de compra fue eliminado."
|
||||
|
||||
#: pretix/control/logdisplay.py:584
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The voucher has been sent to {email} through the waiting list."
|
||||
#, python-brace-format
|
||||
msgid "The voucher has been assigned to {email} through the waiting list."
|
||||
msgstr "El vale se ha enviado a {email} a través de la lista de espera."
|
||||
msgstr "El vale se ha asignado a {email} a través de la lista de espera."
|
||||
|
||||
#: pretix/control/logdisplay.py:593
|
||||
#, python-brace-format
|
||||
@@ -27677,11 +27674,11 @@ msgstr ""
|
||||
"próximos minutos."
|
||||
|
||||
#: pretix/control/views/datasync.py:90 pretix/control/views/datasync.py:104
|
||||
#, fuzzy
|
||||
#| msgid "The voucher \"{voucher}\" has been used in the meantime."
|
||||
msgid ""
|
||||
"The sync job could not be found. It may have been processed in the meantime."
|
||||
msgstr "El vale de compra {voucher} ya ha sido utilizado."
|
||||
msgstr ""
|
||||
"No se ha encontrado la tarea de sincronización. Es posible que se haya "
|
||||
"procesado mientras tanto."
|
||||
|
||||
#: pretix/control/views/datasync.py:93 pretix/control/views/datasync.py:107
|
||||
msgid "The sync job is already in progress."
|
||||
@@ -28674,12 +28671,12 @@ msgstr ""
|
||||
"programado para {name}."
|
||||
|
||||
#: pretix/control/views/orders.py:2849 pretix/control/views/organizer.py:2207
|
||||
#, fuzzy
|
||||
#| msgid "You do not have sufficient permission to perform this export."
|
||||
msgid ""
|
||||
"Your user account does not have sufficient permission to run this report, "
|
||||
"therefore you cannot schedule it."
|
||||
msgstr "No tiene permiso suficiente para realizar esta exportación."
|
||||
msgstr ""
|
||||
"Su cuenta de usuario no tiene permisos suficientes para ejecutar este "
|
||||
"informe, por lo que no puede programarlo."
|
||||
|
||||
#: pretix/control/views/orders.py:2902 pretix/control/views/organizer.py:2259
|
||||
msgid ""
|
||||
@@ -29044,17 +29041,14 @@ msgid "A date can not be deleted if orders already have been placed."
|
||||
msgstr "No se puede borrar una fecha si ya se han realizado pedidos."
|
||||
|
||||
#: pretix/control/views/subevents.py:203
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The channel could not be deleted as some constraints (e.g. data created "
|
||||
#| "by plug-ins) did not allow it."
|
||||
msgctxt "subevent"
|
||||
msgid ""
|
||||
"The date could not be deleted as some constraints (e.g. data created by plug-"
|
||||
"ins) did not allow it. The date was disabled instead."
|
||||
msgstr ""
|
||||
"El canal no ha podido borrarse porque algunas restricciones (por ejemplo, "
|
||||
"datos creados por plug-ins) no lo permitían."
|
||||
"La fecha no se pudo eliminar debido a que algunas restricciones (por "
|
||||
"ejemplo, datos creados por complementos) no lo permitían. En su lugar, se "
|
||||
"desactivó la fecha."
|
||||
|
||||
#: pretix/control/views/subevents.py:207
|
||||
msgctxt "subevent"
|
||||
@@ -33878,18 +33872,12 @@ msgstr ""
|
||||
"formar un contrato válido."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:192
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "After you submitted your order using the button below, it will require "
|
||||
#| "approval by the event organizer before it can be confirmed and forms a "
|
||||
#| "valid contract."
|
||||
msgid ""
|
||||
"After you submitted your order using the button below, it will require "
|
||||
"approval by the event organizer."
|
||||
msgstr ""
|
||||
"Después de enviar su pedido usando el botón a continuación, requerirá la "
|
||||
"aprobación del organizador del evento antes de que pueda confirmarse y "
|
||||
"formar un contrato válido."
|
||||
"Después de enviar el pedido mediante el botón que aparece a continuación, "
|
||||
"será necesario que el organizador del evento lo apruebe."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:195
|
||||
msgid ""
|
||||
@@ -36448,10 +36436,8 @@ msgid "The selected date does not exist in this event series."
|
||||
msgstr "La fecha seleccionada no existe en esta serie de eventos."
|
||||
|
||||
#: pretix/presale/views/widget.py:412
|
||||
#, fuzzy
|
||||
#| msgid "The selected seat \"{seat}\" is not available."
|
||||
msgid "The selected date is not available."
|
||||
msgstr "El asiento seleccionado {seat} no está disponible."
|
||||
msgstr "La fecha seleccionada no está disponible."
|
||||
|
||||
#: pretix/presale/views/widget.py:476
|
||||
#, python-format
|
||||
|
||||
@@ -4,8 +4,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-08-29 17:00+0000\n"
|
||||
"Last-Translator: patch-works-be <webmaster@patch-works.be>\n"
|
||||
"PO-Revision-Date: 2025-09-30 16:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language: fr\n"
|
||||
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 5.13\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -810,28 +810,22 @@ msgstr ""
|
||||
"mail dans votre espace client."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:255
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" does not exist. Please check your {provider_name} "
|
||||
"settings."
|
||||
msgstr ""
|
||||
"Le champ « {field_name} » n'est pas valide pour {available_inputs}. Veuillez "
|
||||
"vérifier vos paramètres {provider_name}."
|
||||
"Le champ « {field_name} » n'existe pas. Veuillez vérifier vos paramètres "
|
||||
"pour {provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:262
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "Field \"{field_name}\" is not valid for {available_inputs}. Please check "
|
||||
#| "your {provider_name} settings."
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Field \"{field_name}\" requires {required_input}, but only got "
|
||||
"{available_inputs}. Please check your {provider_name} settings."
|
||||
msgstr ""
|
||||
"Le champ « {field_name} » n'est pas valide pour {available_inputs}. Veuillez "
|
||||
"vérifier vos paramètres {provider_name}."
|
||||
"Le champ « {field_name} » nécessite {required_input}, mais n'a reçu que "
|
||||
"{available_inputs}. Veuillez vérifier vos paramètres pour {provider_name}."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:273
|
||||
#, python-brace-format
|
||||
@@ -3564,6 +3558,10 @@ msgid ""
|
||||
"in accordance with the procedures and terms set forth in No. 89757/2018 of "
|
||||
"April 30, 2018, issued by the Director of the Revenue Agency."
|
||||
msgstr ""
|
||||
"Ce document PDF est une copie visuelle de la facture et ne constitue pas une "
|
||||
"facture aux fins de la TVA. La facture est émise au format XML, transmise "
|
||||
"conformément aux procédures et conditions énoncées dans le n° 89757/2018 du "
|
||||
"30 avril 2018, émis par le directeur de l'Agence des recettes."
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:141
|
||||
#, python-format
|
||||
@@ -3834,12 +3832,9 @@ msgid "Peppol participant ID"
|
||||
msgstr "Identifiant participant Peppol"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:170
|
||||
#, fuzzy
|
||||
#| msgctxt "italian_invoice"
|
||||
#| msgid "Fiscal code"
|
||||
msgctxt "peppol_invoice"
|
||||
msgid "Visual copy"
|
||||
msgstr "Code fiscal"
|
||||
msgstr "Copie visuelle"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:175
|
||||
msgctxt "peppol_invoice"
|
||||
@@ -3848,6 +3843,9 @@ msgid ""
|
||||
"invoice for VAT purposes. The original invoice is issued in XML format and "
|
||||
"transmitted through the Peppol network."
|
||||
msgstr ""
|
||||
"Ce document PDF est une reproduction visuelle de la facture et ne constitue "
|
||||
"pas une facture au sens de la TVA. La facture originale est émise au format "
|
||||
"XML et transmise via le réseau Peppol."
|
||||
|
||||
#: pretix/base/logentrytype_registry.py:43
|
||||
msgid ""
|
||||
@@ -14513,10 +14511,8 @@ msgid "Canceled (fully or with paid fee)"
|
||||
msgstr "Annulé (entièrement ou avec des frais payés)"
|
||||
|
||||
#: pretix/control/forms/filter.py:228
|
||||
#, fuzzy
|
||||
#| msgid "Cancel this position"
|
||||
msgid "Canceled (at least one position)"
|
||||
msgstr "Annuler cette position"
|
||||
msgstr "Annulé (au moins pour une position)"
|
||||
|
||||
#: pretix/control/forms/filter.py:229
|
||||
msgid "Cancellation requested"
|
||||
@@ -17268,10 +17264,9 @@ msgid "The voucher has been deleted."
|
||||
msgstr "Le bon a été supprimé."
|
||||
|
||||
#: pretix/control/logdisplay.py:584
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The voucher has been sent to {email} through the waiting list."
|
||||
#, python-brace-format
|
||||
msgid "The voucher has been assigned to {email} through the waiting list."
|
||||
msgstr "Le bon a été envoyé à {email} via la liste d'attente."
|
||||
msgstr "Le bon a été attribué à {email} via de la liste d'attente."
|
||||
|
||||
#: pretix/control/logdisplay.py:593
|
||||
#, python-brace-format
|
||||
@@ -27886,11 +27881,11 @@ msgstr ""
|
||||
"les prochaines minutes."
|
||||
|
||||
#: pretix/control/views/datasync.py:90 pretix/control/views/datasync.py:104
|
||||
#, fuzzy
|
||||
#| msgid "The voucher \"{voucher}\" has been used in the meantime."
|
||||
msgid ""
|
||||
"The sync job could not be found. It may have been processed in the meantime."
|
||||
msgstr "Le bon de réduction \"{voucher}\" a été utilisé entre-temps."
|
||||
msgstr ""
|
||||
"La tâche de synchronisation est introuvable. Elle a peut-être été traitée "
|
||||
"entre-temps."
|
||||
|
||||
#: pretix/control/views/datasync.py:93 pretix/control/views/datasync.py:107
|
||||
msgid "The sync job is already in progress."
|
||||
@@ -28894,14 +28889,12 @@ msgstr ""
|
||||
"planifié pour {name}."
|
||||
|
||||
#: pretix/control/views/orders.py:2849 pretix/control/views/organizer.py:2207
|
||||
#, fuzzy
|
||||
#| msgid "You do not have sufficient permission to perform this export."
|
||||
msgid ""
|
||||
"Your user account does not have sufficient permission to run this report, "
|
||||
"therefore you cannot schedule it."
|
||||
msgstr ""
|
||||
"Vous ne disposez pas des autorisations suffisantes pour effectuer cette "
|
||||
"exportation."
|
||||
"Votre compte utilisateur ne dispose pas des autorisations suffisantes pour "
|
||||
"exécuter ce rapport, vous ne pouvez donc pas le planifier."
|
||||
|
||||
#: pretix/control/views/orders.py:2902 pretix/control/views/organizer.py:2259
|
||||
msgid ""
|
||||
@@ -29272,17 +29265,14 @@ msgid "A date can not be deleted if orders already have been placed."
|
||||
msgstr "Une date ne peut pas être supprimée si des ordres ont déjà été passés."
|
||||
|
||||
#: pretix/control/views/subevents.py:203
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The channel could not be deleted as some constraints (e.g. data created "
|
||||
#| "by plug-ins) did not allow it."
|
||||
msgctxt "subevent"
|
||||
msgid ""
|
||||
"The date could not be deleted as some constraints (e.g. data created by plug-"
|
||||
"ins) did not allow it. The date was disabled instead."
|
||||
msgstr ""
|
||||
"Le canal de vente n'a pas pu être supprimé car certaines contraintes (par "
|
||||
"exemple, les données créées par les plug-ins) ne le permettent pas."
|
||||
"La date n'a pas pu être supprimée car certaines contraintes (par exemple, "
|
||||
"les données créées par les plug-ins) ne le permettaient pas. La date a donc "
|
||||
"été désactivée."
|
||||
|
||||
#: pretix/control/views/subevents.py:207
|
||||
msgctxt "subevent"
|
||||
@@ -34147,18 +34137,12 @@ msgstr ""
|
||||
"puisse être confirmée et forme un contrat valide."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:192
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "After you submitted your order using the button below, it will require "
|
||||
#| "approval by the event organizer before it can be confirmed and forms a "
|
||||
#| "valid contract."
|
||||
msgid ""
|
||||
"After you submitted your order using the button below, it will require "
|
||||
"approval by the event organizer."
|
||||
msgstr ""
|
||||
"Après avoir soumis votre commande en utilisant le bouton ci-dessous, elle "
|
||||
"nécessitera l’approbation de l’organisateur de l’événement avant qu’elle "
|
||||
"puisse être confirmée et forme un contrat valide."
|
||||
"Une fois que vous aurez soumis votre commande à l'aide du bouton ci-dessous, "
|
||||
"celle-ci devra être approuvée par l'organisateur de l'événement."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:195
|
||||
msgid ""
|
||||
@@ -36776,10 +36760,8 @@ msgid "The selected date does not exist in this event series."
|
||||
msgstr "La date sélectionnée n’existe pas dans cette série d’événements."
|
||||
|
||||
#: pretix/presale/views/widget.py:412
|
||||
#, fuzzy
|
||||
#| msgid "The selected seat \"{seat}\" is not available."
|
||||
msgid "The selected date is not available."
|
||||
msgstr "La place {seat} sélectionné n'est pas disponible."
|
||||
msgstr "La date sélectionnée n'est pas disponible."
|
||||
|
||||
#: pretix/presale/views/widget.py:476
|
||||
#, python-format
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-09-10 21:00+0000\n"
|
||||
"Last-Translator: Davide Wayan Mores <racecondition99@gmail.com>\n"
|
||||
"PO-Revision-Date: 2025-09-26 13:02+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"it/>\n"
|
||||
"Language: it\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.13.2\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -3588,6 +3588,10 @@ msgid ""
|
||||
"in accordance with the procedures and terms set forth in No. 89757/2018 of "
|
||||
"April 30, 2018, issued by the Director of the Revenue Agency."
|
||||
msgstr ""
|
||||
"Il presente documento pdf non costituisce fattura ai fini IVA. La fattura si "
|
||||
"intende emessa nel formato xml trasmesso nei modi e termini di cui al "
|
||||
"Provvedimento n. 89757/2018 del 30 aprile 2018 del direttore dell'Agenzia "
|
||||
"delle Entrate."
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:141
|
||||
#, python-format
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,16 +7,16 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-08-26 19:00+0000\n"
|
||||
"PO-Revision-Date: 2025-09-30 01:00+0000\n"
|
||||
"Last-Translator: Jan Van Haver <jan.van.haver@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
|
||||
">\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.13\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -3808,6 +3808,8 @@ msgstr "Evenementdatum: {date_range}"
|
||||
msgid ""
|
||||
"A Peppol participant ID always starts with a prefix, followed by a colon (:)."
|
||||
msgstr ""
|
||||
"Een Peppol deelnemer-ID begint altijd met een prefix, gevolgd door een "
|
||||
"dubbele punt (:)."
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:132
|
||||
#, python-format
|
||||
@@ -3815,6 +3817,8 @@ msgid ""
|
||||
"The Peppol participant ID prefix %(number)s is not known to our system. "
|
||||
"Please reach out to us if you are sure this ID is correct."
|
||||
msgstr ""
|
||||
"De prefix van de Peppol deelnemer-ID %(number)s is niet bekend in ons "
|
||||
"systeem. Neem contact met ons op als u zeker weet dat deze ID correct is."
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:136
|
||||
#, python-format
|
||||
@@ -3822,17 +3826,18 @@ msgid ""
|
||||
"The Peppol participant ID does not match the validation rules for the prefix "
|
||||
"%(number)s. Please reach out to us if you are sure this ID is correct."
|
||||
msgstr ""
|
||||
"De Peppol deelnemer-ID komt niet overeen met de validatieregels voor het "
|
||||
"prefix %(number)s. Neem contact met ons op als u zeker weet dat deze ID "
|
||||
"correct is."
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:156
|
||||
msgid "Peppol participant ID"
|
||||
msgstr ""
|
||||
msgstr "Peppol deelnemer-ID"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:170
|
||||
#, fuzzy
|
||||
#| msgid "Gift card code"
|
||||
msgctxt "peppol_invoice"
|
||||
msgid "Visual copy"
|
||||
msgstr "Cadeauboncode"
|
||||
msgstr "Visuele kopie"
|
||||
|
||||
#: pretix/base/invoicing/peppol.py:175
|
||||
msgctxt "peppol_invoice"
|
||||
@@ -3841,6 +3846,9 @@ msgid ""
|
||||
"invoice for VAT purposes. The original invoice is issued in XML format and "
|
||||
"transmitted through the Peppol network."
|
||||
msgstr ""
|
||||
"Dit PDF-document is een visuele kopie van de factuur en vormt geen factuur "
|
||||
"voor BTW-doeleinden. De originele factuur wordt opgemaakt in XML-formaat en "
|
||||
"verzonden via het Peppol-netwerk."
|
||||
|
||||
#: pretix/base/logentrytype_registry.py:43
|
||||
msgid ""
|
||||
|
||||
@@ -170,7 +170,7 @@ OPTIONS = OrderedDict([
|
||||
'cols': 2,
|
||||
'rows': 6,
|
||||
'margins': [28.5 * mm, 30 * mm, 28.5 * mm, 30 * mm],
|
||||
'offsets': [93 * mm, 60 * mm],
|
||||
'offsets': [75 * mm, 40 * mm],
|
||||
'pagesize': pagesizes.A4,
|
||||
}),
|
||||
('herma_50x80', {
|
||||
|
||||
193
src/pretix/static/npm_dir/package-lock.json
generated
193
src/pretix/static/npm_dir/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "pretix",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
@@ -18,18 +18,6 @@
|
||||
"vue-template-compiler": "^2.7.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.1.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -53,20 +41,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.3",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.4",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@babel/traverse": "^7.28.4",
|
||||
"@babel/types": "^7.28.4",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -115,15 +103,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-annotate-as-pure": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
|
||||
@@ -401,23 +380,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
|
||||
"integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
|
||||
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.2"
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
|
||||
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.2"
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -1475,16 +1454,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
|
||||
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
|
||||
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@babel/types": "^7.28.4",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1492,9 +1471,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
@@ -1504,15 +1483,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@@ -1523,14 +1508,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
@@ -3802,15 +3779,6 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.1.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -3827,20 +3795,20 @@
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.3",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.4",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@babel/traverse": "^7.28.4",
|
||||
"@babel/types": "^7.28.4",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -3870,17 +3838,6 @@
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"requires": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-annotate-as-pure": {
|
||||
@@ -4074,20 +4031,20 @@
|
||||
}
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
|
||||
"integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
|
||||
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
|
||||
"requires": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.2"
|
||||
"@babel/types": "^7.28.4"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
|
||||
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.28.2"
|
||||
"@babel/types": "^7.28.4"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
|
||||
@@ -4718,35 +4675,44 @@
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
|
||||
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
|
||||
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@babel/types": "^7.28.4",
|
||||
"debug": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
}
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"requires": {
|
||||
"@jridgewell/set-array": "^1.0.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
@@ -4754,11 +4720,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
|
||||
},
|
||||
"@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
|
||||
@@ -231,7 +231,9 @@ TEST_INVOICE_RES = {
|
||||
"description": "Budget Ticket<br />Attendee: Peter",
|
||||
'subevent': None,
|
||||
'event_date_from': '2017-12-27T10:00:00Z',
|
||||
'event_date_to': None,
|
||||
'event_date_to': '2017-12-27T10:00:00Z',
|
||||
'period_start': '2017-12-27T10:00:00Z',
|
||||
'period_end': '2017-12-27T10:00:00Z',
|
||||
'event_location': None,
|
||||
'attendee_name': 'Peter',
|
||||
'item': None,
|
||||
@@ -249,7 +251,9 @@ TEST_INVOICE_RES = {
|
||||
"description": "Payment fee",
|
||||
'subevent': None,
|
||||
'event_date_from': '2017-12-27T10:00:00Z',
|
||||
'event_date_to': None,
|
||||
'event_date_to': '2017-12-27T10:00:00Z',
|
||||
'period_start': '2017-12-27T10:00:00Z',
|
||||
'period_end': '2017-12-27T10:00:00Z',
|
||||
'event_location': None,
|
||||
'attendee_name': None,
|
||||
'fee_type': "payment",
|
||||
|
||||
@@ -608,7 +608,9 @@ def test_order_create_invoice(token_client, organizer, event, order):
|
||||
'description': 'Budget Ticket<br />Attendee: Peter',
|
||||
'subevent': None,
|
||||
'event_date_from': '2017-12-27T10:00:00Z',
|
||||
'event_date_to': None,
|
||||
'event_date_to': '2017-12-27T10:00:00Z',
|
||||
'period_start': '2017-12-27T10:00:00Z',
|
||||
'period_end': '2017-12-27T10:00:00Z',
|
||||
'event_location': None,
|
||||
'fee_type': None,
|
||||
'fee_internal_type': None,
|
||||
@@ -626,7 +628,9 @@ def test_order_create_invoice(token_client, organizer, event, order):
|
||||
'description': 'Payment fee',
|
||||
'subevent': None,
|
||||
'event_date_from': '2017-12-27T10:00:00Z',
|
||||
'event_date_to': None,
|
||||
'event_date_to': '2017-12-27T10:00:00Z',
|
||||
'period_start': '2017-12-27T10:00:00Z',
|
||||
'period_end': '2017-12-27T10:00:00Z',
|
||||
'event_location': None,
|
||||
'fee_type': "payment",
|
||||
'fee_internal_type': None,
|
||||
|
||||
@@ -327,6 +327,26 @@ def test_enqueue_order_twice(event):
|
||||
SimpleOrderSync.enqueue_order(order, 'testcase_2nd')
|
||||
|
||||
|
||||
class DoNothingSync(SimpleOrderSync):
|
||||
|
||||
def should_sync_order(self, order):
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_should_not_sync(event):
|
||||
_register_with_fake_plugin_name(datasync_providers, DoNothingSync, 'testplugin')
|
||||
|
||||
DoNothingSync.fake_api_client = FakeSyncAPI()
|
||||
|
||||
for order in event.orders.order_by("code").all():
|
||||
DoNothingSync.enqueue_order(order, 'testcase')
|
||||
|
||||
sync_all()
|
||||
|
||||
assert DoNothingSync.fake_api_client.fake_database == {}
|
||||
|
||||
|
||||
StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', (
|
||||
'id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings', 'association_mappings'
|
||||
))
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
@@ -42,6 +42,7 @@ from django.utils.itercompat import is_iterable
|
||||
from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.invoice import addon_aware_groupby
|
||||
from pretix.base.models import (
|
||||
@@ -62,7 +63,8 @@ def env():
|
||||
with scope(organizer=o):
|
||||
event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||
date_from=datetime(2024, 12, 1, 9, 0, 0, tzinfo=timezone.utc),
|
||||
plugins='pretix.plugins.banktransfer'
|
||||
)
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
@@ -660,3 +662,154 @@ def test_addon_aware_groupby():
|
||||
[True, 102, 3.00],
|
||||
]],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("period", ["auto", "event_date"])
|
||||
def test_period_from_event_start(env, period):
|
||||
event, order = env
|
||||
event.settings.invoice_period = period
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == event.date_from
|
||||
assert l1.period_end == event.date_from
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("period", ["auto", "event_date"])
|
||||
def test_period_from_event_range(env, period):
|
||||
event, order = env
|
||||
event.date_to = event.date_from + timedelta(days=1)
|
||||
event.settings.invoice_period = period
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == event.date_from
|
||||
assert l1.period_end == event.date_to
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("period", ["auto", "auto_no_event"])
|
||||
def test_period_from_ticket_validity(env, period):
|
||||
event, order = env
|
||||
p1 = order.positions.first()
|
||||
p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
|
||||
p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
|
||||
p1.save()
|
||||
event.date_to = event.date_from + timedelta(days=1)
|
||||
event.settings.invoice_period = period
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == p1.valid_from
|
||||
assert l1.period_end == p1.valid_until
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("period", ["auto", "auto_no_event"])
|
||||
def test_period_from_subevent(env, period):
|
||||
event, order = env
|
||||
event.has_subevents = True
|
||||
event.save()
|
||||
se1 = event.subevents.create(
|
||||
name=event.name,
|
||||
active=True,
|
||||
date_from=datetime((now().year + 1), 7, 31, 9, 0, 0, tzinfo=timezone.utc),
|
||||
date_to=datetime((now().year + 1), 7, 31, 17, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
p1 = order.positions.first()
|
||||
p1.subevent = se1
|
||||
p1.save()
|
||||
event.date_to = event.date_from + timedelta(days=1)
|
||||
event.settings.invoice_period = period
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == se1.date_from
|
||||
assert l1.period_end == se1.date_to
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("period", ["auto", "auto_no_event"])
|
||||
def test_period_from_memberships(env, period):
|
||||
event, order = env
|
||||
event.date_to = event.date_from + timedelta(days=1)
|
||||
event.settings.invoice_period = period
|
||||
p1 = order.positions.first()
|
||||
membershiptype = event.organizer.membership_types.create(
|
||||
name=LazyI18nString({"en": "Week pass"}),
|
||||
transferable=True,
|
||||
allow_parallel_usage=False,
|
||||
max_usages=15,
|
||||
)
|
||||
customer = event.organizer.customers.create(
|
||||
identifier="8WSAJCJ",
|
||||
email="foo@example.org",
|
||||
name_parts={"_legacy": "Foo"},
|
||||
name_cached="Foo",
|
||||
is_verified=False,
|
||||
)
|
||||
m = customer.memberships.create(
|
||||
membership_type=membershiptype,
|
||||
granted_in=p1,
|
||||
date_start=datetime(2021, 4, 1, 0, 0, 0, 0, tzinfo=timezone.utc),
|
||||
date_end=datetime(2021, 4, 8, 23, 59, 59, 999999, tzinfo=timezone.utc),
|
||||
attendee_name_parts={
|
||||
"_scheme": "given_family",
|
||||
'given_name': 'John',
|
||||
'family_name': 'Doe',
|
||||
}
|
||||
)
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == m.date_start
|
||||
assert l1.period_end == m.date_end
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_period_auto_no_event_from_invoice(env):
|
||||
event, order = env
|
||||
event.settings.invoice_period = "auto_no_event"
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert abs(l1.period_start - now()) < timedelta(seconds=10)
|
||||
assert abs(l1.period_end - now()) < timedelta(seconds=10)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_period_always_invoice_date(env):
|
||||
event, order = env
|
||||
p1 = order.positions.first()
|
||||
p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
|
||||
p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
|
||||
p1.save()
|
||||
event.settings.invoice_period = "invoice_date"
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert abs(l1.period_start - now()) < timedelta(seconds=10)
|
||||
assert abs(l1.period_end - now()) < timedelta(seconds=10)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_period_always_event_date(env):
|
||||
event, order = env
|
||||
p1 = order.positions.first()
|
||||
p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
|
||||
p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
|
||||
p1.save()
|
||||
event.settings.invoice_period = "event_date"
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == event.date_from
|
||||
assert l1.period_end == event.date_from
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_period_always_order_date(env):
|
||||
event, order = env
|
||||
p1 = order.positions.first()
|
||||
p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
|
||||
p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
|
||||
p1.save()
|
||||
event.settings.invoice_period = "order_date"
|
||||
inv = generate_invoice(order)
|
||||
l1 = inv.lines.first()
|
||||
assert l1.period_start == order.datetime
|
||||
assert l1.period_end == order.datetime
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import os
|
||||
import re
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
@@ -42,9 +40,7 @@ from django.core import mail as djmail
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.models import Event, Organizer, User
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
@@ -52,14 +48,10 @@ from pretix.base.services.mail import mail
|
||||
@pytest.fixture
|
||||
def env():
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
prop1 = o.meta_properties.get_or_create(name="Test")[0]
|
||||
prop2 = o.meta_properties.get_or_create(name="Website")[0]
|
||||
event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now()
|
||||
)
|
||||
event.meta_values.update_or_create(property=prop1, defaults={'value': "*Beep*"})
|
||||
event.meta_values.update_or_create(property=prop2, defaults={'value': "https://example.com"})
|
||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
user.email = 'dummy@dummy.dummy'
|
||||
user.save()
|
||||
@@ -166,109 +158,8 @@ def test_send_mail_with_user_locale(env):
|
||||
def test_sendmail_placeholder(env):
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', {"event": event.name}, event)
|
||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', {"event": event}, event)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
assert djmail.outbox[0].subject == 'Dummy Test subject'
|
||||
|
||||
|
||||
def _extract_html(mail):
|
||||
for content, mimetype in mail.alternatives:
|
||||
if "multipart/related" in mimetype:
|
||||
for sp in content._payload:
|
||||
if isinstance(sp, MIMEText):
|
||||
return sp._payload
|
||||
break
|
||||
elif "text/html" in mimetype:
|
||||
return content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_placeholder_html_rendering_from_template(env):
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "<strong>event & co. kg</strong>"
|
||||
event.save()
|
||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456",
|
||||
), event)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
assert 'Event name: <strong>event & co. kg</strong>' 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 'Event website: [<strong>event & co. kg</strong>](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
|
||||
assert '<' not in djmail.outbox[0].body
|
||||
assert '&' not in djmail.outbox[0].body
|
||||
html = _extract_html(djmail.outbox[0])
|
||||
|
||||
assert '<strong>event' not in html
|
||||
assert 'Event name: <strong>event & co. kg</strong>' 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 re.search(
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_placeholder_html_rendering_from_string(env):
|
||||
template = LazyI18nString({
|
||||
"en": "Event name: {event}\n\nPayment info:\n{payment_info}\n\n**Meta**: {meta_Test}\n\n"
|
||||
"Event website: [{event}](https://example.org/{event_slug})\n\n"
|
||||
"Other website: [{event}]({meta_Website})\n\n"
|
||||
"URL: {url}\n\n"
|
||||
"URL with text: <a href=\"{url}\">Test</a>"
|
||||
})
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "<strong>event & co. kg</strong>"
|
||||
event.save()
|
||||
ctx = get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456",
|
||||
)
|
||||
ctx["url"] = "https://google.com"
|
||||
mail('dummy@dummy.dummy', '{event} Test subject', template, ctx, event)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
assert 'Event name: <strong>event & co. kg</strong>' 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>](https://example.com)' 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 'URL: https://google.com' in djmail.outbox[0].body
|
||||
assert 'URL with text: <a href="https://google.com">Test</a>' in djmail.outbox[0].body
|
||||
assert '<' not in djmail.outbox[0].body
|
||||
assert '&' not in djmail.outbox[0].body
|
||||
html = _extract_html(djmail.outbox[0])
|
||||
assert '<strong>event' not in html
|
||||
assert 'Event name: <strong>event & co. kg</strong>' 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 re.search(
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'URL: <a href="https://google.com" rel="noopener" style="[^"]+" target="_blank">https://google.com</a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'URL with text: <a href="https://google.com" rel="noopener" style="[^"]+" target="_blank">Test</a>',
|
||||
html
|
||||
)
|
||||
|
||||
@@ -40,5 +40,6 @@ def test_format_alternatives():
|
||||
)
|
||||
}
|
||||
|
||||
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_IGNORE_RICH) == "Foo {bar}"
|
||||
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_PLAIN) == "Foo plain text"
|
||||
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_HTML) == "Foo <span>HTML version</span>"
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_sales_channel_all(event, item, order, checkin_list):
|
||||
mode=AutoCheckinRule.MODE_PLACED,
|
||||
all_sales_channels=True,
|
||||
)
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
|
||||
@@ -67,12 +67,12 @@ def test_sales_channel_limit(event, item, order, checkin_list):
|
||||
all_sales_channels=False,
|
||||
)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert not order.positions.first().checkins.exists()
|
||||
|
||||
acr.limit_sales_channels.add(order.sales_channel)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_items_all(event, item, order, checkin_list):
|
||||
mode=AutoCheckinRule.MODE_PLACED,
|
||||
all_products=True,
|
||||
)
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ def test_items_limit(event, item, order, checkin_list):
|
||||
all_products=False,
|
||||
)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert not order.positions.first().checkins.exists()
|
||||
|
||||
acr.limit_products.add(item)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def test_variations_limit_mixed_order(event, item, order, checkin_list):
|
||||
)
|
||||
acr.limit_variations.add(var)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
assert not order.positions.last().checkins.exists()
|
||||
|
||||
@@ -143,19 +143,19 @@ def test_variations_limit(event, item, order, checkin_list):
|
||||
all_products=False,
|
||||
)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert not order.positions.first().checkins.exists()
|
||||
|
||||
acr.limit_variations.add(var)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
order.positions.first().checkins.all().delete()
|
||||
acr.limit_products.add(item)
|
||||
acr.limit_variations.clear()
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ def test_mode_placed(event, item, order, checkin_list):
|
||||
order_paid.send(event, order=order)
|
||||
assert not order.positions.first().checkins.exists()
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert order.positions.first().checkins.exists()
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ def test_mode_paid(event, item, order, checkin_list):
|
||||
mode=AutoCheckinRule.MODE_PAID,
|
||||
)
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
assert not order.positions.first().checkins.exists()
|
||||
|
||||
order_paid.send(event, order=order)
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
{% load i18n %}
|
||||
This is a test file for sending mails.
|
||||
Event name: {event}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
|
||||
|
||||
Payment info:
|
||||
{payment_info}
|
||||
|
||||
**Meta**: {meta_Test}
|
||||
|
||||
Event website: [{event}](https://example.org/{event_slug})
|
||||
Other website: [{event}]({meta_Website})
|
||||
Reference in New Issue
Block a user