mirror of
https://github.com/pretix/pretix.git
synced 2026-01-14 23:02:26 +00:00
Compare commits
78 Commits
v2.6.0
...
orderchang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1279c8720f | ||
|
|
7a3afde7b1 | ||
|
|
e4417305a2 | ||
|
|
bc5d0bea00 | ||
|
|
dbce9b0395 | ||
|
|
2eb88840bd | ||
|
|
4838835b1b | ||
|
|
ab452bd9e3 | ||
|
|
ae298bddb8 | ||
|
|
9ad4607d26 | ||
|
|
b3684377cd | ||
|
|
441badfdbd | ||
|
|
0d242a0304 | ||
|
|
2fac8592d4 | ||
|
|
58b1a2f115 | ||
|
|
420d44e909 | ||
|
|
e0063fce52 | ||
|
|
21ef6c7950 | ||
|
|
651f429ffb | ||
|
|
66dd7c448b | ||
|
|
e9b4205145 | ||
|
|
6dedea1025 | ||
|
|
348ed4e909 | ||
|
|
091b3358e4 | ||
|
|
186e2a6b9a | ||
|
|
198b90972c | ||
|
|
4989b6235c | ||
|
|
4cfebab11c | ||
|
|
fe944ec643 | ||
|
|
9d92c7b10f | ||
|
|
3b810a3a76 | ||
|
|
7860417177 | ||
|
|
1438edb3c8 | ||
|
|
a17720062b | ||
|
|
37ab45b352 | ||
|
|
6be212df8c | ||
|
|
b4e85780f4 | ||
|
|
a9732c4788 | ||
|
|
273316be25 | ||
|
|
0f3b269931 | ||
|
|
4462054d0e | ||
|
|
ec53022cc8 | ||
|
|
0b65b18459 | ||
|
|
2fac03f47b | ||
|
|
750d5eda48 | ||
|
|
f2cd9a2002 | ||
|
|
874b38db17 | ||
|
|
0f58e1c396 | ||
|
|
36e0afc09e | ||
|
|
7164124a70 | ||
|
|
887d8832c0 | ||
|
|
beb144f9a0 | ||
|
|
6d1dea7922 | ||
|
|
cb531a7a6a | ||
|
|
d5820d74d3 | ||
|
|
b686978074 | ||
|
|
c372bffc57 | ||
|
|
282c6108bf | ||
|
|
f2437c7ff7 | ||
|
|
dd0b6e6647 | ||
|
|
f3128591d8 | ||
|
|
d395db8142 | ||
|
|
0c82e92882 | ||
|
|
db0c13a3c2 | ||
|
|
19a2f4163a | ||
|
|
76526465c0 | ||
|
|
d0d0f9aa4c | ||
|
|
482f6b1eb8 | ||
|
|
327418299a | ||
|
|
5dfd1e6337 | ||
|
|
bc01124584 | ||
|
|
c0df418265 | ||
|
|
af06f6fc38 | ||
|
|
4c0e8f69ea | ||
|
|
243e4ac4c8 | ||
|
|
b931d27486 | ||
|
|
2810e2a760 | ||
|
|
04465393b2 |
17
.travis.yml
17
.travis.yml
@@ -1,4 +1,5 @@
|
||||
language: python
|
||||
dist: xenial
|
||||
sudo: false
|
||||
install:
|
||||
- pip install -U pip wheel setuptools
|
||||
@@ -12,23 +13,23 @@ services:
|
||||
- postgresql
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=style
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=plugins
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=doc-spelling
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
env: JOB=translation-spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
|
||||
@@ -68,10 +68,6 @@ To build and run pretix, you will need the following debian packages::
|
||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
|
||||
|
||||
.. note:: Python 3.7 is not yet supported, so if you run a very recent OS, make sure to get
|
||||
Python 3.6 from somewhere. You can check the current state of things in our
|
||||
`Python 3.7 issue`_.
|
||||
|
||||
Config file
|
||||
-----------
|
||||
|
||||
@@ -314,4 +310,3 @@ example::
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
.. _Python 3.7 issue: https://github.com/pretix/pretix/issues/1025
|
||||
|
||||
@@ -181,4 +181,37 @@ as the string values ``true`` and ``false``.
|
||||
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
||||
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
||||
|
||||
|
||||
Idempotency
|
||||
-----------
|
||||
|
||||
Our API supports an idempotency mechanism to make sure you can safely retry operations without accidentally performing
|
||||
them twice. This is useful if an API call experiences interruptions in transit, e.g. due to a network failure, and you
|
||||
do not know if it completed successfully.
|
||||
|
||||
To perform an idempotent request, add a ``X-Idempotency-Key`` header with a random string value (we recommend a version
|
||||
4 UUID) to your request. If we see a second request with the same ``X-Idempotency-Key`` and the same ``Authorization``
|
||||
and ``Cookie`` headers, we will not perform the action for a second time but return the exact same response instead.
|
||||
|
||||
Please note that this also goes for most error responses. For example, if we returned you a ``403 Permission Denied``
|
||||
error and you retry with the same ``X-Idempotency-Key``, you will get the same error again, even if you were granted
|
||||
permission in the meantime! This includes internal server errors on our side that might have been fixed in the meantime.
|
||||
|
||||
There are only three exceptions to the rule:
|
||||
|
||||
* Responses with status code ``409 Conflict`` are not cached. If you send the request again, it will be executed as a
|
||||
new request, since these responses are intended to be retried.
|
||||
|
||||
* Rate-limited responses with status code ``429 Too Many Requests`` are not cached and you can safely retry them.
|
||||
|
||||
* Responses with status code ``503 Service Unavailable`` are not cached and you can safely retry them.
|
||||
|
||||
If you send a request with an ``X-Idempotency-Key`` header that we have seen before but that has not yet received a
|
||||
response, you will receive a response with status code ``409 Conflict`` and are asked to retry after five seconds.
|
||||
|
||||
We store idempotency keys for 24 hours, so you should never retry a request after a longer time period.
|
||||
|
||||
All ``POST``, ``PUT``, ``PATCH``, or ``DELETE`` api calls support idempotency keys. Adding an idempotency key to a
|
||||
``GET``, ``HEAD``, or ``OPTIONS`` request has no effect.
|
||||
|
||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||
|
||||
@@ -18,12 +18,18 @@ default_price money (string) The price set d
|
||||
price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price`` (read-only).
|
||||
original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
active boolean If ``false``, this variation will not be sold or shown.
|
||||
description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
position integer An integer, used for sorting
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The attribute ``original_price`` has been added.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
This resource has been added.
|
||||
@@ -67,7 +73,8 @@ Endpoints
|
||||
},
|
||||
"position": 0,
|
||||
"default_price": "223.00",
|
||||
"price": 223.0
|
||||
"price": 223.0,
|
||||
"original_price": null,
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
@@ -120,6 +127,7 @@ Endpoints
|
||||
},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -167,6 +175,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -216,6 +225,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
|
||||
@@ -29,7 +29,8 @@ free_price boolean If ``true``, cu
|
||||
they buy the product (however, the price can't be set
|
||||
lower than the price defined by ``default_price`` or
|
||||
otherwise).
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item.
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
|
||||
set through ``tax_rule``).
|
||||
tax_rule integer The internal ID of the applied tax rule (or ``null``).
|
||||
admission boolean ``true`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``false`` for others
|
||||
@@ -81,6 +82,8 @@ variations list of objects A list with one
|
||||
├ price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price``.
|
||||
├ original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
├ active boolean If ``false``, this variation will not be sold or shown.
|
||||
├ description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
@@ -105,6 +108,10 @@ bundles list of objects Definition of b
|
||||
taxation. This is not added to the price.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The attribute ``original_price`` has been added for ``variations``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
|
||||
@@ -207,6 +214,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -215,6 +223,7 @@ Endpoints
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
@@ -296,6 +305,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -304,6 +314,7 @@ Endpoints
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
@@ -365,6 +376,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -373,6 +385,7 @@ Endpoints
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
@@ -423,6 +436,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -431,6 +445,7 @@ Endpoints
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
@@ -512,6 +527,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
@@ -520,6 +536,7 @@ Endpoints
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
|
||||
@@ -750,6 +750,7 @@ Creating orders
|
||||
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
|
||||
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
||||
charge will be created), this is just informative in case you *handled the payment already*.
|
||||
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
||||
* ``comment`` (optional)
|
||||
* ``checkin_attention`` (optional)
|
||||
* ``invoice_address`` (optional)
|
||||
|
||||
@@ -20,7 +20,7 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
|
||||
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
91
src/pretix/api/middleware.py
Normal file
91
src/pretix/api/middleware.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import json
|
||||
from hashlib import sha1
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import status
|
||||
|
||||
from pretix.api.models import ApiCall
|
||||
|
||||
|
||||
class IdempotencyMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
if request.method in ('GET', 'HEAD', 'OPTIONS'):
|
||||
return self.get_response(request)
|
||||
|
||||
if not request.path.startswith('/api/'):
|
||||
return self.get_response(request)
|
||||
|
||||
if not request.headers.get('X-Idempotency-Key'):
|
||||
return self.get_response(request)
|
||||
|
||||
auth_hash_parts = '{}:{}'.format(
|
||||
request.headers.get('Authorization', ''),
|
||||
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
||||
)
|
||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||
idempotency_key = request.headers.get('X-Idempotency-Key', '')
|
||||
|
||||
with transaction.atomic():
|
||||
call, created = ApiCall.objects.select_for_update().get_or_create(
|
||||
auth_hash=auth_hash,
|
||||
idempotency_key=idempotency_key,
|
||||
defaults={
|
||||
'locked': now(),
|
||||
'request_method': request.method,
|
||||
'request_path': request.path,
|
||||
'response_code': 0,
|
||||
'response_headers': '{}',
|
||||
'response_body': b''
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
resp = self.get_response(request)
|
||||
with transaction.atomic():
|
||||
if resp.status_code in (409, 429, 503):
|
||||
# This is the exception: These calls are *meant* to be retried!
|
||||
call.delete()
|
||||
else:
|
||||
call.response_code = resp.status_code
|
||||
if isinstance(resp.content, str):
|
||||
call.response_body = resp.content.encode()
|
||||
elif isinstance(resp.content, memoryview):
|
||||
call.response_body = resp.content.tobytes()
|
||||
elif isinstance(resp.content, bytes):
|
||||
call.response_body = resp.content
|
||||
elif hasattr(resp.content, 'read'):
|
||||
call.response_body = resp.read()
|
||||
elif hasattr(resp, 'data'):
|
||||
call.response_body = json.dumps(resp.data)
|
||||
else:
|
||||
call.response_body = repr(resp).encode()
|
||||
call.response_headers = json.dumps(resp._headers)
|
||||
call.locked = None
|
||||
call.save(update_fields=['locked', 'response_code', 'response_headers',
|
||||
'response_body'])
|
||||
return resp
|
||||
else:
|
||||
if call.locked:
|
||||
r = JsonResponse(
|
||||
{'detail': 'Concurrent request with idempotency key.'},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
r['Retry-After'] = 5
|
||||
return r
|
||||
|
||||
content = call.response_body
|
||||
if isinstance(content, memoryview):
|
||||
content = content.tobytes()
|
||||
r = HttpResponse(
|
||||
content=content,
|
||||
status=call.response_code,
|
||||
)
|
||||
for k, v in json.loads(call.response_headers).values():
|
||||
r[k] = v
|
||||
return r
|
||||
44
src/pretix/api/migrations/0004_auto_20190405_1048.py
Normal file
44
src/pretix/api/migrations/0004_auto_20190405_1048.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 2.1.5 on 2019-04-05 10:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('pretixbase', '0116_auto_20190402_0722'),
|
||||
('pretixapi', '0003_webhook_webhookcall_webhookeventlistener'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApiCall',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('idempotency_key', models.CharField(db_index=True, max_length=190)),
|
||||
('auth_hash', models.CharField(db_index=True, max_length=190)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('locked', models.DateTimeField(null=True)),
|
||||
('request_method', models.CharField(max_length=20)),
|
||||
('request_path', models.CharField(max_length=255)),
|
||||
('response_code', models.PositiveIntegerField()),
|
||||
('response_headers', models.TextField()),
|
||||
('response_body', models.BinaryField()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='webhookcall',
|
||||
options={'ordering': ('-datetime',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='webhookeventlistener',
|
||||
options={'ordering': ('action_type',)},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='apicall',
|
||||
unique_together={('idempotency_key', 'auth_hash')},
|
||||
),
|
||||
]
|
||||
@@ -77,6 +77,9 @@ class WebHook(models.Model):
|
||||
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('id',)
|
||||
|
||||
@property
|
||||
def action_types(self):
|
||||
return [
|
||||
@@ -106,3 +109,20 @@ class WebHookCall(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ("-datetime",)
|
||||
|
||||
|
||||
class ApiCall(models.Model):
|
||||
idempotency_key = models.CharField(max_length=190, db_index=True)
|
||||
auth_hash = models.CharField(max_length=190, db_index=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
locked = models.DateTimeField(null=True)
|
||||
|
||||
request_method = models.CharField(max_length=20)
|
||||
request_path = models.CharField(max_length=255)
|
||||
|
||||
response_code = models.PositiveIntegerField()
|
||||
response_headers = models.TextField()
|
||||
response_body = models.BinaryField()
|
||||
|
||||
class Meta:
|
||||
unique_together = (('idempotency_key', 'auth_hash'),)
|
||||
|
||||
@@ -31,10 +31,10 @@ class PluginsField(Field):
|
||||
def to_representation(self, obj):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return {
|
||||
return sorted([
|
||||
p.module for p in get_all_plugins()
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||
}
|
||||
])
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {
|
||||
@@ -212,8 +212,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||
|
||||
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set')])
|
||||
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set')])
|
||||
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set', [])])
|
||||
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set', [])])
|
||||
return data
|
||||
|
||||
def validate_item_price_overrides(self, data):
|
||||
|
||||
@@ -19,7 +19,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price')
|
||||
'position', 'default_price', 'price', 'original_price')
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
@@ -29,7 +29,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price')
|
||||
'position', 'default_price', 'price', 'original_price')
|
||||
|
||||
|
||||
class InlineItemBundleSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -14,8 +14,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
||||
Question, QuestionAnswer,
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||
OrderPosition, Question, QuestionAnswer, SubEvent,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -288,6 +288,23 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
return instance
|
||||
|
||||
|
||||
class PriceCalcSerializer(serializers.Serializer):
|
||||
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
||||
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
||||
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
|
||||
locale = serializers.CharField(allow_null=True, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['item'].queryset = event.items.all()
|
||||
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
|
||||
if event.has_subevents:
|
||||
self.fields['subevent'].queryset = event.subevents.all()
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
|
||||
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -458,11 +475,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_info = CompatibleJSONField(required=False)
|
||||
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
force = serializers.BooleanField(default=False, required=False)
|
||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts', 'force')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp not in self.context['event'].get_payment_providers():
|
||||
@@ -533,6 +551,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
payment_provider = validated_data.pop('payment_provider')
|
||||
payment_info = validated_data.pop('payment_info', '{}')
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
@@ -617,7 +636,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
payment_date=now(),
|
||||
payment_date=payment_date,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif payment_provider:
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.api.models import WebHookCall
|
||||
from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import periodic_task
|
||||
|
||||
register_webhook_events = Signal(
|
||||
@@ -19,3 +19,8 @@ instances.
|
||||
@receiver(periodic_task)
|
||||
def cleanup_webhook_logs(sender, **kwargs):
|
||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
def cleanup_api_logs(sender, **kwargs):
|
||||
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
||||
|
||||
@@ -31,10 +31,10 @@ class RichOrderingFilter(OrderingFilter):
|
||||
class ConditionalListView:
|
||||
|
||||
def list(self, request, **kwargs):
|
||||
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
|
||||
if_modified_since = request.headers.get('If-Modified-Since')
|
||||
if if_modified_since:
|
||||
if_modified_since = parse_http_date_safe(if_modified_since)
|
||||
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
|
||||
if_unmodified_since = request.headers.get('If-Unmodified-Since')
|
||||
if if_unmodified_since:
|
||||
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
|
||||
if not hasattr(request, 'event'):
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import DateTimeField
|
||||
from rest_framework.response import Response
|
||||
|
||||
@@ -77,7 +77,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@detail_route(methods=['GET'])
|
||||
@action(detail=True, methods=['GET'])
|
||||
def status(self, *args, **kwargs):
|
||||
clist = self.get_object()
|
||||
cqs = Checkin.objects.filter(
|
||||
@@ -242,7 +242,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
return qs
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def redeem(self, *args, **kwargs):
|
||||
force = bool(self.request.data.get('force', False))
|
||||
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||
|
||||
@@ -13,7 +13,7 @@ from pretix.api.serializers.event import (
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
Device, Event, ItemCategory, TaxRule, TeamAPIToken,
|
||||
CartPosition, Device, Event, ItemCategory, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
@@ -272,6 +272,8 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
CartPosition.objects.filter(addon_to__subevent=instance).delete()
|
||||
instance.cartposition_set.all().delete()
|
||||
super().perform_destroy(instance)
|
||||
except ProtectedError:
|
||||
raise PermissionDenied('The sub-event could not be deleted as some constraints (e.g. data created by '
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
@@ -16,8 +16,8 @@ from pretix.api.serializers.item import (
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
||||
QuestionOption, Quota,
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
|
||||
Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
@@ -84,7 +84,8 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
self.get_object().cartposition_set.all().delete()
|
||||
CartPosition.objects.filter(addon_to__item=instance).delete()
|
||||
instance.cartposition_set.all().delete()
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
@@ -498,7 +499,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
@action(detail=True, methods=['get'])
|
||||
def availability(self, request, *args, **kwargs):
|
||||
quota = self.get_object()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
APIException, NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
@@ -24,14 +24,16 @@ from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
|
||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||
OrderRefundSerializer, OrderSerializer,
|
||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
|
||||
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||
generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||
regenerate_invoice,
|
||||
@@ -41,8 +43,12 @@ from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.signals import order_placed, register_ticket_outputs
|
||||
from pretix.base.signals import (
|
||||
order_modified, order_placed, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class OrderFilter(FilterSet):
|
||||
@@ -125,7 +131,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data, headers={'X-Page-Generated': date})
|
||||
|
||||
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
provider = self._get_output_provider(output)
|
||||
order = self.get_object()
|
||||
@@ -147,7 +153,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return resp
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
@@ -188,7 +194,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_canceled(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
cancellation_fee = request.data.get('cancellation_fee', None)
|
||||
@@ -222,7 +228,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def approve(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
@@ -240,7 +246,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def deny(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
comment = request.data.get('comment', '')
|
||||
@@ -258,7 +264,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_pending(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
@@ -277,7 +283,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_expired(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
@@ -294,7 +300,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_refunded(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
@@ -311,7 +317,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def create_invoice(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
has_inv = order.invoices.exists() and not (
|
||||
@@ -343,7 +349,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def resend_link(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
if not order.email:
|
||||
@@ -357,7 +363,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
@transaction.atomic
|
||||
def regenerate_secrets(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
@@ -368,6 +374,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
order.save(update_fields=['secret'])
|
||||
CachedTicket.objects.filter(order_position__order=order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=order).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk,
|
||||
'order': order.pk})
|
||||
order.log_action(
|
||||
'pretix.event.order.secret.changed',
|
||||
user=self.request.user,
|
||||
@@ -375,7 +383,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def extend(self, request, **kwargs):
|
||||
new_date = request.data.get('expires', None)
|
||||
force = request.data.get('force', False)
|
||||
@@ -451,61 +459,64 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_update(self, serializer):
|
||||
if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.comment',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_comment': self.request.data.get('comment')
|
||||
}
|
||||
)
|
||||
with transaction.atomic():
|
||||
if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.comment',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_comment': self.request.data.get('comment')
|
||||
}
|
||||
)
|
||||
|
||||
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_attention',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_value': self.request.data.get('checkin_attention')
|
||||
}
|
||||
)
|
||||
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_attention',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_value': self.request.data.get('checkin_attention')
|
||||
}
|
||||
)
|
||||
|
||||
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.contact.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'old_email': serializer.instance.email,
|
||||
'new_email': self.request.data.get('email'),
|
||||
}
|
||||
)
|
||||
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.contact.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'old_email': serializer.instance.email,
|
||||
'new_email': self.request.data.get('email'),
|
||||
}
|
||||
)
|
||||
|
||||
if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.locale.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'old_locale': serializer.instance.locale,
|
||||
'new_locale': self.request.data.get('locale'),
|
||||
}
|
||||
)
|
||||
if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.locale.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'old_locale': serializer.instance.locale,
|
||||
'new_locale': self.request.data.get('locale'),
|
||||
}
|
||||
)
|
||||
|
||||
if 'invoice_address' in self.request.data:
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'invoice_data': self.request.data.get('invoice_address'),
|
||||
}
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
|
||||
if 'invoice_address' in self.request.data:
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'invoice_data': self.request.data.get('invoice_address'),
|
||||
}
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
@@ -614,7 +625,84 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
return prov
|
||||
raise NotFound('Unknown output provider.')
|
||||
|
||||
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
@action(detail=True, methods=['POST'], url_name='price_calc')
|
||||
def price_calc(self, request, *args, **kwargs):
|
||||
"""
|
||||
This calculates the price assuming a change of product or subevent. This endpoint
|
||||
is deliberately not documented and considered a private API, only to be used by
|
||||
pretix' web interface.
|
||||
|
||||
Sample input:
|
||||
|
||||
{
|
||||
"item": 2,
|
||||
"variation": null,
|
||||
"subevent": 3
|
||||
}
|
||||
|
||||
Sample output:
|
||||
|
||||
{
|
||||
"gross": "2.34",
|
||||
"gross_formatted": "2,34",
|
||||
"net": "2.34",
|
||||
"tax": "0.00",
|
||||
"rate": "0.00",
|
||||
"name": "VAT"
|
||||
}
|
||||
"""
|
||||
serializer = PriceCalcSerializer(data=request.data, event=request.event)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
pos = self.get_object()
|
||||
|
||||
try:
|
||||
ia = pos.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = InvoiceAddress()
|
||||
|
||||
kwargs = {
|
||||
'item': pos.item,
|
||||
'variation': pos.variation,
|
||||
'voucher': pos.voucher,
|
||||
'subevent': pos.subevent,
|
||||
'addon_to': pos.addon_to,
|
||||
'invoice_address': ia,
|
||||
}
|
||||
|
||||
if data.get('item'):
|
||||
item = data.get('item')
|
||||
kwargs['item'] = item
|
||||
|
||||
if item.has_variations:
|
||||
variation = data.get('variation') or pos.variation
|
||||
if not variation:
|
||||
raise ValidationError('No variation given')
|
||||
if variation.item != item:
|
||||
raise ValidationError('Variation does not belong to item')
|
||||
kwargs['variation'] = variation
|
||||
else:
|
||||
variation = None
|
||||
kwargs['variation'] = None
|
||||
|
||||
if pos.voucher and not pos.voucher.applies_to(item, variation):
|
||||
kwargs['voucher'] = None
|
||||
|
||||
if data.get('subevent'):
|
||||
kwargs['subevent'] = data.get('subevent')
|
||||
|
||||
price = get_price(**kwargs)
|
||||
with language(data.get('locale') or self.request.event.settings.locale):
|
||||
return Response({
|
||||
'gross': price.gross,
|
||||
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
|
||||
'net': price.net,
|
||||
'rate': price.rate,
|
||||
'name': str(price.name),
|
||||
'tax': price.tax,
|
||||
})
|
||||
|
||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
provider = self._get_output_provider(output)
|
||||
pos = self.get_object()
|
||||
@@ -665,7 +753,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||
return order.payments.all()
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def confirm(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
force = request.data.get('force', False)
|
||||
@@ -686,7 +774,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
pass
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def refund(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||
@@ -751,7 +839,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
payment.order.save(update_fields=['status', 'expires'])
|
||||
return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def cancel(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
|
||||
@@ -779,7 +867,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||
return order.refunds.all()
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def cancel(self, request, **kwargs):
|
||||
refund = self.get_object()
|
||||
|
||||
@@ -796,7 +884,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def process(self, request, **kwargs):
|
||||
refund = self.get_object()
|
||||
|
||||
@@ -821,7 +909,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
refund.order.save(update_fields=['status', 'expires'])
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def done(self, request, **kwargs):
|
||||
refund = self.get_object()
|
||||
|
||||
@@ -911,7 +999,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
nr=Concat('prefix', 'invoice_no')
|
||||
)
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True, )
|
||||
def download(self, request, **kwargs):
|
||||
invoice = self.get_object()
|
||||
|
||||
@@ -929,7 +1017,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||
return resp
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def regenerate(self, request, **kwarts):
|
||||
inv = self.get_object()
|
||||
if inv.canceled:
|
||||
@@ -948,7 +1036,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
return Response(status=204)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def reissue(self, request, **kwarts):
|
||||
inv = self.get_object()
|
||||
if inv.canceled:
|
||||
|
||||
@@ -7,7 +7,7 @@ from django_filters.rest_framework import (
|
||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||
)
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
@@ -111,9 +111,12 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
with transaction.atomic():
|
||||
instance.cartposition_set.filter(addon_to__isnull=False).delete()
|
||||
instance.cartposition_set.all().delete()
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@list_route(methods=['POST'])
|
||||
@action(detail=False, methods=['POST'])
|
||||
def batch_create(self, request, *args, **kwargs):
|
||||
if any(self._predict_quota_check(d, None) for d in request.data):
|
||||
lockfn = request.event.lock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import django_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
@@ -69,7 +69,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@action(detail=True, methods=['POST'])
|
||||
def send_voucher(self, *args, **kwargs):
|
||||
try:
|
||||
self.get_object().send_voucher(
|
||||
|
||||
@@ -9,14 +9,17 @@ import vat_moss.exchange_rates
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import get_language, pgettext, ugettext
|
||||
from django.utils.translation import (
|
||||
get_language, pgettext, ugettext, ugettext_lazy,
|
||||
)
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import (
|
||||
@@ -122,6 +125,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
"""
|
||||
stylesheet = StyleSheet1()
|
||||
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
|
||||
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
|
||||
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
|
||||
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
|
||||
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
|
||||
@@ -254,49 +258,64 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
invoice_to_width = 85 * mm
|
||||
invoice_to_height = 50 * mm
|
||||
invoice_to_left = 25 * mm
|
||||
invoice_to_top = 52 * mm
|
||||
|
||||
def _draw_invoice_to(self, canvas):
|
||||
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 85 * mm, 50 * mm)
|
||||
p_size = p.wrap(85 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
|
||||
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||
|
||||
invoice_from_width = 70 * mm
|
||||
invoice_from_height = 50 * mm
|
||||
invoice_from_left = 25 * mm
|
||||
invoice_from_top = 17 * mm
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 70 * mm, 50 * mm)
|
||||
p_size = p.wrap(70 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
canvas.setCreator('pretix.eu')
|
||||
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
|
||||
|
||||
canvas.saveState()
|
||||
canvas.setFont(self.font_regular, 8)
|
||||
|
||||
if self.invoice.order.testmode:
|
||||
canvas.saveState()
|
||||
canvas.setFont('OpenSansBd', 30)
|
||||
canvas.setFillColorRGB(32, 0, 0)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
||||
canvas.restoreState()
|
||||
|
||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
||||
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
|
||||
'InvoiceFrom'])
|
||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
|
||||
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
|
||||
|
||||
def _draw_invoice_from_label(self, canvas):
|
||||
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
self._draw_invoice_from(canvas)
|
||||
|
||||
def _draw_invoice_to_label(self, canvas):
|
||||
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
self._draw_invoice_to(canvas)
|
||||
logo_width = 25 * mm
|
||||
logo_height = 25 * mm
|
||||
logo_left = 95 * mm
|
||||
logo_top = 13 * mm
|
||||
logo_anchor = 'n'
|
||||
|
||||
def _draw_logo(self, canvas):
|
||||
if self.invoice.event.settings.invoice_logo_image:
|
||||
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
||||
ir = ThumbnailingImageReader(logo_file)
|
||||
try:
|
||||
ir.resize(self.logo_width, self.logo_height, 300)
|
||||
except:
|
||||
logger.exception("Can not resize image")
|
||||
pass
|
||||
canvas.drawImage(ir,
|
||||
self.logo_left,
|
||||
self.pagesize[1] - self.logo_height - self.logo_top,
|
||||
width=self.logo_width, height=self.logo_height,
|
||||
preserveAspectRatio=True, anchor=self.logo_anchor,
|
||||
mask='auto')
|
||||
|
||||
def _draw_metadata(self, canvas):
|
||||
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
|
||||
@@ -348,37 +367,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
canvas.drawText(textobject)
|
||||
|
||||
if self.invoice.event.settings.invoice_logo_image:
|
||||
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
||||
ir = ThumbnailingImageReader(logo_file)
|
||||
try:
|
||||
ir.resize(25 * mm, 25 * mm, 300)
|
||||
except:
|
||||
logger.exception("Can not resize image")
|
||||
pass
|
||||
canvas.drawImage(ir,
|
||||
95 * mm, (297 - 38) * mm,
|
||||
width=25 * mm, height=25 * mm,
|
||||
preserveAspectRatio=True, anchor='n',
|
||||
mask='auto')
|
||||
event_left = 125 * mm
|
||||
event_top = 17 * mm
|
||||
event_width = 65 * mm
|
||||
event_height = 50 * mm
|
||||
|
||||
def _draw_event_label(self, canvas):
|
||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
def _draw_event(self, canvas):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(65 * mm, 50 * mm)
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
|
||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(65 * mm, 50 * mm)
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
return txt
|
||||
|
||||
if not self.invoice.event.has_subevents:
|
||||
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
shorten(self.invoice.event.name) + '\n' +
|
||||
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
from_date=self.invoice.event.get_date_from_display(),
|
||||
to_date=self.invoice.event.get_date_to_display())
|
||||
to_date=self.invoice.event.get_date_to_display()
|
||||
)
|
||||
)
|
||||
else:
|
||||
p_str = (
|
||||
@@ -388,15 +407,38 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 65 * mm, 50 * mm)
|
||||
p_size = p.wrap(65 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
|
||||
p.wrapOn(canvas, self.event_width, self.event_height)
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
||||
self._draw_event_label(canvas)
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
|
||||
canvas.drawText(textobject)
|
||||
def _draw_footer(self, canvas):
|
||||
canvas.setFont(self.font_regular, 8)
|
||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
||||
|
||||
def _draw_testmode(self, canvas):
|
||||
if self.invoice.order.testmode:
|
||||
canvas.saveState()
|
||||
canvas.setFont('OpenSansBd', 30)
|
||||
canvas.setFillColorRGB(32, 0, 0)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
||||
canvas.restoreState()
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
canvas.setCreator('pretix.eu')
|
||||
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
|
||||
|
||||
canvas.saveState()
|
||||
self._draw_footer(canvas)
|
||||
self._draw_testmode(canvas)
|
||||
self._draw_invoice_from_label(canvas)
|
||||
self._draw_invoice_from(canvas)
|
||||
self._draw_invoice_to_label(canvas)
|
||||
self._draw_invoice_to(canvas)
|
||||
self._draw_metadata(canvas)
|
||||
self._draw_logo(canvas)
|
||||
self._draw_event(canvas)
|
||||
canvas.restoreState()
|
||||
|
||||
def _get_first_page_frames(self, doc):
|
||||
@@ -434,6 +476,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_vat_id:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Customer VAT ID') + ':<br />' +
|
||||
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_beneficiary:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Beneficiary') + ':<br />' +
|
||||
@@ -554,6 +603,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(Spacer(5 * mm, 5 * mm))
|
||||
story.append(KeepTogether([
|
||||
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
||||
table
|
||||
@@ -607,6 +657,114 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
return story
|
||||
|
||||
|
||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
identifier = 'modern1'
|
||||
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||
bottom_margin = 16.9 * mm
|
||||
top_margin = 16.9 * mm
|
||||
right_margin = 20 * mm
|
||||
invoice_to_height = 27.3 * mm
|
||||
invoice_to_width = 80 * mm
|
||||
invoice_to_left = 25 * mm
|
||||
invoice_to_top = (40 + 17.7) * mm
|
||||
invoice_from_left = 125 * mm
|
||||
invoice_from_top = 50 * mm
|
||||
invoice_from_width = pagesizes.A4[0] - invoice_from_left - right_margin
|
||||
invoice_from_height = 50 * mm
|
||||
|
||||
logo_width = 75 * mm
|
||||
logo_height = 25 * mm
|
||||
logo_left = pagesizes.A4[0] - logo_width - right_margin
|
||||
logo_top = top_margin
|
||||
logo_anchor = 'e'
|
||||
|
||||
event_left = 25 * mm
|
||||
event_top = top_margin
|
||||
event_width = 80 * mm
|
||||
event_height = 25 * mm
|
||||
|
||||
def _get_stylesheet(self):
|
||||
stylesheet = super()._get_stylesheet()
|
||||
stylesheet.add(ParagraphStyle(name='Sender', fontName=self.font_regular, fontSize=8, leading=10))
|
||||
stylesheet['InvoiceFrom'].alignment = TA_RIGHT
|
||||
return stylesheet
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
if not self.invoice.invoice_from:
|
||||
return
|
||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
||||
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||
super()._draw_invoice_from(canvas)
|
||||
|
||||
def _draw_invoice_to_label(self, canvas):
|
||||
pass
|
||||
|
||||
def _draw_invoice_from_label(self, canvas):
|
||||
pass
|
||||
|
||||
def _draw_event_label(self, canvas):
|
||||
pass
|
||||
|
||||
def _get_first_page_frames(self, doc):
|
||||
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
|
||||
return [
|
||||
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
|
||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _draw_metadata(self, canvas):
|
||||
begin_top = 100 * mm
|
||||
|
||||
textobject = canvas.beginText(self.left_margin, self.pagesize[1] - begin_top)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Order code'))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.order.full_code)
|
||||
canvas.drawText(textobject)
|
||||
|
||||
if self.invoice.is_cancellation:
|
||||
textobject = canvas.beginText(self.left_margin + 50 * mm, self.pagesize[1] - begin_top)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation number'))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.number)
|
||||
canvas.drawText(textobject)
|
||||
|
||||
textobject = canvas.beginText(self.left_margin + 100 * mm, self.pagesize[1] - begin_top)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Original invoice'))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.refers.number)
|
||||
canvas.drawText(textobject)
|
||||
else:
|
||||
textobject = canvas.beginText(self.left_margin + 70 * mm, self.pagesize[1] - begin_top)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice number'))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.number)
|
||||
canvas.drawText(textobject)
|
||||
|
||||
p = Paragraph(date_format(self.invoice.date, "DATE_FORMAT"), style=self.stylesheet['Normal'])
|
||||
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||
p.wrapOn(canvas, w, 15 * mm)
|
||||
date_x = self.pagesize[0] - w - self.right_margin
|
||||
p.drawOn(canvas, date_x, self.pagesize[1] - begin_top - 10 - 6)
|
||||
|
||||
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation date'))
|
||||
else:
|
||||
textobject.textLine(pgettext('invoice', 'Invoice date'))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
|
||||
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
|
||||
def recv_classic(sender, **kwargs):
|
||||
return ClassicInvoiceRenderer
|
||||
return [ClassicInvoiceRenderer, Modern1Renderer]
|
||||
|
||||
@@ -97,7 +97,7 @@ def get_language_from_event(request: HttpRequest) -> str:
|
||||
|
||||
|
||||
def get_language_from_browser(request: HttpRequest) -> str:
|
||||
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
|
||||
accept = request.headers.get('Accept-Language', '')
|
||||
for accept_lang, unused in parse_accept_lang_header(accept):
|
||||
if accept_lang == '*':
|
||||
break
|
||||
|
||||
23
src/pretix/base/migrations/0117_auto_20190418_1149.py
Normal file
23
src/pretix/base/migrations/0117_auto_20190418_1149.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2 on 2019-04-18 11:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0116_auto_20190402_0722'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='itemvariation',
|
||||
name='original_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='If set, this will be displayed next to '
|
||||
'the current price to show that the '
|
||||
'current price is a discounted one. '
|
||||
'This is just a cosmetic setting and '
|
||||
'will not actually impact pricing.',
|
||||
max_digits=7, null=True, verbose_name='Original price'),
|
||||
),
|
||||
]
|
||||
@@ -111,6 +111,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
ordering = ('email',)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.email = self.email.lower()
|
||||
|
||||
@@ -759,6 +759,7 @@ class Event(EventMixin, LoggedModel):
|
||||
return not self.orders.exists() and not self.invoices.exists()
|
||||
|
||||
def delete_sub_objects(self):
|
||||
self.cartposition_set.filter(addon_to__isnull=False).delete()
|
||||
self.cartposition_set.all().delete()
|
||||
self.items.all().delete()
|
||||
self.subevents.all().delete()
|
||||
|
||||
@@ -37,7 +37,7 @@ class MultiStringField(TextField):
|
||||
def get_prep_lookup(self, lookup_type, value): # NOQA
|
||||
raise TypeError('Lookups on multi strings are currently not supported.')
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value:
|
||||
return [v for v in value.split(DELIMITER) if v]
|
||||
else:
|
||||
|
||||
@@ -117,12 +117,35 @@ class Invoice(models.Model):
|
||||
self.invoice_from_name,
|
||||
self.invoice_from,
|
||||
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
||||
str(self.invoice_from_country),
|
||||
self.invoice_from_country.name if self.invoice_from_country else "",
|
||||
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
|
||||
pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id if self.invoice_from_tax_id else "",
|
||||
]
|
||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||
|
||||
@property
|
||||
def address_invoice_from(self):
|
||||
parts = [
|
||||
self.invoice_from_name,
|
||||
self.invoice_from,
|
||||
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
||||
self.invoice_from_country.name if self.invoice_from_country else "",
|
||||
]
|
||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||
|
||||
@property
|
||||
def address_invoice_to(self):
|
||||
if self.invoice_to and not self.invoice_to_company and not self.invoice_to_name:
|
||||
return self.invoice_to
|
||||
parts = [
|
||||
self.invoice_to_company,
|
||||
self.invoice_to_name,
|
||||
self.invoice_to_street,
|
||||
(self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or ""),
|
||||
self.invoice_to_country.name if self.invoice_to_country else "",
|
||||
]
|
||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||
|
||||
def _get_numeric_invoice_number(self):
|
||||
numeric_invoices = Invoice.objects.filter(
|
||||
event__organizer=self.event.organizer,
|
||||
|
||||
@@ -550,6 +550,8 @@ class ItemVariation(models.Model):
|
||||
:type active: bool
|
||||
:param default_price: This variation's default price
|
||||
:type default_price: decimal.Decimal
|
||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||
:type original_price: decimal.Decimal
|
||||
"""
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
@@ -578,6 +580,13 @@ class ItemVariation(models.Model):
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Default price"),
|
||||
)
|
||||
original_price = models.DecimalField(
|
||||
verbose_name=_('Original price'),
|
||||
blank=True, null=True,
|
||||
max_digits=7, decimal_places=2,
|
||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Product variation")
|
||||
@@ -1391,7 +1400,7 @@ class Quota(LoggedModel):
|
||||
|
||||
@staticmethod
|
||||
def clean_variations(items, variations):
|
||||
for variation in variations:
|
||||
for variation in (variations or []):
|
||||
if variation.item not in items:
|
||||
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
|
||||
break
|
||||
|
||||
@@ -1159,7 +1159,7 @@ class OrderPayment(models.Model):
|
||||
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
|
||||
order_paid.send(self.order.event, order=self.order)
|
||||
|
||||
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text=''):
|
||||
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', lock=True):
|
||||
"""
|
||||
Marks the payment as complete. If possible, this also marks the order as paid if no further
|
||||
payment is required
|
||||
@@ -1218,7 +1218,7 @@ class OrderPayment(models.Model):
|
||||
if payment_sum - refund_sum < self.order.total:
|
||||
return
|
||||
|
||||
if self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12):
|
||||
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
|
||||
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
|
||||
# database transaction is more than enough.
|
||||
with transaction.atomic():
|
||||
|
||||
@@ -118,6 +118,9 @@ class TaxRule(LoggedModel):
|
||||
)
|
||||
custom_rules = models.TextField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('event', 'rate', 'id')
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import OrderFee, OrderPosition
|
||||
|
||||
|
||||
@@ -721,13 +721,10 @@ class BoxOfficeProvider(BasePaymentProvider):
|
||||
return False
|
||||
|
||||
def payment_control_render(self, request, payment) -> str:
|
||||
template = None
|
||||
payment_info = None
|
||||
|
||||
if payment.info:
|
||||
payment_info = json.loads(payment.info)
|
||||
if payment_info['payment_type'] == "sumup":
|
||||
template = get_template('pretixcontrol/boxoffice/payment_sumup.html')
|
||||
if not payment.info:
|
||||
return
|
||||
payment_info = json.loads(payment.info)
|
||||
template = get_template('pretixcontrol/boxoffice/payment.html')
|
||||
|
||||
ctx = {
|
||||
'request': request,
|
||||
@@ -737,11 +734,7 @@ class BoxOfficeProvider(BasePaymentProvider):
|
||||
'payment': payment,
|
||||
'provider': self,
|
||||
}
|
||||
|
||||
if template:
|
||||
return template.render(ctx)
|
||||
else:
|
||||
return
|
||||
return template.render(ctx)
|
||||
|
||||
|
||||
class ManualPayment(BasePaymentProvider):
|
||||
|
||||
@@ -276,6 +276,10 @@ class CartManager:
|
||||
err = None
|
||||
changed_prices = {}
|
||||
for cp in expired:
|
||||
removed_positions = {op.position.pk for op in self._operations if isinstance(op, self.RemoveOperation)}
|
||||
if cp.pk in removed_positions or (cp.addon_to_id and cp.addon_to_id in removed_positions):
|
||||
continue
|
||||
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
|
||||
@@ -120,7 +120,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
positions = list(
|
||||
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
|
||||
addon_c=Count('addons')
|
||||
)
|
||||
).order_by('positionid', 'id')
|
||||
)
|
||||
|
||||
reverse_charge = False
|
||||
|
||||
@@ -15,6 +15,7 @@ from pretix.base.email import ClassicMailRenderer
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
|
||||
from pretix.base.services.invoices import invoice_pdf_task
|
||||
from pretix.base.services.tasks import TransactionAwareTask
|
||||
from pretix.base.services.tickets import get_tickets_for_order
|
||||
from pretix.base.signals import email_filter
|
||||
from pretix.celery_app import app
|
||||
@@ -177,7 +178,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
chain(*task_chain).apply_async()
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
@app.task(base=TransactionAwareTask, bind=True)
|
||||
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
|
||||
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
|
||||
order: int=None, attach_tickets=False) -> bool:
|
||||
@@ -212,15 +213,30 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
order = None
|
||||
else:
|
||||
if attach_tickets:
|
||||
args = []
|
||||
attach_size = 0
|
||||
for name, ct in get_tickets_for_order(order):
|
||||
try:
|
||||
email.attach(
|
||||
name,
|
||||
ct.file.read(),
|
||||
ct.type
|
||||
)
|
||||
except:
|
||||
pass
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
attach_size += len(content)
|
||||
|
||||
if attach_size < 4 * 1024 * 1024:
|
||||
# Do not attach more than 4MB, it will bounce way to often.
|
||||
for a in args:
|
||||
try:
|
||||
email.attach(*a)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
data={
|
||||
'subject': 'Attachments skipped',
|
||||
'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
)
|
||||
|
||||
email = email_filter.send_chained(event, 'message', message=email, order=order)
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ from pretix.base.models import (
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemBundle
|
||||
from pretix.base.models.orders import (
|
||||
CachedCombinedTicket, CachedTicket, InvoiceAddress, OrderFee, OrderRefund,
|
||||
generate_position_secret, generate_secret,
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_qualified,
|
||||
)
|
||||
@@ -42,7 +43,9 @@ from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.signals import (
|
||||
allow_ticket_download, order_fee_calculation, order_placed, periodic_task,
|
||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
||||
order_denied, order_expired, order_fee_calculation, order_placed,
|
||||
periodic_task,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -134,55 +137,58 @@ def mark_order_refunded(order, user=None, auth=None, api_token=None):
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def mark_order_expired(order, user=None, auth=None):
|
||||
"""
|
||||
Mark this order as expired. This sets the payment status and returns the order object.
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
if isinstance(order, int):
|
||||
order = Order.objects.get(pk=order)
|
||||
if isinstance(user, int):
|
||||
user = User.objects.get(pk=user)
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_EXPIRED
|
||||
order.save(update_fields=['status'])
|
||||
with transaction.atomic():
|
||||
if isinstance(order, int):
|
||||
order = Order.objects.get(pk=order)
|
||||
if isinstance(user, int):
|
||||
user = User.objects.get(pk=user)
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_EXPIRED
|
||||
order.save(update_fields=['status'])
|
||||
|
||||
order.log_action('pretix.event.order.expired', user=user, auth=auth)
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
order.log_action('pretix.event.order.expired', user=user, auth=auth)
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
|
||||
order_expired.send(order.event, order=order)
|
||||
return order
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def approve_order(order, user=None, send_mail: bool=True, auth=None):
|
||||
"""
|
||||
Mark this order as approved
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||
raise OrderError(_('This order is not pending approval.'))
|
||||
with transaction.atomic():
|
||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||
raise OrderError(_('This order is not pending approval.'))
|
||||
|
||||
order.require_approval = False
|
||||
order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
||||
order.save(update_fields=['require_approval', 'expires'])
|
||||
order.require_approval = False
|
||||
order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
||||
order.save(update_fields=['require_approval', 'expires'])
|
||||
|
||||
order.log_action('pretix.event.order.approved', user=user, auth=auth)
|
||||
if order.total == Decimal('0.00'):
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='free',
|
||||
amount=0,
|
||||
fee=None
|
||||
)
|
||||
try:
|
||||
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth)
|
||||
except Quota.QuotaExceededException:
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
order.log_action('pretix.event.order.approved', user=user, auth=auth)
|
||||
if order.total == Decimal('0.00'):
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='free',
|
||||
amount=0,
|
||||
fee=None
|
||||
)
|
||||
try:
|
||||
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth)
|
||||
except Quota.QuotaExceededException:
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
order_approved.send(order.event, order=order)
|
||||
|
||||
invoice = order.invoices.last() # Might be generated by plugin already
|
||||
if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||
@@ -234,30 +240,32 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None):
|
||||
return order.pk
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||
raise OrderError(_('This order is not pending approval.'))
|
||||
with transaction.atomic():
|
||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||
raise OrderError(_('This order is not pending approval.'))
|
||||
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save(update_fields=['status'])
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save(update_fields=['status'])
|
||||
|
||||
order.log_action('pretix.event.order.denied', user=user, auth=auth, data={
|
||||
'comment': comment
|
||||
})
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
order.log_action('pretix.event.order.denied', user=user, auth=auth, data={
|
||||
'comment': comment
|
||||
})
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
|
||||
order_denied.send(order.event, order=order)
|
||||
|
||||
if send_mail:
|
||||
try:
|
||||
@@ -294,7 +302,6 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
return order.pk
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
||||
cancellation_fee=None):
|
||||
"""
|
||||
@@ -302,85 +309,87 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
if isinstance(order, int):
|
||||
order = Order.objects.get(pk=order)
|
||||
if isinstance(user, int):
|
||||
user = User.objects.get(pk=user)
|
||||
if isinstance(api_token, int):
|
||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
||||
if isinstance(device, int):
|
||||
device = Device.objects.get(pk=device)
|
||||
if isinstance(oauth_application, int):
|
||||
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
|
||||
if isinstance(cancellation_fee, str):
|
||||
cancellation_fee = Decimal(cancellation_fee)
|
||||
with transaction.atomic():
|
||||
if isinstance(order, int):
|
||||
order = Order.objects.get(pk=order)
|
||||
if isinstance(user, int):
|
||||
user = User.objects.get(pk=user)
|
||||
if isinstance(api_token, int):
|
||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
||||
if isinstance(device, int):
|
||||
device = Device.objects.get(pk=device)
|
||||
if isinstance(oauth_application, int):
|
||||
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
|
||||
if isinstance(cancellation_fee, str):
|
||||
cancellation_fee = Decimal(cancellation_fee)
|
||||
|
||||
if not order.cancel_allowed():
|
||||
raise OrderError(_('You cannot cancel this order.'))
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
if not order.cancel_allowed():
|
||||
raise OrderError(_('You cannot cancel this order.'))
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
|
||||
if cancellation_fee:
|
||||
with order.event.lock():
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
position.canceled = True
|
||||
position.save(update_fields=['canceled'])
|
||||
for fee in order.fees.all():
|
||||
fee.canceled = True
|
||||
fee.save(update_fields=['canceled'])
|
||||
|
||||
f = OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||
value=cancellation_fee,
|
||||
tax_rule=order.event.settings.tax_rate_default,
|
||||
order=order,
|
||||
)
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
|
||||
if order.payment_refund_sum < cancellation_fee:
|
||||
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
|
||||
order.status = Order.STATUS_PAID
|
||||
order.total = f.value
|
||||
order.save(update_fields=['status', 'total'])
|
||||
|
||||
if i:
|
||||
generate_invoice(order)
|
||||
else:
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save(update_fields=['status'])
|
||||
|
||||
if cancellation_fee:
|
||||
with order.event.lock():
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
position.canceled = True
|
||||
position.save(update_fields=['canceled'])
|
||||
for fee in order.fees.all():
|
||||
fee.canceled = True
|
||||
fee.save(update_fields=['canceled'])
|
||||
|
||||
f = OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||
value=cancellation_fee,
|
||||
tax_rule=order.event.settings.tax_rate_default,
|
||||
order=order,
|
||||
)
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
||||
data={'cancellation_fee': cancellation_fee})
|
||||
|
||||
if order.payment_refund_sum < cancellation_fee:
|
||||
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
|
||||
order.status = Order.STATUS_PAID
|
||||
order.total = f.value
|
||||
order.save(update_fields=['status', 'total'])
|
||||
|
||||
if i:
|
||||
generate_invoice(order)
|
||||
else:
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save(update_fields=['status'])
|
||||
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
|
||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
||||
data={'cancellation_fee': cancellation_fee})
|
||||
|
||||
if send_mail:
|
||||
email_template = order.event.settings.mail_text_order_canceled
|
||||
email_context = {
|
||||
'event': order.event.name,
|
||||
'code': order.code,
|
||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
})
|
||||
}
|
||||
with language(order.locale):
|
||||
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_canceled', user
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order canceled email could not be sent')
|
||||
if send_mail:
|
||||
email_template = order.event.settings.mail_text_order_canceled
|
||||
email_context = {
|
||||
'event': order.event.name,
|
||||
'code': order.code,
|
||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
})
|
||||
}
|
||||
with language(order.locale):
|
||||
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_canceled', user
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order canceled email could not be sent')
|
||||
|
||||
order_canceled.send(order.event, order=order)
|
||||
return order.pk
|
||||
|
||||
|
||||
@@ -559,6 +568,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
meta_info: dict=None, sales_channel: str='web'):
|
||||
fees, pf = _get_fees(positions, payment_provider, address, meta_info, event)
|
||||
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
||||
p = None
|
||||
|
||||
with transaction.atomic():
|
||||
order = Order(
|
||||
@@ -592,7 +602,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
fee.save()
|
||||
|
||||
if payment_provider and not order.require_approval:
|
||||
order.payments.create(
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider=payment_provider.identifier,
|
||||
amount=total,
|
||||
@@ -608,7 +618,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
return order
|
||||
return order, p
|
||||
|
||||
|
||||
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
@@ -640,8 +650,12 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
if len(position_ids) != len(positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
_check_positions(event, now_dt, positions, address=addr)
|
||||
order = _create_order(event, email, positions, now_dt, pprov,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
|
||||
order, payment = _create_order(event, email, positions, now_dt, pprov,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
|
||||
|
||||
free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') and not order.require_approval
|
||||
if free_order_flow:
|
||||
payment.confirm(send_mail=False, lock=False)
|
||||
|
||||
invoice = order.invoices.last() # Might be generated by plugin already
|
||||
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||
@@ -656,7 +670,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
if order.require_approval:
|
||||
email_template = event.settings.mail_text_order_placed_require_approval
|
||||
log_entry = 'pretix.event.order.email.order_placed_require_approval'
|
||||
elif payment_provider == 'free':
|
||||
elif free_order_flow:
|
||||
email_template = event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
else:
|
||||
@@ -832,8 +846,8 @@ class OrderChangeManager:
|
||||
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
|
||||
'subevent_required': _('You need to choose a subevent for the new position.'),
|
||||
}
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
|
||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent', 'price'))
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position',))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
|
||||
@@ -844,6 +858,7 @@ class OrderChangeManager:
|
||||
self.order = order
|
||||
self.user = user
|
||||
self.auth = auth
|
||||
self.event = order.event
|
||||
self.split_order = None
|
||||
self._committed = False
|
||||
self._totaldiff = 0
|
||||
@@ -852,33 +867,18 @@ class OrderChangeManager:
|
||||
self.notify = notify
|
||||
self._invoice_dirty = False
|
||||
|
||||
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation], keep_price=False):
|
||||
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
|
||||
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||
raise OrderError(self.error_messages['product_without_variation'])
|
||||
|
||||
if keep_price:
|
||||
price = TaxedPrice(gross=position.price, net=position.price - position.tax_value,
|
||||
tax=position.tax_value, rate=position.tax_rate,
|
||||
name=position.tax_rule.name if position.tax_rule else None)
|
||||
else:
|
||||
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
|
||||
if price is None: # NOQA
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
|
||||
new_quotas = (variation.quotas.filter(subevent=position.subevent)
|
||||
if variation else item.quotas.filter(subevent=position.subevent))
|
||||
if not new_quotas:
|
||||
raise OrderError(self.error_messages['quota_missing'])
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._totaldiff += price.gross - position.price
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.ItemOperation(position, item, variation, price))
|
||||
self._operations.append(self.ItemOperation(position, item, variation))
|
||||
|
||||
def change_subevent(self, position: OrderPosition, subevent: SubEvent):
|
||||
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
|
||||
@@ -892,19 +892,15 @@ class OrderChangeManager:
|
||||
if not new_quotas:
|
||||
raise OrderError(self.error_messages['quota_missing'])
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._totaldiff += price.gross - position.price
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.SubeventOperation(position, subevent, price))
|
||||
self._operations.append(self.SubeventOperation(position, subevent))
|
||||
|
||||
def regenerate_secret(self, position: OrderPosition):
|
||||
self._operations.append(self.RegenerateSecretOperation(position))
|
||||
|
||||
def change_price(self, position: OrderPosition, price: Decimal):
|
||||
price = position.item.tax(price)
|
||||
price = position.item.tax(price, base_price_is='gross')
|
||||
|
||||
self._totaldiff += price.gross - position.price
|
||||
|
||||
@@ -1061,14 +1057,11 @@ class OrderChangeManager:
|
||||
'new_variation': op.variation.pk if op.variation else None,
|
||||
'old_price': op.position.price,
|
||||
'addon_to': op.position.addon_to_id,
|
||||
'new_price': op.price.gross
|
||||
'new_price': op.position.price
|
||||
})
|
||||
op.position.item = op.item
|
||||
op.position.variation = op.variation
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_rule = op.item.tax_rule
|
||||
op.position._calculate_tax()
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
|
||||
@@ -1077,13 +1070,9 @@ class OrderChangeManager:
|
||||
'old_subevent': op.position.subevent.pk,
|
||||
'new_subevent': op.subevent.pk,
|
||||
'old_price': op.position.price,
|
||||
'new_price': op.price.gross
|
||||
'new_price': op.position.price
|
||||
})
|
||||
op.position.subevent = op.subevent
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_rule = op.position.item.tax_rule
|
||||
op.position.save()
|
||||
elif isinstance(op, self.PriceOperation):
|
||||
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
|
||||
@@ -1094,9 +1083,7 @@ class OrderChangeManager:
|
||||
'new_price': op.price.gross
|
||||
})
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_rule = op.position.item.tax_rule
|
||||
op.position._calculate_tax()
|
||||
op.position.save()
|
||||
elif isinstance(op, self.CancelOperation):
|
||||
for opa in op.position.addons.all():
|
||||
@@ -1146,8 +1133,8 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.RegenerateSecretOperation):
|
||||
op.position.secret = generate_position_secret()
|
||||
op.position.save()
|
||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=self.order).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
'order': self.order.pk})
|
||||
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
@@ -1377,12 +1364,14 @@ class OrderChangeManager:
|
||||
if self.split_order:
|
||||
self._notify_user(self.split_order)
|
||||
|
||||
order_changed.send(self.order.event, order=self.order)
|
||||
|
||||
def _clear_tickets_cache(self):
|
||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=self.order).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
'order': self.order.pk})
|
||||
if self.split_order:
|
||||
CachedTicket.objects.filter(order_position__order=self.split_order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=self.split_order).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
'order': self.split_order.pk})
|
||||
|
||||
def _get_payment_provider(self):
|
||||
lp = self.order.payments.last()
|
||||
|
||||
@@ -123,8 +123,10 @@ def get_tickets_for_order(order):
|
||||
order=order, provider=p.identifier, file__isnull=False
|
||||
).last()
|
||||
if not ct or not ct.file:
|
||||
retval = generate.apply(args=('order', order.pk, p.identifier))
|
||||
ct = CachedCombinedTicket.objects.get(pk=retval.get())
|
||||
retval = generate_order(order.pk, p.identifier)
|
||||
if not retval:
|
||||
continue
|
||||
ct = CachedCombinedTicket.objects.get(pk=retval)
|
||||
tickets.append((
|
||||
"{}-{}-{}{}".format(
|
||||
order.event.slug.upper(), order.code, ct.provider, ct.extension,
|
||||
@@ -140,8 +142,10 @@ def get_tickets_for_order(order):
|
||||
order_position=pos, provider=p.identifier, file__isnull=False
|
||||
).last()
|
||||
if not ct or not ct.file:
|
||||
retval = generate.apply(args=('orderposition', pos.pk, p.identifier))
|
||||
ct = CachedTicket.objects.get(pk=retval.get())
|
||||
retval = generate_orderposition(pos.pk, p.identifier)
|
||||
if not retval:
|
||||
continue
|
||||
ct = CachedCombinedTicket.objects.get(pk=retval)
|
||||
tickets.append((
|
||||
"{}-{}-{}-{}{}".format(
|
||||
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
|
||||
@@ -152,3 +156,26 @@ def get_tickets_for_order(order):
|
||||
logger.exception('Failed to generate ticket.')
|
||||
|
||||
return tickets
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def invalidate_cache(event: int, item: int=None, provider: str=None, order: int=None, **kwargs):
|
||||
event = Event.objects.get(id=event)
|
||||
qs = CachedTicket.objects.filter(order_position__order__event=event)
|
||||
qsc = CachedCombinedTicket.objects.filter(order__event=event)
|
||||
|
||||
if item:
|
||||
qs = qs.filter(order_position__item_id=item)
|
||||
|
||||
if provider:
|
||||
qs = qs.filter(provider=provider)
|
||||
qsc = qsc.filter(provider=provider)
|
||||
|
||||
if order:
|
||||
qs = qs.filter(order_position__order_id=order)
|
||||
qsc = qsc.filter(order_id=order)
|
||||
|
||||
for ct in qs:
|
||||
ct.delete()
|
||||
for ct in qsc:
|
||||
ct.delete()
|
||||
|
||||
@@ -49,6 +49,10 @@ DEFAULTS = {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
},
|
||||
'invoice_address_not_asked_free': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
},
|
||||
'invoice_name_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
|
||||
@@ -275,6 +275,66 @@ because an already-paid order has been split.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_canceled = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order is canceled. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_expired = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order is marked as expired. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_modified = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order's information is modified. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_changed = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order's content is changed. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_approved = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order is being approved. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_denied = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order is being denied. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
logentry_display = EventPluginSignal(
|
||||
providing_args=["logentry"]
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Bad Request" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-frown-o fa-fw big-icon"></i>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Permission denied" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-fw fa-lock big-icon"></i>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Not found" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-meh-o fa-fw big-icon"></i>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Internal Server Error" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-bolt big-icon fa-fw"></i>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Verification failed" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-frown-o big-icon fa-fw"></i>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
{% load i18n %}{% load static from staticfiles %}
|
||||
<link rel="stylesheet" href="{% static 'debug_toolbar/css/print.css' %}" type="text/css" media="print" />
|
||||
<link rel="stylesheet" href="{% static 'debug_toolbar/css/toolbar.css' %}" type="text/css" />
|
||||
|
||||
<!-- Prevent our copy of jQuery from registering as an AMD module on sites that use RequireJS. -->
|
||||
<script src="{% static 'debug_toolbar/js/jquery_pre.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
<script src="{% static 'debug_toolbar/js/jquery_post.js' %}"></script>
|
||||
|
||||
<script src="{% static 'debug_toolbar/js/toolbar.js' %}"></script>
|
||||
<div id="djDebug" class="djdt-hidden" dir="ltr"
|
||||
data-store-id="{{ toolbar.store_id }}" data-render-panel-url="{% url 'djdt:render_panel' %}"
|
||||
{{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}>
|
||||
<div class="djdt-hidden" id="djDebugToolbar">
|
||||
<ul id="djDebugPanelList">
|
||||
{% if toolbar.panels %}
|
||||
<li><a id="djHideToolBarButton" href="#" title="{% trans "Hide toolbar" %}">{% trans "Hide" %} »</a></li>
|
||||
{% else %}
|
||||
<li id="djDebugButton">DEBUG</li>
|
||||
{% endif %}
|
||||
{% for panel in toolbar.panels %}
|
||||
<li class="djDebugPanelButton">
|
||||
<input type="checkbox" data-cookie="djdt{{ panel.panel_id }}" {% if panel.enabled %}checked="checked" title="{% trans "Disable for next and successive requests" %}"{% else %}title="{% trans "Enable for next and successive requests" %}"{% endif %} />
|
||||
{% if panel.has_content and panel.enabled %}
|
||||
<a href="#" title="{{ panel.title }}" class="{{ panel.panel_id }}">
|
||||
{% else %}
|
||||
<div class="djdt-contentless{% if not panel.enabled %} djdt-disabled{% endif %}">
|
||||
{% endif %}
|
||||
{{ panel.nav_title }}
|
||||
{% if panel.enabled %}
|
||||
{% with panel.nav_subtitle as subtitle %}
|
||||
{% if subtitle %}<br /><small>{{ subtitle }}</small>{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if panel.has_content and panel.enabled %}
|
||||
</a>
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="djdt-hidden" id="djDebugToolbarHandle">
|
||||
<span title="{% trans "Show toolbar" %}" id="djShowToolBarButton">«</span>
|
||||
</div>
|
||||
{% for panel in toolbar.panels %}
|
||||
{% if panel.has_content and panel.enabled %}
|
||||
<div id="{{ panel.panel_id }}" class="djdt-panelContent">
|
||||
<div class="djDebugPanelTitle">
|
||||
<a href="" class="djDebugClose"></a>
|
||||
<h3>{{ panel.title|safe }}</h3>
|
||||
</div>
|
||||
<div class="djDebugPanelContent">
|
||||
{% if toolbar.store_id %}
|
||||
<img src="{% static 'debug_toolbar/img/ajax-loader.gif' %}" alt="loading" class="djdt-loader" />
|
||||
<div class="djdt-scroll"></div>
|
||||
{% else %}
|
||||
<div class="djdt-scroll">{{ panel.content }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div id="djDebugWindow" class="djdt-panelContent"></div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -56,7 +56,9 @@ def page_not_found(request, exception):
|
||||
}
|
||||
template = get_template('404.html')
|
||||
body = template.render(context, request)
|
||||
return HttpResponseNotFound(body)
|
||||
r = HttpResponseNotFound(body)
|
||||
r.xframe_options_exempt = True
|
||||
return r
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
@@ -65,7 +67,9 @@ def server_error(request):
|
||||
template = loader.get_template('500.html')
|
||||
except TemplateDoesNotExist:
|
||||
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
|
||||
return HttpResponseServerError(template.render({
|
||||
r = HttpResponseServerError(template.render({
|
||||
'request': request,
|
||||
'sentry_event_id': last_event_id(),
|
||||
}))
|
||||
r.xframe_options_exempt = True
|
||||
return r
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.utils import timezone
|
||||
from django.utils.translation.trans_real import DjangoTranslation
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import etag
|
||||
from django.views.i18n import JavaScriptCatalog, render_javascript_catalog
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
|
||||
# Yes, we want to regenerate this every time the module has been imported to
|
||||
# refresh the cache at least at every code deployment
|
||||
@@ -21,4 +21,5 @@ js_info_dict = {
|
||||
def js_catalog(request, lang):
|
||||
c = JavaScriptCatalog()
|
||||
c.translation = DjangoTranslation(lang, domain='djangojs')
|
||||
return render_javascript_catalog(c.get_catalog(), c.get_plural())
|
||||
context = c.get_context_data()
|
||||
return c.render_to_response(context)
|
||||
|
||||
@@ -20,10 +20,10 @@ def serve_metrics(request):
|
||||
return unauthed_response()
|
||||
|
||||
# check if the user is properly authorized:
|
||||
if "HTTP_AUTHORIZATION" not in request.META:
|
||||
if "Authorization" not in request.headers:
|
||||
return unauthed_response()
|
||||
|
||||
method, credentials = request.META["HTTP_AUTHORIZATION"].split(" ", 1)
|
||||
method, credentials = request.headers["Authorization"].split(" ", 1)
|
||||
if method.lower() != "basic":
|
||||
return unauthed_response()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
@@ -186,25 +187,35 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
return InvoiceAddress(order=self.order)
|
||||
|
||||
@cached_property
|
||||
def address_asked(self):
|
||||
return self.request.event.settings.invoice_address_asked and (
|
||||
self.order.total != Decimal('0.00') or not self.request.event.settings.invoice_address_not_asked_free
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def invoice_form(self):
|
||||
if not self.request.event.settings.invoice_address_asked and self.request.event.settings.invoice_name_required:
|
||||
if not self.address_asked and self.request.event.settings.invoice_name_required:
|
||||
return self.invoice_name_form_class(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
instance=self.invoice_address, validate_vat_id=False,
|
||||
all_optional=self.all_optional
|
||||
)
|
||||
return self.invoice_form_class(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
instance=self.invoice_address, validate_vat_id=False,
|
||||
all_optional=self.all_optional,
|
||||
)
|
||||
if self.address_asked:
|
||||
return self.invoice_form_class(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
instance=self.invoice_address, validate_vat_id=False,
|
||||
all_optional=self.all_optional,
|
||||
)
|
||||
else:
|
||||
return forms.Form(data=self.request.POST if self.request.method == "POST" else None)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['order'] = self.order
|
||||
ctx['formgroups'] = self.formdict.items()
|
||||
ctx['invoice_form'] = self.invoice_form
|
||||
ctx['invoice_address_asked'] = self.address_asked
|
||||
return ctx
|
||||
|
||||
@@ -621,6 +621,10 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
|
||||
required=False
|
||||
)
|
||||
invoice_address_not_asked_free = forms.BooleanField(
|
||||
label=_('Do not ask for invoice address if an order is free'),
|
||||
required=False
|
||||
)
|
||||
invoice_include_free = forms.BooleanField(
|
||||
label=_("Show free products on invoices"),
|
||||
help_text=_("Note that invoices will never be generated for orders that contain only free "
|
||||
|
||||
@@ -43,6 +43,7 @@ class QuestionForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['items'].queryset = self.instance.event.items.all()
|
||||
self.fields['items'].required = True
|
||||
self.fields['dependency_question'].queryset = self.instance.event.questions.filter(
|
||||
type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE)
|
||||
)
|
||||
@@ -462,6 +463,7 @@ class ItemVariationForm(I18nModelForm):
|
||||
'value',
|
||||
'active',
|
||||
'default_price',
|
||||
'original_price',
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Item, ItemAddOn, Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -150,15 +148,6 @@ class CommentForm(I18nModelForm):
|
||||
}
|
||||
|
||||
|
||||
class SubEventChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
p = get_price(self.instance.item, self.instance.variation,
|
||||
voucher=self.instance.voucher,
|
||||
subevent=obj)
|
||||
return '{} – {} ({})'.format(obj.name, obj.get_date_range_display(),
|
||||
p.print(self.instance.order.event.currency))
|
||||
|
||||
|
||||
class OtherOperationsForm(forms.Form):
|
||||
recalculate_taxes = forms.BooleanField(
|
||||
label=_('Re-calculate taxes'),
|
||||
@@ -265,12 +254,13 @@ class OrderPositionAddForm(forms.Form):
|
||||
|
||||
|
||||
class OrderPositionChangeForm(forms.Form):
|
||||
itemvar = forms.ChoiceField()
|
||||
subevent = SubEventChoiceField(
|
||||
itemvar = forms.ChoiceField(
|
||||
required=False,
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
SubEvent.objects.none(),
|
||||
label=pgettext_lazy('subevent', 'New date'),
|
||||
required=True,
|
||||
empty_label=None
|
||||
required=False,
|
||||
empty_label=_('(Unchanged)')
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
@@ -278,53 +268,49 @@ class OrderPositionChangeForm(forms.Form):
|
||||
localize=True,
|
||||
label=_('New price (gross)')
|
||||
)
|
||||
operation = forms.ChoiceField(
|
||||
operation_secret = forms.BooleanField(
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('product', 'Change product'),
|
||||
('price', 'Change price'),
|
||||
('subevent', 'Change event date'),
|
||||
('cancel', 'Remove product'),
|
||||
('split', 'Split into new order'),
|
||||
('secret', 'Regenerate secret'),
|
||||
)
|
||||
label=_('Generate a new secret')
|
||||
)
|
||||
operation_cancel = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Cancel this position')
|
||||
)
|
||||
operation_split = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Split into new order')
|
||||
)
|
||||
change_product_keep_price = forms.BooleanField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.pop('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
try:
|
||||
ia = instance.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
|
||||
if instance:
|
||||
try:
|
||||
if instance.variation:
|
||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||
elif instance.item:
|
||||
initial['itemvar'] = str(instance.item.pk)
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
|
||||
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
|
||||
initial['price'] = instance.price - instance.tax_value
|
||||
else:
|
||||
initial['price'] = instance.price
|
||||
initial['subevent'] = instance.subevent
|
||||
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
|
||||
initial['price'] = instance.price - instance.tax_value
|
||||
else:
|
||||
initial['price'] = instance.price
|
||||
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(*args, **kwargs)
|
||||
if instance.order.event.has_subevents:
|
||||
self.fields['subevent'].instance = instance
|
||||
self.fields['subevent'].queryset = instance.order.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': instance.order.event.slug,
|
||||
'organizer': instance.order.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('(Unchanged)')
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
choices = []
|
||||
choices = [
|
||||
('', _('(Unchanged)'))
|
||||
]
|
||||
for i in instance.order.event.items.prefetch_related('variations').all():
|
||||
pname = str(i)
|
||||
if not i.is_available():
|
||||
@@ -333,14 +319,10 @@ class OrderPositionChangeForm(forms.Form):
|
||||
|
||||
if variations:
|
||||
for v in variations:
|
||||
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent,
|
||||
invoice_address=ia)
|
||||
choices.append(('%d-%d' % (i.pk, v.pk),
|
||||
'%s – %s (%s)' % (pname, v.value, p.print(instance.order.event.currency))))
|
||||
'%s – %s' % (pname, v.value)))
|
||||
else:
|
||||
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent,
|
||||
invoice_address=ia)
|
||||
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(instance.order.event.currency))))
|
||||
choices.append((str(i.pk), pname))
|
||||
self.fields['itemvar'].choices = choices
|
||||
change_decimal_field(self.fields['price'], instance.order.event.currency)
|
||||
|
||||
|
||||
@@ -389,6 +389,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
if logentry.action_type == 'pretix.team.invite.created':
|
||||
return _('{user} has been invited to the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.invite.resent':
|
||||
return _('Invite for {user} has been resent.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.invite.deleted':
|
||||
return _('The invite for {user} has been revoked.').format(user=data.get('email'))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Device ID" %}</dt>
|
||||
<dd>{{ payment_info.pos_id }}</dd>
|
||||
<dt>{% trans "Receipt ID" %}</dt>
|
||||
<dd>{{ payment_info.receipt_id }}</dd>
|
||||
{% if payment_info.payment_type == "sumup" %}
|
||||
<dt>{% trans "Payment provider" %}</dt>
|
||||
<dd>SumUp</dd>
|
||||
<dt>{% trans "Transaction Code" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.tx_code}}</dd>
|
||||
<dd>{{ payment_info.payment_data.tx_code }}</dd>
|
||||
<dt>{% trans "Merchant Code" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.merchant_code }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
@@ -17,6 +21,8 @@
|
||||
<dt>{% trans "Card Entry Mode" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.entry_mode }}</dd>
|
||||
<dt>{% trans "Card number" %}</dt>
|
||||
<dd><i class="fa fa-cc-{{ payment_info.payment_data.card_type|lower }}"></i> **** **** **** {{ payment_info.payment_data.last4 }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
<dd>
|
||||
<i class="fa fa-cc-{{ payment_info.payment_data.card_type|lower }}"></i> **** **** **** {{ payment_info.payment_data.last4 }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
@@ -95,7 +95,15 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.item }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
<td>{{ e.order.email }}</td>
|
||||
<td>
|
||||
{% if e.addon_to and e.addon_to.attendee_email %}
|
||||
{{ e.addon_to.attendee_email }}
|
||||
{% elif e.attendee_email %}
|
||||
{{ e.attendee_email }}
|
||||
{% else %}
|
||||
{{ e.order.email }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if e.addon_to %}
|
||||
{{ e.addon_to.attendee_name }}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
{% bootstrap_field form.invoice_address_company_required layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_vatid layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Your invoice details" %}</legend>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.active layout="control" %}
|
||||
{% bootstrap_field form.default_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.description layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,6 +79,7 @@
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.active layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.original_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.description layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
@@ -71,92 +72,87 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-inline form-order-change">
|
||||
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}">
|
||||
{% bootstrap_form_errors position.form %}
|
||||
{% if position.custom_error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ position.custom_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="help-block">
|
||||
{% trans "Ticket secret:" %} {{ position.secret|slice:":12" }}…
|
||||
</p>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value=""
|
||||
{% if not position.form.operation.value %}checked="checked"{% endif %}>
|
||||
{% trans "Keep unchanged" %}
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-sm-offset-3">
|
||||
<strong>{% trans "Current value" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<strong>{% trans "Change to" %}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="product"
|
||||
{% if position.form.operation.value == "product" %}checked="checked"{% endif %}>
|
||||
{% trans "Change product to" %}
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Product" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.item }}
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.itemvar layout='inline' %}
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
{{ position.form.change_product_keep_price }}
|
||||
{% trans "Keep price the same" %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.event.has_subevents %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="subevent"
|
||||
{% if position.form.operation.value == "subevent" %}checked="checked"{% endif %}>
|
||||
{% trans "Change date to" context "subevent" %}
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Date" context "subevent" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.subevent }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.subevent layout='inline' %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="price"
|
||||
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}>
|
||||
{% trans "Change price to" %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Price" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.price|money:request.event.currency }}<br>
|
||||
{% if position.tax_rate %}
|
||||
<small>{% blocktrans trimmed with rate=position.tax_rate name=position.tax_rule.name %}
|
||||
<strong>incl.</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4 field-container">
|
||||
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
|
||||
{% if position.apply_tax %}
|
||||
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %}
|
||||
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
{% elif position.item.tax_rule %}
|
||||
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
|
||||
<strong>incl.</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% trans "no taxes apply" %}
|
||||
{% endif %}
|
||||
</label>
|
||||
<small><strong>{% trans "including all taxes" %}</strong></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="split"
|
||||
{% if position.form.operation.value == "split" %}checked="checked"{% endif %}>
|
||||
{% trans "Split into new order" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="secret"
|
||||
{% if position.form.operation.value == "secret" %}checked="checked"{% endif %}>
|
||||
{% trans "Generate a new secret" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="cancel"
|
||||
{% if position.form.operation.value == "cancel" %}checked="checked"{% endif %}>
|
||||
{% trans "Cancel position" %}
|
||||
{% if position.addons.exists %}
|
||||
<em class="text-danger">
|
||||
{% trans "Removing this position will also remove all add-ons to this position." %}
|
||||
</em>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Ticket secret" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.secret|slice:":12" }}…
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.operation_secret layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field position.form.operation_cancel layout='inline' %}
|
||||
{% bootstrap_field position.form.operation_split layout='inline' %}
|
||||
{% if position.addons.exists %}
|
||||
<em class="text-danger">
|
||||
{% trans "Removing this position will also remove all add-ons to this position." %}
|
||||
</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<form method="post" class="form-horizontal" href="" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="questions_accordion">
|
||||
{% if request.event.settings.invoice_address_asked or order.invoice_address or request.event.settings.invoice_name_required %}
|
||||
{% if invoice_address_asked or order.invoice_address or request.event.settings.invoice_name_required %}
|
||||
<details class="panel panel-default" open>
|
||||
<summary class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
|
||||
@@ -489,7 +489,27 @@
|
||||
{% if p.html_info %}
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="5">{{ p.html_info|safe }}</td>
|
||||
<td colspan="5">
|
||||
{{ p.html_info|safe }}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
<a href="" class="btn btn-default btn-xs" data-expandpayment data-id="{{ p.pk }}">
|
||||
<span class="fa-eye fa fa-fw"></span>
|
||||
{% trans "Inspect" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif staff_session %}
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="5">
|
||||
<a href="" class="btn btn-default btn-xs" data-expandpayment data-id="{{ p.pk }}">
|
||||
<span class="fa-eye fa fa-fw"></span>
|
||||
{% trans "Inspect" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -574,6 +594,18 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if staff_session %}
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="7">
|
||||
<a href="" class="btn btn-default btn-xs" data-expandrefund
|
||||
data-id="{{ r.pk }}">
|
||||
<span class="fa-eye fa fa-fw"></span>
|
||||
{% trans "Inspect" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
{{ i.email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
<button type="submit" name="resend-invite" value="{{ i.id }}"
|
||||
data-toggle="tooltip" title="{% trans "resend invite" %}"
|
||||
class="btn-invisible">
|
||||
<span class="fa fa-repeat"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" name="remove-invite" value="{{ i.id }}"
|
||||
|
||||
@@ -19,6 +19,8 @@ urlpatterns = [
|
||||
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
||||
url(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
|
||||
url(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
|
||||
url(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
|
||||
url(r'^logdetail/refund/$', global_settings.RefundDetailView.as_view(), name='global.refunddetail'),
|
||||
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
|
||||
url(r'^sudo/$', user.StartStaffSession.as_view(), name='user.sudo'),
|
||||
url(r'^sudo/stop/$', user.StopStaffSession.as_view(), name='user.sudo.stop'),
|
||||
|
||||
@@ -31,8 +31,7 @@ from pytz import timezone
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import LazyCurrencyNumber
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, LogEntry, Order, RequiredAction,
|
||||
TaxRule, Voucher,
|
||||
Event, LogEntry, Order, RequiredAction, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import EventMetaValue
|
||||
from pretix.base.services import tickets
|
||||
@@ -789,12 +788,7 @@ class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
|
||||
for k in provider.form.changed_data
|
||||
}
|
||||
)
|
||||
CachedTicket.objects.filter(
|
||||
order_position__order__event=self.request.event, provider=provider.identifier
|
||||
).delete()
|
||||
CachedCombinedTicket.objects.filter(
|
||||
order__event=self.request.event, provider=provider.identifier
|
||||
).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'provider': provider.identifier})
|
||||
else:
|
||||
success = False
|
||||
form = self.get_form(self.get_form_class())
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from pretix.base.models import LogEntry
|
||||
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
|
||||
from pretix.base.services.update_check import check_result_table, update_check
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.control.forms.global_settings import (
|
||||
@@ -71,3 +71,15 @@ class LogDetailView(AdministratorPermissionRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
le = get_object_or_404(LogEntry, pk=request.GET.get('pk'))
|
||||
return JsonResponse({'data': le.parsed_data})
|
||||
|
||||
|
||||
class PaymentDetailView(AdministratorPermissionRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
p = get_object_or_404(OrderPayment, pk=request.GET.get('pk'))
|
||||
return JsonResponse({'data': p.info_data})
|
||||
|
||||
|
||||
class RefundDetailView(AdministratorPermissionRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
p = get_object_or_404(OrderRefund, pk=request.GET.get('pk'))
|
||||
return JsonResponse({'data': p.info_data})
|
||||
|
||||
@@ -18,11 +18,12 @@ from django.views.generic.edit import DeleteView
|
||||
|
||||
from pretix.base.forms import I18nFormSet
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Item, ItemCategory, ItemVariation, Order, Question,
|
||||
CartPosition, Item, ItemCategory, ItemVariation, Order, Question,
|
||||
QuestionAnswer, QuestionOption, Quota, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle
|
||||
from pretix.base.services.tickets import invalidate_cache
|
||||
from pretix.control.forms.item import (
|
||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||
ItemBundleFormSet, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
||||
@@ -49,7 +50,9 @@ class ItemList(ListView):
|
||||
event=self.request.event
|
||||
).annotate(
|
||||
var_count=Count('variations')
|
||||
).prefetch_related("category")
|
||||
).prefetch_related("category").order_by(
|
||||
'category__position', 'category', 'position'
|
||||
)
|
||||
|
||||
|
||||
def item_move(request, item, up=True):
|
||||
@@ -873,7 +876,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
|
||||
self.object.log_action(
|
||||
'pretix.event.item.changed', user=self.request.user, data=data
|
||||
)
|
||||
CachedTicket.objects.filter(order_position__item=self.item).delete()
|
||||
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
|
||||
for f in self.plugin_forms:
|
||||
f.save()
|
||||
return super().form_valid(form)
|
||||
@@ -1158,6 +1161,7 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
success_url = self.get_success_url()
|
||||
o = self.get_object()
|
||||
if o.allow_delete():
|
||||
CartPosition.objects.filter(addon_to__item=self.get_object()).delete()
|
||||
self.get_object().cartposition_set.all().delete()
|
||||
self.get_object().log_action('pretix.event.item.deleted', user=self.request.user)
|
||||
self.get_object().delete()
|
||||
|
||||
@@ -43,6 +43,7 @@ from pretix.base.models.orders import (
|
||||
)
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.export import export
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
|
||||
@@ -57,7 +58,7 @@ from pretix.base.services.orders import (
|
||||
from pretix.base.services.stats import order_overview
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.signals import (
|
||||
register_data_exporters, register_ticket_outputs,
|
||||
order_modified, register_data_exporters, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
@@ -1239,7 +1240,11 @@ class OrderChange(OrderView):
|
||||
return False
|
||||
|
||||
try:
|
||||
if p.form.cleaned_data['operation'] == 'product':
|
||||
if p.form.cleaned_data['operation_cancel']:
|
||||
ocm.cancel(p)
|
||||
continue
|
||||
|
||||
if p.form.cleaned_data['itemvar']:
|
||||
if '-' in p.form.cleaned_data['itemvar']:
|
||||
itemid, varid = p.form.cleaned_data['itemvar'].split('-')
|
||||
else:
|
||||
@@ -1250,16 +1255,19 @@ class OrderChange(OrderView):
|
||||
variation = ItemVariation.objects.get(pk=varid, item=item)
|
||||
else:
|
||||
variation = None
|
||||
ocm.change_item(p, item, variation, keep_price=p.form.cleaned_data['change_product_keep_price'])
|
||||
elif p.form.cleaned_data['operation'] == 'price':
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
elif p.form.cleaned_data['operation'] == 'subevent':
|
||||
if item != p.item or variation != p.variation:
|
||||
ocm.change_item(p, item, variation)
|
||||
|
||||
if self.request.event.has_subevents and p.form.cleaned_data['subevent'] and p.form.cleaned_data['subevent'] != p.subevent:
|
||||
ocm.change_subevent(p, p.form.cleaned_data['subevent'])
|
||||
elif p.form.cleaned_data['operation'] == 'cancel':
|
||||
ocm.cancel(p)
|
||||
elif p.form.cleaned_data['operation'] == 'split':
|
||||
|
||||
if p.form.cleaned_data['price'] != p.price:
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
|
||||
if p.form.cleaned_data['operation_split']:
|
||||
ocm.split(p)
|
||||
elif p.form.cleaned_data['operation'] == 'secret':
|
||||
|
||||
if p.form.cleaned_data['operation_secret']:
|
||||
ocm.regenerate_secret(p)
|
||||
|
||||
except OrderError as e:
|
||||
@@ -1305,7 +1313,8 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
|
||||
messages.error(self.request,
|
||||
_("We had difficulties processing your input. Please review the errors below."))
|
||||
return self.get(request, *args, **kwargs)
|
||||
self.invoice_form.save()
|
||||
if hasattr(self.invoice_form, 'save'):
|
||||
self.invoice_form.save()
|
||||
self.order.log_action('pretix.event.order.modified', {
|
||||
'invoice_data': self.invoice_form.cleaned_data,
|
||||
'data': [{
|
||||
@@ -1318,8 +1327,9 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
|
||||
'you need to do this manually.')
|
||||
messages.success(self.request, _(success_message))
|
||||
|
||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=self.order).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
|
||||
|
||||
order_modified.send(sender=self.request.event, order=self.order)
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
|
||||
@@ -1360,8 +1370,7 @@ class OrderContactChange(OrderView):
|
||||
for op in self.order.all_positions.all():
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=self.order).delete()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
|
||||
self.order.log_action('pretix.event.order.secret.changed', user=self.request.user)
|
||||
|
||||
self.form.save()
|
||||
|
||||
@@ -518,7 +518,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
if 'remove-member' in request.POST:
|
||||
try:
|
||||
user = User.objects.get(pk=request.POST.get('remove-member'))
|
||||
except User.DoesNotExist:
|
||||
except (User.DoesNotExist, ValueError):
|
||||
pass
|
||||
else:
|
||||
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
|
||||
@@ -542,7 +542,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
elif 'remove-invite' in request.POST:
|
||||
try:
|
||||
invite = self.object.invites.get(pk=request.POST.get('remove-invite'))
|
||||
except TeamInvite.DoesNotExist:
|
||||
except (TeamInvite.DoesNotExist, ValueError):
|
||||
messages.error(self.request, _('Invalid invite selected.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
@@ -555,10 +555,26 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
messages.success(self.request, _('The invite has been revoked.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
elif 'resend-invite' in request.POST:
|
||||
try:
|
||||
invite = self.object.invites.get(pk=request.POST.get('resend-invite'))
|
||||
except (TeamInvite.DoesNotExist, ValueError):
|
||||
messages.error(self.request, _('Invalid invite selected.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
self._send_invite(invite)
|
||||
self.object.log_action(
|
||||
'pretix.team.invite.resent', user=self.request.user, data={
|
||||
'email': invite.email
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The invite has been resent.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
elif 'remove-token' in request.POST:
|
||||
try:
|
||||
token = self.object.tokens.get(pk=request.POST.get('remove-token'))
|
||||
except TeamAPIToken.DoesNotExist:
|
||||
except (TeamAPIToken.DoesNotExist, ValueError):
|
||||
messages.error(self.request, _('Invalid token selected.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
|
||||
@@ -16,6 +16,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from pretix.base.models import CartPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import (
|
||||
@@ -117,6 +118,7 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
else:
|
||||
self.object.log_action('pretix.subevent.deleted', user=self.request.user)
|
||||
CartPosition.objects.filter(addon_to__subevent=self.object).delete()
|
||||
self.object.cartposition_set.all().delete()
|
||||
self.object.delete()
|
||||
messages.success(request, pgettext_lazy('subevent', 'The selected date has been deleted.'))
|
||||
@@ -512,6 +514,7 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
|
||||
elif request.POST.get('action') == 'delete_confirm':
|
||||
for obj in self.objects:
|
||||
if obj.allow_delete():
|
||||
CartPosition.objects.filter(addon_to__subevent=obj).delete()
|
||||
obj.cartposition_set.all().delete()
|
||||
obj.log_action('pretix.subevent.deleted', user=self.request.user)
|
||||
obj.delete()
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.views.generic import (
|
||||
CreateView, DeleteView, ListView, TemplateView, UpdateView, View,
|
||||
)
|
||||
|
||||
from pretix.base.models import LogEntry, Voucher
|
||||
from pretix.base.models import CartPosition, LogEntry, OrderPosition, Voucher
|
||||
from pretix.base.models.vouchers import _generate_random_code
|
||||
from pretix.control.forms.filter import VoucherFilterForm
|
||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||
@@ -143,6 +143,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
messages.error(request, _('A voucher can not be deleted if it already has been redeemed.'))
|
||||
else:
|
||||
self.object.log_action('pretix.voucher.deleted', user=self.request.user)
|
||||
CartPosition.objects.filter(addon_to__voucher=False).delete()
|
||||
self.object.cartposition_set.all().delete()
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected voucher has been deleted.'))
|
||||
@@ -348,6 +349,7 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
|
||||
for obj in self.objects:
|
||||
if obj.allow_delete():
|
||||
obj.log_action('pretix.voucher.deleted', user=self.request.user)
|
||||
OrderPosition.objects.filter(addon_to__voucher=obj).delete()
|
||||
obj.cartposition_set.all().delete()
|
||||
obj.delete()
|
||||
else:
|
||||
|
||||
@@ -13,7 +13,7 @@ class SessionReauthRequired(Exception):
|
||||
|
||||
|
||||
def get_user_agent_hash(request):
|
||||
return hashlib.sha256(request.META['HTTP_USER_AGENT'].encode()).hexdigest()
|
||||
return hashlib.sha256(request.headers['User-Agent'].encode()).hexdigest()
|
||||
|
||||
|
||||
def assert_session_valid(request):
|
||||
@@ -26,7 +26,7 @@ def assert_session_valid(request):
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
|
||||
raise SessionReauthRequired()
|
||||
|
||||
if 'HTTP_USER_AGENT' in request.META:
|
||||
if 'User-Agent' in request.headers:
|
||||
if 'pinned_user_agent' in request.session:
|
||||
if request.session.get('pinned_user_agent') != get_user_agent_hash(request):
|
||||
raise SessionInvalid()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
|
||||
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2019-03-22 15:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2019-03-22 15:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2019-03-31 08:00+0000\n"
|
||||
"Last-Translator: oocf <oswaldocerna@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2018-10-28 10:23+0000\n"
|
||||
"Last-Translator: Arnaud Vergnet <keplyx@gmail.com>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2019-01-02 08:20+0000\n"
|
||||
"Last-Translator: amefad <fame@libero.it>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"PO-Revision-Date: 2019-03-24 05:00+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: 2019-04-16 05:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"nl/>\n"
|
||||
@@ -249,10 +249,9 @@ msgstr "Winkelwagen is verlopen"
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:46
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] ""
|
||||
"De items in uw winkelwagen zijn voor u gereserveerd voor één minuut."
|
||||
msgstr[0] "De items in uw winkelwagen zijn nog één voor u gereserveerd."
|
||||
msgstr[1] ""
|
||||
"De items in uw winkelwagen zijn voor u gereserveerd voor {num} minuten."
|
||||
"De items in uw winkelwagen zijn nog {num} minuten voor u gereserveerd."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:14
|
||||
msgctxt "widget"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
|
||||
"POT-Creation-Date: 2019-04-16 11:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user