Compare commits

...

32 Commits

Author SHA1 Message Date
Raphael Michel
dc298c4202 Bundle behaviour 2019-12-11 13:31:56 +01:00
Raphael Michel
8822d572f5 Allow to redeem a voucher for an existing cart 2019-12-11 12:58:20 +01:00
Raphael Michel
1c99e01af9 Fix test failure introduced in last commit 2019-12-11 11:22:52 +01:00
Raphael Michel
66183e805e Check-in list export: Filter by checkin_attention 2019-12-11 09:16:58 +01:00
Raphael Michel
d33c9332c6 Show event time in list of events on organizer page 2019-12-11 09:11:54 +01:00
Raphael Michel
2284def607 Fix package name of dependency 2019-12-10 18:10:29 +01:00
Raphael Michel
15c25a5a0d PDF renderer: Support for arabic 2019-12-10 17:57:48 +01:00
Martin Gross
cf5ac6af4b Organizer-level override for giftcard code length 2019-12-09 13:55:26 +01:00
Raphael Michel
2a929200b5 Vouchers: Fix CSV export for all-product vouchers 2019-12-09 10:23:50 +01:00
Raphael Michel
3f77d34026 Use the arab league flag to represent Arabic, for now
https://www.quora.com/Which-flag-represents-the-Arabic-language
http://www.flagsarenotlanguages.com/blog/the-arab-league-flag-for-arabic-language/
2019-12-07 15:30:14 +01:00
Raphael Michel
a395b24b80 Add arabic, re-order languages 2019-12-07 15:08:57 +01:00
Raphael Michel
984ef60099 Fix issues in translation 2019-12-07 15:06:15 +01:00
saad91
5b6f0df963 Translated on translate.pretix.eu (Arabic)
Currently translated at 98.5% (3309 of 3359 strings)

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

powered by weblate
2019-12-07 13:56:30 +00:00
saad91
509c7d98cc Translated on translate.pretix.eu (Arabic)
Currently translated at 97.4% (3271 of 3359 strings)

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

powered by weblate
2019-12-07 13:20:50 +00:00
saad91
3bd4959efe Translated on translate.pretix.eu (Arabic)
Currently translated at 97.4% (3271 of 3359 strings)

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

powered by weblate
2019-12-07 13:20:50 +00:00
Maarten van den Berg
4faaa8e521 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-12-07 13:20:49 +00:00
Maarten van den Berg
0e8832fd54 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3360 of 3360 strings)

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

powered by weblate
2019-12-07 13:20:49 +00:00
Raphael Michel
4faa76d9c7 Translated on translate.pretix.eu (Arabic)
Currently translated at 98.5% (3274 of 3323 strings)

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

powered by weblate
2019-12-07 13:20:49 +00:00
Raphael Michel
8d1f9bf0f3 Fix TypeError during widget loading 2019-12-07 14:18:04 +01:00
Raphael Michel
4afef62cbd Fix typo 2019-12-07 13:24:35 +01:00
Raphael Michel
3d5cfdd9c7 Fix conflict between are-you-sure.js and async tasks
Thanks @luto for reporting and helping to debug
2019-12-07 13:23:33 +01:00
Raphael Michel
b3b1d09690 Use "Tax Invoice" as the invoice headline in Australia 2019-12-07 12:12:20 +01:00
Raphael Michel
381ecd6d75 Update German translation 2019-12-06 20:47:51 +01:00
Raphael Michel
a12fea71e5 Include expire date on invoices 2019-12-06 20:43:01 +01:00
Raphael Michel
a6dd6ac537 Fix AttributeError in e275677a0 2019-12-06 20:31:17 +01:00
Raphael Michel
c3041aa8c4 Fix ItemBundle.MultipleObjectsReturned error when extending cart lifetimes 2019-12-06 20:30:35 +01:00
Raphael Michel
e275677a0a Default to modern invoice renderer for new events 2019-12-06 20:25:05 +01:00
Raphael Michel
fff14c31ba Add Event.set_defaults 2019-12-06 20:24:34 +01:00
Raphael Michel
a74bde60eb Show invoice address form once again before generating a new invoice 2019-12-06 20:03:22 +01:00
Raphael Michel
12b9d23efb Hide "Generate invoice" button if no payment method is selected 2019-12-06 20:03:11 +01:00
Raphael Michel
afec39ce57 Fix exception when submitting cart positions with invalid subevent IDs 2019-12-06 15:58:13 +01:00
Raphael Michel
4ae22c4a1e Bump to 3.5.0.dev0 2019-12-06 15:36:27 +01:00
42 changed files with 6165 additions and 4328 deletions

View File

@@ -1 +1 @@
__version__ = "3.4.0"
__version__ = "3.5.0.dev0"

View File

@@ -133,6 +133,7 @@ class EventViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(organizer=self.request.organizer)
serializer.instance.set_defaults()
serializer.instance.log_action(
'pretix.event.added',
user=self.request.user,

View File

@@ -490,10 +490,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not self.invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']),
Paragraph(
(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]

View File

@@ -363,6 +363,14 @@ class Event(EventMixin, LoggedModel):
def __str__(self):
return str(self.name)
def set_defaults(self):
"""
This will be called after event creation, but only if the event was not created by copying an existing one.
This way, we can use this to introduce new default settings to pretix that do not affect existing events.
"""
self.settings.invoice_renderer = 'modern1'
self.settings.invoice_include_expire_date = True
@property
def social_image(self):
from pretix.multidomain.urlreverse import build_absolute_uri

View File

@@ -10,10 +10,10 @@ from pretix.base.banlist import banned
from pretix.base.models import LoggedModel
def gen_giftcard_secret():
def gen_giftcard_secret(length):
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=settings.ENTROPY['giftcard_secret'], allowed_chars=charset)
code = get_random_string(length=length, allowed_chars=charset)
if not banned(code) and not GiftCard.objects.filter(secret=code).exists():
return code
@@ -48,7 +48,6 @@ class GiftCard(LoggedModel):
)
secret = models.CharField(
max_length=190,
default=gen_giftcard_secret,
db_index=True,
verbose_name=_('Gift card code'),
)
@@ -69,6 +68,12 @@ class GiftCard(LoggedModel):
def accepted_by(self, organizer):
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
def save(self, *args, **kwargs):
if not self.secret:
self.secret = gen_giftcard_secret(self.issuer.settings.giftcard_length)
super().save(*args, **kwargs)
class Meta:
unique_together = (('secret', 'issuer'),)

View File

