Compare commits

...

112 Commits

Author SHA1 Message Date
Raphael Michel
1279c8720f Add tests for price calculation API 2019-04-22 11:14:33 +02:00
Raphael Michel
7a3afde7b1 Change semantics of changing orders
This basically does two things to the "Change products" view of orders and the
OrderChangeManager program API:

1) It decouples changing items or subevents from changing prices.
   OrderChangeManager.change_item() and .change_subevent() no longer
   touch the price of a position. Instead .change_price() needs to be
   called explicitly. However, a client-side JavaScript component now
   *proposes* a new price based on the changed item or subevent.

2) The user interface now exposes the possibility of doing multiple
   things at the same time, i.e. changing the item, subevent and price
   in the same operation. OrderChangeManager already allowed this
   before.

(1) is basically a consequence of (2), while (2) is a prerequesite for
e.g. the `seating` branch, where changing the subevent will always
require changing the seat.
2019-04-21 23:02:32 +02:00
Raphael Michel
e4417305a2 Fix updatestyles not being sent to background queue 2019-04-18 17:44:14 +02:00
Raphael Michel
bc5d0bea00 updatestyles: Prioritize future events over past ones 2019-04-18 17:27:34 +02:00
Raphael Michel
dbce9b0395 Allow error pages to be embedded in frames (to ease widget troubleshooting) 2019-04-18 17:19:42 +02:00
Martin Gross
2eb88840bd Original price for variations (#1258)
* Original price for variations

* Documentation

* API-GET

* Fix existing tests to accomodate new attribute

* Test for variation's original_price on API
2019-04-18 16:13:49 +02:00
Martin Gross
4838835b1b Remove debug-toolbar template override 2019-04-18 12:21:42 +02:00
Raphael Michel
ab452bd9e3 Fix typo 2019-04-18 09:50:07 +02:00
Raphael Michel
ae298bddb8 Make FakeRedis play nice with metrics 2019-04-18 09:17:55 +02:00
Raphael Michel
9ad4607d26 Move ticket cache invalidation to background task 2019-04-18 09:17:01 +02:00
Raphael Michel
b3684377cd Fix crash in item validation
Fixes Sentry PRETIXEU-10B
2019-04-17 15:40:25 +02:00
Raphael Michel
441badfdbd Bank transfer: Move ack field 2019-04-17 15:38:26 +02:00
Raphael Michel
0d242a0304 Fix internal error during validation
Sentry PRETIXEU-10A
2019-04-17 15:21:42 +02:00
Raphael Michel
2fac8592d4 Add modern invoice renderer 2019-04-17 15:08:58 +02:00
Raphael Michel
58b1a2f115 Fix timezone handling in widget 2019-04-17 14:42:00 +02:00
Raphael Michel
420d44e909 Fix #1170 -- E-mail address in check-in list 2019-04-17 12:12:07 +02:00
Raphael Michel
e0063fce52 Allow superusers to inspect payments and refunds 2019-04-17 10:15:14 +02:00
Raphael Michel
21ef6c7950 Update framework classifier 2019-04-17 10:07:02 +02:00
Sohalt
651f429ffb Fix #1247 -- Allow team invites to be resent (#1250)
* Fix #1247 -- Allow team invites to be resent

* Test resending invalid invites

* Fix tooltip

* Fix test

* Handle invalid types for pk parameter

* Style button
2019-04-16 16:39:31 +02:00
Raphael Michel
66dd7c448b Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-16 13:35:36 +02:00
Raphael Michel
e9b4205145 Fix translation of widget headlines 2019-04-16 13:35:07 +02:00
Raphael Michel
6dedea1025 Items API: Note that tax_rate is read-only 2019-04-16 13:35:07 +02:00
Raphael Michel
348ed4e909 Merge pull request #1244 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-16 13:34:26 +02:00
Maarten van den Berg
091b3358e4 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-16 05:00:07 +00:00
Maarten van den Berg
186e2a6b9a Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-16 05:00:06 +00:00
Maarten van den Berg
198b90972c Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-16 05:00:06 +00:00
Maarten van den Berg
4989b6235c Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-16 05:00:05 +00:00
mussol
4cfebab11c Translated on translate.pretix.eu (Catalan)
Currently translated at 35.1% (1073 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Martin Gross
fe944ec643 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Martin Gross
9d92c7b10f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
3b810a3a76 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
7860417177 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 99.4% (3038 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
1438edb3c8 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
a17720062b Translated on translate.pretix.eu (Catalan)
Currently translated at 31.8% (972 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
37ab45b352 Translated on translate.pretix.eu (Catalan)
Currently translated at 31.6% (966 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
6be212df8c Translated on translate.pretix.eu (Catalan)
Currently translated at 31.1% (952 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
b4e85780f4 Translated on translate.pretix.eu (Catalan)
Currently translated at 30.0% (917 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
a9732c4788 Translated on translate.pretix.eu (Catalan)
Currently translated at 30.0% (917 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
273316be25 Translated on translate.pretix.eu (Catalan)
Currently translated at 29.4% (900 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
0f3b269931 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 98.9% (3023 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
4462054d0e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Raphael Michel
ec53022cc8 Do not call task synchronously inside task (celery doesn't allow it any more) 2019-04-15 15:46:37 +02:00
Raphael Michel
0b65b18459 Send emails in an TransactionAwareTask 2019-04-15 15:22:58 +02:00
Raphael Michel
2fac03f47b Add a test case for free orders 2019-04-15 15:14:35 +02:00
Raphael Michel
750d5eda48 Do not mark free orders as paid that require approval 2019-04-15 15:12:26 +02:00
Raphael Michel
f2cd9a2002 Fix logic bug in attachment size check 2019-04-15 12:58:36 +02:00
Raphael Michel
874b38db17 Mark order as paid immediately 2019-04-15 12:58:20 +02:00
Raphael Michel
0f58e1c396 CSV import: Do not skip rows without a reference 2019-04-08 17:55:28 +02:00
Raphael Michel
36e0afc09e Further improvements to the print stylesheet 2019-04-08 17:42:06 +02:00
Raphael Michel
7164124a70 Display category description in add-on step 2019-04-08 15:23:40 +02:00
Raphael Michel
887d8832c0 Improve print CSS of order details 2019-04-07 18:12:12 +02:00
Raphael Michel
beb144f9a0 Fix API log cleanup 2019-04-07 15:31:35 +02:00
Raphael Michel
6d1dea7922 Upgrade to Django 2.2 and modern DRF and py.test (#1246)
* Upgrade django and stuff

* Update to Django 2.2 and recent versions of similar packages

* Provide explicit orderings to all models used in paginated queries

* Resolve naive datetime warnings in test suite

* Deal with deprecation warnings

* Fix sqlparse version
2019-04-07 14:09:49 +01:00
Raphael Michel
cb531a7a6a Cut test time by 65% by caching templates and not compiling sass 2019-04-07 13:53:59 +02:00
Raphael Michel
d5820d74d3 Fix #1025 -- Python 3.7 support (#1245)
* Fix #1025 -- Python 3.7 support

* Upgrade redis-py

* Travis: xenial

* Fix version specifier
2019-04-06 22:58:36 +01:00
Raphael Michel
b686978074 Add order lifecycle signals 2019-04-06 15:05:39 +02:00
Raphael Michel
c372bffc57 Fix tests on PostgreSQL 2019-04-05 16:17:57 +02:00
Raphael Michel
282c6108bf Remove duplicate test 2019-04-05 15:32:25 +02:00
Raphael Michel
f2437c7ff7 Correcly read bytesfield 2019-04-05 15:04:47 +02:00
Raphael Michel
dd0b6e6647 Adjust test to internal type change 2019-04-05 14:59:05 +02:00
Raphael Michel
f3128591d8 More flexible response content handling 2019-04-05 14:54:36 +02:00
Raphael Michel
d395db8142 Box office payments: Always display device and receipt ID 2019-04-05 14:40:58 +02:00
Raphael Michel
0c82e92882 REST API: Add support for idempotency keys 2019-04-05 14:21:51 +02:00
Raphael Michel
db0c13a3c2 REST API: Order creation: Allow to set payment_date 2019-04-05 08:55:57 +02:00
Raphael Michel
19a2f4163a Add a few permission tests 2019-04-04 18:17:56 +02:00
Raphael Michel
76526465c0 Fix a test failure in test_items 2019-04-04 18:14:27 +02:00
Raphael Michel
d0d0f9aa4c Fix logic flaw in cart position deletion 2019-04-04 17:18:12 +02:00
Martin Gross
482f6b1eb8 Fix Item/Question tests to also include obligatory items[] as imposed by b931d27486 2019-04-04 16:12:20 +02:00
Raphael Michel
327418299a Cart view: Make questions a little bit less bold 2019-04-04 14:22:36 +02:00
Raphael Michel
5dfd1e6337 Prefill attendee name/email of first ticket with contact email and invoice recipient 2019-04-04 14:13:08 +02:00
Raphael Michel
bc01124584 Fix stepping back to the invoice address 2019-04-04 14:12:51 +02:00
Raphael Michel
c0df418265 Make sure package pinning is copied to setup.py 2019-04-04 13:45:07 +02:00
Martin Gross
af06f6fc38 Pin pytest-xdist to 1.27.*, as 1.28.0++ requires pytest>=4.4.0 2019-04-04 10:24:59 +02:00
Raphael Michel
4c0e8f69ea Cancellation: Do not display refund notices if not required 2019-04-04 09:57:57 +02:00
Raphael Michel
243e4ac4c8 Allow not to ask for invoice addresses on free orders 2019-04-04 09:57:57 +02:00
Raphael Michel
b931d27486 Solve cart deletion issues once and for all 2019-04-04 09:57:57 +02:00
Raphael Michel
2810e2a760 CartManager: Do not try to extend positions while they are being removed 2019-04-04 09:57:57 +02:00
Martin Gross
04465393b2 Set explicit description for Stripe Charges 2019-04-03 19:30:56 +02:00
Raphael Michel
4c9032f2a8 Bump version to 2.6.0 2019-04-03 16:02:39 +02:00
Raphael Michel
cae2bb944a Merge pull request #1243 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-03 15:02:23 +01:00
Raphael Michel
724e745b8d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-03 14:02:01 +00:00
Raphael Michel
f4cead1c20 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-03 13:26:15 +00:00
Raphael Michel
7cab1924bb Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-03 15:19:57 +02:00
Raphael Michel
641148fecc Merge pull request #1239 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-03 14:19:23 +01:00
mussol
9b3860e5fd Translated on translate.pretix.eu (Catalan)
Currently translated at 27.5% (839 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
mussol
cb9d4c10df Translated on translate.pretix.eu (Catalan)
Currently translated at 22.0% (669 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
oocf
84105b9585 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
oocf
3f38caeb24 Translated on translate.pretix.eu (Spanish)
Currently translated at 99.3% (3026 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
mussol
eae552e474 Translated on translate.pretix.eu (Catalan)
Currently translated at 21.1% (643 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
mussol
f27c10c2ac Translated on translate.pretix.eu (Catalan)
Currently translated at 8.8% (267 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
Raphael Michel
abd237b969 Checkout redirection: Respect cart_namespace 2019-04-03 13:12:49 +02:00
Raphael Michel
99c61c9060 Orders API: Add a missing sorting method to the documentation 2019-04-03 11:18:13 +02:00
Raphael Michel
246f307e21 Pin version of pillow (incompatibility with reportlab) 2019-04-02 11:31:01 +02:00
Raphael Michel
1f672e7df2 Fix incorrect test 2019-04-02 11:30:47 +02:00
Raphael Michel
b261a2041a Actually set the revoked flag 2019-04-02 09:44:31 +02:00
Raphael Michel
2d37c6d94d Make device token revokation more explicit 2019-04-02 09:36:07 +02:00
Raphael Michel
e75ae80fb5 REST API: Allow to filter orders by datetime 2019-03-29 17:15:15 +01:00
Raphael Michel
73ec5bac79 Allow to set a custom error message when presale is ended 2019-03-29 16:38:47 +01:00
Raphael Michel
46166159b0 Allow to force order creation through the API 2019-03-28 18:11:06 +01:00
Raphael Michel
598693fab2 Add Chinese as a selectable language 2019-03-28 17:06:28 +01:00
Raphael Michel
2420d884fc Merge pull request #1232 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-28 16:06:26 +00:00
Raphael Michel
f95005a8d4 Added translation on translate.pretix.eu (Catalan) 2019-03-28 16:04:15 +00:00
Raphael Michel
e773096df3 Added translation on translate.pretix.eu (Catalan) 2019-03-28 16:03:59 +00:00
yichengsd
c42905421d Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Alvaro Enrique Ruano
46c2e28def Translated on translate.pretix.eu (Spanish)
Currently translated at 99.0% (3018 of 3047 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
yichengsd
07bc3df6d3 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 99.8% (3041 of 3047 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
2992c4c48a Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
c53718381e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3047 of 3047 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
98e5f0b95d Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 71.9% (69 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
7f11f06f3f Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Raphael Michel
949057a9cc Allow to persist filter attributes in session 2019-03-28 16:58:05 +01:00
Raphael Michel
edd643cc32 Event index: Filter subevent list as well 2019-03-28 16:54:21 +01:00
181 changed files with 33677 additions and 14359 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -373,8 +373,8 @@ List of all orders
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code`` and
``status``. Default: ``datetime``
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
@@ -385,6 +385,7 @@ List of all orders
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
@@ -749,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)
@@ -788,6 +790,8 @@ Creating orders
* ``internal_type``
* ``tax_rule``
* ``force`` (optional). If set to ``true``, quotas will be ignored.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come

View File

@@ -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
--------

View File

@@ -1 +1 @@
__version__ = "2.6.0.dev0"
__version__ = "2.6.0"

View File

@@ -19,7 +19,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
if not device.initialized:
raise exceptions.AuthenticationFailed('Device has not been initialized.')
if not device.api_token:
if device.revoked:
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device

View 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

View 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')},
),
]

View File

@@ -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'),)

View File

@@ -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):

View File

@@ -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):

View File

@@ -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:
@@ -457,11 +474,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_provider = serializers.CharField(required=True)
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')
'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():
@@ -532,6 +551,8 @@ 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:
iadata = validated_data.pop('invoice_address')
@@ -565,29 +586,30 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
for quota in new_quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if not force:
for i, pos_data in enumerate(positions_data):
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
for quota in new_quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
quotadiff.update(new_quotas)
quotadiff.update(new_quotas)
if any(errs):
raise ValidationError({'positions': errs})
@@ -614,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:

View File

@@ -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()

View File

@@ -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'):

View File

@@ -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))

View File

@@ -105,7 +105,7 @@ class RevokeKeyView(APIView):
def post(self, request, format=None):
device = request.auth
device.api_token = None
device.revoked = True
device.save()
device.log_action('pretix.device.revoked', auth=device)

View File

@@ -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 '

View File

@@ -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()

View File

@@ -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):
@@ -50,6 +56,7 @@ class OrderFilter(FilterSet):
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
class Meta:
model = Order
@@ -124,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()
@@ -146,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()
@@ -187,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)
@@ -221,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)
@@ -239,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', '')
@@ -257,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()
@@ -276,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()
@@ -293,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()
@@ -310,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 (
@@ -342,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:
@@ -356,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()
@@ -367,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,
@@ -374,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)
@@ -450,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()
@@ -613,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()
@@ -664,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)
@@ -685,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(
@@ -750,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()
@@ -778,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()
@@ -795,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()
@@ -820,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()
@@ -910,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()
@@ -928,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:
@@ -947,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:

View File

@@ -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

View File

@@ -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(

View File

@@ -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]

View File

@@ -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

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.1.5 on 2019-04-02 07:22
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0115_auto_20190323_2238'),
]
operations = [
migrations.AddField(
model_name='device',
name='revoked',
field=models.BooleanField(default=False),
),
]

View 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'),
),
]

View File

@@ -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()

View File

@@ -41,6 +41,7 @@ class Device(LoggedModel):
api_token = models.CharField(max_length=190, unique=True, null=True)
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
revoked = models.BooleanField(default=False)
name = models.CharField(
max_length=190,
verbose_name=_('Name')

View File

@@ -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()

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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,
@@ -101,6 +105,10 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'presale_has_ended_text': {
'default': '',
'type': LazyI18nString
},
'payment_explanation': {
'default': '',
'type': LazyI18nString

View File

@@ -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"]
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" %} &#187;</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">&#171;</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>

View File

@@ -1,6 +1,6 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html>
<head>

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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 "
@@ -1049,6 +1053,14 @@ class DisplaySettingsForm(SettingsForm):
required=False,
widget=I18nTextarea
)
presale_has_ended_text = I18nFormField(
label=_("End of presale text"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
"is over. You can use it to describe other options to get a ticket, such as a box office.")
)
voucher_explanation_text = I18nFormField(
label=_("Voucher explanation"),
required=False,

View File

@@ -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',
]

View File

@@ -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)

View File

@@ -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'))

View File

@@ -1,6 +1,6 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html>
<head>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -11,6 +11,7 @@
<legend>{% trans "Event page" %}</legend>
{% bootstrap_field form.logo_image layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.presale_has_ended_text layout="control" %}
{% bootstrap_field form.voucher_explanation_text layout="control" %}
{% bootstrap_field form.show_variations_expanded layout="control" %}
{% bootstrap_field form.meta_noindex layout="control" %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -9,6 +9,13 @@
<strong>{% blocktrans %}Are you sure you want remove access for this device?{% endblocktrans %}</strong>
{% trans "All data of this device will stay available, but you can't use the device any more." %}
</p>
<div class="alert alert-warning">
<ul>
<li>{% trans "All data uploaded by this device will stay available online." %}</li>
<li>{% trans "If data (e.g. POS transactions or check-ins) has been created on this device and has not been uploaded, you will no longer be able to upload it." %}</li>
<li>{% trans "If the device software supports it, personal data such as orders will be deleted from the device on the next synchronization attempt. Non-personal data such as event metadata and POS transactions will persist until you uninstall or reset the software manually." %}</li>
</ul>
</div>
<div class="form-group submit-group">
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}

View File

@@ -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 }}"

View File

@@ -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'),

View File

@@ -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())

View File

@@ -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})

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:
@@ -756,7 +772,7 @@ class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.api_token = None
self.object.revoked = True
self.object.save()
self.object.log_action('pretix.device.revoked', user=self.request.user)
messages.success(request, _('Access for this device has been revoked.'))

View File

@@ -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()

View File

@@ -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:

View File

@@ -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

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -0,0 +1,465 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr ""
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Placed orders"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Paid orders"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
msgid "Total revenue"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
msgid "Contacting Stripe …"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:56
msgid "Total"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:39
#: pretix/static/pretixbase/js/asynctask.js:95
msgid ""
"Your request has been queued on the server and will now be processed. "
"Depending on the size of your event, this might take up to a few minutes."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:45
#: pretix/static/pretixbase/js/asynctask.js:101
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:69
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:115
#: pretix/static/pretixcontrol/js/ui/mail.js:20
msgid "The request took to long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:127
#: pretix/static/pretixcontrol/js/ui/mail.js:25
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:148
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:156
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixcontrol/js/ui/main.js:20
msgid "Close message"
msgstr ""
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr ""
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:43
msgid "Lead Scan QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:45
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:249
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:418
msgid "Group of objects"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:424
msgid "Text object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:426
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:428
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:430
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:434
msgid "Ticket design"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:687
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:52
msgid "Generating messages …"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:55
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:217
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:221
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:225
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:595
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:646
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:71
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:120
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:121
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
msgid "(one more date)"
msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:41
msgid "Cart expired"
msgstr ""
#: 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] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+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

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-03-22 14:50+0000\n"
"PO-Revision-Date: 2019-02-20 03:00+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-"
"js/es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.4\n"
"X-Generator: Weblate 3.5.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -220,7 +220,7 @@ msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:646
msgid "Click to close"
msgstr ""
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
@@ -232,13 +232,11 @@ msgstr "Cantidad"
#: pretix/static/pretixcontrol/js/ui/question.js:120
msgid "Yes"
msgstr ""
msgstr "Si"
#: pretix/static/pretixcontrol/js/ui/question.js:121
#, fuzzy
#| msgid "None"
msgid "No"
msgstr "Ninguno"
msgstr "No"
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
msgid "(one more date)"
@@ -300,12 +298,12 @@ msgstr "más %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
msgstr "incl. impuestos"
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgstr ""
msgstr "más impuestos"
#: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format
@@ -400,102 +398,100 @@ msgid "See variations"
msgstr "Ver variaciones"
#: pretix/static/pretixpresale/js/widget/widget.js:41
#, fuzzy
#| msgid "Use a different name internally"
msgctxt "widget"
msgid "Choose a different event"
msgstr "Usar un nombre diferente internamente"
msgstr "Elige un evento diferente"
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
msgstr "Atrás"
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgstr ""
msgstr "Siguiente mes"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgstr ""
msgstr "Mes anterior"
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
msgstr "Me"
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgstr ""
msgstr "Ma"
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgstr ""
msgstr "Mie"
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgstr ""
msgstr "Ju"
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgstr ""
msgstr "Vi"
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgstr ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Su"
msgstr ""
msgstr "Do"
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgid "January"
msgstr ""
msgstr "Enero"
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgid "February"
msgstr ""
msgstr "Febrero"
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "March"
msgstr ""
msgstr "Marzo"
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "April"
msgstr ""
msgstr "Abril"
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "May"
msgstr ""
msgstr "Mayo"
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "June"
msgstr ""
msgstr "Junio"
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "July"
msgstr ""
msgstr "Julio"
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "August"
msgstr ""
msgstr "Agosto"
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "September"
msgstr ""
msgstr "Septiembre"
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "October"
msgstr ""
msgstr "Octubre"
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "November"
msgstr ""
msgstr "Noviembre"
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "December"
msgstr ""
msgstr "Diciembre"
#~ msgid ""
#~ "Your request has been queued on the server and will now be processed. If "

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More