mirror of
https://github.com/pretix/pretix.git
synced 2026-04-26 23:52:35 +00:00
Compare commits
9 Commits
voucher-wi
...
shorter-lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0b73bf19 | ||
|
|
ed31f31c04 | ||
|
|
b1e78b5b78 | ||
|
|
4e2d31154a | ||
|
|
2e5a598b5f | ||
|
|
4b535b067a | ||
|
|
4f6eb903c7 | ||
|
|
4d916df7c0 | ||
|
|
61a331493e |
@@ -201,10 +201,6 @@ You can use an existing memcached server as pretix's caching backend::
|
|||||||
|
|
||||||
If no memcached is configured, pretix will use Django's built-in local-memory caching method.
|
If no memcached is configured, pretix will use Django's built-in local-memory caching method.
|
||||||
|
|
||||||
.. note:: If you use memcached and you deploy pretix across multiple servers, you should use *one*
|
|
||||||
shared memcached instance, not multiple ones, because cache invalidations would not be
|
|
||||||
propagated otherwise.
|
|
||||||
|
|
||||||
|
|
||||||
Redis
|
Redis
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
|||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following commands
|
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following comamnds
|
||||||
to enable and start the service::
|
to enable and start the service::
|
||||||
|
|
||||||
# systemctl daemon-reload
|
# systemctl daemon-reload
|
||||||
|
|||||||
@@ -5,33 +5,51 @@ General remarks
|
|||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
To use pretix, you wull need the following things:
|
To use pretix, the most minimal setup consists of:
|
||||||
|
|
||||||
* **pretix** and the python packages it depends on
|
* **pretix** and the python packages it depends on
|
||||||
|
|
||||||
* An **WSGI application server** (we recommend gunicorn)
|
* An **WSGI application server** (we recommend gunicorn)
|
||||||
|
|
||||||
* A periodic task runner, e.g. ``cron``
|
* A periodic task runner, e.g. ``cron``
|
||||||
|
|
||||||
* **A database**. This needs to be a SQL-based that is supported by Django. We highly recommend to either
|
To run pretix, you will need **at least Python 3.4**. We only recommend installations on **Linux**, Windows is not
|
||||||
go for **PostgreSQL** or **MySQL/MariaDB**. If you do not provide one, pretix will run on SQLite, which is useful
|
officially supported (but might work).
|
||||||
for evaluation and development purposes.
|
|
||||||
|
|
||||||
.. warning:: Do not ever use SQLite in production. It will break.
|
Optional requirements
|
||||||
|
---------------------
|
||||||
|
|
||||||
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
|
pretix is built in a way that makes many of the following requirements optional. However, performance or security might
|
||||||
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
|
be very low if you skip some of them, therefore they are only partly optional.
|
||||||
faster. Also, you need a proxying web server in front to provide SSL encryption.
|
|
||||||
|
|
||||||
.. warning:: Do not ever run without SSL in production. Your users deserve encrypted connections and thanks to
|
Database
|
||||||
`Let's Encrypt`_ SSL certificates can be obtained for free these days.
|
A good SQL-based database to run on that is supported by Django. We highly recommend to either go for **PostgreSQL**
|
||||||
|
or **MySQL/MariaDB**.
|
||||||
|
If you do not provide one, pretix will run on SQLite, which is useful for evaluation and development purposes.
|
||||||
|
|
||||||
* A **redis** server. This will be used for caching, session storage and task queuing.
|
.. warning:: Do not ever use SQLite in production. It will break.
|
||||||
|
|
||||||
.. warning:: pretix can run without redis, however this is only intended for development and should never be
|
Reverse proxy
|
||||||
used in production.
|
pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix is capable of
|
||||||
|
doing this, having this handled by a proper web server like **nginx** or **Apache** will be much faster. Also, you
|
||||||
|
need a proxying web server in front to provide SSL encryption.
|
||||||
|
|
||||||
* Optionally: RabbitMQ or memcached. Both of them might provide speedups, but if they are not present,
|
.. warning:: Do not ever run without SSL in production. Your users deserve encrypted connections and thanks to
|
||||||
redis will take over their job.
|
`Let's Encrypt`_ SSL certificates can be obtained for free these days.
|
||||||
|
|
||||||
|
Task worker
|
||||||
|
When pretix has to do heavy stuff, it is better to offload it into a background process instead of having the
|
||||||
|
users connection wait. Therefore pretix provides a background service that can be used to work on those
|
||||||
|
longer-running tasks.
|
||||||
|
|
||||||
|
This requires at least Redis (and optionally RabbitMQ).
|
||||||
|
|
||||||
|
Redis
|
||||||
|
If you provide a redis instance, pretix is able to make use of it in the three following ways:
|
||||||
|
|
||||||
|
* Caching
|
||||||
|
* Fast session storage
|
||||||
|
* Queuing and result storage for the task worker queue
|
||||||
|
|
||||||
|
RabbitMQ
|
||||||
|
RabbitMQ can be used as a more advanced queue manager for the task workers if necessary.
|
||||||
|
|
||||||
.. _Let's Encrypt: https://letsencrypt.org/
|
.. _Let's Encrypt: https://letsencrypt.org/
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ The provider class
|
|||||||
|
|
||||||
.. automethod:: is_allowed
|
.. automethod:: is_allowed
|
||||||
|
|
||||||
|
.. automethod:: is_allowed_for_order
|
||||||
|
|
||||||
.. autoattribute:: payment_form_fields
|
.. autoattribute:: payment_form_fields
|
||||||
|
|
||||||
.. automethod:: checkout_prepare
|
.. automethod:: checkout_prepare
|
||||||
|
|||||||
@@ -65,3 +65,5 @@ The output class
|
|||||||
.. automethod:: generate
|
.. automethod:: generate
|
||||||
|
|
||||||
.. autoattribute:: download_button_text
|
.. autoattribute:: download_button_text
|
||||||
|
|
||||||
|
.. autoattribute:: download_button_icon
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ localecompile:
|
|||||||
|
|
||||||
localegen:
|
localegen:
|
||||||
./manage.py makemessages --all --ignore "pretix/helpers/*"
|
./manage.py makemessages --all --ignore "pretix/helpers/*"
|
||||||
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "static/jsi18n/*"
|
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*"
|
||||||
|
|
||||||
staticfiles: jsi18n
|
staticfiles: jsi18n
|
||||||
./manage.py collectstatic --noinput
|
./manage.py collectstatic --noinput
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.3 on 2016-11-29 13:30
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0047_auto_20161126_1300'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='voucher',
|
|
||||||
name='price_mode',
|
|
||||||
field=models.CharField(choices=[('none', 'No effect'), ('set', 'Set product price to'), ('subtract', 'Subtract from product price'), ('percent', 'Reduce product price by (%)')], default='set', max_length=100, verbose_name='Price mode'),
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='voucher',
|
|
||||||
old_name='price',
|
|
||||||
new_name='value',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='voucher',
|
|
||||||
name='value',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Voucher value'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -8,8 +8,6 @@ import pytz
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.db.models.signals import post_delete
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -281,12 +279,13 @@ class Order(LoggedModel):
|
|||||||
|
|
||||||
if now() > last_date:
|
if now() > last_date:
|
||||||
return error_messages['late']
|
return error_messages['late']
|
||||||
if self.status == self.STATUS_PENDING:
|
|
||||||
return True
|
|
||||||
if not self.event.settings.get('payment_term_accept_late'):
|
if not self.event.settings.get('payment_term_accept_late'):
|
||||||
return error_messages['late']
|
return error_messages['late']
|
||||||
|
|
||||||
return self._is_still_available()
|
if self.status == self.STATUS_PENDING:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self._is_still_available()
|
||||||
|
|
||||||
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
|
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
|
||||||
error_messages = {
|
error_messages = {
|
||||||
@@ -565,9 +564,3 @@ class CachedTicket(models.Model):
|
|||||||
order_position = models.ForeignKey(OrderPosition, on_delete=models.CASCADE)
|
order_position = models.ForeignKey(OrderPosition, on_delete=models.CASCADE)
|
||||||
cachedfile = models.ForeignKey(CachedFile, on_delete=models.CASCADE, null=True)
|
cachedfile = models.ForeignKey(CachedFile, on_delete=models.CASCADE, null=True)
|
||||||
provider = models.CharField(max_length=255)
|
provider = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedTicket)
|
|
||||||
def cached_file_delete(sender, instance, **kwargs):
|
|
||||||
if instance.cachedfile:
|
|
||||||
instance.cachedfile.delete()
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -7,7 +5,6 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from ..decimal import round_decimal
|
|
||||||
from .base import LoggedModel
|
from .base import LoggedModel
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .items import Item, ItemVariation, Quota
|
from .items import Item, ItemVariation, Quota
|
||||||
@@ -43,11 +40,8 @@ class Voucher(LoggedModel):
|
|||||||
:type block_quota: bool
|
:type block_quota: bool
|
||||||
:param allow_ignore_quota: If set to true, this voucher can be redeemed even if the event is sold out
|
:param allow_ignore_quota: If set to true, this voucher can be redeemed even if the event is sold out
|
||||||
:type allow_ignore_quota: bool
|
:type allow_ignore_quota: bool
|
||||||
:param price_mode: Sets how this voucher affects a product's price. Can be ``none``, ``set``, ``subtract``
|
:param price: If set, the voucher will allow the sale of associated items for this price
|
||||||
or ``percent``.
|
:type price: decimal.Decimal
|
||||||
:type price_mode: str
|
|
||||||
:param value: The value by which the price should be modified in the way specified by ``price_mode``.
|
|
||||||
:type value: decimal.Decimal
|
|
||||||
:param item: If set, the item to sell
|
:param item: If set, the item to sell
|
||||||
:type item: Item
|
:type item: Item
|
||||||
:param variation: If set, the variation to sell
|
:param variation: If set, the variation to sell
|
||||||
@@ -65,13 +59,6 @@ class Voucher(LoggedModel):
|
|||||||
* You need to either select a quota or an item
|
* You need to either select a quota or an item
|
||||||
* If you select an item that has variations but do not select a variation, you cannot set block_quota
|
* If you select an item that has variations but do not select a variation, you cannot set block_quota
|
||||||
"""
|
"""
|
||||||
PRICE_MODES = (
|
|
||||||
('none', _('No effect')),
|
|
||||||
('set', _('Set product price to')),
|
|
||||||
('subtract', _('Subtract from product price')),
|
|
||||||
('percent', _('Reduce product price by (%)')),
|
|
||||||
)
|
|
||||||
|
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
Event,
|
Event,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@@ -111,15 +98,10 @@ class Voucher(LoggedModel):
|
|||||||
"If activated, a holder of this voucher code can buy tickets, even if there are none left."
|
"If activated, a holder of this voucher code can buy tickets, even if there are none left."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
price_mode = models.CharField(
|
price = models.DecimalField(
|
||||||
verbose_name=_("Price mode"),
|
verbose_name=_("Set product price to"),
|
||||||
max_length=100,
|
|
||||||
choices=PRICE_MODES,
|
|
||||||
default='set'
|
|
||||||
)
|
|
||||||
value = models.DecimalField(
|
|
||||||
verbose_name=_("Voucher value"),
|
|
||||||
decimal_places=2, max_digits=10, null=True, blank=True,
|
decimal_places=2, max_digits=10, null=True, blank=True,
|
||||||
|
help_text=_('If empty, the product will cost its normal price.')
|
||||||
)
|
)
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
Item, related_name='vouchers',
|
Item, related_name='vouchers',
|
||||||
@@ -226,19 +208,3 @@ class Voucher(LoggedModel):
|
|||||||
if self.valid_until and self.valid_until < now():
|
if self.valid_until and self.valid_until < now():
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def calculate_price(self, original_price: Decimal) -> Decimal:
|
|
||||||
"""
|
|
||||||
Returns how the price given in original_price would be modified if this
|
|
||||||
voucher is applied, i.e. replaced by a different price or reduced by a
|
|
||||||
certain percentage. If the voucher does not modify the price, the
|
|
||||||
original price will be returned.
|
|
||||||
"""
|
|
||||||
if self.value:
|
|
||||||
if self.price_mode == 'set':
|
|
||||||
return self.value
|
|
||||||
elif self.price_mode == 'subtract':
|
|
||||||
return original_price - self.value
|
|
||||||
elif self.price_mode == 'percent':
|
|
||||||
return round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
|
|
||||||
return original_price
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
from collections import Counter
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from pretix.base.i18n import LazyLocaleException
|
from pretix.base.i18n import LazyLocaleException
|
||||||
@@ -35,8 +37,6 @@ error_messages = {
|
|||||||
'voucher_invalid': _('This voucher code is not known in our database.'),
|
'voucher_invalid': _('This voucher code is not known in our database.'),
|
||||||
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
|
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
|
||||||
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
|
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
|
||||||
'voucher_double': _('You already used this voucher code. Remove the associated line from your '
|
|
||||||
'cart if you want to use it for a different product.'),
|
|
||||||
'voucher_expired': _('This voucher is expired.'),
|
'voucher_expired': _('This voucher is expired.'),
|
||||||
'voucher_invalid_item': _('This voucher is not valid for this product.'),
|
'voucher_invalid_item': _('This voucher is not valid for this product.'),
|
||||||
'voucher_required': _('You need a valid voucher code to order this product.'),
|
'voucher_required': _('You need a valid voucher code to order this product.'),
|
||||||
@@ -65,7 +65,7 @@ def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str, now
|
|||||||
'variation': cp.variation_id,
|
'variation': cp.variation_id,
|
||||||
'count': 1,
|
'count': 1,
|
||||||
'price': cp.price,
|
'price': cp.price,
|
||||||
'cp': cp,
|
'_cp': cp,
|
||||||
'voucher': cp.voucher.code if cp.voucher else None
|
'voucher': cp.voucher.code if cp.voucher else None
|
||||||
})
|
})
|
||||||
positions.add(cp)
|
positions.add(cp)
|
||||||
@@ -74,7 +74,7 @@ def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str, now
|
|||||||
|
|
||||||
def _delete_expired(expired: List[CartPosition], now_dt: datetime) -> None:
|
def _delete_expired(expired: List[CartPosition], now_dt: datetime) -> None:
|
||||||
for cp in expired:
|
for cp in expired:
|
||||||
if cp.expires <= now_dt:
|
if cp.expires <= now_dt: # Has not been extended
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
|
||||||
|
|
||||||
@@ -85,8 +85,17 @@ def _check_date(event: Event, now_dt: datetime) -> None:
|
|||||||
raise CartError(error_messages['ended'])
|
raise CartError(error_messages['ended'])
|
||||||
|
|
||||||
|
|
||||||
def _add_new_items(event: Event, items: List[dict],
|
def _parse_items_and_check_constraints(event: Event, items: List[dict], cart_id: str,
|
||||||
cart_id: str, expiry: datetime, now_dt: datetime) -> Optional[str]:
|
now_dt: datetime) -> Counter:
|
||||||
|
"""
|
||||||
|
This method does three things:
|
||||||
|
|
||||||
|
* Extend the item list with the database objects for the item, variation, etc.
|
||||||
|
|
||||||
|
* Check all constraints that are placed on the items, vouchers etc. to be valid and calculates the correct prices
|
||||||
|
|
||||||
|
* Return a counter object that contains the quota changes that are required to perform the operation
|
||||||
|
"""
|
||||||
err = None
|
err = None
|
||||||
|
|
||||||
# Fetch items from the database
|
# Fetch items from the database
|
||||||
@@ -99,6 +108,9 @@ def _add_new_items(event: Event, items: List[dict],
|
|||||||
).select_related("item", "item__event").prefetch_related("quotas")
|
).select_related("item", "item__event").prefetch_related("quotas")
|
||||||
variations_cache = {v.id: v for v in variations_query}
|
variations_cache = {v.id: v for v in variations_query}
|
||||||
|
|
||||||
|
quotadiff = Counter()
|
||||||
|
vouchers = Counter()
|
||||||
|
|
||||||
for i in items:
|
for i in items:
|
||||||
# Check whether the specified items are part of what we just fetched from the database
|
# Check whether the specified items are part of what we just fetched from the database
|
||||||
# If they are not, the user supplied item IDs which either do not exist or belong to
|
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||||
@@ -116,106 +128,159 @@ def _add_new_items(event: Event, items: List[dict],
|
|||||||
try:
|
try:
|
||||||
voucher = Voucher.objects.get(code=i.get('voucher').strip(), event=event)
|
voucher = Voucher.objects.get(code=i.get('voucher').strip(), event=event)
|
||||||
if voucher.redeemed >= voucher.max_usages:
|
if voucher.redeemed >= voucher.max_usages:
|
||||||
return error_messages['voucher_redeemed']
|
raise CartError(error_messages['voucher_redeemed'])
|
||||||
if voucher.valid_until is not None and voucher.valid_until < now_dt:
|
if voucher.valid_until is not None and voucher.valid_until < now_dt:
|
||||||
return error_messages['voucher_expired']
|
raise CartError(error_messages['voucher_expired'])
|
||||||
if not voucher.applies_to(item, variation):
|
if not voucher.applies_to(item, variation):
|
||||||
return error_messages['voucher_invalid_item']
|
raise CartError(error_messages['voucher_invalid_item'])
|
||||||
|
|
||||||
redeemed_in_carts = CartPosition.objects.filter(
|
redeemed_in_carts = CartPosition.objects.filter(
|
||||||
Q(voucher=voucher) & Q(event=event) &
|
Q(voucher=voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||||
(Q(expires__gte=now_dt) | Q(cart_id=cart_id))
|
|
||||||
)
|
)
|
||||||
if 'cp' in i:
|
if 'cp' in i:
|
||||||
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['cp'].pk)
|
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['_cp'].pk)
|
||||||
v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count()
|
v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count()
|
||||||
|
|
||||||
if v_avail < 1:
|
if v_avail < 1:
|
||||||
return error_messages['voucher_redeemed']
|
raise CartError(error_messages['voucher_redeemed'])
|
||||||
if i['count'] > v_avail:
|
if i['count'] > v_avail - vouchers[voucher]:
|
||||||
return error_messages['voucher_redeemed_partial'] % v_avail
|
raise CartError(error_messages['voucher_redeemed_partial'] % v_avail)
|
||||||
|
|
||||||
|
vouchers[voucher] += i['count']
|
||||||
except Voucher.DoesNotExist:
|
except Voucher.DoesNotExist:
|
||||||
return error_messages['voucher_invalid']
|
raise CartError(error_messages['voucher_invalid'])
|
||||||
|
|
||||||
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
||||||
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
|
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
|
||||||
|
|
||||||
if voucher and voucher.quota and voucher.quota.pk not in [q.pk for q in quotas]:
|
if voucher and voucher.quota and voucher.quota.pk not in [q.pk for q in quotas]:
|
||||||
return error_messages['voucher_invalid_item']
|
raise CartError(error_messages['voucher_invalid_item'])
|
||||||
|
|
||||||
if item.require_voucher and voucher is None:
|
if item.require_voucher and voucher is None:
|
||||||
return error_messages['voucher_required']
|
raise CartError(error_messages['voucher_required'])
|
||||||
|
|
||||||
if item.hide_without_voucher and (voucher is None or voucher.item is None or voucher.item.pk != item.pk):
|
if item.hide_without_voucher and (voucher is None or voucher.item is None or voucher.item.pk != item.pk):
|
||||||
return error_messages['voucher_required']
|
raise CartError(error_messages['voucher_required'])
|
||||||
|
|
||||||
if len(quotas) == 0 or not item.is_available() or (variation and not variation.active):
|
if len(quotas) == 0 or not item.is_available() or (variation and not variation.active):
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check that all quotas allow us to buy i['count'] instances of the object
|
if voucher and voucher.price is not None:
|
||||||
quota_ok = i['count']
|
price = voucher.price
|
||||||
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
|
else:
|
||||||
for quota in quotas:
|
price = item.default_price if variation is None else (
|
||||||
avail = quota.availability()
|
variation.default_price if variation.default_price is not None else item.default_price)
|
||||||
if avail[1] is not None and avail[1] < i['count']:
|
|
||||||
# This quota is not available or less than i['count'] items are left, so we have to
|
|
||||||
# reduce the number of bought items
|
|
||||||
if avail[0] != Quota.AVAILABILITY_OK:
|
|
||||||
err = err or error_messages['unavailable']
|
|
||||||
else:
|
|
||||||
err = err or error_messages['in_part']
|
|
||||||
quota_ok = min(quota_ok, avail[1])
|
|
||||||
|
|
||||||
price = item.default_price if variation is None else (
|
|
||||||
variation.default_price if variation.default_price is not None else item.default_price)
|
|
||||||
if voucher:
|
|
||||||
price = voucher.calculate_price(price)
|
|
||||||
|
|
||||||
if item.free_price and 'price' in i and i['price'] is not None and i['price'] != "":
|
if item.free_price and 'price' in i and i['price'] is not None and i['price'] != "":
|
||||||
custom_price = i['price']
|
custom_price = i['price']
|
||||||
if not isinstance(custom_price, Decimal):
|
if not isinstance(custom_price, Decimal):
|
||||||
custom_price = Decimal(custom_price.replace(",", "."))
|
custom_price = Decimal(custom_price.replace(",", "."))
|
||||||
if custom_price > 100000000:
|
if custom_price > 100000000:
|
||||||
return error_messages['price_too_high']
|
raise CartError(error_messages['price_too_high'])
|
||||||
price = max(custom_price, price)
|
price = max(custom_price, price)
|
||||||
|
|
||||||
# Create a CartPosition for as much items as we can
|
# Check that all quotas allow us to buy i['count'] instances of the object
|
||||||
for k in range(quota_ok):
|
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
|
||||||
if 'cp' in i and i['count'] == 1:
|
for quota in quotas:
|
||||||
# Recreating
|
quotadiff[quota] += i['count']
|
||||||
cp = i['cp']
|
i['_quotas'] = quotas
|
||||||
cp.expires = expiry
|
else:
|
||||||
cp.price = price
|
i['_quotas'] = []
|
||||||
cp.save()
|
|
||||||
|
i['_price'] = price
|
||||||
|
i['_item'] = item
|
||||||
|
i['_variation'] = variation
|
||||||
|
i['_voucher'] = voucher
|
||||||
|
|
||||||
|
if err:
|
||||||
|
raise CartError(err)
|
||||||
|
|
||||||
|
return quotadiff
|
||||||
|
|
||||||
|
|
||||||
|
def _check_quota_and_create_positions(event: Event, items: List[dict], cart_id: str, now_dt: datetime,
|
||||||
|
expiry: datetime, quotadiff: Counter):
|
||||||
|
"""
|
||||||
|
This method takes the modified items and the quotadiff from _parse_items_and_check_constraints
|
||||||
|
and then
|
||||||
|
|
||||||
|
* checks that the given quotas are available
|
||||||
|
|
||||||
|
* creates as many cart positions as possible
|
||||||
|
"""
|
||||||
|
err = None
|
||||||
|
quotas_ok = {}
|
||||||
|
cartpositions = []
|
||||||
|
|
||||||
|
with event.lock():
|
||||||
|
for quota, count in quotadiff.items():
|
||||||
|
avail = quota.availability(now_dt)
|
||||||
|
if avail[1] is not None and avail[1] < count:
|
||||||
|
# This quota is not available or less than i['count'] items are left, so we have to
|
||||||
|
# reduce the number of bought items
|
||||||
|
if avail[0] != Quota.AVAILABILITY_OK:
|
||||||
|
err = err or error_messages['unavailable']
|
||||||
|
else:
|
||||||
|
err = err or error_messages['in_part']
|
||||||
|
quotas_ok[quota] = min(count, avail[1])
|
||||||
else:
|
else:
|
||||||
CartPosition.objects.create(
|
quotas_ok[quota] = count
|
||||||
event=event, item=item, variation=variation,
|
|
||||||
price=price,
|
for i in items:
|
||||||
expires=expiry,
|
# Create a CartPosition for as much items as we can
|
||||||
cart_id=cart_id, voucher=voucher
|
requested_count = i['count']
|
||||||
)
|
available_count = requested_count
|
||||||
return err
|
if i['_quotas']:
|
||||||
|
available_count = min(requested_count, min(quotas_ok[q] for q in i['_quotas']))
|
||||||
|
|
||||||
|
for q in i['_quotas']:
|
||||||
|
quotas_ok[q] -= available_count
|
||||||
|
|
||||||
|
for k in range(available_count):
|
||||||
|
if '_cp' in i and i['count'] == 1:
|
||||||
|
# Recreating an existing position
|
||||||
|
cp = i['_cp']
|
||||||
|
cp.expires = expiry
|
||||||
|
cp.price = i['_price']
|
||||||
|
cp.save()
|
||||||
|
else:
|
||||||
|
cartpositions.append(CartPosition(
|
||||||
|
event=event, item=i['_item'], variation=i['_variation'],
|
||||||
|
price=i['_price'],
|
||||||
|
expires=expiry,
|
||||||
|
cart_id=cart_id, voucher=i['_voucher']
|
||||||
|
))
|
||||||
|
|
||||||
|
CartPosition.objects.bulk_create(cartpositions)
|
||||||
|
|
||||||
|
if err:
|
||||||
|
raise CartError(err)
|
||||||
|
|
||||||
|
|
||||||
def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> None:
|
def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> None:
|
||||||
with event.lock() as now_dt:
|
now_dt = now()
|
||||||
_check_date(event, now_dt)
|
_check_date(event, now_dt)
|
||||||
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
|
|
||||||
if sum(i['count'] for i in items) + existing > int(event.settings.max_items_per_order):
|
|
||||||
# TODO: i18n plurals
|
|
||||||
raise CartError(error_messages['max_items'], (event.settings.max_items_per_order,))
|
|
||||||
|
|
||||||
expiry = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
|
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
|
||||||
_extend_existing(event, cart_id, expiry, now_dt)
|
if sum(i['count'] for i in items) + existing > int(event.settings.max_items_per_order):
|
||||||
|
# TODO: i18n plurals
|
||||||
|
raise CartError(error_messages['max_items'], (event.settings.max_items_per_order,))
|
||||||
|
|
||||||
expired = _re_add_expired_positions(items, event, cart_id, now_dt)
|
expiry = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
|
||||||
|
_extend_existing(event, cart_id, expiry, now_dt)
|
||||||
|
|
||||||
|
expired = _re_add_expired_positions(items, event, cart_id, now_dt)
|
||||||
|
|
||||||
|
try:
|
||||||
if items:
|
if items:
|
||||||
err = _add_new_items(event, items, cart_id, expiry, now_dt)
|
quotadiff = _parse_items_and_check_constraints(event, items, cart_id, now_dt)
|
||||||
_delete_expired(expired, now_dt)
|
_check_quota_and_create_positions(event, items, cart_id, now_dt, expiry, quotadiff)
|
||||||
if err:
|
except CartError as e:
|
||||||
raise CartError(err)
|
_delete_expired(expired, now_dt)
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
_delete_expired(expired, now_dt)
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
|
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
|
||||||
|
|||||||
@@ -82,15 +82,11 @@ def mail(email: str, subject: str, template: str,
|
|||||||
if order:
|
if order:
|
||||||
body += "\r\n"
|
body += "\r\n"
|
||||||
body += _(
|
body += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\r\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).format(event=event.name, orderurl=build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||||
event=event.name, orderurl=build_absolute_uri(
|
'order': order.code,
|
||||||
order.event, 'presale:event.order', kwargs={
|
'secret': order.secret
|
||||||
'order': order.code,
|
}))
|
||||||
'secret': order.secret
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
body += "\r\n"
|
body += "\r\n"
|
||||||
return mail_send([email], subject, body, sender, event.id if event else None, headers)
|
return mail_send([email], subject, body, sender, event.id if event else None, headers)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections import Counter, namedtuple
|
from collections import Counter, namedtuple
|
||||||
@@ -21,7 +22,7 @@ from pretix.base.models import (
|
|||||||
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
|
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
|
||||||
User, Voucher,
|
User, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import CachedTicket, InvoiceAddress
|
from pretix.base.models.orders import InvoiceAddress
|
||||||
from pretix.base.payment import BasePaymentProvider
|
from pretix.base.payment import BasePaymentProvider
|
||||||
from pretix.base.services.async import ProfiledTask
|
from pretix.base.services.async import ProfiledTask
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
@@ -50,6 +51,8 @@ error_messages = {
|
|||||||
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
|
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
|
||||||
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
|
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
|
||||||
'number of times allowed. We removed this item from your cart.'),
|
'number of times allowed. We removed this item from your cart.'),
|
||||||
|
'voucher_redeemed_partial': _('The voucher code used for one of the items in your cart can only be redeemed %d '
|
||||||
|
'more times. We removed this item from your cart.'),
|
||||||
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
|
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
|
||||||
'from your cart.'),
|
'from your cart.'),
|
||||||
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
|
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
|
||||||
@@ -82,7 +85,15 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
|||||||
:param user: The user that performed the change
|
:param user: The user that performed the change
|
||||||
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
|
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
|
||||||
"""
|
"""
|
||||||
with order.event.lock() as now_dt:
|
lock_func = order.event.lock
|
||||||
|
if order.status == order.STATUS_PENDING and order.expires > now() + timedelta(minutes=10):
|
||||||
|
# No lock necessary in this case. The 10 minute offset is just to be safe and prevent
|
||||||
|
# collisions with the cronjob.
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def lock_func():
|
||||||
|
yield now()
|
||||||
|
|
||||||
|
with lock_func() as now_dt:
|
||||||
can_be_paid = order._can_be_paid()
|
can_be_paid = order._can_be_paid()
|
||||||
if not force and can_be_paid is not True:
|
if not force and can_be_paid is not True:
|
||||||
raise Quota.QuotaExceededException(can_be_paid)
|
raise Quota.QuotaExceededException(can_be_paid)
|
||||||
@@ -185,6 +196,9 @@ def _check_date(event: Event, now_dt: datetime):
|
|||||||
|
|
||||||
|
|
||||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition]):
|
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition]):
|
||||||
|
"""
|
||||||
|
Checks constraints on all positions except quota
|
||||||
|
"""
|
||||||
err = None
|
err = None
|
||||||
_check_date(event, now_dt)
|
_check_date(event, now_dt)
|
||||||
|
|
||||||
@@ -193,17 +207,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
cp.delete()
|
cp.delete()
|
||||||
continue
|
continue
|
||||||
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
cp._quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
||||||
|
|
||||||
if cp.voucher:
|
|
||||||
redeemed_in_carts = CartPosition.objects.filter(
|
|
||||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
|
||||||
).exclude(pk=cp.pk)
|
|
||||||
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
|
||||||
if v_avail < 1:
|
|
||||||
err = err or error_messages['voucher_redeemed']
|
|
||||||
cp.delete() # Sorry!
|
|
||||||
continue
|
|
||||||
|
|
||||||
if cp.item.require_voucher and cp.voucher is None:
|
if cp.item.require_voucher and cp.voucher is None:
|
||||||
cp.delete()
|
cp.delete()
|
||||||
@@ -223,7 +227,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
price = cp.item.default_price if cp.variation is None else (
|
price = cp.item.default_price if cp.variation is None else (
|
||||||
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
|
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
|
||||||
|
|
||||||
if price is False or len(quotas) == 0:
|
if price is False or len(cp._quotas) == 0:
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
cp.delete()
|
cp.delete()
|
||||||
continue
|
continue
|
||||||
@@ -233,7 +237,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
err = err or error_messages['voucher_expired']
|
err = err or error_messages['voucher_expired']
|
||||||
cp.delete()
|
cp.delete()
|
||||||
continue
|
continue
|
||||||
price = cp.voucher.calculate_price(price)
|
if cp.voucher.price is not None:
|
||||||
|
price = cp.voucher.price
|
||||||
|
|
||||||
if price != cp.price and not (cp.item.free_price and cp.price > price):
|
if price != cp.price and not (cp.item.free_price and cp.price > price):
|
||||||
positions[i] = cp
|
positions[i] = cp
|
||||||
@@ -241,63 +246,19 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
cp.save()
|
cp.save()
|
||||||
err = err or error_messages['price_changed']
|
err = err or error_messages['price_changed']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
quota_ok = True
|
|
||||||
|
|
||||||
ignore_all_quotas = cp.expires >= now_dt or (
|
|
||||||
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
|
|
||||||
|
|
||||||
if not ignore_all_quotas:
|
|
||||||
for quota in quotas:
|
|
||||||
if cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id == quota.pk:
|
|
||||||
continue
|
|
||||||
avail = quota.availability(now_dt)
|
|
||||||
if avail[0] != Quota.AVAILABILITY_OK:
|
|
||||||
# This quota is sold out/currently unavailable, so do not sell this at all
|
|
||||||
err = err or error_messages['unavailable']
|
|
||||||
quota_ok = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if quota_ok:
|
|
||||||
positions[i] = cp
|
|
||||||
cp.expires = now_dt + timedelta(
|
|
||||||
minutes=event.settings.get('reservation_time', as_type=int))
|
|
||||||
cp.save()
|
|
||||||
else:
|
|
||||||
cp.delete() # Sorry!
|
|
||||||
if err:
|
if err:
|
||||||
raise OrderError(err)
|
raise OrderError(err)
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||||
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
|
payment_provider: BasePaymentProvider, expires: datetime, locale: str=None, address: int=None,
|
||||||
meta_info: dict=None):
|
meta_info: dict=None):
|
||||||
from datetime import date, time
|
|
||||||
|
|
||||||
total = sum([c.price for c in positions])
|
total = sum([c.price for c in positions])
|
||||||
payment_fee = payment_provider.calculate_fee(total)
|
payment_fee = payment_provider.calculate_fee(total)
|
||||||
total += payment_fee
|
total += payment_fee
|
||||||
|
|
||||||
tz = pytz.timezone(event.settings.timezone)
|
|
||||||
exp_by_date = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
|
|
||||||
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
|
||||||
if event.settings.get('payment_term_weekdays'):
|
|
||||||
if exp_by_date.weekday() == 5:
|
|
||||||
exp_by_date += timedelta(days=2)
|
|
||||||
elif exp_by_date.weekday() == 6:
|
|
||||||
exp_by_date += timedelta(days=1)
|
|
||||||
|
|
||||||
expires = exp_by_date
|
|
||||||
|
|
||||||
if event.settings.get('payment_term_last'):
|
|
||||||
last_date = make_aware(datetime.combine(
|
|
||||||
event.settings.get('payment_term_last', as_type=date),
|
|
||||||
time(hour=23, minute=59, second=59)
|
|
||||||
), tz)
|
|
||||||
if last_date < expires:
|
|
||||||
expires = last_date
|
|
||||||
|
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
status=Order.STATUS_PENDING,
|
status=Order.STATUS_PENDING,
|
||||||
event=event,
|
event=event,
|
||||||
@@ -329,6 +290,94 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def _check_quota_on_expired_positions(event: Event, positions: List[CartPosition], now_dt: datetime):
|
||||||
|
err = None
|
||||||
|
quotadiff = Counter()
|
||||||
|
vouchers = Counter()
|
||||||
|
for cp in positions:
|
||||||
|
if not cp.id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ignore_all_quotas = cp.expires >= now_dt or (
|
||||||
|
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
|
||||||
|
|
||||||
|
if ignore_all_quotas:
|
||||||
|
cp._quotas = []
|
||||||
|
elif cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id:
|
||||||
|
cp._quotas = [q for q in cp._quotas if cp.voucher.quota_id != q.pk]
|
||||||
|
|
||||||
|
for quota in cp._quotas:
|
||||||
|
quotadiff[quota] += 1
|
||||||
|
|
||||||
|
quotas_ok = {}
|
||||||
|
for quota, count in quotadiff.items():
|
||||||
|
avail = quota.availability(now_dt)
|
||||||
|
if avail[1] is not None and avail[1] < count:
|
||||||
|
# This quota is not available or less than items are than requested left, so we have to
|
||||||
|
# reduce the number of bought items
|
||||||
|
if avail[0] != Quota.AVAILABILITY_OK:
|
||||||
|
err = err or error_messages['unavailable']
|
||||||
|
else:
|
||||||
|
err = err or error_messages['in_part']
|
||||||
|
quotas_ok[quota] = min(count, avail[1])
|
||||||
|
else:
|
||||||
|
quotas_ok[quota] = count
|
||||||
|
|
||||||
|
for cp in positions:
|
||||||
|
if not cp.id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cp.voucher:
|
||||||
|
redeemed_in_carts = CartPosition.objects.filter(
|
||||||
|
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||||
|
).exclude(pk__in=[cp2.pk for cp2 in positions])
|
||||||
|
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||||
|
if v_avail < 1:
|
||||||
|
err = err or error_messages['voucher_redeemed']
|
||||||
|
cp.delete() # Sorry!
|
||||||
|
continue
|
||||||
|
if v_avail - vouchers[cp.voucher] < 1:
|
||||||
|
err = err or (error_messages['voucher_redeemed_partial'] % v_avail)
|
||||||
|
cp.delete() # Sorry!
|
||||||
|
continue
|
||||||
|
vouchers[cp.voucher] += 1
|
||||||
|
|
||||||
|
if cp._quotas:
|
||||||
|
if min(quotas_ok[q] for q in cp._quotas) > 0:
|
||||||
|
cp.expires = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
|
||||||
|
cp.save()
|
||||||
|
for q in cp._quotas:
|
||||||
|
quotas_ok[q] -= 1
|
||||||
|
else:
|
||||||
|
cp.delete()
|
||||||
|
|
||||||
|
if err:
|
||||||
|
raise OrderError(err)
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_expiry(event: Event, now_dt: datetime):
|
||||||
|
from datetime import date, time
|
||||||
|
|
||||||
|
tz = pytz.timezone(event.settings.timezone)
|
||||||
|
expires = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
|
||||||
|
expires = expires.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||||
|
if event.settings.get('payment_term_weekdays'):
|
||||||
|
if expires.weekday() == 5:
|
||||||
|
expires += timedelta(days=2)
|
||||||
|
elif expires.weekday() == 6:
|
||||||
|
expires += timedelta(days=1)
|
||||||
|
|
||||||
|
if event.settings.get('payment_term_last'):
|
||||||
|
last_date = make_aware(datetime.combine(
|
||||||
|
event.settings.get('payment_term_last', as_type=date),
|
||||||
|
time(hour=23, minute=59, second=59)
|
||||||
|
), tz)
|
||||||
|
if last_date < expires:
|
||||||
|
expires = last_date
|
||||||
|
|
||||||
|
return expires
|
||||||
|
|
||||||
|
|
||||||
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||||
email: str, locale: str, address: int, meta_info: dict=None):
|
email: str, locale: str, address: int, meta_info: dict=None):
|
||||||
|
|
||||||
@@ -342,13 +391,18 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
|||||||
if not pprov:
|
if not pprov:
|
||||||
raise OrderError(error_messages['internal'])
|
raise OrderError(error_messages['internal'])
|
||||||
|
|
||||||
|
now_dt = now()
|
||||||
|
|
||||||
|
positions = list(CartPosition.objects.filter(id__in=position_ids).select_related('item', 'variation'))
|
||||||
|
if set(str(p) for p in position_ids) != set(str(p.id) for p in positions):
|
||||||
|
raise OrderError(error_messages['internal'])
|
||||||
|
|
||||||
|
_check_positions(event, now_dt, positions)
|
||||||
|
expires = _calculate_expiry(event, now_dt)
|
||||||
|
|
||||||
with event.lock() as now_dt:
|
with event.lock() as now_dt:
|
||||||
positions = list(CartPosition.objects.filter(
|
_check_quota_on_expired_positions(event, positions, now_dt)
|
||||||
id__in=position_ids).select_related('item', 'variation'))
|
order = _create_order(event, email, positions, now_dt, pprov, expires,
|
||||||
if len(position_ids) != len(positions):
|
|
||||||
raise OrderError(error_messages['internal'])
|
|
||||||
_check_positions(event, now_dt, positions)
|
|
||||||
order = _create_order(event, email, positions, now_dt, pprov,
|
|
||||||
locale=locale, address=address, meta_info=meta_info)
|
locale=locale, address=address, meta_info=meta_info)
|
||||||
|
|
||||||
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||||
@@ -569,22 +623,18 @@ class OrderChangeManager:
|
|||||||
# Do nothing
|
# Do nothing
|
||||||
return
|
return
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
self._check_free_to_paid()
|
||||||
|
self._check_complete_cancel()
|
||||||
with self.order.event.lock():
|
with self.order.event.lock():
|
||||||
if self.order.status != Order.STATUS_PENDING:
|
if self.order.status != Order.STATUS_PENDING:
|
||||||
raise OrderError(self.error_messages['not_pending'])
|
raise OrderError(self.error_messages['not_pending'])
|
||||||
self._check_free_to_paid()
|
|
||||||
self._check_quotas()
|
self._check_quotas()
|
||||||
self._check_complete_cancel()
|
|
||||||
self._perform_operations()
|
self._perform_operations()
|
||||||
self._recalculate_total_and_payment_fee()
|
self._recalculate_total_and_payment_fee()
|
||||||
self._reissue_invoice()
|
self._reissue_invoice()
|
||||||
self._clear_tickets_cache()
|
|
||||||
self._check_paid_to_free()
|
self._check_paid_to_free()
|
||||||
self._notify_user()
|
self._notify_user()
|
||||||
|
|
||||||
def _clear_tickets_cache(self):
|
|
||||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
|
||||||
|
|
||||||
def _get_payment_provider(self):
|
def _get_payment_provider(self):
|
||||||
responses = register_payment_providers.send(self.order.event)
|
responses = register_payment_providers.send(self.order.event)
|
||||||
pprov = None
|
pprov = None
|
||||||
|
|||||||
@@ -105,12 +105,6 @@ class OrderPositionChangeForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class OrderContactForm(forms.ModelForm):
|
class OrderContactForm(forms.ModelForm):
|
||||||
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
|
|
||||||
help_text=_('Regenerates the order and ticket secrets. You will '
|
|
||||||
'need to re-send the link to the order page to the user and '
|
|
||||||
'the user will need to download his tickets again. The old '
|
|
||||||
'versions will be invalid.'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ['email']
|
fields = ['email']
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
||||||
from pretix.base.models.vouchers import _generate_random_code
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherForm(I18nModelForm):
|
class VoucherForm(I18nModelForm):
|
||||||
@@ -23,8 +22,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
model = Voucher
|
model = Voucher
|
||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag',
|
||||||
'comment', 'max_usages', 'price_mode'
|
'comment', 'max_usages'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||||
@@ -91,8 +90,9 @@ class VoucherForm(I18nModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'number' in data:
|
if 'codes' in data:
|
||||||
cnt = data['number'] * data['max_usages']
|
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||||
|
cnt = len(data['codes']) * data['max_usages']
|
||||||
else:
|
else:
|
||||||
cnt = data['max_usages']
|
cnt = data['max_usages']
|
||||||
|
|
||||||
@@ -174,37 +174,21 @@ class VoucherForm(I18nModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VoucherBulkForm(VoucherForm):
|
class VoucherBulkForm(VoucherForm):
|
||||||
number = forms.IntegerField(
|
codes = forms.CharField(
|
||||||
label=_("Number"),
|
widget=forms.Textarea,
|
||||||
|
label=_("Codes"),
|
||||||
|
help_text=_(
|
||||||
|
"Add one voucher code per line. We suggest that you copy this list and save it into a file."
|
||||||
|
),
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
itemvar = forms.ChoiceField(
|
|
||||||
label=_("Product"),
|
|
||||||
widget=forms.RadioSelect
|
|
||||||
)
|
|
||||||
price_mode = forms.ChoiceField(
|
|
||||||
choices=Voucher.PRICE_MODES,
|
|
||||||
)
|
|
||||||
has_valid_until = forms.BooleanField()
|
|
||||||
value_percent = forms.DecimalField(
|
|
||||||
required=False,
|
|
||||||
max_digits=10, decimal_places=2
|
|
||||||
)
|
|
||||||
value_subtract = forms.DecimalField(
|
|
||||||
required=False,
|
|
||||||
max_digits=10, decimal_places=2
|
|
||||||
)
|
|
||||||
value_set = forms.DecimalField(
|
|
||||||
required=False,
|
|
||||||
max_digits=10, decimal_places=2
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag', 'comment',
|
||||||
'max_usages', 'price_mode'
|
'max_usages'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||||
@@ -219,35 +203,21 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
if data.get('has_valid_until', False) and not data.get('valid_until'):
|
if Voucher.objects.filter(code__in=data['codes'], event=self.instance.event).exists():
|
||||||
raise ValidationError(_('You did not specify an expiration date for the vouchers.'))
|
raise ValidationError(_('A voucher with one of this codes already exists.'))
|
||||||
|
|
||||||
if data.get('price_mode', 'none') != 'none':
|
|
||||||
if data.get('value_%s' % data['price_mode']) is None:
|
|
||||||
raise ValidationError(_('You specified that the vouchers should modify the products price '
|
|
||||||
'but did not specify a value.'))
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self, event, *args, **kwargs):
|
def save(self, event, *args, **kwargs):
|
||||||
objs = []
|
objs = []
|
||||||
|
for code in self.cleaned_data['codes']:
|
||||||
codes = set()
|
|
||||||
while len(codes) < self.cleaned_data['number']:
|
|
||||||
new_codes = set()
|
|
||||||
for i in range(min(self.cleaned_data['number'] - len(codes), 500)):
|
|
||||||
# Work around SQLite's SQLITE_MAX_VARIABLE_NUMBER
|
|
||||||
new_codes.add(_generate_random_code())
|
|
||||||
new_codes -= set([v['code'] for v in Voucher.objects.filter(code__in=new_codes).values('code')])
|
|
||||||
codes |= new_codes
|
|
||||||
|
|
||||||
for code in codes:
|
|
||||||
obj = copy.copy(self.instance)
|
obj = copy.copy(self.instance)
|
||||||
obj.event = event
|
obj.event = event
|
||||||
obj.code = code
|
obj.code = code
|
||||||
data = dict(self.cleaned_data)
|
data = dict(self.cleaned_data)
|
||||||
data['code'] = code
|
data['code'] = code
|
||||||
data['bulk'] = True
|
data['bulk'] = True
|
||||||
|
del data['codes']
|
||||||
obj.save()
|
obj.save()
|
||||||
objs.append(obj)
|
objs.append(obj)
|
||||||
return objs
|
return objs
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/voucher.js" %}"></script>
|
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{{ html_head|safe }}
|
{{ html_head|safe }}
|
||||||
|
|||||||
@@ -99,8 +99,8 @@
|
|||||||
<th>{{ total.num_canceled|togglesum }}</th>
|
<th>{{ total.num_canceled|togglesum }}</th>
|
||||||
<th>{{ total.num_refunded|togglesum }}</th>
|
<th>{{ total.num_refunded|togglesum }}</th>
|
||||||
<th>{{ total.num_expired|togglesum }}</th>
|
<th>{{ total.num_expired|togglesum }}</th>
|
||||||
<th>{{ total.num_pending|togglesum }}</th>
|
|
||||||
<th>{{ total.num_paid|togglesum }}</th>
|
<th>{{ total.num_paid|togglesum }}</th>
|
||||||
|
<th>{{ total.num_pending|togglesum }}</th>
|
||||||
<th>{{ total.num_total|togglesum }}</th>
|
<th>{{ total.num_total|togglesum }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|||||||
@@ -2,139 +2,59 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load eventsignal %}
|
{% load eventsignal %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load capture_tags %}
|
|
||||||
{% block title %}{% trans "Voucher" %}{% endblock %}
|
{% block title %}{% trans "Voucher" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Create new vouchers" %}</h1>
|
<h1>{% trans "Create multiple voucher" %}</h1>
|
||||||
<form action="" method="post" class="form-inline" id="voucher-create">
|
<form action="" method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% capture as number_field silent %} {% bootstrap_field form.number layout="inline" %} {% endcapture %}
|
|
||||||
{% capture as max_usages_field silent %} {% bootstrap_field form.max_usages layout="inline" %} {% endcapture %}
|
|
||||||
{% capture as valid_until_field silent %} {% bootstrap_field form.valid_until layout="inline" %} {% endcapture %}
|
|
||||||
{% capture as value_field_percent silent %} {% bootstrap_field form.value_percent layout="inline" %} {% endcapture %}
|
|
||||||
{% capture as value_field_subtract silent %} {% bootstrap_field form.value_subtract layout="inline" %} {% endcapture %}
|
|
||||||
{% capture as value_field_set silent %} {% bootstrap_field form.value_set layout="inline" %} {% endcapture %}
|
|
||||||
|
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
|
<fieldset>
|
||||||
<div class="wizard-step" id="step-number">
|
<legend>{% trans "Voucher codes" %}</legend>
|
||||||
<div class="wizard-step-inner">
|
<div class="form-group">
|
||||||
<h4>{% trans "How many vouchers do you want to create?" %}</h4>
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<div class="form-line">
|
<div class="input-group">
|
||||||
{% blocktrans trimmed %}
|
<input type="text" class="form-control input-xs"
|
||||||
Create {{ number_field }} voucher codes. Each of them can be redeemed {{ max_usages_field }}
|
id="voucher-bulk-codes-num"
|
||||||
times.
|
placeholder="{% trans "Number" %}">
|
||||||
{% endblocktrans %}
|
<div class="input-group-btn">
|
||||||
</div>
|
<button class="btn btn-default" type="button" id="voucher-bulk-codes-generate"
|
||||||
</div>
|
data-rng-url="{% url 'control:event.vouchers.rng' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
</div>
|
{% trans "Generate random codes" %}
|
||||||
|
</button>
|
||||||
<div class="wizard-step" id="step-valid">
|
</div>
|
||||||
<div class="wizard-step-inner">
|
|
||||||
<h4>{% trans "How long should the vouchers be valid?" %}</h4>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="has_valid_until" value="" {% if request.POST and not "has_valid_until" in request.POST %}checked="checked"{% endif %}>
|
|
||||||
{% trans "The whole presale period" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="has_valid_until" value="on" {% if "on" == request.POST.has_valid_until %}checked="checked"{% endif %}>
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
Only valid until {{ valid_until_field }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wizard-step" id="step-products">
|
|
||||||
<div class="wizard-step-inner">
|
|
||||||
<h4>{% trans "For which products should the vouchers be applicable?" %}</h4>
|
|
||||||
{% bootstrap_field form.itemvar layout="inline" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wizard-step" id="step-price">
|
|
||||||
<div class="wizard-step-inner">
|
|
||||||
<h4>{% trans "Should the vouchers modify the product's price?" %}</h4>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="price_mode" value="none" {% if "none" == request.POST.price_mode %}checked="checked"{% endif %}>
|
|
||||||
{% trans "No, just allow to buy this product" %}
|
|
||||||
<span class="help-block">
|
|
||||||
{% trans "This is useful if you have products that can only be bought using vouchers. It also allows you to just block quota for someone (see next step)." %}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="price_mode" value="percent" {% if "percent" == request.POST.price_mode %}checked="checked"{% endif %}>
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
Reduce price by {{ value_field_percent }} %
|
|
||||||
{% endblocktrans %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="price_mode" value="subtract" {% if "subtract" == request.POST.price_mode %}checked="checked"{% endif %}>
|
|
||||||
{% blocktrans trimmed with currency=request.event.currency %}
|
|
||||||
Reduce price by {{ value_field_subtract }} {{ currency }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="price_mode" value="set" {% if "set" == request.POST.price_mode %}checked="checked"{% endif %}>
|
|
||||||
{% blocktrans trimmed with currency=request.event.currency %}
|
|
||||||
Change price to {{ value_field_set }} {{ currency }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wizard-step" id="step-block">
|
|
||||||
<div class="wizard-step-inner">
|
|
||||||
<h4>{% trans "Should the vouchers block quota?" %}</h4>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="block_quota" value="" {% if request.POST and not "block_quota" in request.POST %}checked="checked"{% endif %}>
|
|
||||||
{% trans "No" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio radio-alt">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="block_quota" value="on" {% if "on" == request.POST.block_quota %}checked="checked"{% endif %}>
|
|
||||||
{% trans "Yes" %}
|
|
||||||
<span class="help-block">
|
|
||||||
{% trans "If you select this option, these vouchers will be guaranteed, i.e. the applicable quotas will be reduced in a way that these vouchers can be redeemed as long as they are valid, even if your event sells out otherwise." %}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wizard-step" id="step-advanced">
|
|
||||||
<div class="wizard-step-inner">
|
|
||||||
<a href="#" id="wizard-advanced-show">Show advanced options</a>
|
|
||||||
<div class="wizard-advanced">
|
|
||||||
<h4>{% trans "Advanced options" %}</h4>
|
|
||||||
{% bootstrap_field form.allow_ignore_quota %}
|
|
||||||
<p><strong>{% trans "Comment" %}</strong></p>
|
|
||||||
{% bootstrap_field form.comment layout="inline" form_group_class="comment" %}
|
|
||||||
<div class="help-block">
|
|
||||||
{% trans "The text entered in this field will not be visible to the user and is available for your convenience." %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% bootstrap_field form.codes layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.max_usages layout="horizontal" %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Voucher details" %}</legend>
|
||||||
|
{% bootstrap_field form.valid_until layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.block_quota layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.price layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.itemvar layout="horizontal" %}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
<div class="controls">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
If you choose "any product" for a specific quota and choose to reserve quota for this
|
||||||
|
voucher above, the product can still be unavailable to the voucher holder if another quota
|
||||||
|
associated with the product is sold out!
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% bootstrap_field form.tag layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.comment layout="horizontal" %}
|
||||||
|
</fieldset>
|
||||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||||
<div class="submit-group" id="step-save">
|
<div class="form-group submit-group">
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
{% trans "Create" %}
|
{% trans "Save" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -27,15 +27,7 @@
|
|||||||
{% bootstrap_field form.valid_until layout="horizontal" %}
|
{% bootstrap_field form.valid_until layout="horizontal" %}
|
||||||
{% bootstrap_field form.block_quota layout="horizontal" %}
|
{% bootstrap_field form.block_quota layout="horizontal" %}
|
||||||
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
|
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
|
||||||
<div class="form-group">
|
{% bootstrap_field form.price layout="horizontal" %}
|
||||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
|
||||||
<div class="col-md-5">
|
|
||||||
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% bootstrap_field form.itemvar layout="horizontal" %}
|
{% bootstrap_field form.itemvar layout="horizontal" %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
|||||||
@@ -37,12 +37,17 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create new vouchers" %}</a>
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
||||||
|
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create multiple new vouchers" %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new vouchers" %}</a>
|
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
||||||
|
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
|
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||||
|
{% trans "Create multiple new vouchers" %}</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-quotas">
|
<table class="table table-hover table-quotas">
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ urlpatterns = [
|
|||||||
url(r'^vouchers/(?P<voucher>\d+)/delete$', vouchers.VoucherDelete.as_view(),
|
url(r'^vouchers/(?P<voucher>\d+)/delete$', vouchers.VoucherDelete.as_view(),
|
||||||
name='event.voucher.delete'),
|
name='event.voucher.delete'),
|
||||||
url(r'^vouchers/add$', vouchers.VoucherCreate.as_view(), name='event.vouchers.add'),
|
url(r'^vouchers/add$', vouchers.VoucherCreate.as_view(), name='event.vouchers.add'),
|
||||||
|
url(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
|
||||||
url(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
url(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
||||||
name='event.order.transition'),
|
name='event.order.transition'),
|
||||||
url(r'^orders/(?P<code>[0-9A-Z]+)/resend$', orders.OrderResendLink.as_view(),
|
url(r'^orders/(?P<code>[0-9A-Z]+)/resend$', orders.OrderResendLink.as_view(),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from django.views.generic.detail import SingleObjectMixin
|
|||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedTicket, Event, EventPermission, Item, ItemVariation, User,
|
Event, EventPermission, Item, ItemVariation, User,
|
||||||
)
|
)
|
||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||||
@@ -450,9 +450,6 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
|||||||
for k in provider.form.changed_data
|
for k in provider.form.changed_data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
CachedTicket.objects.filter(
|
|
||||||
order_position__order__event=self.request.event, provider=provider.identifier
|
|
||||||
).delete()
|
|
||||||
else:
|
else:
|
||||||
success = False
|
success = False
|
||||||
form = self.get_form(self.get_form_class())
|
form = self.get_form(self.get_form_class())
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from django.views.generic.edit import DeleteView
|
|||||||
|
|
||||||
from pretix.base.forms import I18nFormSet
|
from pretix.base.forms import I18nFormSet
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedTicket, Item, ItemCategory, ItemVariation, Order, Question,
|
Item, ItemCategory, ItemVariation, Order, Question, QuestionAnswer,
|
||||||
QuestionAnswer, QuestionOption, Quota,
|
QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.item import (
|
from pretix.control.forms.item import (
|
||||||
CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
||||||
@@ -787,7 +787,6 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
|
|||||||
for k in form.changed_data
|
for k in form.changed_data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
CachedTicket.objects.filter(order_position__item=self.item).delete()
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ from django.views.generic import DetailView, ListView, TemplateView, View
|
|||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, CachedTicket, Invoice, Item, ItemVariation, Order, Quota,
|
CachedFile, Invoice, Item, ItemVariation, Order, Quota,
|
||||||
generate_position_secret, generate_secret,
|
|
||||||
)
|
)
|
||||||
from pretix.base.services.export import export
|
from pretix.base.services.export import export
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
@@ -529,16 +528,8 @@ class OrderContactChange(OrderView):
|
|||||||
if self.form.is_valid():
|
if self.form.is_valid():
|
||||||
self.order.log_action('pretix.event.order.contact.changed', {
|
self.order.log_action('pretix.event.order.contact.changed', {
|
||||||
'old_email': self.order.email,
|
'old_email': self.order.email,
|
||||||
'new_email': self.form.cleaned_data['email'],
|
'new_email': self.form.cleaned_data['email']
|
||||||
'regenerate_secrets': self.form.cleaned_data['regenerate_secrets']
|
|
||||||
})
|
})
|
||||||
if self.form.cleaned_data['regenerate_secrets']:
|
|
||||||
self.order.secret = generate_secret()
|
|
||||||
for op in self.order.positions.all():
|
|
||||||
op.secret = generate_position_secret()
|
|
||||||
op.save()
|
|
||||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
|
||||||
|
|
||||||
self.form.save()
|
self.form.save()
|
||||||
messages.success(self.request, _('The order has been changed.'))
|
messages.success(self.request, _('The order has been changed.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class VoucherList(EventPermissionRequiredMixin, ListView):
|
|||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
|
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
|
||||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages')
|
_('Price'), _('Tag'), _('Redeemed'), _('Maximum usages')
|
||||||
]
|
]
|
||||||
writer.writerow(headers)
|
writer.writerow(headers)
|
||||||
|
|
||||||
@@ -77,8 +77,7 @@ class VoucherList(EventPermissionRequiredMixin, ListView):
|
|||||||
prod,
|
prod,
|
||||||
_("Yes") if v.block_quota else _("No"),
|
_("Yes") if v.block_quota else _("No"),
|
||||||
_("Yes") if v.allow_ignore_quota else _("No"),
|
_("Yes") if v.allow_ignore_quota else _("No"),
|
||||||
v.get_price_mode_display(),
|
str(v.price) if v.price else "",
|
||||||
str(v.value) if v.value else "",
|
|
||||||
v.tag,
|
v.tag,
|
||||||
str(v.redeemed),
|
str(v.redeemed),
|
||||||
str(v.max_usages)
|
str(v.max_usages)
|
||||||
@@ -189,6 +188,44 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class VoucherCreate(EventPermissionRequiredMixin, CreateView):
|
class VoucherCreate(EventPermissionRequiredMixin, CreateView):
|
||||||
|
model = Voucher
|
||||||
|
template_name = 'pretixcontrol/vouchers/detail.html'
|
||||||
|
permission = 'can_change_vouchers'
|
||||||
|
context_object_name = 'voucher'
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
form_class = VoucherForm
|
||||||
|
for receiver, response in voucher_form_class.send(self.request.event, cls=form_class):
|
||||||
|
if response:
|
||||||
|
form_class = response
|
||||||
|
return form_class
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return reverse('control:event.vouchers', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['instance'] = Voucher(event=self.request.event)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.event = self.request.event
|
||||||
|
messages.success(self.request, _('The new voucher has been created.'))
|
||||||
|
ret = super().form_valid(form)
|
||||||
|
form.instance.log_action('pretix.voucher.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# TODO: Transform this into an asynchronous call?
|
||||||
|
with request.event.lock():
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||||
model = Voucher
|
model = Voucher
|
||||||
template_name = 'pretixcontrol/vouchers/bulk.html'
|
template_name = 'pretixcontrol/vouchers/bulk.html'
|
||||||
permission = 'can_change_vouchers'
|
permission = 'can_change_vouchers'
|
||||||
@@ -203,11 +240,6 @@ class VoucherCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['instance'] = Voucher(event=self.request.event)
|
kwargs['instance'] = Voucher(event=self.request.event)
|
||||||
initial = {
|
|
||||||
}
|
|
||||||
if 'initial' in kwargs:
|
|
||||||
initial.update(kwargs['initial'])
|
|
||||||
kwargs['initial'] = initial
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2016-11-27 18:45+0000\n"
|
"POT-Creation-Date: 2016-11-08 19:21+0000\n"
|
||||||
"PO-Revision-Date: 2016-11-27 19:46+0100\n"
|
"PO-Revision-Date: 2016-11-08 20:23+0100\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
@@ -16,7 +16,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: Poedit 1.8.11\n"
|
"X-Generator: Poedit 1.8.9\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
||||||
@@ -46,7 +46,7 @@ msgstr "Gesamtumsatz"
|
|||||||
msgid "Contacting Stripe …"
|
msgid "Contacting Stripe …"
|
||||||
msgstr "Kontaktiere Stripe …"
|
msgstr "Kontaktiere Stripe …"
|
||||||
|
|
||||||
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
|
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:57
|
||||||
msgid "Close message"
|
msgid "Close message"
|
||||||
msgstr "Schließen"
|
msgstr "Schließen"
|
||||||
|
|
||||||
@@ -63,22 +63,17 @@ msgid "Count"
|
|||||||
msgstr "Anzahl"
|
msgstr "Anzahl"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:27
|
#: static/pretixpresale/js/ui/asynctask.js:27
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:62
|
msgid "Your request has been queued on the server and will now be processed."
|
||||||
msgid ""
|
|
||||||
"Your request has been queued on the server and will now be processed. If "
|
|
||||||
"this takes longer than two minutes, please contact us or go back in your "
|
|
||||||
"browser and try again."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ihre Anfrage ist auf dem Server angekommen und wird nun verarbeitet. Wenn "
|
"Ihre Anfrage befindet sich beim Server in der Warteschlange und wird nun "
|
||||||
"dies länger als zwei Minuten dauert, kontaktieren Sie uns bitte oder gehen "
|
"verarbeitet."
|
||||||
"Sie in Ihrem Browser einen Schritt zurück und versuchen es erneut."
|
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:41
|
#: static/pretixpresale/js/ui/asynctask.js:40
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:83
|
#: static/pretixpresale/js/ui/asynctask.js:74
|
||||||
msgid "An error of type {code} occured."
|
msgid "An error of type {code} occured."
|
||||||
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:44
|
#: static/pretixpresale/js/ui/asynctask.js:43
|
||||||
msgid ""
|
msgid ""
|
||||||
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
||||||
"{code}"
|
"{code}"
|
||||||
@@ -86,31 +81,17 @@ msgstr ""
|
|||||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||||
"Letzter Fehlercode: {code}"
|
"Letzter Fehlercode: {code}"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:74
|
#: static/pretixpresale/js/ui/asynctask.js:77
|
||||||
msgid "The request took to long. Please try again."
|
|
||||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:86
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
|
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
|
||||||
"einmal. Fehlercode: {code}"
|
"einmal. Fehlercode: {code}"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:101
|
#: static/pretixpresale/js/ui/asynctask.js:92
|
||||||
msgid "We are processing your request …"
|
msgid "We are processing your request …"
|
||||||
msgstr "Wir verarbeiten deine Anfrage …"
|
msgstr "Wir verarbeiten deine Anfrage …"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:102
|
|
||||||
msgid ""
|
|
||||||
"We are currently sending your request to the server. If this takes longer "
|
|
||||||
"than one minute, please check your internet connection and then reload this "
|
|
||||||
"page and try again."
|
|
||||||
msgstr ""
|
|
||||||
"Ihre Anfrage wird an den Server gesendet. Wenn dies länger als eine Minute "
|
|
||||||
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
|
|
||||||
"Seite neu laden und es erneut versuchen."
|
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/cart.js:10
|
#: static/pretixpresale/js/ui/cart.js:10
|
||||||
msgid "The items in your cart are no longer reserved for you."
|
msgid "The items in your cart are no longer reserved for you."
|
||||||
msgstr "Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert."
|
msgstr "Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert."
|
||||||
@@ -122,9 +103,3 @@ msgstr[0] ""
|
|||||||
"Die Produkte in Ihrem Warenkorb sind noch eine Minute für Sie reserviert."
|
"Die Produkte in Ihrem Warenkorb sind noch eine Minute für Sie reserviert."
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
"Die Produkte in Ihrem Warenkorb sind noch {num} Minuten für Sie reserviert."
|
"Die Produkte in Ihrem Warenkorb sind noch {num} Minuten für Sie reserviert."
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "Your request has been queued on the server and will now be processed."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Ihre Anfrage befindet sich beim Server in der Warteschlange und wird nun "
|
|
||||||
#~ "verarbeitet."
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2016-11-27 18:45+0000\n"
|
"POT-Creation-Date: 2016-11-08 19:21+0000\n"
|
||||||
"PO-Revision-Date: 2016-11-27 19:52+0100\n"
|
"PO-Revision-Date: 2016-11-08 20:22+0100\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
@@ -16,7 +16,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: Poedit 1.8.11\n"
|
"X-Generator: Poedit 1.8.9\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
||||||
@@ -46,7 +46,7 @@ msgstr "Gesamtumsatz"
|
|||||||
msgid "Contacting Stripe …"
|
msgid "Contacting Stripe …"
|
||||||
msgstr "Kontaktiere Stripe …"
|
msgstr "Kontaktiere Stripe …"
|
||||||
|
|
||||||
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
|
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:57
|
||||||
msgid "Close message"
|
msgid "Close message"
|
||||||
msgstr "Schließen"
|
msgstr "Schließen"
|
||||||
|
|
||||||
@@ -63,22 +63,17 @@ msgid "Count"
|
|||||||
msgstr "Anzahl"
|
msgstr "Anzahl"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:27
|
#: static/pretixpresale/js/ui/asynctask.js:27
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:62
|
msgid "Your request has been queued on the server and will now be processed."
|
||||||
msgid ""
|
|
||||||
"Your request has been queued on the server and will now be processed. If "
|
|
||||||
"this takes longer than two minutes, please contact us or go back in your "
|
|
||||||
"browser and try again."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Deine Anfrage ist auf dem Server angekommen und wird nun verarbeitet. Wenn "
|
"Deine Anfrage befindet sich beim Server in der Warteschlange und wird nun "
|
||||||
"dies länger als zwei Minuten dauert, kontaktiere uns bitte oder gehe in "
|
"verarbeitet."
|
||||||
"deinem Browser einen Schritt zurück und versuche es erneut."
|
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:41
|
#: static/pretixpresale/js/ui/asynctask.js:40
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:83
|
#: static/pretixpresale/js/ui/asynctask.js:74
|
||||||
msgid "An error of type {code} occured."
|
msgid "An error of type {code} occured."
|
||||||
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:44
|
#: static/pretixpresale/js/ui/asynctask.js:43
|
||||||
msgid ""
|
msgid ""
|
||||||
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
||||||
"{code}"
|
"{code}"
|
||||||
@@ -86,31 +81,17 @@ msgstr ""
|
|||||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||||
"Letzter Fehlercode: {code}"
|
"Letzter Fehlercode: {code}"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:74
|
#: static/pretixpresale/js/ui/asynctask.js:77
|
||||||
msgid "The request took to long. Please try again."
|
|
||||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:86
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
|
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
|
||||||
"einmal. Fehlercode: {code}"
|
"einmal. Fehlercode: {code}"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:101
|
#: static/pretixpresale/js/ui/asynctask.js:92
|
||||||
msgid "We are processing your request …"
|
msgid "We are processing your request …"
|
||||||
msgstr "Wir verarbeiten deine Anfrage …"
|
msgstr "Wir verarbeiten deine Anfrage …"
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/asynctask.js:102
|
|
||||||
msgid ""
|
|
||||||
"We are currently sending your request to the server. If this takes longer "
|
|
||||||
"than one minute, please check your internet connection and then reload this "
|
|
||||||
"page and try again."
|
|
||||||
msgstr ""
|
|
||||||
"Deine Anfrage wird an den Server gesendet. Wenn dies länger als eine Minute "
|
|
||||||
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
|
|
||||||
"neu laden und es erneut versuchen."
|
|
||||||
|
|
||||||
#: static/pretixpresale/js/ui/cart.js:10
|
#: static/pretixpresale/js/ui/cart.js:10
|
||||||
msgid "The items in your cart are no longer reserved for you."
|
msgid "The items in your cart are no longer reserved for you."
|
||||||
msgstr "Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert."
|
msgstr "Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert."
|
||||||
@@ -122,9 +103,3 @@ msgstr[0] ""
|
|||||||
"Die Produkte in deinem Warenkorb sind noch eine Minute für dich reserviert."
|
"Die Produkte in deinem Warenkorb sind noch eine Minute für dich reserviert."
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
"Die Produkte in deinem Warenkorb sind noch {num} Minuten für dich reserviert."
|
"Die Produkte in deinem Warenkorb sind noch {num} Minuten für dich reserviert."
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "Your request has been queued on the server and will now be processed."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Deine Anfrage befindet sich beim Server in der Warteschlange und wird nun "
|
|
||||||
#~ "verarbeitet."
|
|
||||||
|
|||||||
@@ -64,6 +64,23 @@ class Paypal(BasePaymentProvider):
|
|||||||
|
|
||||||
def checkout_prepare(self, request, cart):
|
def checkout_prepare(self, request, cart):
|
||||||
self.init_api()
|
self.init_api()
|
||||||
|
items = []
|
||||||
|
for cp in cart['positions']:
|
||||||
|
items.append({
|
||||||
|
"name": str(cp.item.name),
|
||||||
|
"description": str(cp.variation) if cp.variation else "",
|
||||||
|
"quantity": cp.count,
|
||||||
|
"price": str(cp.price),
|
||||||
|
"currency": request.event.currency
|
||||||
|
})
|
||||||
|
if cart['payment_fee']:
|
||||||
|
items.append({
|
||||||
|
"name": __('Payment method fee'),
|
||||||
|
"description": "",
|
||||||
|
"quantity": 1,
|
||||||
|
"currency": request.event.currency,
|
||||||
|
"price": str(cart['payment_fee'])
|
||||||
|
})
|
||||||
payment = paypalrestsdk.Payment({
|
payment = paypalrestsdk.Payment({
|
||||||
'intent': 'sale',
|
'intent': 'sale',
|
||||||
'payer': {
|
'payer': {
|
||||||
@@ -76,20 +93,13 @@ class Paypal(BasePaymentProvider):
|
|||||||
"transactions": [
|
"transactions": [
|
||||||
{
|
{
|
||||||
"item_list": {
|
"item_list": {
|
||||||
"items": [
|
"items": items
|
||||||
{
|
|
||||||
"name": __('Order for %s') % str(request.event),
|
|
||||||
"quantity": 1,
|
|
||||||
"price": str(cart['total']),
|
|
||||||
"currency": request.event.currency
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"amount": {
|
"amount": {
|
||||||
"currency": request.event.currency,
|
"currency": request.event.currency,
|
||||||
"total": str(cart['total'])
|
"total": str(cart['total'])
|
||||||
},
|
},
|
||||||
"description": __('Event tickets for {event}').format(event=request.event.name)
|
"description": __('Event tickets for %s') % request.event.name
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -104,6 +114,7 @@ class Paypal(BasePaymentProvider):
|
|||||||
logger.error('Invalid payment state: ' + str(payment))
|
logger.error('Invalid payment state: ' + str(payment))
|
||||||
return
|
return
|
||||||
request.session['payment_paypal_id'] = payment.id
|
request.session['payment_paypal_id'] = payment.id
|
||||||
|
request.session['payment_paypal_event'] = self.event.id
|
||||||
for link in payment.links:
|
for link in payment.links:
|
||||||
if link.method == "REDIRECT" and link.rel == "approval_url":
|
if link.method == "REDIRECT" and link.rel == "approval_url":
|
||||||
return str(link.href)
|
return str(link.href)
|
||||||
@@ -153,30 +164,6 @@ class Paypal(BasePaymentProvider):
|
|||||||
return self._execute_payment(payment, request, order)
|
return self._execute_payment(payment, request, order)
|
||||||
|
|
||||||
def _execute_payment(self, payment, request, order):
|
def _execute_payment(self, payment, request, order):
|
||||||
payment.replace([
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/transactions/0/item_list",
|
|
||||||
"value": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
|
|
||||||
"quantity": 1,
|
|
||||||
"price": str(order.total),
|
|
||||||
"currency": order.event.currency
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/transactions/0/description",
|
|
||||||
"value": __('Order {order} for {event}').format(
|
|
||||||
event=request.event.name,
|
|
||||||
order=order.code
|
|
||||||
)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
||||||
|
|
||||||
if payment.state == 'pending':
|
if payment.state == 'pending':
|
||||||
@@ -275,7 +262,7 @@ class Paypal(BasePaymentProvider):
|
|||||||
"item_list": {
|
"item_list": {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
|
"name": 'Order %s' % order.code,
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"price": str(order.total),
|
"price": str(order.total),
|
||||||
"currency": order.event.currency
|
"currency": order.event.currency
|
||||||
@@ -286,10 +273,7 @@ class Paypal(BasePaymentProvider):
|
|||||||
"currency": request.event.currency,
|
"currency": request.event.currency,
|
||||||
"total": str(order.total)
|
"total": str(order.total)
|
||||||
},
|
},
|
||||||
"description": __('Order {order} for {event}').format(
|
"description": __('Event tickets for %s') % request.event.name
|
||||||
event=request.event.name,
|
|
||||||
order=order.code
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,48 +4,41 @@ from django.contrib import messages
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Order
|
from pretix.base.models import Event, Order
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
from pretix.plugins.paypal.payment import Paypal
|
from pretix.plugins.paypal.payment import Paypal
|
||||||
from pretix.presale.utils import event_view
|
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.plugins.paypal')
|
logger = logging.getLogger('pretix.plugins.paypal')
|
||||||
|
|
||||||
|
|
||||||
@event_view(require_live=False)
|
def success(request, organizer=None, event=None):
|
||||||
def success(request, *args, **kwargs):
|
|
||||||
pid = request.GET.get('paymentId')
|
pid = request.GET.get('paymentId')
|
||||||
token = request.GET.get('token')
|
token = request.GET.get('token')
|
||||||
payer = request.GET.get('PayerID')
|
payer = request.GET.get('PayerID')
|
||||||
request.session['payment_paypal_token'] = token
|
|
||||||
request.session['payment_paypal_payer'] = payer
|
|
||||||
|
|
||||||
if request.session.get('payment_paypal_order'):
|
|
||||||
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
|
|
||||||
else:
|
|
||||||
order = None
|
|
||||||
|
|
||||||
if pid == request.session.get('payment_paypal_id', None):
|
if pid == request.session.get('payment_paypal_id', None):
|
||||||
if order:
|
request.session['payment_paypal_token'] = token
|
||||||
prov = Paypal(request.event)
|
request.session['payment_paypal_payer'] = payer
|
||||||
resp = prov.payment_perform(request, order)
|
try:
|
||||||
if resp:
|
event = Event.objects.get(id=request.session['payment_paypal_event'])
|
||||||
return resp
|
if request.session.get('payment_paypal_order'):
|
||||||
|
prov = Paypal(event)
|
||||||
|
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
|
||||||
|
resp = prov.payment_perform(request, order)
|
||||||
|
return redirect(resp or eventreverse(event, 'presale:event.order', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret
|
||||||
|
}) + '?paid=yes')
|
||||||
|
return redirect(eventreverse(event, 'presale:event.checkout', kwargs={'step': 'confirm'}))
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
pass # TODO: Handle this
|
||||||
else:
|
else:
|
||||||
messages.error(request, _('Invalid response from PayPal received.'))
|
pass # TODO: Handle this
|
||||||
logger.error('Session did not contain payment_paypal_id')
|
|
||||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
|
||||||
|
|
||||||
if order:
|
|
||||||
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret
|
|
||||||
}) + ('?paid=yes' if order.status == Order.STATUS_PAID else ''))
|
|
||||||
else:
|
|
||||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'confirm'}))
|
|
||||||
|
|
||||||
|
|
||||||
@event_view(require_live=False)
|
def abort(request, organizer=None, event=None):
|
||||||
def abort(request, *args, **kwargs):
|
|
||||||
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
try:
|
||||||
|
event = Event.objects.get(id=request.session['payment_paypal_event'])
|
||||||
|
return redirect(eventreverse(event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
pass # TODO: Handle this
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ class OverviewReport(Report):
|
|||||||
str(total['num_canceled'][0]), str(total['num_canceled'][1]),
|
str(total['num_canceled'][0]), str(total['num_canceled'][1]),
|
||||||
str(total['num_refunded'][0]), str(total['num_refunded'][1]),
|
str(total['num_refunded'][0]), str(total['num_refunded'][1]),
|
||||||
str(total['num_expired'][0]), str(total['num_expired'][1]),
|
str(total['num_expired'][0]), str(total['num_expired'][1]),
|
||||||
str(total['num_pending'][0]), str(total['num_pending'][1]),
|
|
||||||
str(total['num_paid'][0]), str(total['num_paid'][1]),
|
str(total['num_paid'][0]), str(total['num_paid'][1]),
|
||||||
|
str(total['num_pending'][0]), str(total['num_pending'][1]),
|
||||||
str(total['num_total'][0]), str(total['num_total'][1]),
|
str(total['num_total'][0]), str(total['num_total'][1]),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from pretix.base.models import Order
|
from pretix.base.models import Order
|
||||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
from pretix.base.services.orders import mark_order_refunded
|
||||||
from pretix.plugins.stripe.payment import Stripe
|
from pretix.plugins.stripe.payment import Stripe
|
||||||
from pretix.presale.utils import event_view
|
from pretix.presale.utils import event_view
|
||||||
|
|
||||||
@@ -54,10 +54,7 @@ def webhook(request, *args, **kwargs):
|
|||||||
|
|
||||||
order.log_action('pretix.plugins.stripe.event', data=event_json)
|
order.log_action('pretix.plugins.stripe.event', data=event_json)
|
||||||
|
|
||||||
is_refund = charge['refunds']['total_count'] or charge['dispute']
|
if order.status == Order.STATUS_PAID and (charge['refunds']['total_count'] or charge['dispute']):
|
||||||
if order.status == Order.STATUS_PAID and is_refund:
|
|
||||||
mark_order_refunded(order, user=None)
|
mark_order_refunded(order, user=None)
|
||||||
elif order.status == Order.STATUS_PENDING and charge['status'] == 'succeeded' and not is_refund:
|
|
||||||
mark_order_paid(order, user=None)
|
|
||||||
|
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
|
|||||||
@@ -198,16 +198,20 @@ class RedeemView(EventViewMixin, TemplateView):
|
|||||||
item.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
item.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
||||||
else:
|
else:
|
||||||
item.cached_availability = item.check_quotas()
|
item.cached_availability = item.check_quotas()
|
||||||
item.price = self.voucher.calculate_price(item.default_price)
|
if self.voucher.price is not None:
|
||||||
|
item.price = self.voucher.price
|
||||||
|
else:
|
||||||
|
item.price = item.default_price
|
||||||
else:
|
else:
|
||||||
for var in item.available_variations:
|
for var in item.available_variations:
|
||||||
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
|
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
|
||||||
var.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
var.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
||||||
else:
|
else:
|
||||||
var.cached_availability = list(var.check_quotas())
|
var.cached_availability = list(var.check_quotas())
|
||||||
var.price = self.voucher.calculate_price(
|
if self.voucher.price is not None:
|
||||||
var.default_price if var.default_price is not None else item.default_price
|
var.price = self.voucher.price
|
||||||
)
|
else:
|
||||||
|
var.price = var.default_price if var.default_price is not None else item.default_price
|
||||||
|
|
||||||
if len(item.available_variations) > 0:
|
if len(item.available_variations) > 0:
|
||||||
item.min_price = min([v.price for v in item.available_variations])
|
item.min_price = min([v.price for v in item.available_variations])
|
||||||
|
|||||||
@@ -401,8 +401,6 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
|
|||||||
success_message = ('Your invoice address has been updated. Please contact us if you need us '
|
success_message = ('Your invoice address has been updated. Please contact us if you need us '
|
||||||
'to regenerate your invoice.')
|
'to regenerate your invoice.')
|
||||||
messages.success(self.request, _(success_message))
|
messages.success(self.request, _(success_message))
|
||||||
|
|
||||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ METRICS_PASSPHRASE = config.get('metrics', 'passphrase', fallback="")
|
|||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
'LOCATION': 'unique-snowflake',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
REAL_CACHE_USED = False
|
REAL_CACHE_USED = False
|
||||||
@@ -193,7 +194,6 @@ INSTALLED_APPS = [
|
|||||||
'django_otp.plugins.otp_totp',
|
'django_otp.plugins.otp_totp',
|
||||||
'django_otp.plugins.otp_static',
|
'django_otp.plugins.otp_static',
|
||||||
'statici18n',
|
'statici18n',
|
||||||
'capture_tag',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
paypalrestsdk==1.12.*
|
paypalrestsdk>=1.9,<1.10,<2.0
|
||||||
pycparser==2.13 # https://github.com/eliben/pycparser/issues/147
|
pycparser==2.13 # https://github.com/eliben/pycparser/issues/147
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ python-u2flib-server==4.*
|
|||||||
# https://github.com/celery/celery/pull/3199
|
# https://github.com/celery/celery/pull/3199
|
||||||
git+https://github.com/pretix/celery.git@pretix#egg=celery
|
git+https://github.com/pretix/celery.git@pretix#egg=celery
|
||||||
django-statici18n==1.2.*
|
django-statici18n==1.2.*
|
||||||
django-capture-tag==1.0
|
|
||||||
|
|
||||||
# Deployment / static file compilation requirements
|
# Deployment / static file compilation requirements
|
||||||
BeautifulSoup4
|
BeautifulSoup4
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ setup(
|
|||||||
'easy-thumbnails>=2.2,<3'
|
'easy-thumbnails>=2.2,<3'
|
||||||
'PyPDF2', 'BeautifulSoup4', 'html5lib',
|
'PyPDF2', 'BeautifulSoup4', 'html5lib',
|
||||||
'slimit', 'lxml', 'static3==0.6.1', 'dj-static', 'chardet',
|
'slimit', 'lxml', 'static3==0.6.1', 'dj-static', 'chardet',
|
||||||
'csscompressor', 'mt-940', 'django-markup', 'markdown',
|
'csscompressor', 'mt-940', 'django-markup', 'markdown'
|
||||||
'django-capture-tag'
|
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'dev': ['django-debug-toolbar>=1.3.0,<2.0'],
|
'dev': ['django-debug-toolbar>=1.3.0,<2.0'],
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
/*globals $, Morris, gettext*/
|
|
||||||
$(function () {
|
|
||||||
if (!$("#voucher-create").length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_step(state_el) {
|
|
||||||
var was_visible = state_el.is(':visible');
|
|
||||||
state_el.animate({
|
|
||||||
'height': 'show',
|
|
||||||
'opacity': 'show',
|
|
||||||
'padding-top': 'show',
|
|
||||||
'padding-bottom': 'show',
|
|
||||||
'margin-top': 'show',
|
|
||||||
'margin-bottom': 'show'
|
|
||||||
}, 400);
|
|
||||||
var offset = state_el.offset();
|
|
||||||
var body = $("html, body");
|
|
||||||
if (!was_visible && offset.top > $("body").scrollTop() + $(window).height() - 160) {
|
|
||||||
body.animate({scrollTop: offset.top + 200}, '400', 'swing');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($(".alert-danger").length === 0) {
|
|
||||||
$(".wizard-step, .wizard-advanced, #step-save").hide();
|
|
||||||
$(".wizard-step").first().show();
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#id_number, #id_max_usages").on("change keydown keyup", function () {
|
|
||||||
if ($("#id_number").val() && $("#id_max_usages").val()) {
|
|
||||||
show_step($("#step-valid"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#id_valid_until").on("focus change", function () {
|
|
||||||
$("input[name=has_valid_until][value=no]").prop("checked", false);
|
|
||||||
$("input[name=has_valid_until][value=yes]").prop("checked", true);
|
|
||||||
}).on("change dp.change", function () {
|
|
||||||
if ($("input[name=has_valid_until][value=no]").prop("checked") || $("#id_valid_until").val()) {
|
|
||||||
show_step($("#step-products"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[name=has_valid_until]").on("change", function () {
|
|
||||||
if ($("input[name=has_valid_until]").not("[value=on]").prop("checked") || $("#id_valid_until").val()) {
|
|
||||||
show_step($("#step-products"));
|
|
||||||
} else {
|
|
||||||
$("#id_valid_until").focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[name=itemvar]").on("change", function () {
|
|
||||||
show_step($("#step-price"));
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#step-price input").on("change keydown keyup", function () {
|
|
||||||
var mode = $("input[name=price_mode]:checked").val();
|
|
||||||
var show_next = (mode === 'none' || $("input[name='value_" + mode + "']").val());
|
|
||||||
if (show_next) {
|
|
||||||
show_step($("#step-block"));
|
|
||||||
} else {
|
|
||||||
$("input[name='value_" + mode + "']").focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$("#step-price input[type=text]").on("focus change keyup keydown", function () {
|
|
||||||
$("#step-price input[type=radio]").prop("checked", false);
|
|
||||||
$(this).closest(".radio").find("input[type=radio]").prop("checked", true);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[name=block_quota]").on("change", function () {
|
|
||||||
show_step($("#step-advanced"));
|
|
||||||
show_step($("#step-save"));
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#wizard-advanced-show").on("click", function (e) {
|
|
||||||
show_step($(".wizard-advanced"));
|
|
||||||
$(this).animate({'opacity': '0'}, 400);
|
|
||||||
e.preventDefault();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -117,4 +117,3 @@ div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], d
|
|||||||
.ticketoutput-panel .panel-title {
|
.ticketoutput-panel .panel-title {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#voucher-create {
|
|
||||||
.wizard-step {
|
|
||||||
}
|
|
||||||
.wizard-step-inner {
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
#id_number, #id_max_usages {
|
|
||||||
width: 75px;
|
|
||||||
}
|
|
||||||
.price {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
.form-options {
|
|
||||||
list-style: none;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.radio {
|
|
||||||
display: block;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
.radio-alt {
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
.radio .help-block {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 17px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.comment {
|
|
||||||
display: block;
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ $fa-font-path: static("fontawesome/fonts");
|
|||||||
@import "_flags.scss";
|
@import "_flags.scss";
|
||||||
@import "_orders.scss";
|
@import "_orders.scss";
|
||||||
@import "_dashboard.scss";
|
@import "_dashboard.scss";
|
||||||
@import "_vouchers.scss";
|
|
||||||
@import "../../pretixbase/scss/webfont.scss";
|
@import "../../pretixbase/scss/webfont.scss";
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ def test_metrics_view(monkeypatch, client):
|
|||||||
metricsview.metrics.http_requests_total.inc(counter_value, code="200", handler="/foo", method="GET")
|
metricsview.metrics.http_requests_total.inc(counter_value, code="200", handler="/foo", method="GET")
|
||||||
|
|
||||||
# test unauthorized-page
|
# test unauthorized-page
|
||||||
|
assert "You are not authorized" in metricsview.serve_metrics(None).content.decode('utf-8')
|
||||||
assert "You are not authorized" in client.get('/metrics').content.decode('utf-8')
|
assert "You are not authorized" in client.get('/metrics').content.decode('utf-8')
|
||||||
assert "{} {}".format(fullname, counter_value) not in client.get('/metrics')
|
assert "{} {}".format(fullname, counter_value) not in client.get('/metrics')
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import sys
|
import sys
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
@@ -247,8 +244,6 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||||
|
|
||||||
def test_voucher_multiuse_count_overredeemed(self):
|
def test_voucher_multiuse_count_overredeemed(self):
|
||||||
if 'sqlite' not in settings.DATABASES['default']['ENGINE']:
|
|
||||||
pytest.xfail('This should raise a type error on most databases')
|
|
||||||
Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=2, redeemed=4)
|
Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=2, redeemed=4)
|
||||||
self.assertEqual(self.quota.count_blocking_vouchers(), 0)
|
self.assertEqual(self.quota.count_blocking_vouchers(), 0)
|
||||||
|
|
||||||
@@ -407,29 +402,6 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
v.clean()
|
v.clean()
|
||||||
|
|
||||||
|
|
||||||
class VoucherTestCase(BaseQuotaTestCase):
|
|
||||||
|
|
||||||
def test_calculate_price_none(self):
|
|
||||||
v = Voucher.objects.create(event=self.event, price_mode='none', value=Decimal('10.00'))
|
|
||||||
v.calculate_price(Decimal('23.42')) == Decimal('23.42')
|
|
||||||
|
|
||||||
def test_calculate_price_set_empty(self):
|
|
||||||
v = Voucher.objects.create(event=self.event, price_mode='set')
|
|
||||||
v.calculate_price(Decimal('23.42')) == Decimal('23.42')
|
|
||||||
|
|
||||||
def test_calculate_price_set(self):
|
|
||||||
v = Voucher.objects.create(event=self.event, price_mode='set', value=Decimal('10.00'))
|
|
||||||
v.calculate_price(Decimal('23.42')) == Decimal('10.00')
|
|
||||||
|
|
||||||
def test_calculate_price_subtract(self):
|
|
||||||
v = Voucher.objects.create(event=self.event, price_mode='subtract', value=Decimal('10.00'))
|
|
||||||
v.calculate_price(Decimal('23.42')) == Decimal('13.42')
|
|
||||||
|
|
||||||
def test_calculate_price_percent(self):
|
|
||||||
v = Voucher.objects.create(event=self.event, price_mode='percent', value=Decimal('23.00'))
|
|
||||||
v.calculate_price(Decimal('100.00')) == Decimal('77.00')
|
|
||||||
|
|
||||||
|
|
||||||
class OrderTestCase(BaseQuotaTestCase):
|
class OrderTestCase(BaseQuotaTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ from datetime import datetime, timedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytz
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
|
from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
|
||||||
from pretix.base.payment import FreeOrderProvider
|
|
||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
OrderChangeManager, OrderError, _create_order, expire_orders,
|
OrderChangeManager, OrderError, _calculate_expiry, expire_orders,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -29,10 +27,7 @@ def test_expiry_days(event):
|
|||||||
today = now()
|
today = now()
|
||||||
event.settings.set('payment_term_days', 5)
|
event.settings.set('payment_term_days', 5)
|
||||||
event.settings.set('payment_term_weekdays', False)
|
event.settings.set('payment_term_weekdays', False)
|
||||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
assert (_calculate_expiry(event, today) - today).days == 5
|
||||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
|
||||||
locale='de')
|
|
||||||
assert (order.expires - today).days == 5
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -40,18 +35,12 @@ def test_expiry_weekdays(event):
|
|||||||
today = make_aware(datetime(2016, 9, 20, 15, 0, 0, 0))
|
today = make_aware(datetime(2016, 9, 20, 15, 0, 0, 0))
|
||||||
event.settings.set('payment_term_days', 5)
|
event.settings.set('payment_term_days', 5)
|
||||||
event.settings.set('payment_term_weekdays', True)
|
event.settings.set('payment_term_weekdays', True)
|
||||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
assert (_calculate_expiry(event, today) - today).days == 6
|
||||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
assert _calculate_expiry(event, today).weekday() == 0
|
||||||
locale='de')
|
|
||||||
assert (order.expires - today).days == 6
|
|
||||||
assert order.expires.weekday() == 0
|
|
||||||
|
|
||||||
today = make_aware(datetime(2016, 9, 19, 15, 0, 0, 0))
|
today = make_aware(datetime(2016, 9, 19, 15, 0, 0, 0))
|
||||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
assert (_calculate_expiry(event, today) - today).days == 7
|
||||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
assert _calculate_expiry(event, today).weekday() == 0
|
||||||
locale='de')
|
|
||||||
assert (order.expires - today).days == 7
|
|
||||||
assert order.expires.weekday() == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -60,28 +49,9 @@ def test_expiry_last(event):
|
|||||||
event.settings.set('payment_term_days', 5)
|
event.settings.set('payment_term_days', 5)
|
||||||
event.settings.set('payment_term_weekdays', False)
|
event.settings.set('payment_term_weekdays', False)
|
||||||
event.settings.set('payment_term_last', now() + timedelta(days=3))
|
event.settings.set('payment_term_last', now() + timedelta(days=3))
|
||||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
assert (_calculate_expiry(event, today) - today).days == 3
|
||||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
|
||||||
locale='de')
|
|
||||||
assert (order.expires - today).days == 3
|
|
||||||
event.settings.set('payment_term_last', now() + timedelta(days=7))
|
event.settings.set('payment_term_last', now() + timedelta(days=7))
|
||||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
assert (_calculate_expiry(event, today) - today).days == 5
|
||||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
|
||||||
locale='de')
|
|
||||||
assert (order.expires - today).days == 5
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_expiry_dst(event):
|
|
||||||
event.settings.set('timezone', 'Europe/Berlin')
|
|
||||||
tz = pytz.timezone('Europe/Berlin')
|
|
||||||
utc = pytz.timezone('UTC')
|
|
||||||
today = tz.localize(datetime(2016, 10, 29, 12, 0, 0)).astimezone(utc)
|
|
||||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
|
||||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
|
||||||
locale='de')
|
|
||||||
localex = order.expires.astimezone(tz)
|
|
||||||
assert (localex.hour, localex.minute) == (23, 59)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ class VoucherFormTest(SoupTest):
|
|||||||
def test_csv(self):
|
def test_csv(self):
|
||||||
self.event.vouchers.create(item=self.ticket, code='ABCDEFG')
|
self.event.vouchers.create(item=self.ticket, code='ABCDEFG')
|
||||||
doc = self.client.get('/control/event/%s/%s/vouchers/?download=yes' % (self.orga.slug, self.event.slug))
|
doc = self.client.get('/control/event/%s/%s/vouchers/?download=yes' % (self.orga.slug, self.event.slug))
|
||||||
assert doc.content.strip() == '"Voucher code","Valid until","Product","Reserve quota","Bypass quota",' \
|
assert doc.content.strip() == '"Voucher code","Valid until","Product","Reserve quota","Bypass quota","Price",' \
|
||||||
'"Price effect","Value","Tag","Redeemed","Maximum usages"\r\n"ABCDEFG","",' \
|
'"Tag","Redeemed","Maximum usages"\r\n"ABCDEFG","","Early-bird ticket","No",' \
|
||||||
'"Early-bird ticket","No","No","Set product price to","","","0","1"'.encode('utf-8')
|
'"No","","","0","1"'.encode('utf-8')
|
||||||
|
|
||||||
def test_filter_status_valid(self):
|
def test_filter_status_valid(self):
|
||||||
v = self.event.vouchers.create(item=self.ticket)
|
v = self.event.vouchers.create(item=self.ticket)
|
||||||
|
|||||||
@@ -121,39 +121,6 @@ def test_webhook_all_good(env, client, monkeypatch):
|
|||||||
assert order.status == Order.STATUS_PAID
|
assert order.status == Order.STATUS_PAID
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_webhook_mark_paid(env, client, monkeypatch):
|
|
||||||
order = env[1]
|
|
||||||
order.status = Order.STATUS_PENDING
|
|
||||||
order.save()
|
|
||||||
|
|
||||||
charge = get_test_charge(env[1])
|
|
||||||
monkeypatch.setattr("stripe.Charge.retrieve", lambda *args: charge)
|
|
||||||
|
|
||||||
client.post('/dummy/dummy/stripe/webhook/', json.dumps(
|
|
||||||
{
|
|
||||||
"id": "evt_18otImGGWE2Ias8TUyVRDB1G",
|
|
||||||
"object": "event",
|
|
||||||
"api_version": "2016-03-07",
|
|
||||||
"created": 1472729052,
|
|
||||||
"data": {
|
|
||||||
"object": {
|
|
||||||
"id": "ch_18TY6GGGWE2Ias8TZHanef25",
|
|
||||||
"object": "charge",
|
|
||||||
# Rest of object is ignored anway
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"livemode": True,
|
|
||||||
"pending_webhooks": 1,
|
|
||||||
"request": "req_977XOWC8zk51Z9",
|
|
||||||
"type": "charge.succeeded"
|
|
||||||
}
|
|
||||||
), content_type='application_json')
|
|
||||||
|
|
||||||
order.refresh_from_db()
|
|
||||||
assert order.status == Order.STATUS_PAID
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_webhook_partial_refund(env, client, monkeypatch):
|
def test_webhook_partial_refund(env, client, monkeypatch):
|
||||||
charge = get_test_charge(env[1])
|
charge = get_test_charge(env[1])
|
||||||
|
|||||||
@@ -349,6 +349,20 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.assertIsNone(objs[0].variation)
|
self.assertIsNone(objs[0].variation)
|
||||||
self.assertEqual(objs[0].price, 23)
|
self.assertEqual(objs[0].price, 23)
|
||||||
|
|
||||||
|
def test_quota_partly_multiple_products(self):
|
||||||
|
self.quota_tickets.size = 1
|
||||||
|
self.quota_tickets.save()
|
||||||
|
self.quota_tickets.items.add(self.shirt)
|
||||||
|
self.quota_tickets.variations.add(self.shirt_red)
|
||||||
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
|
'item_%d' % self.ticket.id: '1',
|
||||||
|
'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1'
|
||||||
|
}, follow=True)
|
||||||
|
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
||||||
|
target_status_code=200)
|
||||||
|
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||||
|
self.assertEqual(len(objs), 1)
|
||||||
|
|
||||||
def test_renew_in_time(self):
|
def test_renew_in_time(self):
|
||||||
cp = CartPosition.objects.create(
|
cp = CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -488,7 +502,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_red,
|
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_red,
|
||||||
price=14, expires=now() + timedelta(minutes=10)
|
price=14, expires=now() + timedelta(minutes=10)
|
||||||
)
|
)
|
||||||
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, value=Decimal('10.00'), event=self.event)
|
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, price=Decimal('10.00'), event=self.event)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code,
|
'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code,
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
@@ -539,10 +553,10 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
)
|
)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1',
|
'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1',
|
||||||
'_voucher_code': v.code,
|
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
obj = CartPosition.objects.get(id=cp1.id)
|
obj = CartPosition.objects.get(id=cp1.id)
|
||||||
self.assertGreater(obj.expires, now())
|
self.assertGreater(obj.expires, now())
|
||||||
|
self.assertEqual(obj.voucher, v)
|
||||||
|
|
||||||
def test_voucher_variation(self):
|
def test_voucher_variation(self):
|
||||||
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, event=self.event)
|
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, event=self.event)
|
||||||
@@ -594,7 +608,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.assertEqual(len(objs), 0)
|
self.assertEqual(len(objs), 0)
|
||||||
|
|
||||||
def test_voucher_price(self):
|
def test_voucher_price(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event)
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
'_voucher_code': v.code,
|
'_voucher_code': v.code,
|
||||||
@@ -605,76 +619,8 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.assertIsNone(objs[0].variation)
|
self.assertIsNone(objs[0].variation)
|
||||||
self.assertEqual(objs[0].price, Decimal('12.00'))
|
self.assertEqual(objs[0].price, Decimal('12.00'))
|
||||||
|
|
||||||
def test_voucher_price_percent(self):
|
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
|
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
|
||||||
'item_%d' % self.ticket.id: '1',
|
|
||||||
'_voucher_code': v.code,
|
|
||||||
}, follow=True)
|
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
|
||||||
self.assertEqual(len(objs), 1)
|
|
||||||
self.assertEqual(objs[0].item, self.ticket)
|
|
||||||
self.assertIsNone(objs[0].variation)
|
|
||||||
self.assertEqual(objs[0].price, Decimal('20.70'))
|
|
||||||
|
|
||||||
def test_voucher_price_subtract(self):
|
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='subtract', event=self.event)
|
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
|
||||||
'item_%d' % self.ticket.id: '1',
|
|
||||||
'_voucher_code': v.code,
|
|
||||||
}, follow=True)
|
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
|
||||||
self.assertEqual(len(objs), 1)
|
|
||||||
self.assertEqual(objs[0].item, self.ticket)
|
|
||||||
self.assertIsNone(objs[0].variation)
|
|
||||||
self.assertEqual(objs[0].price, Decimal('13.00'))
|
|
||||||
|
|
||||||
def test_voucher_free_price(self):
|
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
|
|
||||||
self.ticket.free_price = True
|
|
||||||
self.ticket.save()
|
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
|
||||||
'item_%d' % self.ticket.id: '1',
|
|
||||||
'price_%d' % self.ticket.id: '21.00',
|
|
||||||
'_voucher_code': v.code,
|
|
||||||
}, follow=True)
|
|
||||||
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
|
||||||
target_status_code=200)
|
|
||||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
|
||||||
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
|
|
||||||
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
|
|
||||||
self.assertIn('21', doc.select('.cart .cart-row')[0].select('.price')[0].text)
|
|
||||||
self.assertIn('21', doc.select('.cart .cart-row')[0].select('.price')[1].text)
|
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
|
||||||
self.assertEqual(len(objs), 1)
|
|
||||||
self.assertEqual(objs[0].item, self.ticket)
|
|
||||||
self.assertIsNone(objs[0].variation)
|
|
||||||
self.assertEqual(objs[0].price, Decimal('21.00'))
|
|
||||||
|
|
||||||
def test_voucher_free_price_lower_bound(self):
|
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
|
|
||||||
self.ticket.free_price = False
|
|
||||||
self.ticket.save()
|
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
|
||||||
'item_%d' % self.ticket.id: '1',
|
|
||||||
'price_%d' % self.ticket.id: '20.00',
|
|
||||||
'_voucher_code': v.code,
|
|
||||||
}, follow=True)
|
|
||||||
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
|
||||||
target_status_code=200)
|
|
||||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
|
||||||
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
|
|
||||||
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
|
|
||||||
self.assertIn('20.70', doc.select('.cart .cart-row')[0].select('.price')[0].text)
|
|
||||||
self.assertIn('20.70', doc.select('.cart .cart-row')[0].select('.price')[1].text)
|
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
|
||||||
self.assertEqual(len(objs), 1)
|
|
||||||
self.assertEqual(objs[0].item, self.ticket)
|
|
||||||
self.assertIsNone(objs[0].variation)
|
|
||||||
self.assertEqual(objs[0].price, Decimal('20.70'))
|
|
||||||
|
|
||||||
def test_voucher_redemed(self):
|
def test_voucher_redemed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event, redeemed=1)
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, redeemed=1)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
'_voucher_code': v.code,
|
'_voucher_code': v.code,
|
||||||
@@ -684,7 +630,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
||||||
|
|
||||||
def test_voucher_expired(self):
|
def test_voucher_expired(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() - timedelta(days=2))
|
valid_until=now() - timedelta(days=2))
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
@@ -706,7 +652,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
def test_voucher_quota_empty(self):
|
def test_voucher_quota_empty(self):
|
||||||
self.quota_tickets.size = 0
|
self.quota_tickets.size = 0
|
||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event)
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
'_voucher_code': v.code,
|
'_voucher_code': v.code,
|
||||||
@@ -718,7 +664,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
def test_voucher_quota_ignore(self):
|
def test_voucher_quota_ignore(self):
|
||||||
self.quota_tickets.size = 0
|
self.quota_tickets.size = 0
|
||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
allow_ignore_quota=True)
|
allow_ignore_quota=True)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
@@ -733,7 +679,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
def test_voucher_quota_block(self):
|
def test_voucher_quota_block(self):
|
||||||
self.quota_tickets.size = 1
|
self.quota_tickets.size = 1
|
||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
block_quota=True)
|
block_quota=True)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
@@ -752,7 +698,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.assertEqual(objs[0].price, Decimal('12.00'))
|
self.assertEqual(objs[0].price, Decimal('12.00'))
|
||||||
|
|
||||||
def test_voucher_doubled(self):
|
def test_voucher_doubled(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event)
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
'_voucher_code': v.code,
|
'_voucher_code': v.code,
|
||||||
@@ -826,7 +772,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.assertEqual(len(objs), 0)
|
self.assertEqual(len(objs), 0)
|
||||||
|
|
||||||
def test_voucher_multiuse_ok(self):
|
def test_voucher_multiuse_ok(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=0)
|
max_usages=2, redeemed=0)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '2',
|
'item_%d' % self.ticket.id: '2',
|
||||||
@@ -837,7 +783,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert all(cp.voucher == v for cp in positions)
|
assert all(cp.voucher == v for cp in positions)
|
||||||
|
|
||||||
def test_voucher_multiuse_multiprod_ok(self):
|
def test_voucher_multiuse_multiprod_ok(self):
|
||||||
v = Voucher.objects.create(quota=self.quota_all, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(quota=self.quota_all, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=0)
|
max_usages=2, redeemed=0)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
@@ -849,7 +795,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert all(cp.voucher == v for cp in positions)
|
assert all(cp.voucher == v for cp in positions)
|
||||||
|
|
||||||
def test_voucher_multiuse_partially(self):
|
def test_voucher_multiuse_partially(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=1)
|
max_usages=2, redeemed=1)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '2',
|
'item_%d' % self.ticket.id: '2',
|
||||||
@@ -861,7 +807,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert not positions.exists()
|
assert not positions.exists()
|
||||||
|
|
||||||
def test_voucher_multiuse_multiprod_partially(self):
|
def test_voucher_multiuse_multiprod_partially(self):
|
||||||
v = Voucher.objects.create(quota=self.quota_all, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(quota=self.quota_all, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=1)
|
max_usages=2, redeemed=1)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
@@ -869,13 +815,12 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
'_voucher_code': v.code,
|
'_voucher_code': v.code,
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
self.assertIn('only be redeemed 1 more time', doc.select('.alert-danger')[0].text)
|
||||||
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
|
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
|
||||||
assert positions.count() == 1
|
assert positions.count() == 0
|
||||||
assert all(cp.voucher == v for cp in positions)
|
|
||||||
|
|
||||||
def test_voucher_multiuse_redeemed(self):
|
def test_voucher_multiuse_redeemed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=2)
|
max_usages=2, redeemed=2)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '2',
|
'item_%d' % self.ticket.id: '2',
|
||||||
@@ -887,7 +832,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert not positions.exists()
|
assert not positions.exists()
|
||||||
|
|
||||||
def test_voucher_multiuse_multiprod_redeemed(self):
|
def test_voucher_multiuse_multiprod_redeemed(self):
|
||||||
v = Voucher.objects.create(quota=self.quota_all, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(quota=self.quota_all, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=2)
|
max_usages=2, redeemed=2)
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
'item_%d' % self.ticket.id: '1',
|
'item_%d' % self.ticket.id: '1',
|
||||||
@@ -900,7 +845,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert not positions.exists()
|
assert not positions.exists()
|
||||||
|
|
||||||
def test_voucher_multiuse_redeemed_in_my_cart(self):
|
def test_voucher_multiuse_redeemed_in_my_cart(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=1)
|
max_usages=2, redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
expires=now() - timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'),
|
expires=now() - timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'),
|
||||||
@@ -911,12 +856,12 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
'_voucher_code': v.code,
|
'_voucher_code': v.code,
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
self.assertIn('only be redeemed 1 more time', doc.select('.alert-danger')[0].text)
|
||||||
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
|
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
|
||||||
assert positions.count() == 1
|
assert positions.count() == 0
|
||||||
|
|
||||||
def test_voucher_multiuse_redeemed_in_other_cart(self):
|
def test_voucher_multiuse_redeemed_in_other_cart(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=1)
|
max_usages=2, redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
expires=now() + timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'),
|
expires=now() + timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'),
|
||||||
@@ -932,7 +877,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert not positions.exists()
|
assert not positions.exists()
|
||||||
|
|
||||||
def test_voucher_multiuse_redeemed_in_other_expired_cart(self):
|
def test_voucher_multiuse_redeemed_in_other_expired_cart(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
max_usages=2, redeemed=1)
|
max_usages=2, redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
expires=now() - timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'),
|
expires=now() - timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'),
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
self.assertEqual(cr1.price, 24)
|
self.assertEqual(cr1.price, 24)
|
||||||
|
|
||||||
def test_voucher(self):
|
def test_voucher(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2))
|
valid_until=now() + timedelta(days=2))
|
||||||
cr1 = CartPosition.objects.create(
|
cr1 = CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -312,7 +312,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
self.assertEqual(Voucher.objects.get(pk=v.pk).redeemed, 1)
|
self.assertEqual(Voucher.objects.get(pk=v.pk).redeemed, 1)
|
||||||
|
|
||||||
def test_voucher_required(self):
|
def test_voucher_required(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2))
|
valid_until=now() + timedelta(days=2))
|
||||||
self.ticket.require_voucher = True
|
self.ticket.require_voucher = True
|
||||||
self.ticket.save()
|
self.ticket.save()
|
||||||
@@ -341,7 +341,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
assert doc.select(".alert-danger")
|
assert doc.select(".alert-danger")
|
||||||
|
|
||||||
def test_voucher_price_changed(self):
|
def test_voucher_price_changed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2))
|
valid_until=now() + timedelta(days=2))
|
||||||
cr1 = CartPosition.objects.create(
|
cr1 = CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -355,8 +355,20 @@ class CheckoutTestCase(TestCase):
|
|||||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||||
self.assertEqual(cr1.price, Decimal('12.00'))
|
self.assertEqual(cr1.price, Decimal('12.00'))
|
||||||
|
|
||||||
|
def test_voucher_expired(self):
|
||||||
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
|
valid_until=now() - timedelta(days=2))
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=12, expires=now() - timedelta(minutes=10), voucher=v
|
||||||
|
)
|
||||||
|
self._set_session('payment', 'banktransfer')
|
||||||
|
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
|
self.assertIn("expired", doc.select(".alert-danger")[0].text)
|
||||||
|
|
||||||
def test_voucher_redeemed(self):
|
def test_voucher_redeemed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), redeemed=1)
|
valid_until=now() + timedelta(days=2), redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -368,7 +380,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
||||||
|
|
||||||
def test_voucher_multiuse_redeemed(self):
|
def test_voucher_multiuse_redeemed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=3)
|
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=3)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -380,7 +392,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
||||||
|
|
||||||
def test_voucher_multiuse_partially(self):
|
def test_voucher_multiuse_partially(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=2)
|
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=2)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -393,11 +405,32 @@ class CheckoutTestCase(TestCase):
|
|||||||
self._set_session('payment', 'banktransfer')
|
self._set_session('payment', 'banktransfer')
|
||||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
self.assertIn("only be redeemed 1 more time", doc.select(".alert-danger")[0].text)
|
||||||
assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1
|
assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1
|
||||||
|
|
||||||
def test_voucher_multiuse_ok(self):
|
def test_voucher_multiuse_ok(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
|
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=12, expires=now() + timedelta(minutes=10), voucher=v
|
||||||
|
)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=12, expires=now() + timedelta(minutes=10), voucher=v
|
||||||
|
)
|
||||||
|
self._set_session('payment', 'banktransfer')
|
||||||
|
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
|
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||||
|
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key).exists())
|
||||||
|
self.assertEqual(Order.objects.count(), 1)
|
||||||
|
self.assertEqual(OrderPosition.objects.count(), 2)
|
||||||
|
v.refresh_from_db()
|
||||||
|
assert v.redeemed == 3
|
||||||
|
|
||||||
|
def test_voucher_multiuse_ok_expired(self):
|
||||||
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -418,7 +451,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
assert v.redeemed == 3
|
assert v.redeemed == 3
|
||||||
|
|
||||||
def test_voucher_multiuse_in_other_cart_expired(self):
|
def test_voucher_multiuse_in_other_cart_expired(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id='other', item=self.ticket,
|
event=self.event, cart_id='other', item=self.ticket,
|
||||||
@@ -443,7 +476,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
assert v.redeemed == 3
|
assert v.redeemed == 3
|
||||||
|
|
||||||
def test_voucher_multiuse_in_other_cart(self):
|
def test_voucher_multiuse_in_other_cart(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id='other', item=self.ticket,
|
event=self.event, cart_id='other', item=self.ticket,
|
||||||
@@ -460,13 +493,13 @@ class CheckoutTestCase(TestCase):
|
|||||||
self._set_session('payment', 'banktransfer')
|
self._set_session('payment', 'banktransfer')
|
||||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
self.assertIn("only be redeemed 1 more time", doc.select(".alert-danger")[0].text)
|
||||||
assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1
|
assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1
|
||||||
|
|
||||||
def test_voucher_ignore_quota(self):
|
def test_voucher_ignore_quota(self):
|
||||||
self.quota_tickets.size = 0
|
self.quota_tickets.size = 0
|
||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), allow_ignore_quota=True)
|
valid_until=now() + timedelta(days=2), allow_ignore_quota=True)
|
||||||
cr1 = CartPosition.objects.create(
|
cr1 = CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -484,7 +517,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
def test_voucher_block_quota(self):
|
def test_voucher_block_quota(self):
|
||||||
self.quota_tickets.size = 1
|
self.quota_tickets.size = 1
|
||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), block_quota=True)
|
valid_until=now() + timedelta(days=2), block_quota=True)
|
||||||
cr1 = CartPosition.objects.create(
|
cr1 = CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
@@ -511,7 +544,7 @@ class CheckoutTestCase(TestCase):
|
|||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
q2 = self.event.quotas.create(name='Testquota', size=0)
|
q2 = self.event.quotas.create(name='Testquota', size=0)
|
||||||
q2.items.add(self.ticket)
|
q2.items.add(self.ticket)
|
||||||
v = Voucher.objects.create(quota=self.quota_tickets, value=Decimal('12.00'), event=self.event,
|
v = Voucher.objects.create(quota=self.quota_tickets, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() + timedelta(days=2), block_quota=True)
|
valid_until=now() + timedelta(days=2), block_quota=True)
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
|||||||
@@ -285,27 +285,19 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
|
|||||||
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
||||||
assert "_voucher_item" in html.rendered_content
|
assert "_voucher_item" in html.rendered_content
|
||||||
|
|
||||||
def test_voucher_price(self):
|
def test_special_price(self):
|
||||||
self.v.value = Decimal("10.00")
|
self.v.price = Decimal("10.00")
|
||||||
self.v.save()
|
self.v.save()
|
||||||
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
||||||
assert "Early-bird" in html.rendered_content
|
assert "Early-bird" in html.rendered_content
|
||||||
assert "10.00" in html.rendered_content
|
assert "10.00" in html.rendered_content
|
||||||
|
|
||||||
def test_voucher_price_percentage(self):
|
def test_special_price_variations(self):
|
||||||
self.v.value = Decimal("10.00")
|
|
||||||
self.v.price_mode = 'percent'
|
|
||||||
self.v.save()
|
|
||||||
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
|
||||||
assert "Early-bird" in html.rendered_content
|
|
||||||
assert "10.80" in html.rendered_content
|
|
||||||
|
|
||||||
def test_voucher_price_variations(self):
|
|
||||||
var1 = ItemVariation.objects.create(item=self.item, value='Red', default_price=14, position=1)
|
var1 = ItemVariation.objects.create(item=self.item, value='Red', default_price=14, position=1)
|
||||||
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2)
|
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2)
|
||||||
self.q.variations.add(var1)
|
self.q.variations.add(var1)
|
||||||
self.q.variations.add(var2)
|
self.q.variations.add(var2)
|
||||||
self.v.value = Decimal("10.00")
|
self.v.price = Decimal("10.00")
|
||||||
self.v.save()
|
self.v.save()
|
||||||
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
|
||||||
assert "Early-bird" in html.rendered_content
|
assert "Early-bird" in html.rendered_content
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ TEMPLATES[0]['DIRS'].append(os.path.join(TEST_DIR, 'templates')) # NOQA
|
|||||||
INSTALLED_APPS.append('tests.testdummy') # NOQA
|
INSTALLED_APPS.append('tests.testdummy') # NOQA
|
||||||
|
|
||||||
for a in PLUGINS:
|
for a in PLUGINS:
|
||||||
INSTALLED_APPS.remove(a)
|
INSTALLED_APPS.remove(a)
|
||||||
|
|
||||||
|
DATABASES['default'] = {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user