mirror of
https://github.com/pretix/pretix.git
synced 2026-05-17 17:14:04 +00:00
Compare commits
5 Commits
taxrate-de
...
fix-mail-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1efe8e01b8 | ||
|
|
fe2132435c | ||
|
|
f4fcca19a4 | ||
|
|
24d26a9455 | ||
|
|
589f51454e |
@@ -31,6 +31,7 @@ RUN apt-get update && \
|
||||
mkdir /etc/pretix && \
|
||||
mkdir /data && \
|
||||
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
|
||||
chmod 0755 /pretix \
|
||||
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||
mkdir /static && \
|
||||
mkdir /etc/supervisord
|
||||
|
||||
@@ -16,6 +16,7 @@ Field Type Description
|
||||
id integer Internal ID of the program time
|
||||
start datetime The start date time for this program time slot.
|
||||
end datetime The end date time for this program time slot.
|
||||
location multi-lingual string The program time slot's location (or ``null``)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: TODO
|
||||
@@ -54,17 +55,20 @@ Endpoints
|
||||
{
|
||||
"id": 2,
|
||||
"start": "2025-08-14T22:00:00Z",
|
||||
"end": "2025-08-15T00:00:00Z"
|
||||
"end": "2025-08-15T00:00:00Z",
|
||||
"location": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"start": "2025-08-12T22:00:00Z",
|
||||
"end": "2025-08-13T22:00:00Z"
|
||||
"end": "2025-08-13T22:00:00Z",
|
||||
"location": null
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"start": "2025-08-15T22:00:00Z",
|
||||
"end": "2025-08-17T22:00:00Z"
|
||||
"end": "2025-08-17T22:00:00Z",
|
||||
"location": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -99,7 +103,8 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"start": "2025-08-15T22:00:00Z",
|
||||
"end": "2025-10-27T23:00:00Z"
|
||||
"end": "2025-10-27T23:00:00Z",
|
||||
"location": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -125,7 +130,8 @@ Endpoints
|
||||
|
||||
{
|
||||
"start": "2025-08-15T10:00:00Z",
|
||||
"end": "2025-08-15T22:00:00Z"
|
||||
"end": "2025-08-15T22:00:00Z",
|
||||
"location": null
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -139,7 +145,8 @@ Endpoints
|
||||
{
|
||||
"id": 17,
|
||||
"start": "2025-08-15T10:00:00Z",
|
||||
"end": "2025-08-15T22:00:00Z"
|
||||
"end": "2025-08-15T22:00:00Z",
|
||||
"location": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.db.models import prefetch_related_objects
|
||||
from rest_framework import serializers
|
||||
@@ -136,30 +135,3 @@ class SalesChannelMigrationMixin:
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
return value
|
||||
|
||||
|
||||
class CompatDecimalField(serializers.DecimalField):
|
||||
"""
|
||||
Historically, pretix recorded tax rates as decimals with two places. Today, pretix supports tax rates with up to
|
||||
four places. Since our API outputs decimals with the stored precision, this would have changed the API output from
|
||||
"19.00" to "19.0000" without warning. While this is semantically the same thing, we need to assume some pretix API
|
||||
users might run into trouble, either because they treat the value as a string and then map something
|
||||
(e.g. ``if tax_rate == "19.00"``) or process it with a language where this is a significant difference. For example,
|
||||
while in Python ``Decimal("19.00") == Decimal("19.0000")`` is true, in Java
|
||||
``(new BigDecimal("19.00")).equals(new BigDecimal("19.0000"))`` is false and only
|
||||
``(new BigDecimal("19.00")).compareTo(new BigDecimal("19.0000")) == 0`` is true.
|
||||
|
||||
Therefore, we stay backwards compatible by outputting two decimal places *as long as the trailing digits are zero-valued.
|
||||
"""
|
||||
|
||||
regex = re.compile(r"^([0-9]+\.[0-9]{2})0+$")
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.localize:
|
||||
raise ValueError("localization not supported")
|
||||
value = super().to_representation(value)
|
||||
if value and "." not in value:
|
||||
return f"{value}.00"
|
||||
if m := self.regex.match(value):
|
||||
return m.group(1)
|
||||
return value
|
||||
|
||||
@@ -48,7 +48,7 @@ from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatDecimalField, CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.fields import PluginsField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -681,7 +681,6 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
|
||||
@@ -42,9 +42,7 @@ from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatDecimalField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -193,7 +191,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemProgramTime
|
||||
fields = ('start', 'end')
|
||||
fields = ('start', 'end', 'location')
|
||||
|
||||
|
||||
class ItemBundleSerializer(serializers.ModelSerializer):
|
||||
@@ -224,7 +222,7 @@ class ItemBundleSerializer(serializers.ModelSerializer):
|
||||
class ItemProgramTimeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemProgramTime
|
||||
fields = ('id', 'start', 'end')
|
||||
fields = ('id', 'start', 'end', 'location')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -278,10 +276,10 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||
return value
|
||||
|
||||
|
||||
class ItemTaxRateField(CompatDecimalField):
|
||||
class ItemTaxRateField(serializers.Field):
|
||||
def to_representation(self, i):
|
||||
if i.tax_rule:
|
||||
return super().to_representation(Decimal(i.tax_rule.rate))
|
||||
return str(Decimal(i.tax_rule.rate))
|
||||
else:
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
@@ -291,7 +289,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True, max_digits=7, decimal_places=4)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
|
||||
@@ -41,7 +41,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers import CompatDecimalField, CompatibleJSONField
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -591,7 +591,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
|
||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = OrderPositionListSerializer
|
||||
@@ -748,8 +747,6 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
|
||||
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
||||
@@ -1419,6 +1416,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*[q for q, d in quota_diff_for_locking.items() if d > 0])
|
||||
qa.compute()
|
||||
v_avail = {}
|
||||
|
||||
# These are not technically correct as diff use due to the time offset applied above, so let's prevent accidental
|
||||
# use further down
|
||||
@@ -1448,11 +1446,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
voucher_usage[v] += 1
|
||||
if voucher_usage[v] > 0:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
|
||||
).exclude(pk__in=[cp.pk for cp in delete_cps])
|
||||
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < voucher_usage[v]:
|
||||
if v not in v_avail:
|
||||
v.refresh_from_db(fields=['redeemed'])
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=v) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
|
||||
).exclude(pk__in=[cp.pk for cp in delete_cps])
|
||||
v_avail[v] = v.max_usages - v.redeemed - redeemed_in_carts.count()
|
||||
if v_avail[v] < voucher_usage[v]:
|
||||
errs[i]['voucher'] = [
|
||||
'The voucher has already been used the maximum number of times.'
|
||||
]
|
||||
@@ -1900,7 +1900,6 @@ 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")
|
||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
@@ -1984,7 +1983,6 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class TransactionSerializer(I18nAwareModelSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
|
||||
19
src/pretix/base/migrations/0299_itemprogramtime_location.py
Normal file
19
src/pretix/base/migrations/0299_itemprogramtime_location.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.27 on 2026-01-21 12:06
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0298_pluggable_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="itemprogramtime",
|
||||
name="location",
|
||||
field=i18nfield.fields.I18nTextField(max_length=200, null=True),
|
||||
)
|
||||
]
|
||||
@@ -1,48 +0,0 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-15 20:10
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.helpers.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0298_pluggable_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='tax_rate',
|
||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceline',
|
||||
name='tax_rate',
|
||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='tax_rate',
|
||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='tax_rate',
|
||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='tax_rate',
|
||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taxrule',
|
||||
name='rate',
|
||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||
),
|
||||
|
||||
]
|
||||
@@ -49,7 +49,6 @@ from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
from pretix.helpers.countries import FastCountryField
|
||||
from pretix.helpers.models import NormalizedDecimalField
|
||||
|
||||
|
||||
def invoice_filename(instance, filename: str) -> str:
|
||||
@@ -451,7 +450,7 @@ class InvoiceLine(models.Model):
|
||||
description = models.TextField()
|
||||
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
|
||||
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_rate = NormalizedDecimalField(max_digits=7, decimal_places=4, default=Decimal('0'))
|
||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||
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)
|
||||
|
||||
@@ -2306,10 +2306,17 @@ class ItemProgramTime(models.Model):
|
||||
:type start: datetime
|
||||
:param end: The date and time this program time ends
|
||||
:type end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
"""
|
||||
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
|
||||
start = models.DateTimeField(verbose_name=_("Start"))
|
||||
end = models.DateTimeField(verbose_name=_("End"))
|
||||
location = I18nTextField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if hasattr(self, 'item') and self.item and self.item.event.has_subevents:
|
||||
|
||||
@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.models import NormalizedDecimalField
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -2335,8 +2334,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
)
|
||||
description = models.CharField(max_length=190, blank=True)
|
||||
internal_type = models.CharField(max_length=255, blank=True)
|
||||
tax_rate = NormalizedDecimalField(
|
||||
max_digits=7, decimal_places=4,
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
@@ -2534,8 +2533,8 @@ class OrderPosition(AbstractPosition):
|
||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||
)
|
||||
|
||||
tax_rate = NormalizedDecimalField(
|
||||
max_digits=7, decimal_places=4,
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
@@ -3053,8 +3052,8 @@ class Transaction(models.Model):
|
||||
price_includes_rounding_correction = models.DecimalField(
|
||||
max_digits=13, decimal_places=2, default=Decimal("0.00")
|
||||
)
|
||||
tax_rate = NormalizedDecimalField(
|
||||
max_digits=7, decimal_places=4,
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
@@ -3169,8 +3168,8 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Limit for extending expiration date"),
|
||||
null=True
|
||||
)
|
||||
tax_rate = NormalizedDecimalField(
|
||||
max_digits=7, decimal_places=4, default=Decimal('0'),
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=Decimal('0.00'),
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_code = models.CharField(
|
||||
|
||||
@@ -40,7 +40,6 @@ from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.countries import FastCountryField
|
||||
from pretix.helpers.models import NormalizedDecimalField
|
||||
|
||||
|
||||
class TaxedPrice:
|
||||
@@ -336,9 +335,9 @@ class TaxRule(LoggedModel):
|
||||
max_length=190,
|
||||
choices=TAX_CODE_LISTS,
|
||||
)
|
||||
rate = NormalizedDecimalField(
|
||||
max_digits=7,
|
||||
decimal_places=4,
|
||||
rate = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
validators=[
|
||||
MaxValueValidator(
|
||||
limit_value=Decimal("100.00"),
|
||||
|
||||
@@ -498,9 +498,9 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
) if op.valid_until else ""
|
||||
}),
|
||||
("program_times", {
|
||||
"label": _("Program times: date and time"),
|
||||
"label": _("Program times"),
|
||||
"editor_sample": _(
|
||||
"2017-05-31 10:00 – 12:00\n2017-05-31 14:00 – 16:00\n2017-05-31 14:00 – 2017-06-01 14:00"),
|
||||
"2017-05-31 10:00 – 12:00, Room 1\n2017-05-31 14:00 – 16:00, Room 2\n2017-05-31 14:00 – 2017-06-01 14:00, Building A"),
|
||||
"evaluate": lambda op, order, ev: get_program_times(op, ev)
|
||||
}),
|
||||
("medium_identifier", {
|
||||
@@ -748,13 +748,19 @@ def get_seat(op: OrderPosition):
|
||||
|
||||
|
||||
def get_program_times(op: OrderPosition, ev: Event):
|
||||
return '\n'.join([
|
||||
datetimerange(
|
||||
pt.start.astimezone(ev.timezone),
|
||||
pt.end.astimezone(ev.timezone),
|
||||
as_html=False
|
||||
) for pt in op.item.program_times.all()
|
||||
])
|
||||
ptstr = []
|
||||
for pt in op.item.program_times.all():
|
||||
ptstr.append([
|
||||
datetimerange(
|
||||
pt.start.astimezone(ev.timezone),
|
||||
pt.end.astimezone(ev.timezone),
|
||||
as_html=False
|
||||
),
|
||||
(', ' + ', '.join(
|
||||
l.strip() for l in str(pt.location).splitlines() if l.strip())
|
||||
) if str(pt.location).strip() else ''
|
||||
])
|
||||
return '\n'.join(''.join(l) for l in ptstr)
|
||||
|
||||
|
||||
def generate_compressed_addon_list(op, order, event, only_checked_in=False):
|
||||
|
||||
@@ -727,8 +727,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
_check_date(event, time_machine_now_dt)
|
||||
|
||||
products_seen = Counter()
|
||||
q_avail = Counter()
|
||||
v_avail = Counter()
|
||||
v_usages = Counter()
|
||||
v_budget = {}
|
||||
deleted_positions = set()
|
||||
@@ -793,6 +791,9 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
shared_lock_objects=[event]
|
||||
)
|
||||
|
||||
q_avail = Counter()
|
||||
v_avail = Counter()
|
||||
|
||||
# Check maximum order size
|
||||
limit = min(int(event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
|
||||
if sum(1 for cp in sorted_positions if not cp.addon_to) > limit:
|
||||
|
||||
@@ -26,8 +26,6 @@ from babel.numbers import format_currency
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.utils import formats
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from pretix.base.i18n import get_babel_locale
|
||||
|
||||
@@ -84,19 +82,3 @@ def money_numberfield_filter(value: Decimal, arg=''):
|
||||
|
||||
places = settings.CURRENCY_PLACES.get(arg, 2)
|
||||
return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP))
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def tax_rate_format(number):
|
||||
"""
|
||||
Display a Decimal to its significant decimal places, used for tax rates.
|
||||
"""
|
||||
assert isinstance(number, Decimal)
|
||||
return mark_safe(
|
||||
formats.number_format(
|
||||
number.normalize(),
|
||||
-number.as_tuple().exponent,
|
||||
use_l10n=True,
|
||||
force_grouping=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -574,7 +574,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
|
||||
count=b.count, designated_price=b.designated_price)
|
||||
for pt in self.cleaned_data['copy_from'].program_times.all():
|
||||
instance.program_times.create(start=pt.start, end=pt.end)
|
||||
instance.program_times.create(start=pt.start, end=pt.end, location=pt.location)
|
||||
|
||||
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
|
||||
|
||||
@@ -1354,6 +1354,10 @@ class ItemProgramTimeForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||
'Sample Conference Center, Heidelberg, Germany'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemProgramTime
|
||||
@@ -1361,6 +1365,7 @@ class ItemProgramTimeForm(I18nModelForm):
|
||||
fields = [
|
||||
'start',
|
||||
'end',
|
||||
'location'
|
||||
]
|
||||
field_classes = {
|
||||
'start': forms.SplitDateTimeField,
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.start layout="control" %}
|
||||
{% bootstrap_field form.end layout="control" %}
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -59,6 +60,7 @@
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.start layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.end layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.location layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -156,11 +156,11 @@
|
||||
<br/>
|
||||
<small class="text-muted">
|
||||
{% if not i.tax_rule.price_includes_tax %}
|
||||
{% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name %}
|
||||
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
|
||||
incl. {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
@@ -670,7 +670,7 @@
|
||||
{% if line.tax_rate %}
|
||||
<br/>
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
|
||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -680,7 +680,7 @@
|
||||
{% if line.tax_rate and line.price %}
|
||||
<br/>
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
|
||||
incl. {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -720,7 +720,7 @@
|
||||
{% if fee.tax_rate %}
|
||||
<br/>
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -730,7 +730,7 @@
|
||||
{% if fee.tax_rate %}
|
||||
<br/>
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
incl. {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import copy
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models.fields import DecimalField
|
||||
|
||||
|
||||
class Thumbnail(models.Model):
|
||||
@@ -56,33 +54,3 @@ def flatten_choices(choices):
|
||||
yield from label_or_nested
|
||||
else:
|
||||
yield value_or_group, label_or_nested
|
||||
|
||||
|
||||
def _normalize_decimal(d: Decimal) -> Decimal:
|
||||
"""
|
||||
Strips trailing zeros, e.g.
|
||||
|
||||
20.000 → 20
|
||||
20.010 → 20.01
|
||||
20.100 → 20.1
|
||||
|
||||
But unlike of Decimal.normalize(), 20.000 will not become 2e+1. Very small decimals might still be represented
|
||||
in scientific notation when printed.
|
||||
"""
|
||||
normalized = d.normalize()
|
||||
sign, digit, exponent = normalized.as_tuple()
|
||||
if exponent > 0:
|
||||
return normalized.quantize(1)
|
||||
return normalized
|
||||
|
||||
|
||||
class NormalizedDecimalField(DecimalField):
|
||||
"""
|
||||
Variant of DecimalField that never outputs the trailing zeros, so we always have normalized decimals internally.
|
||||
Use this only for fields where the trailing zeros are pointless (e.g. percentages), not for monetary amounts.
|
||||
"""
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is not None:
|
||||
value = _normalize_decimal(value)
|
||||
return value
|
||||
|
||||
@@ -57,7 +57,7 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
||||
from pypdf import PageObject, PdfReader, PdfWriter, Transformation
|
||||
from pypdf.generic import RectangleObject
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.units import inch, mm
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
@@ -133,6 +133,14 @@ OPTIONS = OrderedDict([
|
||||
'offsets': [66.1 * mm, 29.6 * mm],
|
||||
'pagesize': pagesizes.A4,
|
||||
}),
|
||||
('avery_4inx3in', {
|
||||
'name': 'Avery 4" x 3" (74459)',
|
||||
'cols': 2,
|
||||
'rows': 3,
|
||||
'margins': [1 * inch, .25 * inch, 1 * inch, .25 * inch],
|
||||
'offsets': [4 * inch, 3 * inch],
|
||||
'pagesize': pagesizes.LETTER,
|
||||
}),
|
||||
('avery_80x50', {
|
||||
'name': 'Avery Zweckform 80 x 50 mm (L4785)',
|
||||
'cols': 2,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.units import inch, mm
|
||||
|
||||
|
||||
def _simple_template(w, h):
|
||||
@@ -261,4 +261,9 @@ TEMPLATES = {
|
||||
"pagesize": (88.9 * mm, 33.87 * mm),
|
||||
"layout": _simple_template(88.9 * mm, 33.87 * mm),
|
||||
},
|
||||
"4inx3in": {
|
||||
"label": format_lazy(_("{width} x {height} inch label"), width=4, height=3),
|
||||
"pagesize": (4 * inch, 3 * inch),
|
||||
"layout": _simple_template(4 * inch, 3 * inch),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ from decimal import Decimal
|
||||
from django import forms
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
||||
@@ -43,7 +43,7 @@ from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.models import (
|
||||
GiftCardTransaction, OrderFee, OrderPayment, OrderRefund, Transaction,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter, tax_rate_format
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.timeframes import (
|
||||
DateFrameField,
|
||||
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||
@@ -382,7 +382,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
else "",
|
||||
tstyle_right,
|
||||
),
|
||||
Paragraph(tax_rate_format(r["tax_rate"]) + " %", tstyle_right),
|
||||
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
|
||||
Paragraph(str(r["sum_cont"]), tstyle_right),
|
||||
Paragraph(
|
||||
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
|
||||
@@ -408,7 +408,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
[
|
||||
FontFallbackParagraph(_("Sum"), tstyle),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph(tax_rate_format(tax_rate) + " %", tstyle_right),
|
||||
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph(
|
||||
money_filter(
|
||||
|
||||
@@ -153,7 +153,7 @@ def get_private_icals(event, positions):
|
||||
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
|
||||
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
|
||||
description = '\n'.join(descr)
|
||||
location = None
|
||||
location = ", ".join(l.strip() for l in str(pt.location).splitlines() if l.strip())
|
||||
dtstart = pt.start.astimezone(tz)
|
||||
dtend = pt.end.astimezone(tz)
|
||||
uid = 'pretix-{}-{}-{}-{}@{}'.format(
|
||||
|
||||
@@ -173,11 +173,11 @@
|
||||
<small>{% trans "incl. taxes" %}</small>
|
||||
{% endif %}
|
||||
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% elif var.display_price.rate and var.display_price.gross %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
@@ -313,11 +313,11 @@
|
||||
<small>{% trans "incl. taxes" %}</small>
|
||||
{% endif %}
|
||||
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% elif item.display_price.rate and item.display_price.gross %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
|
||||
@@ -357,7 +357,7 @@
|
||||
{% if line.tax_rate and line.total %}
|
||||
<br />
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
|
||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -367,7 +367,7 @@
|
||||
{% if line.tax_rate and line.total %}
|
||||
<br />
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
|
||||
incl. {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -416,7 +416,7 @@
|
||||
{% if fee.tax_rate %}
|
||||
<br />
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -426,7 +426,7 @@
|
||||
{% if fee.tax_rate %}
|
||||
<br />
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
|
||||
incl. {{ rate }}% {{ taxname }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
|
||||
@@ -207,13 +207,13 @@
|
||||
{% endif %}
|
||||
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% elif var.display_price.rate and var.display_price.gross %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -372,13 +372,13 @@
|
||||
{% endif %}
|
||||
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% elif item.display_price.rate and item.display_price.gross %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
|
||||
@@ -201,13 +201,13 @@
|
||||
{% endif %}
|
||||
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% elif var.display_price.rate and var.display_price.gross %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
|
||||
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
@@ -356,13 +356,13 @@
|
||||
{% endif %}
|
||||
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% elif item.display_price.rate and item.display_price.gross %}
|
||||
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
|
||||
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
|
||||
@@ -265,7 +265,7 @@ EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
|
||||
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
|
||||
EMAIL_SUBJECT_PREFIX = '[pretix] '
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
|
||||
EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
EMAIL_TIMEOUT = 60
|
||||
|
||||
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
|
||||
'use strict';
|
||||
{
|
||||
const globals = this;
|
||||
|
||||
@@ -530,6 +530,7 @@ def test_item_detail_program_times(token_client, organizer, event, team, item, c
|
||||
res["program_times"] = [{
|
||||
"start": "2017-12-27T00:00:00Z",
|
||||
"end": "2017-12-28T00:00:00Z",
|
||||
"location": None
|
||||
}]
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
|
||||
item.pk))
|
||||
@@ -1972,32 +1973,54 @@ def program_time2(item, category):
|
||||
end=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def program_time3(item, category):
|
||||
return item.program_times.create(start=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc),
|
||||
end=datetime(2017, 12, 31, 0, 0, 0, tzinfo=timezone.utc),
|
||||
location='Testlocation')
|
||||
|
||||
|
||||
TEST_PROGRAM_TIMES_RES = {
|
||||
0: {
|
||||
"start": "2017-12-27T00:00:00Z",
|
||||
"end": "2017-12-28T00:00:00Z",
|
||||
"location": None,
|
||||
},
|
||||
1: {
|
||||
"start": "2017-12-29T00:00:00Z",
|
||||
"end": "2017-12-30T00:00:00Z",
|
||||
"location": None,
|
||||
},
|
||||
2: {
|
||||
"start": "2017-12-30T00:00:00Z",
|
||||
"end": "2017-12-31T00:00:00Z",
|
||||
"location": {"en": "Testlocation"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2):
|
||||
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2, program_time3):
|
||||
res = dict(TEST_PROGRAM_TIMES_RES)
|
||||
res[0]["id"] = program_time.pk
|
||||
res[1]["id"] = program_time2.pk
|
||||
res[2]["id"] = program_time3.pk
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug,
|
||||
item.pk))
|
||||
assert resp.status_code == 200
|
||||
assert res[0]['start'] == resp.data['results'][0]['start']
|
||||
assert res[0]['end'] == resp.data['results'][0]['end']
|
||||
assert res[0]['id'] == resp.data['results'][0]['id']
|
||||
assert res[0] == resp.data['results'][0]
|
||||
assert res[1]['start'] == resp.data['results'][1]['start']
|
||||
assert res[1]['end'] == resp.data['results'][1]['end']
|
||||
assert res[1]['id'] == resp.data['results'][1]['id']
|
||||
assert res[1] == resp.data['results'][1]
|
||||
assert res[2]['start'] == resp.data['results'][2]['start']
|
||||
assert res[2]['end'] == resp.data['results'][2]['end']
|
||||
assert res[2]['location'] == resp.data['results'][2]['location']
|
||||
assert res[2]['id'] == resp.data['results'][2]['id']
|
||||
assert res[2] == resp.data['results'][2]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -2039,6 +2062,59 @@ def test_program_times_create(token_client, organizer, event, item):
|
||||
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_program_times_create_location(token_client, organizer, event, item):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
|
||||
{
|
||||
"start": "2017-12-27T00:00:00Z",
|
||||
"end": "2017-12-28T00:00:00Z",
|
||||
"location": {
|
||||
"en": "Testlocation",
|
||||
"de": "Testort"
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
|
||||
assert "Testlocation" == program_time.location.localize("en")
|
||||
assert "Testort" == program_time.location.localize("de")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_program_times_create_without_location(token_client, organizer, event, item):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
|
||||
{
|
||||
"start": "2017-12-27T00:00:00Z",
|
||||
"end": "2017-12-28T00:00:00Z"
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['location'] is None
|
||||
with scopes_disabled():
|
||||
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
|
||||
assert str(program_time.location) == ""
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
|
||||
{
|
||||
"start": "2017-12-27T00:00:00Z",
|
||||
"end": "2017-12-28T00:00:00Z",
|
||||
"location": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['location'] is None
|
||||
with scopes_disabled():
|
||||
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
|
||||
assert str(program_time.location) == ""
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_program_times_update(token_client, organizer, event, item, program_time):
|
||||
resp = token_client.patch(
|
||||
|
||||
@@ -82,7 +82,11 @@ def test_full_clone_same_organizer():
|
||||
assert item1.meta_data
|
||||
ItemProgramTime.objects.create(item=item1,
|
||||
start=datetime.datetime(2017, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc))
|
||||
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
location={
|
||||
"en": "Testlocation",
|
||||
"de": "Testort"
|
||||
})
|
||||
assert item1.program_times
|
||||
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15,
|
||||
hidden_if_item_available=item1)
|
||||
@@ -169,6 +173,7 @@ def test_full_clone_same_organizer():
|
||||
assert copied_item1.meta_data == item1.meta_data
|
||||
assert copied_item1.program_times.first().start == item1.program_times.first().start
|
||||
assert copied_item1.program_times.first().end == item1.program_times.first().end
|
||||
assert copied_item1.program_times.first().location == item1.program_times.first().location
|
||||
assert copied_item2.variations.get().meta_data == item2v.meta_data
|
||||
assert copied_item1.hidden_if_available == copied_q2
|
||||
assert copied_item1.grant_membership_type == membership_type
|
||||
|
||||
@@ -692,7 +692,8 @@ class ItemsTest(ItemFormTest):
|
||||
self.item2.program_times.create(start=datetime.datetime(2017, 12, 27, 0, 0, 0,
|
||||
tzinfo=datetime.timezone.utc),
|
||||
end=datetime.datetime(2017, 12, 28, 0, 0, 0,
|
||||
tzinfo=datetime.timezone.utc))
|
||||
tzinfo=datetime.timezone.utc),
|
||||
location={"en": "Testlocation", "de": "Testort"})
|
||||
|
||||
doc = self.get_doc('/control/event/%s/%s/items/add?copy_from=%d' % (self.orga1.slug, self.event1.slug, self.item2.pk))
|
||||
data = extract_form_fields(doc.select("form")[0])
|
||||
@@ -723,6 +724,7 @@ class ItemsTest(ItemFormTest):
|
||||
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])
|
||||
assert i_old.program_times.first().start == i_new.program_times.first().start
|
||||
assert i_old.program_times.first().end == i_new.program_times.first().end
|
||||
assert i_old.program_times.first().location == i_new.program_times.first().location
|
||||
|
||||
def test_add_to_existing_quota(self):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from decimal import Decimal
|
||||
|
||||
from pretix.helpers.models import _normalize_decimal
|
||||
|
||||
|
||||
def test_normalize_decimal():
|
||||
assert str(_normalize_decimal(Decimal("20.0000"))) == "20"
|
||||
assert str(_normalize_decimal(Decimal("20.0001"))) == "20.0001"
|
||||
assert str(_normalize_decimal(Decimal("20.0100"))) == "20.01"
|
||||
assert str(_normalize_decimal(Decimal("2000000.0000"))) == "2000000"
|
||||
assert str(_normalize_decimal(Decimal("0.0001"))) == "0.0001"
|
||||
@@ -189,8 +189,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 0,
|
||||
@@ -226,9 +226,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"id": self.shirt_red.pk,
|
||||
'original_price': None,
|
||||
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
|
||||
"rate": "19", "includes_mixed_tax_rate": False},
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
|
||||
"rate": "19", "includes_mixed_tax_rate": False},
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2,
|
||||
@@ -239,9 +239,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"id": self.shirt_blue.pk,
|
||||
'original_price': None,
|
||||
"price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
|
||||
"rate": "19", "includes_mixed_tax_rate": False},
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
|
||||
"rate": "19", "includes_mixed_tax_rate": False},
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2,
|
||||
@@ -278,9 +278,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
||||
"includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
|
||||
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
||||
"includes_mixed_tax_rate": False},
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
@@ -343,9 +343,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"id": self.shirt_red.pk,
|
||||
'original_price': None,
|
||||
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
|
||||
"rate": "19", "includes_mixed_tax_rate": False},
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
|
||||
"rate": "19", "includes_mixed_tax_rate": False},
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2,
|
||||
@@ -395,8 +395,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 0,
|
||||
@@ -481,7 +481,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
'gross': '14.00',
|
||||
'net': '11.76',
|
||||
'tax': '2.24',
|
||||
'rate': '19',
|
||||
'rate': '19.00',
|
||||
'name': '',
|
||||
'includes_mixed_tax_rate': False
|
||||
},
|
||||
@@ -489,7 +489,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
'gross': '14.00',
|
||||
'net': '11.76',
|
||||
'tax': '2.24',
|
||||
'rate': '19',
|
||||
'rate': '19.00',
|
||||
'name': '',
|
||||
'includes_mixed_tax_rate': False
|
||||
},
|
||||
@@ -601,7 +601,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"net": "19.52",
|
||||
"tax": "3.48",
|
||||
"name": "MIXED!",
|
||||
"rate": "19",
|
||||
"rate": "19.00",
|
||||
"includes_mixed_tax_rate": True
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user