@@ -2053,6 +2053,13 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
@property
def is_empty(self):
return (
not self.name_cached and not self.company and not self.street and not self.zipcode and not self.city
and not self.internal_reference and not self.beneficiary
)
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))

View File

@@ -10,6 +10,8 @@ from functools import partial
from io import BytesIO
import bleach
from arabic_reshaper import ArabicReshaper
from bidi.algorithm import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.dispatch import receiver
@@ -449,6 +451,16 @@ class Renderer:
tags=["br"], attributes={}, styles=[], strip=True
)
)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
configuration = {
'delete_harakat': True,
'support_ligatures': False,
}
reshaper = ArabicReshaper(configuration=configuration)
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
p = Paragraph(text, style=style)
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)

View File

@@ -15,7 +15,7 @@ from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation, Seat,
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
@@ -82,6 +82,9 @@ error_messages = {
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_invalid_seat': _('This voucher is not valid for this seat.'),
'voucher_no_match': _('We did not find any position in your cart that we could use this voucher for. If you want '
'to add something new to your cart using that voucher, you can do so with the voucher '
'redemption option on the bottom of the page.'),
'voucher_item_not_available': _(
'Your voucher is valid for a product that is currently not for sale.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
@@ -107,10 +110,12 @@ class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat'))
RemoveOperation = namedtuple('RemoveOperation', ('position',))
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price'))
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
'quotas', 'subevent', 'seat'))
order = {
RemoveOperation: 10,
VoucherOperation: 15,
ExtendOperation: 20,
AddOperation: 30
}
@@ -364,10 +369,10 @@ class CartManager:
cp.item.requires_seat = cp.requires_seat
if cp.is_bundled:
try:
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
if bundle:
price = bundle.designated_price or 0
except ItemBundle.DoesNotExist:
else:
price = cp.price
changed_prices[cp.pk] = price
@@ -419,6 +424,58 @@ class CartManager:
self._operations.append(op)
return err
def apply_voucher(self, voucher_code: str):
if self._operations:
raise CartError('Applying a voucher to the whole cart should not be combined with other operations.')
try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
except Voucher.DoesNotExist:
raise CartError(error_messages['voucher_invalid'])
voucher_use_diff = Counter()
ops = []
if not voucher.is_active():
raise CartError(error_messages['voucher_expired'])
for p in self.positions:
if p.voucher_id:
continue
if not voucher.applies_to(p.item, p.variation):
continue
if voucher.seat and voucher.seat != p.seat:
continue
if voucher.subevent_id and voucher.subevent_id != p.subevent_id:
continue
if p.is_bundled:
continue
bundled_sum = Decimal('0.00')
if not p.addon_to_id:
for bundledp in p.addons.all():
if bundledp.is_bundled:
bundledprice = bundledp.price
bundled_sum += bundledprice
price = self._get_price(p.item, p.variation, voucher, None, p.subevent, bundled_sum=bundled_sum)
if price.gross > p.price:
continue
voucher_use_diff[voucher] += 1
ops.append((p.price - price.gross, self.VoucherOperation(p, voucher, price)))
# If there are not enough voucher usages left for the full cart, let's apply them in the order that benefits
# the user the most.
ops.sort(key=lambda k: k[0], reverse=True)
self._operations += [k[1] for k in ops]\
if not voucher_use_diff:
raise CartError(error_messages['voucher_no_match'])
self._voucher_use_diff += voucher_use_diff
def add_new_items(self, items: List[dict]):
# Fetch items from the database
self._update_items_cache([i['item'] for i in items], [i['variation'] for i in items])
@@ -429,7 +486,7 @@ class CartManager:
for i in items:
if self.event.has_subevents:
if not i.get('subevent'):
if not i.get('subevent') or int(i.get('subevent')) not in self._subevents_cache:
raise CartError(error_messages['subevent_required'])
subevent = self._subevents_cache[int(i.get('subevent'))]
else:
@@ -762,7 +819,7 @@ class CartManager:
self._operations.sort(key=lambda a: self.order[type(a)])
seats_seen = set()
for op in self._operations:
for iop, op in enumerate(self._operations):
if isinstance(op, self.RemoveOperation):
if op.position.expires > self.now_dt:
for q in op.position.quotas:
@@ -896,6 +953,19 @@ class CartManager:
op.position.delete()
else:
raise AssertionError("ExtendOperation cannot affect more than one item")
elif isinstance(op, self.VoucherOperation):
if vouchers_ok[op.voucher] < 1:
if iop == 0:
raise CartError(error_messages['voucher_redeemed'])
else:
# We fail silently if we could only apply the voucher to part of the cart, since that might
# be expected
continue
op.position.price = op.price.gross
op.position.voucher = op.voucher
op.position.save()
vouchers_ok[op.voucher] -= 1
for p in new_cart_positions:
if getattr(p, '_answers', None):
@@ -1060,6 +1130,26 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param voucher: A voucher code
:param session: Session ID of a guest
"""
with language(locale):
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
cm.apply_voucher(voucher)
cm.commit()
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en') -> None:
"""

View File

@@ -13,6 +13,7 @@ from django.db import transaction
from django.db.models import Count
from django.dispatch import receiver
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext as _
from django_countries.fields import Country
@@ -52,11 +53,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if lp and lp.payment_provider:
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = lp.payment_provider.render_invoice_text(invoice.order, lp)
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:
payment = lp.payment_provider.render_invoice_text(invoice.order)
payment = str(lp.payment_provider.render_invoice_text(invoice.order))
else:
payment = ""
if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING:
if payment:
payment += "<br />"
payment += pgettext("invoice", "Please complete your payment before {expire_date}.").format(
expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
)
invoice.introductory_text = str(introductory).replace('\n', '<br />')
invoice.additional_text = str(additional).replace('\n', '<br />')

View File

