forked from CGM_Public/pretix_original
Compare commits
32 Commits
v3.4.0
...
cart-vouch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc298c4202 | ||
|
|
8822d572f5 | ||
|
|
1c99e01af9 | ||
|
|
66183e805e | ||
|
|
d33c9332c6 | ||
|
|
2284def607 | ||
|
|
15c25a5a0d | ||
|
|
cf5ac6af4b | ||
|
|
2a929200b5 | ||
|
|
3f77d34026 | ||
|
|
a395b24b80 | ||
|
|
984ef60099 | ||
|
|
5b6f0df963 | ||
|
|
509c7d98cc | ||
|
|
3bd4959efe | ||
|
|
4faaa8e521 | ||
|
|
0e8832fd54 | ||
|
|
4faa76d9c7 | ||
|
|
8d1f9bf0f3 | ||
|
|
4afef62cbd | ||
|
|
3d5cfdd9c7 | ||
|
|
b3b1d09690 | ||
|
|
381ecd6d75 | ||
|
|
a12fea71e5 | ||
|
|
a6dd6ac537 | ||
|
|
c3041aa8c4 | ||
|
|
e275677a0a | ||
|
|
fff14c31ba | ||
|
|
a74bde60eb | ||
|
|
12b9d23efb | ||
|
|
afec39ce57 | ||
|
|
4ae22c4a1e |
@@ -1 +1 @@
|
||||
__version__ = "3.4.0"
|
||||
__version__ = "3.5.0.dev0"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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 />')
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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
@@ -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)"
|
||||
|
||||
@@ -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…"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
BIN
src/pretix/static/pretixbase/img/flags/arab-league.png
Normal file
BIN
src/pretix/static/pretixbase/img/flags/arab-league.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 417 B |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')); }
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.apply-voucher {
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $screen-sm-max) {
|
||||
.cart-row {
|
||||
.download-mobile {
|
||||
|
||||
@@ -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
|
||||
@@ -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': [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user