@@ -85,6 +85,10 @@ DEFAULTS = {
'default': 'True',
'type': bool,
},
'invoice_include_expire_date': {
'default': 'False',
'type': bool,
},
'invoice_numbers_consecutive': {
'default': 'True',
'type': bool,
@@ -742,6 +746,10 @@ Your {event} team"""))
'name_scheme': {
'default': 'full',
'type': str
},
'giftcard_length': {
'default': settings.ENTROPY['giftcard_secret'],
'type': int
}
}
PERSON_NAME_TITLE_GROUPS = OrderedDict([

View File

@@ -870,6 +870,11 @@ class InvoiceSettingsForm(SettingsForm):
label=_("Show attendee names on invoices"),
required=False
)
invoice_include_expire_date = forms.BooleanField(
label=_("Show expiration date of order"),
help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."),
required=False
)
invoice_email_attachment = forms.BooleanField(
label=_("Attach invoices to emails"),
help_text=_("If invoices are automatically generated for all orders, they will be attached to the order "

View File

@@ -302,6 +302,12 @@ class OrganizerSettingsForm(SettingsForm):
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
'We recommend a size of at least 200x200px to accomodate most devices.')
)
giftcard_length = forms.IntegerField(
label=_('Length of gift card codes'),
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -42,6 +42,7 @@
<legend>{% trans "Invoice customization" %}</legend>
{% bootstrap_field form.invoice_renderer layout="control" %}
{% bootstrap_field form.invoice_attendee_name layout="control" %}
{% bootstrap_field form.invoice_include_expire_date layout="control" %}
{% bootstrap_field form.invoice_introductory_text layout="control" %}
{% bootstrap_field form.invoice_additional_text layout="control" %}
{% bootstrap_field form.invoice_footer_text layout="control" %}

View File

@@ -32,6 +32,7 @@
{% endif %}
{% bootstrap_field sform.organizer_info_text layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Organizer page" %}</legend>

View File

@@ -267,17 +267,19 @@ class EventWizard(SafeSessionWizardView):
event.copy_data_from(from_event)
elif self.clone_from:
event.copy_data_from(self.clone_from)
elif event.has_subevents:
event.checkin_lists.create(
name=str(se),
all_products=True,
subevent=se
)
else:
event.checkin_lists.create(
name=_('Default'),
all_products=True
)
if event.has_subevents:
event.checkin_lists.create(
name=str(se),
all_products=True,
subevent=se
)
else:
event.checkin_lists.create(
name=_('Default'),
all_products=True
)
event.set_defaults()
if basics_data['tax_rate']:
if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']:

View File

@@ -77,6 +77,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
prod = '%s' % str(v.item)
elif v.quota:
prod = _('Any product in quota "{quota}"').format(quota=str(v.quota.name))
else:
prod = _('Any product')
row = [
v.code,
v.valid_until.isoformat() if v.valid_until else "",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-12-05 13:33+0000\n"
"PO-Revision-Date: 2019-11-19 15:55+0000\n"
"PO-Revision-Date: 2019-12-07 06:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@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"
@@ -191,10 +191,8 @@ msgid "Circular dependency between questions detected."
msgstr "Circulaire afhankelijkheid tussen vragen gedetecteerd."
#: pretix/api/serializers/item.py:271 pretix/control/forms/item.py:86
#, fuzzy
#| msgid "This question will be asked during check-in."
msgid "This type of question cannot be asked during check-in."
msgstr "Deze vraag zal bij het inchecken worden gesteld."
msgstr "Deze soort vraag kan niet bij het inchecken worden gesteld."
#: pretix/api/serializers/organizer.py:43 pretix/control/forms/organizer.py:363
msgid ""
@@ -2397,10 +2395,8 @@ msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "Landcode (ISO 3166-1 alpha-2)"
#: pretix/base/models/items.py:991
#, fuzzy
#| msgid "Line number"
msgid "Phone number"
msgstr "Regelnummer"
msgstr "Telefoonnummer"
#: pretix/base/models/items.py:1002 pretix/base/models/items.py:1056
#: pretix/control/forms/item.py:43
@@ -3148,7 +3144,7 @@ msgstr ""
#: pretix/base/models/vouchers.py:178
msgid "Specific seat"
msgstr ""
msgstr "Specifieke zitplaats"
#: pretix/base/models/vouchers.py:182
#: pretix/control/templates/pretixcontrol/vouchers/index.html:114
@@ -3216,12 +3212,12 @@ msgstr ""
"maken."
#: pretix/base/models/vouchers.py:245 pretix/base/models/vouchers.py:338
#, fuzzy
#| msgid "You cannot select a quota and a specific product at the same time."
msgid ""
"You need to select a specific product or quota if this voucher should "
"reserve tickets."
msgstr "U kunt niet tegelijk een quotum en een specifiek product selecteren."
msgstr ""
"U moet een specifiek product of quotum selecteren als er tickets moeten "
"worden gereserveerd voor deze voucher."
#: pretix/base/models/vouchers.py:255
#, python-format
@@ -3258,16 +3254,13 @@ msgid "A voucher with this code already exists."
msgstr "Er bestaat al een voucher met deze code."
#: pretix/base/models/vouchers.py:355
#, fuzzy
#| msgid "You need to select a specific seat."
msgid "You need to choose a date if you select a seat."
msgstr "U moet een specifieke stoel kiezen."
msgstr "U moet een datum kiezen als u een specifieke zitplaats selecteert."
#: pretix/base/models/vouchers.py:364
#, fuzzy, python-brace-format
#| msgid "The selected date does not exist in this event series."
#, python-brace-format
msgid "The specified seat ID \"{id}\" does not exist for this event."
msgstr "De geselecteerde datum bestaat niet in deze evenementenreeks."
msgstr "De gekozen stoel met nummer \"{id}\" bestaat niet voor dit evenement."
#: pretix/base/models/vouchers.py:368
#, python-brace-format
@@ -3275,31 +3268,27 @@ msgid ""
"The seat \"{id}\" is currently unavailable (blocked, already sold or a "
"different voucher)."
msgstr ""
"De stoel \"{id}\" is momenteel niet beschikbaar (geblokkeerd, al verkocht of "
"toegewezen aan een andere voucher)."
#: pretix/base/models/vouchers.py:373
#, fuzzy
#| msgid "You need to select a specific seat."
msgid "You need to choose a specific product if you select a seat."
msgstr "U moet een specifieke stoel kiezen."
msgstr "U moet een specifiek product kiezen als u een stoel kiest."
#: pretix/base/models/vouchers.py:376
#, fuzzy
#| msgid "This gift card can only be used in test mode."
msgid "Seat-specific vouchers can only be used once."
msgstr "Deze cadeaubon kan alleen in de testmodus worden gebruikt."
msgstr ""
"Vouchers voor een specifieke stoel kunnen maar één keer worden gebruikt."
#: pretix/base/models/vouchers.py:379
#, fuzzy, python-brace-format
#| msgid "You need to choose exactly one option from this category."
#| msgid_plural "You need to choose %(min_count)s options from this category."
#, python-brace-format
msgid "You need to choose the product \"{prod}\" for this seat."
msgstr "U moet precies één optie kiezen uit deze categorie."
msgstr "U moet het product \"{prod}\" kiezen voor deze stoel."
#: pretix/base/models/vouchers.py:382
#, fuzzy, python-brace-format
#| msgid "The identifier \"{}\" is already used for a different option."
#, python-brace-format
msgid "The seat \"{id}\" is already sold or currently blocked."
msgstr "Het kenmerk \"{}\" wordt al voor een andere optie gebruikt."
msgstr "De stoel \"{id}\" is al verkocht of geblokkeerd."
#: pretix/base/models/waitinglist.py:37
msgid "On waiting list since"
@@ -3581,12 +3570,14 @@ msgstr ""
#: pretix/base/payment.py:291
msgid "Restrict to specific sales channels"
msgstr ""
msgstr "Beperken tot specifieke verkoopkanalen"
#: pretix/base/payment.py:299
msgid ""
"Only allow the usage of this payment provider in the following sales channels"
msgstr ""
"Sta het gebruik van deze betalingsprovider alleen toe voor de volgende "
"verkoopkanalen"
#: pretix/base/payment.py:331
msgctxt "invoice"
@@ -3770,10 +3761,8 @@ msgid "Ticket code (barcode content)"
msgstr "Ticket code (waarde van QR-code)"
#: pretix/base/pdf.py:57
#, fuzzy
#| msgid "Order position"
msgid "Order position number"
msgstr "Besteld product"
msgstr "Plaatsnummer van bestelling"
#: pretix/base/pdf.py:62 pretix/control/forms/event.py:1563
#: pretix/control/templates/pretixcontrol/items/index.html:33
@@ -4179,11 +4168,8 @@ msgid "This voucher is not valid for this product."
msgstr "Deze voucher is niet geldig voor dit product."
#: pretix/base/services/cart.py:84
#, fuzzy
#| msgctxt "subevent"
#| msgid "This voucher is not valid for this event date."
msgid "This voucher is not valid for this seat."
msgstr "Deze voucher is niet geldig voor deze evenementsdatum."
msgstr "Deze voucher is niet geldig voor deze stoel."
#: pretix/base/services/cart.py:86
msgid "Your voucher is valid for a product that is currently not for sale."
@@ -4681,28 +4667,22 @@ msgstr ""
"contact op met de organisator van het evenement voor meer informatie."
#: pretix/base/services/seating.py:35 pretix/base/services/seating.py:86
#, fuzzy, python-format
#| msgid ""
#| "You can not change the plan since seat \"{}\" is not present in the new "
#| "plan and is already sold."
#, python-format
msgid ""
"You can not change the plan since seat \"%s\" is not present in the new plan "
"and is already sold."
msgstr ""
"U kunt de plattegrond niet veranderen, omdat stoel \"{}\" niet aanwezig is "
"U kunt de plattegrond niet veranderen, omdat stoel \"%s\" niet aanwezig is "
"in de nieuwe plattegrond, en deze stoel al is verkocht."
#: pretix/base/services/seating.py:89
#, fuzzy, python-format
#| msgid ""
#| "You can not change the plan since seat \"{}\" is not present in the new "
#| "plan and is already sold."
#, python-format
msgid ""
"You can not change the plan since seat \"%s\" is not present in the new plan "
"and is already used in a voucher."
msgstr ""
"U kunt de plattegrond niet veranderen, omdat stoel \"{}\" niet aanwezig is "
"in de nieuwe plattegrond, en deze stoel al is verkocht."
"U kunt de plattegrond niet veranderen, omdat stoel \"%s\" niet aanwezig is "
"in de nieuwe plattegrond, en deze stoel al is gebruikt voor een voucher."
#: pretix/base/services/shredder.py:71
msgid ""
@@ -5791,7 +5771,7 @@ msgstr ""
#: pretix/control/forms/event.py:105
msgid "Grant access to team"
msgstr ""
msgstr "Geef toegang aan team"
#: pretix/control/forms/event.py:106
msgid ""
@@ -5799,10 +5779,13 @@ msgid ""
"have permission to edit all events under this organizer. Please select one "
"of your existing teams that will be granted access to this event."
msgstr ""
"U kunt evenementen aanmaken voor deze organisator, maar u heeft geen "
"toestemming om alle evenementen van deze organisator te bewerken. Geef één "
"van de teams waar u deel van uitmaakt toegang tot dit evenement."
#: pretix/control/forms/event.py:111
msgid "Create a new team for this event with me as the only member"
msgstr ""
msgstr "Maak een nieuw team voor dit evenement aan met mij als het enige lid"
#: pretix/control/forms/event.py:153 pretix/control/forms/event.py:291
msgid ""
@@ -6161,7 +6144,7 @@ msgstr ""
#: pretix/control/forms/event.py:518
msgid "Social media image"
msgstr ""
msgstr "Social media-afbeelding"
#: pretix/control/forms/event.py:521
msgid ""
@@ -6171,10 +6154,17 @@ msgid ""
"preview, so we recommend to make sure it still looks good only the center "
"square is shown. If you do not fill this, we will use the logo given above."
msgstr ""
"Deze afbeelding zal worden gebruikt als u links naar uw ticketwinkel op "
"sociale media plaatst. Facebook raadt aan om een afbeeldingsgrootte van 1200 "
"bij 630 pixels te gebruiken, maar sommige platforms zoals WhatsApp en Reddit "
"tonen alleen een vierkante voorvertoning. We raden aan om uw afbeelding zo "
"te ontwerpen zodat hij er nog steeds goed uitziet als alleen het middelste "
"vierkant wordt getoond. Als u hier geen afbeelding uploadt zullen we het "
"logo dat hierboven is geüpload gebruiken."
#: pretix/control/forms/event.py:532
msgid "Help text of the email field"
msgstr ""
msgstr "Helptekst van het e-mailveld"
#: pretix/control/forms/event.py:538
msgid "End of presale text"
@@ -7521,7 +7511,7 @@ msgstr ""
#: pretix/control/forms/item.py:428
msgid "Shown independently of other products"
msgstr ""
msgstr "Toon onafhankelijk van andere producten"
#: pretix/control/forms/item.py:507
#, python-format
@@ -7821,7 +7811,7 @@ msgstr ""
#: pretix/control/forms/organizer.py:216
msgid "Allow creating a new team during event creation"
msgstr ""
msgstr "Sta het aanmaken van nieuwe teams bij het aanmaken van evenementen toe"
#: pretix/control/forms/organizer.py:217
msgid ""
@@ -7830,6 +7820,11 @@ msgid ""
"allows users to create an event-specified team on-the-fly, even when they do "
"not have \"Can change teams and permissions\" permission."
msgstr ""
"Gebruikers die geen toegang hebben tot alle evenementen onder deze "
"organisator moeten een van hun teams selecteren om toegang te geven aan hun "
"aangemaakte evenement. Deze instelling staat gebruikers toe om een nieuw "
"team aan te maken tijdens het aanmaken van een evenement, zelfs als de "
"gebruikers niet de permissie \"Kan teams en machtigingen aanpassen\" hebben."
#: pretix/control/forms/organizer.py:244
msgid "We strongly suggest to use a shade of red."
@@ -7987,7 +7982,7 @@ msgstr "Uw wijzigingen konden niet worden opgeslagen. Zie onder voor details."
#: pretix/control/forms/vouchers.py:120
msgid "Specific seat ID"
msgstr ""
msgstr "Specifiek stoelnummer"
#: pretix/control/forms/vouchers.py:155
msgid "Invalid product selected."
@@ -8080,7 +8075,7 @@ msgstr "Het aantal keren dat ELKE van deze vouchers kan worden gebruikt."
#: pretix/control/forms/vouchers.py:293
msgid "Specific seat IDs"
msgstr ""
msgstr "Specifieke stoelnummers"
#: pretix/control/forms/vouchers.py:307
msgid "CSV input needs to contain a header row in the first line."
@@ -8126,10 +8121,8 @@ msgstr ""
"U genereerde {codes} vouchers, maar gaf ontvangers voor {recp} vouchers op."
#: pretix/control/forms/vouchers.py:361
#, fuzzy
#| msgid "You need to specify either a quota or a product."
msgid "You need to specify as many seats as voucher codes."
msgstr "U moet een quotum of een product opgeven."
msgstr "U moet evenveel stoelnummers als vouchercodes opgeven."
#: pretix/control/logdisplay.py:30
msgid "The order has been changed:"
@@ -8453,10 +8446,9 @@ msgid "Payment {local_id} has been canceled."
msgstr "Betaling {local_id} is geannuleerd."
#: pretix/control/logdisplay.py:228
#, fuzzy, python-brace-format
#| msgid "Payment {local_id} has failed."
#, python-brace-format
msgid "Cancelling payment {local_id} has failed."
msgstr "Betaling {local_id} is mislukt."
msgstr "Het annuleren van betaling {local_id} is mislukt."
#: pretix/control/logdisplay.py:229
#, python-brace-format
@@ -11106,16 +11098,13 @@ msgstr ""
"het alleen binnen een bepaalde tijd beschikbaar moet zijn."
#: pretix/control/templates/pretixcontrol/item/base.html:29
#, fuzzy
#| msgid ""
#| "This product is currently not being sold since you configured below that "
#| "it should only be available in a certain timeframe."
msgid ""
"This product is currently not being shown since you configured below that it "
"should only be visible if a certain other quota is already sold out."
msgstr ""
"Dit product is momenteel niet te koop, omdat u hieronder heeft ingesteld dat "
"het alleen binnen een bepaalde tijd beschikbaar moet zijn."
"Dit product wordt momenteel niet getoond, omdat u hieronder heeft ingesteld "
"dat het product alleen moet worden getoond wanneer een bepaald ander quotum "
"al is uitverkocht."
#: pretix/control/templates/pretixcontrol/item/create.html:23
msgid "Quota settings"
@@ -11748,7 +11737,7 @@ msgstr "Beheer uw eigen applicaties"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:18
msgid "Permissions"
msgstr "Rechten"
msgstr "Permissies"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:59
msgid "No applications have access to your pretix account."
@@ -12429,12 +12418,11 @@ msgid "Create a new gift card"
msgstr "Nieuwe cadeaubon aanmaken"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:103
#, fuzzy
#| msgid "This gift card is not accepted by this event organizer."
msgid ""
"The gift card can be used to buy tickets for all events of this organizer."
msgstr ""
"Deze cadeaubon wordt niet geaccepteerd door de organisator van dit evenement."
"Deze cadeaubon kan worden gebruikt om tickets te kopen voor alle evenementen "
"van deze organisator."
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:111
msgid "Manual refund"
@@ -15088,17 +15076,15 @@ msgstr ""
"kunt de terugbetaling hieronder als voltooid aanmerken."
#: pretix/control/views/orders.py:848
#, fuzzy
#| msgid "The gift card has been created and can now be used."
msgid ""
"A new gift card was created. You can now send the user their gift card code."
msgstr "De cadeaubon is aangemaakt en kan nu worden gebruikt."
msgstr ""
"De cadeaubon is aangemaakt. U kunt de cadeauboncode nu naar de gebruiker "
"sturen."
#: pretix/control/views/orders.py:855
#, fuzzy
#| msgid "Gift card code"
msgid "Your gift card code"
msgstr "Cadeauboncode"
msgstr "Uw cadeauboncode"
#: pretix/control/views/orders.py:856
#, python-brace-format
@@ -15112,6 +15098,14 @@ msgid ""
"\n"
"Your {event} team"
msgstr ""
"Hallo,\n"
"\n"
"We hebben u {amount} terugbetaald voor uw bestelling.\n"
"\n"
"U kunt de cadeauboncode {giftcard} gebruiken om te betalen voor toekomstige "
"bestellingen in onze winkel.\n"
"\n"
"De organisatie van {event}"
#: pretix/control/views/orders.py:866
msgid "The refunds you selected do not match the selected total refund amount."
@@ -16251,6 +16245,8 @@ msgid ""
"Negative amount but refund can't be logged, please create manual refund "
"first."
msgstr ""
"Negatief bedrag maar terugbetaling kan niet worden opgeslagen, maak eerst "
"een handmatige terugbetaling aan."
#: pretix/plugins/banktransfer/views.py:90
msgid "The order is already marked as paid."
@@ -19018,11 +19014,9 @@ msgstr ""
"weer tickets beschikbaar zijn."
#: pretix/presale/views/widget.py:243
#, fuzzy
#| msgid "<a %(a_attr)s>event ticketing powered by pretix</a>"
msgctxt "widget"
msgid "event ticketing powered by pretix"
msgstr "<a %(a_attr)s>ticketverkoop mogelijk gemaakt door pretix</a>"
msgstr "ticketverkoop mogelijk gemaakt door pretix"
#: pretix/presale/views/widget.py:258
msgid "This ticket shop is currently disabled."
@@ -19095,11 +19089,11 @@ msgstr "Italiaans"
#: pretix/settings.py:413
msgid "Russian"
msgstr ""
msgstr "Russisch"
#: pretix/settings.py:414
msgid "Latvian"
msgstr ""
msgstr "Lets"
#: pretix/settings.py:415
msgid "Chinese (simplified)"

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-12-05 13:34+0000\n"
"PO-Revision-Date: 2019-08-03 22:00+0000\n"
"PO-Revision-Date: 2019-12-07 06:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n"
@@ -96,8 +96,6 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:125
#: pretix/static/pretixcontrol/js/ui/mail.js:21
#, fuzzy
#| msgid "The request took to long. Please try again."
msgid "The request took too long. Please try again."
msgstr "De aanvraag duurde te lang, probeer het alstublieft opnieuw."
@@ -231,7 +229,7 @@ msgstr "Klik om te sluiten"
#: pretix/static/pretixcontrol/js/ui/main.js:749
msgid "You have unsaved changes!"
msgstr ""
msgstr "U heeft nog niet opgeslagen wijzigingen!"
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
msgid "Calculating default price…"

View File

@@ -3,7 +3,9 @@ from collections import OrderedDict
import dateutil.parser
from django import forms
from django.conf import settings
from django.db.models import Case, Exists, Max, OuterRef, Subquery, Value, When
from django.db.models import (
Case, Exists, Max, OuterRef, Q, Subquery, Value, When,
)
from django.db.models.functions import Coalesce, NullIf
from django.urls import reverse
from django.utils.formats import date_format
@@ -44,6 +46,11 @@ class CheckInListMixin(BaseExporter):
label=_('Include QR-code secret'),
required=False
)),
('attention_only',
forms.BooleanField(
label=_('Only tickets requiring special attention'),
required=False
)),
('sort',
forms.ChoiceField(
label=_('Sort by'),
@@ -136,6 +143,9 @@ class CheckInListMixin(BaseExporter):
'resolved_name_part'
)
if form_data.get('attention_only'):
qs = qs.filter(Q(item__checkin_attention=True) | Q(order__checkin_attention=True))
if not cl.include_pending:
qs = qs.filter(order__status=Order.STATUS_PAID)
else:

View File

@@ -246,11 +246,31 @@
<div class="product">
<strong>{% trans "Total" %}</strong>
</div>
<div class="count hidden-xs">
<div class="count hidden-xs hidden-sm">
<strong>{{ cart.itemcount }}</strong>
</div>
<div class="col-md-3 col-xs-6 col-md-offset-3 price">
<strong>{{ cart.total|money:event.currency }}</strong>
{% if editable and show_vouchers and not cart.all_with_voucher %}
<br>
<a class="js-only apply-voucher-toggle" href="#">
<span class="fa fa-tag"></span> {% trans "Redeem a voucher" %}
</a>
<form action="{% eventurl event "presale:event.cart.voucher" cart_namespace=cart_namespace|default_if_none:"" %}"
data-asynctask-headline="{% trans "We're applying this voucher to your cart..." %}"
method="post" data-asynctask class="apply-voucher">
{% csrf_token %}
<div class="input-group">
<input type="text" class="form-control" name="voucher" placeholder="{% trans "Voucher code" %}">
<span class="input-group-btn">
<button class="btn btn-primary" type="submit">
<span class="fa fa-check"></span>
</button>
</span>
</div>
</form>
{% endif %}
</div>
<div class="clearfix"></div>
</div>

View File

@@ -182,13 +182,24 @@
</h3>
</div>
<div class="panel-body">
<form class="form-inline helper-display-inline" method="post"
action="{% eventurl event "presale:event.order.geninvoice" order=order.code secret=order.secret %}">
{% csrf_token %}
<button class="btn btn-default">
{% if generate_invoice_requires == "payment" %}
<div class="alert alert-info">
{% trans "You need to select a payment method above before you can request an invoice." %}
</div>
{% elif invoice_address_asked and order.invoice_address.is_empty %}
<a href="{% eventurl event "presale:event.order.modify" secret=order.secret order=order.code %}?generate_invoice=true"
class="btn btn-default">
{% trans "Request invoice" %}
</button>
</form>
</a>
{% else %}
<form class="form-inline helper-display-inline" method="post"
action="{% eventurl event "presale:event.order.geninvoice" order=order.code secret=order.secret %}">
{% csrf_token %}
<button class="btn btn-default">
{% trans "Request invoice" %}
</button>
</form>
{% endif %}
</div>
</div>
</div>

View File

@@ -13,7 +13,7 @@
{% csrf_token %}
<div class="panel-group" id="questions_accordion">
{% if invoice_address_asked or event.settings.invoice_name_required %}
{% if invoice_address_asked %}
{% if invoice_address_asked and not request.GET.generate_invoice == "true" %}
<div class="alert alert-info">
{% blocktrans trimmed %}
Modifying your invoice address will not automatically generate a new invoice.
@@ -81,7 +81,11 @@
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-primary btn-lg" type="submit">
{% trans "Save changes" %}
{% if request.GET.generate_invoice == "true" %}
{% trans "Request invoice" %}
{% else %}
{% trans "Save changes" %}
{% endif %}
</button>
</div>
<div class="clearfix"></div>

View File

@@ -67,6 +67,14 @@
</td>
<td>
{{ e.daterange|default:e.get_date_range_display }}
{% if e.settings.show_times %}
<br><small class="text-muted">
{{ e.date_from|date:"TIME_FORMAT" }}
{% if e.settings.show_date_to and e.date_to and e.date_to.date == e.date_from.date %}
{{ e.date_to|date:"TIME_FORMAT" }}
{% endif %}
</small>
{% endif %}
</td>
<td>
{% if e.has_subevents %}

View File

@@ -18,6 +18,7 @@ import pretix.presale.views.widget
frame_wrapped_urls = [
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
url(r'^cart/voucher$', pretix.presale.views.cart.CartApplyVoucher.as_view(), name='event.cart.voucher'),
url(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
url(r'^cart/answer/(?P<answer>[^/]+)/$',
pretix.presale.views.cart.AnswerDownload.as_view(),

View File

@@ -164,6 +164,7 @@ class CartMixin:
return {
'positions': positions,
'all_with_voucher': all(p.voucher_id for p in positions),
'raw': cartpos,
'total': total,
'net_total': net_total,

View File

@@ -23,7 +23,8 @@ from pretix.base.models import (
CartPosition, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
)
from pretix.base.services.cart import (
CartError, add_items_to_cart, clear_cart, remove_cart_position,
CartError, add_items_to_cart, apply_voucher, clear_cart,
remove_cart_position,
)
from pretix.base.views.tasks import AsyncAction
from pretix.multidomain.urlreverse import eventreverse
@@ -327,6 +328,26 @@ def cart_session(request):
return request.session['carts'][cart_id]
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartApplyVoucher(EventViewMixin, CartActionMixin, AsyncAction, View):
task = apply_voucher
known_errortypes = ['CartError']
def get_success_message(self, value):
return _('We applied the voucher to as many products in your cart as we could.')
def post(self, request, *args, **kwargs):
if 'voucher' in request.POST:
return self.do(self.request.event.id, request.POST.get('voucher'), get_or_create_cart_id(self.request), translation.get_language())
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
'redirect': self.get_error_url()
})
else:
return redirect(self.get_error_url())
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
task = remove_cart_position

View File

@@ -176,17 +176,12 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
[p.generate_ticket for p in ctx['cart']['positions']].count(True) > 1
)
ctx['invoices'] = list(self.order.invoices.all())
can_generate_invoice = (
self.order.sales_channel in self.request.event.settings.get('invoice_generate_sales_channels')
and (
self.request.event.settings.get('invoice_generate') in ('user', 'True')
or (
self.request.event.settings.get('invoice_generate') == 'paid'
and self.order.status == Order.STATUS_PAID
)
)
)
ctx['can_generate_invoice'] = invoice_qualified(self.order) and can_generate_invoice
ctx['can_generate_invoice'] = can_generate_invoice(self.request.event, self.order, True)
if ctx['can_generate_invoice']:
if not self.order.payments.exclude(
state__in=[OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED]
).exists() and self.order.status == Order.STATUS_PENDING:
ctx['generate_invoice_requires'] = 'payment'
ctx['url'] = build_absolute_uri(
self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
@@ -585,6 +580,28 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
})
def can_generate_invoice(event, order, ignore_payments=False):
v = (
order.sales_channel in event.settings.get('invoice_generate_sales_channels')
and (
event.settings.get('invoice_generate') in ('user', 'True')
or (
event.settings.get('invoice_generate') == 'paid'
and order.status == Order.STATUS_PAID
)
) and (
invoice_qualified(order)
)
)
if not ignore_payments:
v = v and not (
not order.payments.exclude(
state__in=[OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED]
).exists() and order.status == Order.STATUS_PENDING
)
return v
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
@@ -595,17 +612,7 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
can_generate_invoice = (
self.order.sales_channel in self.request.event.settings.get('invoice_generate_sales_channels')
and (
self.request.event.settings.get('invoice_generate') in ('user', 'True')
or (
self.request.event.settings.get('invoice_generate') == 'paid'
and self.order.status == Order.STATUS_PAID
)
)
)
if not can_generate_invoice or not invoice_qualified(self.order):
if not can_generate_invoice(self.request.event, self.order):
messages.error(self.request, _('You cannot generate an invoice for this order.'))
elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.'))
@@ -624,6 +631,12 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
invoice_form_class = InvoiceAddressForm
template_name = "pretixpresale/event/order_modify.html"
@cached_property
def positions(self):
if self.request.GET.get('generate_invoice') == 'true':
return []
return super().positions
def post(self, request, *args, **kwargs):
failed = not self.save() or not self.invoice_form.is_valid()
if failed:
@@ -642,11 +655,17 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
} for f in self.forms]
})
order_modified.send(sender=self.request.event, order=self.order)
if self.invoice_form.has_changed():
messages.success(self.request, _('Your invoice address has been updated. Please contact us if you need us '
'to regenerate your invoice.'))
else:
messages.success(self.request, _('Your changes have been saved.'))
if request.GET.get('generate_invoice') == 'true':
if not can_generate_invoice(self.request.event, self.order):
messages.error(self.request, _('You cannot generate an invoice for this order.'))
elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.'))
else:
i = generate_invoice(self.order)
self.order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
messages.success(self.request, _('The invoice has been generated.'))
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
CachedTicket.objects.filter(order_position__order=self.order).delete()

View File

@@ -10,7 +10,7 @@ import pytz
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.files.base import ContentFile, File
from django.core.files.storage import default_storage
from django.db.models import Q
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
@@ -136,6 +136,8 @@ def widget_js(request, lang, **kwargs):
fname = gs.settings.get('widget_file_{}'.format(lang))
resp = None
if fname and not settings.DEBUG:
if isinstance(fname, File):
fname = fname.name
try:
resp = HttpResponse(default_storage.open(fname).read(), content_type='text/javascript')
except:

View File

@@ -401,19 +401,20 @@ ALL_LANGUAGES = [
('en', _('English')),
('de', _('German')),
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('zh-hans', _('Chinese (simplified)')),
('da', _('Danish')),
('nl', _('Dutch')),
('nl-informal', _('Dutch (informal)')),
('da', _('Danish')),
('fr', _('French')),
('el', _('Greek')),
('it', _('Italian')),
('lv', _('Latvian')),
('pl', _('Polish')),
('pt-br', _('Portuguese (Brazil)')),
('ru', _('Russian')),
('es', _('Spanish')),
('tr', _('Turkish')),
('pl', _('Polish')),
('it', _('Italian')),
('ru', _('Russian')),
('lv', _('Latvian')),
('zh-hans', _('Chinese (simplified)')),
('el', _('Greek'))
]
LANGUAGES_OFFICIAL = {
'en', 'de', 'de-informal'

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -157,6 +157,7 @@ $(function () {
"use strict";
$("body").on('submit', 'form[data-asynctask]', function (e) {
e.preventDefault();
$(this).removeClass("dirty"); // Avoid problems with are-you-sure.js
if ($("body").data('ajaxing')) {
return;
}

View File

@@ -28,7 +28,7 @@ pre[lang=al], input[lang=al], textarea[lang=al], div[lang=al] { background-image
pre[lang=am], input[lang=am], textarea[lang=am], div[lang=am] { background-image: url(static('pretixbase/img/flags/am.png')); }
pre[lang=an], input[lang=an], textarea[lang=an], div[lang=an] { background-image: url(static('pretixbase/img/flags/an.png')); }
pre[lang=ao], input[lang=ao], textarea[lang=ao], div[lang=ao] { background-image: url(static('pretixbase/img/flags/ao.png')); }
pre[lang=ar], input[lang=ar], textarea[lang=ar], div[lang=ar] { background-image: url(static('pretixbase/img/flags/ar.png')); }
pre[lang=ar], input[lang=ar], textarea[lang=ar], div[lang=ar] { background-image: url(static('pretixbase/img/flags/arab-league.png')); }
pre[lang=as], input[lang=as], textarea[lang=as], div[lang=as] { background-image: url(static('pretixbase/img/flags/as.png')); }
pre[lang=at], input[lang=at], textarea[lang=at], div[lang=at] { background-image: url(static('pretixbase/img/flags/at.png')); }
pre[lang=au], input[lang=au], textarea[lang=au], div[lang=au] { background-image: url(static('pretixbase/img/flags/au.png')); }

View File

@@ -70,4 +70,13 @@ $(function () {
if ($("#cart-deadline").length) {
cart.init();
}
$(".apply-voucher").hide();
$(".apply-voucher-toggle").click(function (e) {
$(".apply-voucher-toggle").hide();
$(".apply-voucher").show();
$(".apply-voucher input[ŧype=text]").first().focus();
e.preventDefault();
return true;
});
});

View File

@@ -73,6 +73,12 @@
}
}
.apply-voucher {
input {
height: 32px;
}
}
@media(max-width: $screen-sm-max) {
.cart-row {
.download-mobile {

View File

@@ -62,3 +62,5 @@ django-redis==4.10.*
redis==3.2.*
django-phonenumber-field==3.0.*
phonenumberslite==8.10.*
python-bidi==0.4.* # Support for arabic in reportlab
arabic-reshaper==2.0.15 # Support for Aabic in reportlab

View File

@@ -148,6 +148,8 @@ setup(
'urllib3==1.24.*', # required by current requests
'django-phonenumber-field==3.0.*',
'phonenumberslite==8.10.*',
'python-bidi==0.4.*', # Support for Arabic in reportlab
'arabic-reshaper==2.0.15', # Support for Arabic in reportlab
],
extras_require={
'dev': [

View File

@@ -1725,6 +1725,178 @@ class CartTest(CartTestMixin, TestCase):
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
assert positions.count() == 1
def test_voucher_apply_matching(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=15, expires=now() + timedelta(minutes=10)
)
v = Voucher.objects.create(
event=self.event, item=self.ticket, price_mode='set', value=Decimal('4.00')
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-success' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher == v
assert cp1.price == Decimal('4.00')
assert cp2.voucher is None
def test_voucher_apply_partial_in_price_order(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=150, expires=now() + timedelta(minutes=10)
)
v = Voucher.objects.create(
event=self.event, price_mode='set', value=Decimal('4.00'), max_usages=100, redeemed=99
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-success' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher is None
assert cp1.price == Decimal('23.00')
assert cp2.voucher == v
assert cp2.price == Decimal('4.00')
def test_voucher_apply_multiple(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=150, expires=now() + timedelta(minutes=10)
)
v = Voucher.objects.create(
event=self.event, price_mode='set', quota=self.quota_all, value=Decimal('4.00'), max_usages=100
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-success' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher == v
assert cp1.price == Decimal('4.00')
assert cp2.voucher == v
assert cp2.price == Decimal('4.00')
def test_voucher_apply_only_one_per_line(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
v2 = Voucher.objects.create(
event=self.event, price_mode='set', quota=self.quota_all, value=Decimal('4.00'), max_usages=100
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=150, expires=now() + timedelta(minutes=10), voucher=v2
)
v = Voucher.objects.create(
event=self.event, price_mode='set', quota=self.quota_all, value=Decimal('4.00'), max_usages=100
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-success' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher == v
assert cp1.price == Decimal('4.00')
assert cp2.voucher == v2
assert cp2.price == Decimal('150.00')
def test_voucher_apply_only_positive(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=15, expires=now() + timedelta(minutes=10)
)
v = Voucher.objects.create(
event=self.event, price_mode='set', value=Decimal('40.00'), max_usages=100
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-danger' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher is None
assert cp2.voucher is None
def test_voucher_apply_expired(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=15, expires=now() + timedelta(minutes=10)
)
v = Voucher.objects.create(
event=self.event, price_mode='set', value=Decimal('40.00'), max_usages=100,
valid_until=now() - timedelta(days=1)
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-danger' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher is None
assert cp2.voucher is None
def test_voucher_apply_used(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
price=15, expires=now() + timedelta(minutes=10)
)
v = Voucher.objects.create(
event=self.event, price_mode='set', value=Decimal('40.00'), max_usages=100, redeemed=100
)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
'voucher': v.code,
}, follow=True)
assert 'alert-danger' in response.rendered_content
with scopes_disabled():
cp1.refresh_from_db()
cp2.refresh_from_db()
assert cp1.voucher is None
assert cp2.voucher is None
class CartAddonTest(CartTestMixin, TestCase):
@scopes_disabled()
@@ -2685,6 +2857,48 @@ class CartBundleTest(CartTestMixin, TestCase):
assert cp.price == 0
assert b.price == 40
@classscope(attr='orga')
def test_voucher_apply_multiple(self):
cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=21.5, expires=now() + timedelta(minutes=10)
)
b = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.trans, addon_to=cp,
price=1.5, expires=now() + timedelta(minutes=10), is_bundled=True
)
v = Voucher.objects.create(
event=self.event, price_mode='set', value=Decimal('4.00'), max_usages=100
)
self.cm.apply_voucher(v.code)
self.cm.commit()
cp.refresh_from_db()
b.refresh_from_db()
assert cp.price == Decimal('2.50')
assert b.price == Decimal('1.50')
@classscope(attr='orga')
def test_voucher_apply_multiple_reduce_beyond_designated_price(self):
cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=21.5, expires=now() + timedelta(minutes=10)
)
b = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.trans, addon_to=cp,
price=1.5, expires=now() + timedelta(minutes=10), is_bundled=True
)
v = Voucher.objects.create(
event=self.event, price_mode='set', value=Decimal('0.00'), max_usages=100
)
self.cm.apply_voucher(v.code)
self.cm.commit()
cp.refresh_from_db()
b.refresh_from_db()
assert cp.price == Decimal('0.00')
assert b.price == Decimal('1.50')
@classscope(attr='orga')
def test_extend_base_price_changed(self):
cp = CartPosition.objects.create(

View File

@@ -446,8 +446,20 @@ class OrdersTest(BaseOrdersTest):
{})
assert 404 == response.status_code
def test_invoice_create_require_payment(self):
self.event.settings.set('invoice_generate', 'user')
response = self.client.post(
'/%s/%s/order/%s/%s/invoice' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
{}, follow=True)
assert 'alert-danger' in response.rendered_content
with scopes_disabled():
assert not self.order.invoices.exists()
def test_invoice_create_ok(self):
self.event.settings.set('invoice_generate', 'user')
with scopes_disabled():
self.order.payments.create(provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
amount=self.order.total)
response = self.client.post(
'/%s/%s/order/%s/%s/invoice' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
{}, follow=True)